Line data Source code
1 : /*
2 : * Copyright (C) 2021 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 112 : import cockpit from "cockpit";
7 112 : import React from 'react';
8 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
9 : import {
10 : Modal, ModalBody, ModalFooter, ModalHeader
11 : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
12 : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
13 : import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
14 : import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
15 : import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
16 : import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
17 : import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
18 :
19 : import { ServerTime } from 'serverTime.js';
20 : import * as timeformat from "timeformat";
21 : import { DialogsContext } from "dialogs.jsx";
22 : import { FormHelper } from "cockpit-components-form-helper";
23 :
24 : import { SimpleSelect } from "cockpit-components-simple-select";
25 :
26 : import "cockpit-components-shutdown.scss";
27 :
28 112 : const _ = cockpit.gettext;
29 :
30 112 : export class ShutdownModal extends React.Component {
31 112 : static contextType = DialogsContext;
32 :
33 2 : constructor(props) {
34 2 : super(props);
35 2 : this.date_spawn = null;
36 2 : this.state = {
37 2 : error: "",
38 2 : dateError: "",
39 2 : message: "",
40 2 : isOpen: false,
41 2 : selected: "1",
42 2 : dateObject: undefined,
43 2 : startDate: undefined,
44 2 : date: "",
45 2 : time: "",
46 2 : when: "+1",
47 2 : formFilled: false,
48 2 : };
49 2 : this.onSubmit = this.onSubmit.bind(this);
50 2 : this.updateDate = this.updateDate.bind(this);
51 2 : this.updateTime = this.updateTime.bind(this);
52 2 : this.calculate = this.calculate.bind(this);
53 2 : this.dateRangeValidator = this.dateRangeValidator.bind(this);
54 :
55 2 : this.server_time = new ServerTime();
56 2 : }
57 :
58 2 : componentDidMount() {
59 2 : this.server_time.wait()
60 2 : .then(() => {
61 2 : const dateObject = this.server_time.utc_fake_now;
62 2 : const date = dateObject.toISOString().split("T")[0];
63 2 : const hour = dateObject.getUTCHours();
64 2 : const minute = dateObject.getUTCMinutes();
65 2 : this.setState({
66 2 : dateObject,
67 2 : date,
68 2 : startDate: new Date(dateObject.toDateString()),
69 2 : time: hour.toString().padStart(2, "0") + ":" + minute.toString().padStart(2, "0"),
70 2 : });
71 2 : })
72 2 : .always(() => this.setState({ formFilled: true }));
73 2 : }
74 :
75 1 : updateDate(value, dateObject) {
76 1 : this.setState({ date: value, dateObject }, this.calculate);
77 1 : }
78 :
79 1 : updateTime(value, hour, minute) {
80 1 : this.setState({ time: value, hour, minute }, this.calculate);
81 1 : }
82 :
83 2 : calculate() {
84 2 : if (this.date_spawn)
85 1 : this.date_spawn.close("cancelled");
86 :
87 1 : if (this.state.selected != "x") {
88 1 : this.setState(prevState => ({
89 1 : when: "+" + prevState.selected,
90 1 : error: "",
91 1 : dateError: "",
92 1 : }));
93 1 : return;
94 1 : }
95 :
96 1 : const time_error = this.state.hour === null || this.state.minute === null;
97 2 : const date_error = !this.state.dateObject;
98 :
99 0 : if (time_error && date_error) {
100 0 : this.setState({ dateError: _("Invalid date format and invalid time format") });
101 0 : return;
102 0 : } else if (time_error) {
103 1 : this.setState({ dateError: _("Invalid time format") });
104 1 : return;
105 1 : } else if (date_error) {
106 1 : this.setState({ dateError: _("Invalid date format") });
107 1 : return;
108 1 : }
109 :
110 1 : const cmd = ["date", "--date=" + (new Intl.DateTimeFormat('en-us').format(this.state.dateObject)) + " " + this.state.time, "+%s"];
111 1 : this.date_spawn = cockpit.spawn(cmd, { err: "message" });
112 1 : this.date_spawn.then(data => {
113 1 : const input_timestamp = parseInt(data, 10);
114 1 : const server_timestamp = parseInt(this.server_time.now.getTime() / 1000, 10);
115 1 : let offset = Math.ceil((input_timestamp - server_timestamp) / 60);
116 :
117 : /* If the time in minutes just changed, make it happen now */
118 0 : if (offset === -1) {
119 0 : offset = 0;
120 0 : } else if (offset < 0) { // Otherwise it is a failure
121 0 : this.setState({ dateError: _("Cannot schedule event in the past") });
122 0 : return;
123 0 : }
124 :
125 1 : this.setState({
126 1 : when: "+" + offset,
127 1 : error: "",
128 1 : dateError: "",
129 1 : });
130 1 : });
131 1 : this.date_spawn.catch(e => {
132 1 : if (e.problem == "cancelled")
133 1 : return;
134 0 : this.setState({ error: e.toString() });
135 1 : });
136 1 : this.date_spawn.finally(() => { this.date_spawn = null });
137 2 : }
138 :
139 2 : onSubmit(event) {
140 2 : const Dialogs = this.context;
141 0 : const arg = this.props.shutdown ? "--poweroff" : "--reboot";
142 2 : if (!this.props.shutdown)
143 1 : cockpit.hint("restart");
144 :
145 2 : cockpit.spawn(["shutdown", arg, this.state.when, this.state.message], { superuser: "require", err: "message" })
146 2 : .then(this.props.onClose || Dialogs.close)
147 0 : .catch(e => this.setState({ error: e.toString() }));
148 :
149 2 : event.preventDefault();
150 2 : return false;
151 2 : }
152 :
153 1 : dateRangeValidator(date) {
154 1 : if (this.state.startDate && date < this.state.startDate) {
155 1 : return _("Cannot schedule event in the past");
156 1 : }
157 1 : return '';
158 1 : }
159 :
160 2 : render() {
161 2 : const Dialogs = this.context;
162 2 : const options = [
163 2 : { value: "0", content: _("No delay") },
164 2 : { decorator: "divider", key: "divider" },
165 2 : { value: "1", content: _("1 minute") },
166 2 : { value: "5", content: _("5 minutes") },
167 2 : { value: "20", content: _("20 minutes") },
168 2 : { value: "40", content: _("40 minutes") },
169 2 : { value: "60", content: _("60 minutes") },
170 2 : { decorator: "divider", key: "divider-2" },
171 2 : { value: "x", content: _("Specific time") },
172 2 : ];
173 :
174 2 : return (
175 2 : <Modal isOpen position="top" variant="medium"
176 2 : onClose={this.props.onClose || Dialogs.close}
177 2 : id="shutdown-dialog"
178 : >
179 0 : <ModalHeader title={this.props.shutdown ? _("Shut down") : _("Reboot")} />
180 2 : <ModalBody>
181 2 : <Form isHorizontal onSubmit={this.onSubmit}>
182 2 : <FormGroup fieldId="message" label={_("Message to logged in users")}>
183 0 : <TextArea id="message" resizeOrientation="vertical" value={this.state.message} onChange={(_, v) => this.setState({ message: v })} />
184 2 : </FormGroup>
185 2 : <FormGroup fieldId="delay" label={_("Delay")}>
186 2 : <Flex className="shutdown-delay-group" alignItems={{ default: 'alignItemsCenter' }}>
187 2 : <SimpleSelect
188 2 : toggleProps={{ id: "delay", className: 'shutdown-select-delay' }}
189 2 : options={options}
190 2 : selected={this.state.selected}
191 2 : isDisabled={!this.state.formFilled}
192 2 : onSelect={s => this.setState({ selected: s }, this.calculate)} />
193 1 : {this.state.selected === "x" && <>
194 1 : <DatePicker aria-label={_("Pick date")}
195 1 : buttonAriaLabel={_("Toggle date picker")}
196 1 : className='shutdown-date-picker'
197 1 : invalidFormatText=""
198 1 : isDisabled={!this.state.formFilled}
199 1 : locale={timeformat.dateFormatLang()}
200 1 : weekStart={timeformat.firstDayOfWeek()}
201 1 : onBlur={this.calculate}
202 1 : onChange={(_, d, ds) => this.updateDate(d, ds)}
203 1 : validators={[this.dateRangeValidator]}
204 1 : value={this.state.date}
205 1 : appendTo={() => document.body} />
206 1 : <TimePicker time={this.state.time} is24Hour
207 1 : className='shutdown-time-picker'
208 1 : id="shutdown-time"
209 1 : isDisabled={!this.state.formFilled}
210 1 : invalidFormatErrorMessage=""
211 1 : menuAppendTo={() => document.body}
212 1 : onBlur={this.calculate}
213 1 : onChange={(_, time, h, m) => this.updateTime(time, h, m) } />
214 1 : </>}
215 2 : </Flex>
216 2 : <FormHelper fieldId="delay" helperTextInvalid={this.state.dateError} />
217 2 : </FormGroup>
218 2 : </Form>
219 0 : {this.state.error && <Alert isInline variant='danger' title={this.state.error} />}
220 2 : </ModalBody>
221 2 : <ModalFooter>
222 0 : <Button variant='danger' isDisabled={this.state.error || this.state.dateError} onClick={this.onSubmit}>{this.props.shutdown ? _("Shut down") : _("Reboot")}</Button>
223 2 : <Button variant='link' onClick={this.props.onClose || Dialogs.close}>{_("Cancel")}</Button>
224 2 : </ModalFooter>
225 2 : </Modal>
226 : );
227 2 : }
228 112 : }
|