240 lines
10 KiB
JavaScript
240 lines
10 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2021 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import cockpit from "cockpit";
|
|
import React from 'react';
|
|
import {
|
|
Button, Select, SelectOption, Modal, Alert, Flex, Form,
|
|
Divider, FormGroup, TextArea, DatePicker,
|
|
TimePicker,
|
|
} from '@patternfly/react-core';
|
|
|
|
import { ServerTime } from 'serverTime.js';
|
|
import * as timeformat from "timeformat.js";
|
|
import { DialogsContext } from "dialogs.jsx";
|
|
|
|
import "cockpit-components-shutdown.scss";
|
|
|
|
const _ = cockpit.gettext;
|
|
|
|
export class ShutdownModal extends React.Component {
|
|
static contextType = DialogsContext;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.date_spawn = null;
|
|
this.state = {
|
|
error: "",
|
|
dateError: "",
|
|
message: "",
|
|
isOpen: false,
|
|
selected: "1",
|
|
dateObject: undefined,
|
|
startDate: undefined,
|
|
date: "",
|
|
time: "",
|
|
when: "+1",
|
|
formFilled: false,
|
|
};
|
|
this.onSubmit = this.onSubmit.bind(this);
|
|
this.updateDate = this.updateDate.bind(this);
|
|
this.updateTime = this.updateTime.bind(this);
|
|
this.calculate = this.calculate.bind(this);
|
|
this.dateRangeValidator = this.dateRangeValidator.bind(this);
|
|
|
|
this.server_time = new ServerTime();
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.server_time.wait()
|
|
.then(() => {
|
|
const dateObject = this.server_time.utc_fake_now;
|
|
const date = timeformat.dateShort(dateObject);
|
|
const hour = this.server_time.utc_fake_now.getUTCHours();
|
|
const minute = this.server_time.utc_fake_now.getUTCMinutes();
|
|
this.setState({
|
|
dateObject,
|
|
date,
|
|
startDate: new Date(dateObject.toDateString()),
|
|
time: hour.toString().padStart(2, "0") + ":" + minute.toString().padStart(2, "0"),
|
|
});
|
|
})
|
|
.always(() => this.setState({ formFilled: true }));
|
|
}
|
|
|
|
updateDate(value, dateObject) {
|
|
this.setState({ date: value, dateObject }, this.calculate);
|
|
}
|
|
|
|
updateTime(value, hour, minute) {
|
|
this.setState({ time: value, hour, minute }, this.calculate);
|
|
}
|
|
|
|
calculate() {
|
|
if (this.date_spawn)
|
|
this.date_spawn.close("cancelled");
|
|
|
|
if (this.state.selected != "x") {
|
|
this.setState({
|
|
when: "+" + this.state.selected,
|
|
error: "",
|
|
dateError: "",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const time_error = this.state.hour === null || this.state.minute === null;
|
|
const date_error = !this.state.dateObject;
|
|
|
|
if (time_error && date_error) {
|
|
this.setState({ dateError: _("Invalid date format and invalid time format") });
|
|
return;
|
|
} else if (time_error) {
|
|
this.setState({ dateError: _("Invalid time format") });
|
|
return;
|
|
} else if (date_error) {
|
|
this.setState({ dateError: _("Invalid date format") });
|
|
return;
|
|
}
|
|
|
|
const cmd = ["date", "--date=" + (new Intl.DateTimeFormat('en-us').format(this.state.dateObject)) + " " + this.state.time, "+%s"];
|
|
this.date_spawn = cockpit.spawn(cmd, { err: "message" });
|
|
this.date_spawn.then(data => {
|
|
const input_timestamp = parseInt(data, 10);
|
|
const server_timestamp = parseInt(this.server_time.now.getTime() / 1000, 10);
|
|
let offset = Math.ceil((input_timestamp - server_timestamp) / 60);
|
|
|
|
/* If the time in minutes just changed, make it happen now */
|
|
if (offset === -1) {
|
|
offset = 0;
|
|
} else if (offset < 0) { // Otherwise it is a failure
|
|
this.setState({ dateError: _("Cannot schedule event in the past") });
|
|
return;
|
|
}
|
|
|
|
this.setState({
|
|
when: "+" + offset,
|
|
error: "",
|
|
dateError: "",
|
|
});
|
|
});
|
|
this.date_spawn.catch(e => {
|
|
if (e.problem == "cancelled")
|
|
return;
|
|
this.setState({ error: e.message });
|
|
});
|
|
this.date_spawn.finally(() => { this.date_spawn = null });
|
|
}
|
|
|
|
onSubmit(event) {
|
|
const Dialogs = this.context;
|
|
const arg = this.props.shutdown ? "--poweroff" : "--reboot";
|
|
if (!this.props.shutdown)
|
|
cockpit.hint("restart");
|
|
|
|
cockpit.spawn(["shutdown", arg, this.state.when, this.state.message], { superuser: true, err: "message" })
|
|
.then(this.props.onClose || Dialogs.close)
|
|
.catch(e => this.setState({ error: e }));
|
|
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
dateRangeValidator(date) {
|
|
if (this.state.startDate && date < this.state.startDate) {
|
|
return _("Cannot schedule event in the past");
|
|
}
|
|
return '';
|
|
}
|
|
|
|
render() {
|
|
const Dialogs = this.context;
|
|
const options = [
|
|
<SelectOption value="0" key="0">{_("No delay")}</SelectOption>,
|
|
<Divider key="divider" component="li" />,
|
|
<SelectOption value="1" key="1">{_("1 minute")}</SelectOption>,
|
|
<SelectOption value="5" key="5">{_("5 minutes")}</SelectOption>,
|
|
<SelectOption value="20" key="20">{_("20 minutes")}</SelectOption>,
|
|
<SelectOption value="40" key="40">{_("40 minutes")}</SelectOption>,
|
|
<SelectOption value="60" key="60">{_("60 minutes")}</SelectOption>,
|
|
<Divider key="divider-2" component="li" />,
|
|
<SelectOption value="x" key="x">{_("Specific time")}</SelectOption>
|
|
];
|
|
|
|
return (
|
|
<Modal isOpen position="top" variant="medium"
|
|
onClose={this.props.onClose || Dialogs.close}
|
|
id="shutdown-dialog"
|
|
title={this.props.shutdown ? _("Shut down") : _("Reboot")}
|
|
footer={<>
|
|
<Button variant='danger' isDisabled={this.state.error || this.state.dateError} onClick={this.onSubmit}>{this.props.shutdown ? _("Shut down") : _("Reboot")}</Button>
|
|
<Button variant='link' onClick={this.props.onClose || Dialogs.close}>{_("Cancel")}</Button>
|
|
</>}
|
|
>
|
|
<>
|
|
<Form isHorizontal onSubmit={this.onSubmit}>
|
|
<FormGroup fieldId="message" label={_("Message to logged in users")}>
|
|
<TextArea id="message" resizeOrientation="vertical" value={this.state.message} onChange={v => this.setState({ message: v })} />
|
|
</FormGroup>
|
|
<FormGroup fieldId="delay" label={_("Delay")}
|
|
helperTextInvalid={this.state.dateError}
|
|
validated={this.state.dateError ? "error" : "default"}>
|
|
<Flex className="shutdown-delay-group" alignItems={{ default: 'alignItemsCenter' }}>
|
|
<Select toggleId="delay" isOpen={this.state.isOpen} selections={this.state.selected}
|
|
isDisabled={!this.state.formFilled}
|
|
className='shutdown-select-delay'
|
|
onToggle={o => this.setState({ isOpen: o })} menuAppendTo="parent"
|
|
onSelect={(e, s) => this.setState({ selected: s, isOpen: false }, this.calculate)}>
|
|
{options}
|
|
</Select>
|
|
{this.state.selected === "x" && <>
|
|
<DatePicker aria-label={_("Pick date")}
|
|
buttonAriaLabel={_("Toggle date picker")}
|
|
className='shutdown-date-picker'
|
|
dateFormat={timeformat.dateShort}
|
|
dateParse={timeformat.parseShortDate}
|
|
invalidFormatText=""
|
|
isDisabled={!this.state.formFilled}
|
|
locale={timeformat.dateFormatLang()}
|
|
weekStart={timeformat.firstDayOfWeek()}
|
|
onBlur={this.calculate}
|
|
onChange={(d, ds) => this.updateDate(d, ds)}
|
|
placeholder={timeformat.dateShortFormat()}
|
|
validators={[this.dateRangeValidator]}
|
|
value={this.state.date}
|
|
appendTo={() => document.body} />
|
|
<TimePicker time={this.state.time} is24Hour
|
|
className='shutdown-time-picker'
|
|
id="shutdown-time"
|
|
isDisabled={!this.state.formFilled}
|
|
invalidFormatErrorMessage=""
|
|
menuAppendTo={() => document.body}
|
|
onBlur={this.calculate}
|
|
onChange={(time, h, m) => this.updateTime(time, h, m) } />
|
|
</>}
|
|
</Flex>
|
|
</FormGroup>
|
|
</Form>
|
|
{this.state.error && <Alert isInline variant='danger' title={this.state.error} />}
|
|
</>
|
|
</Modal>
|
|
);
|
|
}
|
|
}
|