Line data Source code
1 : /*
2 : * Copyright (C) 2021 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 34 : import cockpit from 'cockpit';
7 34 : 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 34 : 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 : };
|