LCOV - code coverage report
Current view: top level - pkg/systemd - timer-dialog.jsx Coverage Total Hit
Test: cockpit Lines: 92.4 % 328 303
Test Date: 2026-06-17 06:28:00

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6           31 : import cockpit from 'cockpit';
       7           31 : import React, { useState } from 'react';
       8              : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
       9              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      10              : import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
      11              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      12              : import { Form, FormGroup, FormAlert } from "@patternfly/react-core/dist/esm/components/Form/index.js";
      13              : import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
      14              : import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
      15              : import {
      16              :     Modal, ModalBody, ModalFooter, ModalHeader
      17              : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      18              : import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
      19              : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
      20              : import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
      21              : import { MinusIcon, PlusIcon } from '@patternfly/react-icons';
      22              : 
      23              : import { FormHelper } from "cockpit-components-form-helper";
      24              : import { ModalError } from 'cockpit-components-inline-notification.jsx';
      25              : import { useDialogs } from "dialogs.jsx";
      26              : 
      27              : import { updateTime } from './services.jsx';
      28              : import { create_timer } from './timer-dialog-helpers.js';
      29              : import * as timeformat from "timeformat";
      30              : 
      31              : import "./timers.scss";
      32              : 
      33           31 : const _ = cockpit.gettext;
      34              : 
      35            3 : export const CreateTimerDialogButton = ({ owner, isLoading }) => {
      36            3 :     const Dialogs = useDialogs();
      37            3 :     return (
      38            3 :         <Button key='create-timer-action'
      39            3 :                 variant="secondary"
      40            3 :                 id="create-timer"
      41            3 :                 isDisabled={isLoading}
      42            2 :                 onClick={() => {
      43            2 :                     updateTime();
      44            2 :                     Dialogs.show(<TimerDialog owner={owner} />);
      45            2 :                 }}>
      46            3 :             {_("Create timer")}
      47            3 :         </Button>
      48              :     );
      49            3 : };
      50              : 
      51            2 : export const TimerDialog = ({ owner, timer }) => {
      52            2 :     const Dialogs = useDialogs();
      53            1 :     const [command, setCommand] = useState(timer?.command || '');
      54            1 :     const [delay, setDelay] = useState(timer?.delay || 'specific-time');
      55            1 :     const [delayNumber, setDelayNumber] = useState(timer?.delayNumber || 0);
      56            1 :     const [delayUnit, setDelayUnit] = useState(timer?.delayUnit || 'sec');
      57            1 :     const [description, setDescription] = useState(timer?.description || '');
      58            2 :     const [dialogError, setDialogError] = useState(undefined);
      59            2 :     const [inProgress, setInProgress] = useState(false);
      60            1 :     const [name, setName] = useState(timer?.name || '');
      61            1 :     const [repeat, setRepeat] = useState(timer?.repeat || 'no');
      62            1 :     const [repeatPatterns, setRepeatPatterns] = useState(timer?.repeatPatterns || []);
      63            1 :     const [specificTime, setSpecificTime] = useState(timer?.specificTime || "00:00");
      64            2 :     const [isSpecificTimeOpen, setSpecificTimeOpen] = useState(false);
      65            2 :     const [submitted, setSubmitted] = useState(false);
      66            2 :     const validationFailed = {};
      67              : 
      68            2 :     if (!name.trim().length || !/^[a-zA-Z0-9:_.@-]+$/.test(name))
      69            2 :         validationFailed.name = true;
      70            2 :     if (!description.trim().length)
      71            2 :         validationFailed.description = true;
      72            2 :     if (!command.trim().length)
      73            2 :         validationFailed.command = true;
      74            2 :     if (!/^[0-9]+$/.test(delayNumber))
      75            0 :         validationFailed.delayNumber = true;
      76              : 
      77            2 :     const timePicker = (idx) => (
      78            2 :         <TimePicker className="create-timer-time-picker"
      79            0 :                     time={repeatPatterns[idx].time || "00:00"}
      80            2 :                     is24Hour
      81            2 :                     isOpen={repeatPatterns[idx].isOpen || false}
      82            2 :                     setIsOpen={isOpen => setRepeatPatterns(old => {
      83            2 :                         const arr = [...old];
      84            2 :                         arr[idx].isOpen = isOpen;
      85            2 :                         return arr;
      86            2 :                     })}
      87            2 :                     menuAppendTo={() => document.body}
      88            2 :                     onChange={(_, time) => setRepeatPatterns(old => {
      89            2 :                         const arr = [...old];
      90            2 :                         arr[idx].time = time;
      91            2 :                         return arr;
      92            2 :                     })}
      93            2 :         />
      94              :     );
      95              : 
      96            2 :     function onSubmit(event) {
      97            2 :         setSubmitted(true);
      98              : 
      99            2 :         if (event)
     100            2 :             event.preventDefault();
     101              : 
     102            2 :         if (Object.keys(validationFailed).length)
     103            1 :             return false;
     104              : 
     105            2 :         create_timer({ name, description, command, delay, delayUnit, delayNumber, repeat, specificTime, repeatPatterns, owner })
     106            1 :                 .then(Dialogs.close, exc => {
     107            1 :                     setDialogError(exc.message);
     108            1 :                     setInProgress(false);
     109            1 :                 });
     110              : 
     111            2 :         return false;
     112            2 :     }
     113              : 
     114            2 :     return (
     115            2 :         <Modal id="timer-dialog"
     116            2 :             className="timer-dialog" position="top"
     117            2 :             variant="medium" isOpen onClose={Dialogs.close}
     118              :         >
     119            1 :             <ModalHeader title={!timer ? _("Create timer") : _("Edit timer")} />
     120            2 :             <ModalBody>
     121            1 :                 {dialogError && <ModalError dialogError={_("Timer creation failed")} dialogErrorDetail={dialogError} />}
     122            2 :                 <Form isHorizontal onSubmit={onSubmit}>
     123            0 :                     {timer && !timer.delay && <FormAlert>
     124            0 :                         <Alert variant="danger" title={_("Failed to get the starting conditions for the timer")} isInline />
     125            0 :                     </FormAlert>}
     126            0 :                     {timer && !timer.command && <FormAlert>
     127            0 :                         <Alert variant="danger" title={_("Failed to get the timer command")} isInline />
     128            0 :                     </FormAlert>}
     129            2 :                     <FormGroup label={_("Name")}
     130            2 :                                fieldId="servicename">
     131            2 :                         <TextInput id='servicename'
     132            2 :                                    value={name}
     133            1 :                                    validated={submitted && validationFailed.name ? "error" : "default"}
     134            2 :                                    onChange={(_event, value) => setName(value)}
     135            2 :                                    readOnlyVariant={!!timer} />
     136            2 :                         <FormHelper fieldId="servicename"
     137            0 :                                     helperTextInvalid={submitted && validationFailed.name && (!name.trim().length ? _("This field cannot be empty") : _("Only alphabets, numbers, : , _ , . , @ , - are allowed"))} />
     138            2 :                     </FormGroup>
     139            2 :                     <FormGroup label={_("Description")}
     140            2 :                                fieldId="description">
     141            2 :                         <TextInput id='description'
     142            2 :                                    value={description}
     143            1 :                                    validated={submitted && validationFailed.description ? "error" : "default"}
     144            2 :                                    onChange={(_event, value) => setDescription(value)} />
     145            1 :                         <FormHelper fieldId="description" helperTextInvalid={submitted && validationFailed.description && _("This field cannot be empty")} />
     146            2 :                     </FormGroup>
     147            2 :                     <FormGroup label={_("Shell command")}
     148            2 :                                fieldId="command">
     149            2 :                         <TextInput id='command'
     150            2 :                                    value={command}
     151            0 :                                    validated={submitted && validationFailed.command ? "error" : "default"}
     152            2 :                                    onChange={(_event, str) => { setCommand(str) }} />
     153            2 :                         <FormHelper fieldId="command"
     154            2 :                                     helperText={_("This command will be executed by /bin/sh.")}
     155            0 :                                     helperTextInvalid={submitted && validationFailed.command && _("This field cannot be empty")} />
     156            2 :                     </FormGroup>
     157            2 :                     <FormGroup label={_("Trigger")} hasNoPaddingTop>
     158            2 :                         <Flex>
     159            2 :                             <Radio value="specific-time"
     160            2 :                                    id="specific-time"
     161            2 :                                    name="boot-or-specific-time"
     162            0 :                                    onChange={() => setDelay("specific-time")}
     163            2 :                                    isChecked={delay == "specific-time"}
     164            2 :                                    label={_("At specific time")} />
     165            2 :                             <Radio value="system-boot"
     166            2 :                                    id="system-boot"
     167            2 :                                    name="boot-or-specific-time"
     168            2 :                                    onChange={() => setDelay("system-boot")}
     169            2 :                                    isChecked={delay == "system-boot"}
     170            2 :                                    label={_("After system boot")} />
     171            2 :                         </Flex>
     172            2 :                         { delay == "system-boot" &&
     173            2 :                         <FormGroup className="delay-group"
     174            2 :                                    label={_("Delay")}>
     175            2 :                             <Flex>
     176            2 :                                 <TextInput className="delay-number"
     177            2 :                                            value={delayNumber}
     178            0 :                                            validated={submitted && validationFailed.delayNumber ? "error" : "default"}
     179            2 :                                            onChange={(_event, value) => setDelayNumber(value)} />
     180            2 :                                 <FormSelect className="delay-unit"
     181            2 :                                             value={delayUnit}
     182            0 :                                             onChange={(_, val) => setDelayUnit(val)}
     183            2 :                                             aria-label={_("Delay")}>
     184            2 :                                     <FormSelectOption value="sec" label={_("Seconds")} />
     185            2 :                                     <FormSelectOption value="min" label={_("Minutes")} />
     186            2 :                                     <FormSelectOption value="hr" label={_("Hours")} />
     187            2 :                                     <FormSelectOption value="weeks" label={_("Weeks")} />
     188            2 :                                 </FormSelect>
     189            2 :                             </Flex>
     190            0 :                             <FormHelper helperTextInvalid={submitted && validationFailed.delayNumber && _("Delay must be a number")} />
     191            2 :                         </FormGroup> }
     192            2 :                         { delay == "specific-time" &&
     193            2 :                         <>
     194            2 :                             <FormGroup label={_("Repeat")}>
     195            2 :                                 <FormSelect value={repeat}
     196            2 :                                             id="drop-repeat"
     197            2 :                                             onChange={(_, value) => {
     198            2 :                                                 if (value == repeat)
     199            2 :                                                     return;
     200              : 
     201            2 :                                                 setRepeat(value);
     202            2 :                                                 if (value == "minutely")
     203            2 :                                                     setRepeatPatterns([{ key: 0, second: "0" }]);
     204            2 :                                                 else if (value == "hourly")
     205            2 :                                                     setRepeatPatterns([{ key: 0, minute: "0" }]);
     206            2 :                                                 else if (value == "daily")
     207            2 :                                                     setRepeatPatterns([{ key: 0, time: "00:00" }]);
     208            2 :                                                 else if (value == "weekly")
     209            2 :                                                     setRepeatPatterns([{ key: 0, day: "mon", time: "00:00" }]);
     210            2 :                                                 else if (value == "monthly")
     211            2 :                                                     setRepeatPatterns([{ key: 0, day: 1, time: "00:00" }]);
     212            2 :                                                 else if (value == "yearly")
     213            2 :                                                     setRepeatPatterns([{ key: 0, date: undefined, time: "00:00" }]);
     214            2 :                                             }}
     215            2 :                                             aria-label={_("Repeat")}>
     216            2 :                                     <FormSelectOption value="no" label={_("Don't repeat")} />
     217            2 :                                     <FormSelectOption value="minutely" label={_("Minutely")} />
     218            2 :                                     <FormSelectOption value="hourly" label={_("Hourly")} />
     219            2 :                                     <FormSelectOption value="daily" label={_("Daily")} />
     220            2 :                                     <FormSelectOption value="weekly" label={_("Weekly")} />
     221            2 :                                     <FormSelectOption value="monthly" label={_("Monthly")} />
     222            2 :                                     <FormSelectOption value="yearly" label={_("Yearly")} />
     223            2 :                                 </FormSelect>
     224            2 :                             </FormGroup>
     225            2 :                             {repeat == "no" &&
     226            2 :                             <FormGroup label={_("Run at")}>
     227            2 :                                 <TimePicker className="create-timer-time-picker specific-no-repeat"
     228            2 :                                             isOpen={isSpecificTimeOpen} setIsOpen={setSpecificTimeOpen}
     229            2 :                                             menuAppendTo={() => document.body} time={specificTime} is24Hour onChange={(_, val) => setSpecificTime(val)} />
     230            2 :                             </FormGroup>}
     231            2 :                             {repeatPatterns.map((item, idx) => {
     232            2 :                                 let label;
     233            2 :                                 if (repeat == "minutely")
     234            2 :                                     label = _("At second");
     235            2 :                                 else if (repeat == "hourly")
     236            2 :                                     label = _("At minute");
     237            2 :                                 else if (repeat == "daily")
     238            2 :                                     label = _("Run at");
     239            2 :                                 else if (repeat == "weekly" || repeat == "monthly" || repeat == "yearly")
     240            2 :                                     label = _("Run on");
     241              : 
     242            2 :                                 let helperTextInvalid;
     243            2 :                                 const min = repeatPatterns[idx].minute;
     244            2 :                                 const validationFailedMinute = !(/^[0-9]+$/.test(min) && min <= 59 && min >= 0);
     245              : 
     246            0 :                                 if (submitted && repeat == 'hourly' && validationFailedMinute) {
     247            0 :                                     helperTextInvalid = _("Minute needs to be a number between 0-59");
     248            0 :                                 }
     249              : 
     250            2 :                                 const sec = repeatPatterns[idx].second;
     251            2 :                                 const validationFailedSecond = !(/^[0-9]+$/.test(sec) && sec <= 59 && sec >= 0);
     252              : 
     253            0 :                                 if (submitted && repeat == 'minutely' && validationFailedSecond) {
     254            0 :                                     helperTextInvalid = _("Second needs to be a number between 0-59");
     255            0 :                                 }
     256              : 
     257            2 :                                 return (
     258            2 :                                     <FormGroup label={label} key={item.key}>
     259            2 :                                         <Flex className="specific-repeat-group" data-index={idx}>
     260            2 :                                             {repeat == "minutely" &&
     261            2 :                                                 <TextInput className='delay-number'
     262            2 :                                                            id={repeat}
     263            2 :                                                            value={repeatPatterns[idx].second}
     264            2 :                                                            onChange={(_event, second) => setRepeatPatterns(old => {
     265            2 :                                                                const arr = [...old];
     266            2 :                                                                arr[idx].second = second;
     267            2 :                                                                return arr;
     268            2 :                                                            })}
     269            0 :                                                            validated={submitted && validationFailedSecond ? "error" : "default"} />
     270              :                                             }
     271            2 :                                             {repeat == "hourly" &&
     272            2 :                                                 <TextInput className='delay-number'
     273            2 :                                                            value={repeatPatterns[idx].minute}
     274            2 :                                                            onChange={(_event, minute) => setRepeatPatterns(old => {
     275            2 :                                                                const arr = [...old];
     276            2 :                                                                arr[idx].minute = minute;
     277            2 :                                                                return arr;
     278            2 :                                                            })}
     279            0 :                                                            validated={submitted && validationFailedMinute ? "error" : "default"} />
     280              :                                             }
     281            2 :                                             {repeat == "daily" && timePicker(idx)}
     282            2 :                                             {repeat == "weekly" && <>
     283            2 :                                                 <FormSelect value={repeatPatterns[idx].day}
     284            2 :                                                             className="week-days"
     285            2 :                                                             onChange={(_, day) => setRepeatPatterns(old => {
     286            2 :                                                                 const arr = [...old];
     287            2 :                                                                 arr[idx].day = day;
     288            2 :                                                                 return arr;
     289            2 :                                                             })}
     290            2 :                                                             aria-label={_("Repeat weekly")}>
     291            2 :                                                     <FormSelectOption value="mon" label={_("Mondays")} />
     292            2 :                                                     <FormSelectOption value="tue" label={_("Tuesdays")} />
     293            2 :                                                     <FormSelectOption value="wed" label={_("Wednesdays")} />
     294            2 :                                                     <FormSelectOption value="thu" label={_("Thursdays")} />
     295            2 :                                                     <FormSelectOption value="fri" label={_("Fridays")} />
     296            2 :                                                     <FormSelectOption value="sat" label={_("Saturdays")} />
     297            2 :                                                     <FormSelectOption value="sun" label={_("Sundays")} />
     298            2 :                                                 </FormSelect>
     299            2 :                                                 {timePicker(idx)}
     300            2 :                                             </>}
     301            2 :                                             {repeat == "monthly" && <>
     302            2 :                                                 <FormSelect value={repeatPatterns[idx].day}
     303            2 :                                                             className="month-days"
     304            2 :                                                             onChange={(_, day) => setRepeatPatterns(old => {
     305            2 :                                                                 const arr = [...old];
     306            2 :                                                                 arr[idx].day = day;
     307            2 :                                                                 return arr;
     308            2 :                                                             })}
     309            2 :                                                             aria-label={_("Repeat monthly")}>
     310            2 :                                                     {[_("1st"), _("2nd"), _("3rd"), _("4th"), _("5th"),
     311            2 :                                                         _("6th"), _("7th"), _("8th"), _("9th"), _("10th"),
     312            2 :                                                         _("11th"), _("12th"), _("13th"), _("14th"), _("15th"),
     313            2 :                                                         _("16th"), _("17th"), _("18th"), _("19th"), _("20th"),
     314            2 :                                                         _("21th"), _("22th"), _("23th"), _("24th"), _("25th"),
     315            2 :                                                         _("26th"), _("27th"), _("28th"), _("29th"), _("30th"), _("31st")
     316            2 :                                                     ].map((day, index) => <FormSelectOption key={day} value={index + 1} label={day} />)}
     317            2 :                                                 </FormSelect>
     318            2 :                                                 {timePicker(idx)}
     319            2 :                                             </>}
     320            2 :                                             {repeat == "yearly" && <>
     321            2 :                                                 <DatePicker aria-label={_("Pick date")}
     322            2 :                                                             buttonAriaLabel={_("Toggle date picker")}
     323            2 :                                                             locale={timeformat.dateFormatLang()}
     324            2 :                                                             weekStart={timeformat.firstDayOfWeek()}
     325            2 :                                                             onChange={(_, str, data) => setRepeatPatterns(old => {
     326            2 :                                                                 const arr = [...old];
     327            2 :                                                                 arr[idx].date = str;
     328            2 :                                                                 return arr;
     329            2 :                                                             })}
     330            0 :                                                             appendTo={() => document.body}
     331            2 :                                                             value={repeatPatterns[idx].date || ""} />
     332            2 :                                                 {timePicker(idx)}
     333            2 :                                             </>}
     334            2 :                                             {repeat !== "no" && <FlexItem align={{ default: 'alignRight' }}>
     335            2 :                                                 <InputGroup>
     336            2 :                                                     <Button icon={<MinusIcon />} aria-label={_("Remove")}
     337            2 :                                                             variant="secondary"
     338            2 :                                                             isDisabled={repeatPatterns.length == 1}
     339            0 :                                                         onClick={() => setRepeatPatterns(old => old.filter((item, item_idx) => idx != item_idx))} />
     340            2 :                                                     <Button icon={<PlusIcon />} aria-label={_("Add")}
     341            2 :                                                             variant="secondary"
     342            2 :                                                             onClick={() => {
     343            2 :                                                                 if (repeat == "minutely")
     344            2 :                                                                     setRepeatPatterns(old => [...old, { key: repeatPatterns.length, second: "0" }]);
     345            2 :                                                                 else if (repeat == "hourly")
     346            2 :                                                                     setRepeatPatterns(old => [...old, { key: repeatPatterns.length, minute: "0" }]);
     347            2 :                                                                 else if (repeat == "daily")
     348            2 :                                                                     setRepeatPatterns(old => [...old, { key: repeatPatterns.length, time: "00:00" }]);
     349            2 :                                                                 else if (repeat == "weekly")
     350            2 :                                                                     setRepeatPatterns(old => [...old, { key: repeatPatterns.length, day: "mon", time: "00:00" }]);
     351            2 :                                                                 else if (repeat == "monthly")
     352            2 :                                                                     setRepeatPatterns(old => [...old, { key: repeatPatterns.length, day: 1, time: "00:00" }]);
     353            2 :                                                                 else if (repeat == "yearly")
     354            2 :                                                                     setRepeatPatterns(old => [...old, { key: repeatPatterns.length, date: undefined, time: "00:00" }]);
     355            2 :                                                             }} />
     356            2 :                                                 </InputGroup>
     357            2 :                                             </FlexItem>}
     358            2 :                                         </Flex>
     359            2 :                                         <FormHelper helperTextInvalid={helperTextInvalid} />
     360            2 :                                     </FormGroup>
     361              :                                 );
     362            2 :                             })}
     363            2 :                         </>}
     364            2 :                     </FormGroup>
     365            2 :                 </Form>
     366            2 :             </ModalBody>
     367            2 :             <ModalFooter>
     368            2 :                 <Button variant='primary'
     369            2 :                         id="timer-save-button"
     370            2 :                         isLoading={inProgress}
     371            2 :                         isDisabled={inProgress}
     372            2 :                         onClick={onSubmit}>
     373            2 :                     {_("Save")}
     374            2 :                 </Button>
     375            2 :                 <Button id="timer-dialog-close-button" variant='link' onClick={Dialogs.close}>
     376            2 :                     {_("Cancel")}
     377            2 :                 </Button>
     378            2 :             </ModalFooter>
     379            2 :         </Modal>
     380              :     );
     381            2 : };
        

Generated by: LCOV version 2.0-1