LCOV - code coverage report
Current view: top level - pkg/lib - cockpit-components-shutdown.jsx Coverage Total Hit
Test: cockpit Lines: 90.6 % 181 164
Test Date: 2026-06-25 09:20:42

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          114 : import cockpit from "cockpit";
       7          114 : 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          114 : const _ = cockpit.gettext;
      29              : 
      30          114 : export class ShutdownModal extends React.Component {
      31          114 :     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          114 : }
        

Generated by: LCOV version 2.0-1