🏡 index : quine-global-labs/mirror-cockpit-project-cockpit.git

/*
 * Copyright (C) 2021 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import cockpit from 'cockpit';
import React, { useState } from 'react';
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Form, FormGroup, FormAlert } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
import {
    Modal, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
import { MinusIcon, PlusIcon } from '@patternfly/react-icons';

import { FormHelper } from "cockpit-components-form-helper";
import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { useDialogs } from "dialogs.jsx";

import { updateTime } from './services.jsx';
import { create_timer } from './timer-dialog-helpers.js';
import * as timeformat from "timeformat";

import "./timers.scss";

const _ = cockpit.gettext;

export const CreateTimerDialogButton = ({ owner, isLoading }) => {
    const Dialogs = useDialogs();
    return (
        <Button key='create-timer-action'
                variant="secondary"
                id="create-timer"
                isDisabled={isLoading}
                onClick={() => {
                    updateTime();
                    Dialogs.show(<TimerDialog owner={owner} />);
                }}>
            {_("Create timer")}
        </Button>
    );
};

export const TimerDialog = ({ owner, timer }) => {
    const Dialogs = useDialogs();
    const [command, setCommand] = useState(timer?.command || '');
    const [delay, setDelay] = useState(timer?.delay || 'specific-time');
    const [delayNumber, setDelayNumber] = useState(timer?.delayNumber || 0);
    const [delayUnit, setDelayUnit] = useState(timer?.delayUnit || 'sec');
    const [description, setDescription] = useState(timer?.description || '');
    const [dialogError, setDialogError] = useState(undefined);
    const [inProgress, setInProgress] = useState(false);
    const [name, setName] = useState(timer?.name || '');
    const [repeat, setRepeat] = useState(timer?.repeat || 'no');
    const [repeatPatterns, setRepeatPatterns] = useState(timer?.repeatPatterns || []);
    const [specificTime, setSpecificTime] = useState(timer?.specificTime || "00:00");
    const [isSpecificTimeOpen, setSpecificTimeOpen] = useState(false);
    const [submitted, setSubmitted] = useState(false);
    const validationFailed = {};

    if (!name.trim().length || !/^[a-zA-Z0-9:_.@-]+$/.test(name))
        validationFailed.name = true;
    if (!description.trim().length)
        validationFailed.description = true;
    if (!command.trim().length)
        validationFailed.command = true;
    if (!/^[0-9]+$/.test(delayNumber))
        validationFailed.delayNumber = true;

    const timePicker = (idx) => (
        <TimePicker className="create-timer-time-picker"
                    time={repeatPatterns[idx].time || "00:00"}
                    is24Hour
                    isOpen={repeatPatterns[idx].isOpen || false}
                    setIsOpen={isOpen => setRepeatPatterns(old => {
                        const arr = [...old];
                        arr[idx].isOpen = isOpen;
                        return arr;
                    })}
                    menuAppendTo={() => document.body}
                    onChange={(_, time) => setRepeatPatterns(old => {
                        const arr = [...old];
                        arr[idx].time = time;
                        return arr;
                    })}
        />
    );

    function onSubmit(event) {
        setSubmitted(true);

        if (event)
            event.preventDefault();

        if (Object.keys(validationFailed).length)
            return false;

        create_timer({ name, description, command, delay, delayUnit, delayNumber, repeat, specificTime, repeatPatterns, owner })
                .then(Dialogs.close, exc => {
                    setDialogError(exc.message);
                    setInProgress(false);
                });

        return false;
    }

    return (
        <Modal id="timer-dialog"
            className="timer-dialog" position="top"
            variant="medium" isOpen onClose={Dialogs.close}
        >
            <ModalHeader title={!timer ? _("Create timer") : _("Edit timer")} />
            <ModalBody>
                {dialogError && <ModalError dialogError={_("Timer creation failed")} dialogErrorDetail={dialogError} />}
                <Form isHorizontal onSubmit={onSubmit}>
                    {timer && !timer.delay && <FormAlert>
                        <Alert variant="danger" title={_("Failed to get the starting conditions for the timer")} isInline />
                    </FormAlert>}
                    {timer && !timer.command && <FormAlert>
                        <Alert variant="danger" title={_("Failed to get the timer command")} isInline />
                    </FormAlert>}
                    <FormGroup label={_("Name")}
                               fieldId="servicename">
                        <TextInput id='servicename'
                                   value={name}
                                   validated={submitted && validationFailed.name ? "error" : "default"}
                                   onChange={(_event, value) => setName(value)}
                                   readOnlyVariant={!!timer} />
                        <FormHelper fieldId="servicename"
                                    helperTextInvalid={submitted && validationFailed.name && (!name.trim().length ? _("This field cannot be empty") : _("Only alphabets, numbers, : , _ , . , @ , - are allowed"))} />
                    </FormGroup>
                    <FormGroup label={_("Description")}
                               fieldId="description">
                        <TextInput id='description'
                                   value={description}
                                   validated={submitted && validationFailed.description ? "error" : "default"}
                                   onChange={(_event, value) => setDescription(value)} />
                        <FormHelper fieldId="description" helperTextInvalid={submitted && validationFailed.description && _("This field cannot be empty")} />
                    </FormGroup>
                    <FormGroup label={_("Shell command")}
                               fieldId="command">
                        <TextInput id='command'
                                   value={command}
                                   validated={submitted && validationFailed.command ? "error" : "default"}
                                   onChange={(_event, str) => { setCommand(str) }} />
                        <FormHelper fieldId="command"
                                    helperText={_("This command will be executed by /bin/sh.")}
                                    helperTextInvalid={submitted && validationFailed.command && _("This field cannot be empty")} />
                    </FormGroup>
                    <FormGroup label={_("Trigger")} hasNoPaddingTop>
                        <Flex>
                            <Radio value="specific-time"
                                   id="specific-time"
                                   name="boot-or-specific-time"
                                   onChange={() => setDelay("specific-time")}
                                   isChecked={delay === "specific-time"}
                                   label={_("At specific time")} />
                            <Radio value="system-boot"
                                   id="system-boot"
                                   name="boot-or-specific-time"
                                   onChange={() => setDelay("system-boot")}
                                   isChecked={delay === "system-boot"}
                                   label={_("After system boot")} />
                        </Flex>
                        { delay === "system-boot" &&
                        <FormGroup className="delay-group"
                                   label={_("Delay")}>
                            <Flex>
                                <TextInput className="delay-number"
                                           value={delayNumber}
                                           validated={submitted && validationFailed.delayNumber ? "error" : "default"}
                                           onChange={(_event, value) => setDelayNumber(value)} />
                                <FormSelect className="delay-unit"
                                            value={delayUnit}
                                            onChange={(_, val) => setDelayUnit(val)}
                                            aria-label={_("Delay")}>
                                    <FormSelectOption value="sec" label={_("Seconds")} />
                                    <FormSelectOption value="min" label={_("Minutes")} />
                                    <FormSelectOption value="hr" label={_("Hours")} />
                                    <FormSelectOption value="weeks" label={_("Weeks")} />
                                </FormSelect>
                            </Flex>
                            <FormHelper helperTextInvalid={submitted && validationFailed.delayNumber && _("Delay must be a number")} />
                        </FormGroup> }
                        { delay === "specific-time" &&
                        <>
                            <FormGroup label={_("Repeat")}>
                                <FormSelect value={repeat}
                                            id="drop-repeat"
                                            onChange={(_, value) => {
                                                if (value === repeat)
                                                    return;

                                                setRepeat(value);
                                                if (value === "minutely")
                                                    setRepeatPatterns([{ key: 0, second: "0" }]);
                                                else if (value === "hourly")
                                                    setRepeatPatterns([{ key: 0, minute: "0" }]);
                                                else if (value === "daily")
                                                    setRepeatPatterns([{ key: 0, time: "00:00" }]);
                                                else if (value === "weekly")
                                                    setRepeatPatterns([{ key: 0, day: "mon", time: "00:00" }]);
                                                else if (value === "monthly")
                                                    setRepeatPatterns([{ key: 0, day: 1, time: "00:00" }]);
                                                else if (value === "yearly")
                                                    setRepeatPatterns([{ key: 0, date: undefined, time: "00:00" }]);
                                            }}
                                            aria-label={_("Repeat")}>
                                    <FormSelectOption value="no" label={_("Don't repeat")} />
                                    <FormSelectOption value="minutely" label={_("Minutely")} />
                                    <FormSelectOption value="hourly" label={_("Hourly")} />
                                    <FormSelectOption value="daily" label={_("Daily")} />
                                    <FormSelectOption value="weekly" label={_("Weekly")} />
                                    <FormSelectOption value="monthly" label={_("Monthly")} />
                                    <FormSelectOption value="yearly" label={_("Yearly")} />
                                </FormSelect>
                            </FormGroup>
                            {repeat === "no" &&
                            <FormGroup label={_("Run at")}>
                                <TimePicker className="create-timer-time-picker specific-no-repeat"
                                            isOpen={isSpecificTimeOpen} setIsOpen={setSpecificTimeOpen}
                                            menuAppendTo={() => document.body} time={specificTime} is24Hour onChange={(_, val) => setSpecificTime(val)} />
                            </FormGroup>}
                            {repeatPatterns.map((item, idx) => {
                                let label;
                                if (repeat === "minutely")
                                    label = _("At second");
                                else if (repeat === "hourly")
                                    label = _("At minute");
                                else if (repeat === "daily")
                                    label = _("Run at");
                                else if (repeat === "weekly" || repeat === "monthly" || repeat === "yearly")
                                    label = _("Run on");

                                let helperTextInvalid;
                                const min = repeatPatterns[idx].minute;
                                const validationFailedMinute = !(/^[0-9]+$/.test(min) && min <= 59 && min >= 0);

                                if (submitted && repeat === 'hourly' && validationFailedMinute) {
                                    helperTextInvalid = _("Minute needs to be a number between 0-59");
                                }

                                const sec = repeatPatterns[idx].second;
                                const validationFailedSecond = !(/^[0-9]+$/.test(sec) && sec <= 59 && sec >= 0);

                                if (submitted && repeat === 'minutely' && validationFailedSecond) {
                                    helperTextInvalid = _("Second needs to be a number between 0-59");
                                }

                                return (
                                    <FormGroup label={label} key={item.key}>
                                        <Flex className="specific-repeat-group" data-index={idx}>
                                            {repeat === "minutely" &&
                                                <TextInput className='delay-number'
                                                           id={repeat}
                                                           value={repeatPatterns[idx].second}
                                                           onChange={(_event, second) => setRepeatPatterns(old => {
                                                               const arr = [...old];
                                                               arr[idx].second = second;
                                                               return arr;
                                                           })}
                                                           validated={submitted && validationFailedSecond ? "error" : "default"} />
                                            }
                                            {repeat === "hourly" &&
                                                <TextInput className='delay-number'
                                                           value={repeatPatterns[idx].minute}
                                                           onChange={(_event, minute) => setRepeatPatterns(old => {
                                                               const arr = [...old];
                                                               arr[idx].minute = minute;
                                                               return arr;
                                                           })}
                                                           validated={submitted && validationFailedMinute ? "error" : "default"} />
                                            }
                                            {repeat === "daily" && timePicker(idx)}
                                            {repeat === "weekly" && <>
                                                <FormSelect value={repeatPatterns[idx].day}
                                                            className="week-days"
                                                            onChange={(_, day) => setRepeatPatterns(old => {
                                                                const arr = [...old];
                                                                arr[idx].day = day;
                                                                return arr;
                                                            })}
                                                            aria-label={_("Repeat weekly")}>
                                                    <FormSelectOption value="mon" label={_("Mondays")} />
                                                    <FormSelectOption value="tue" label={_("Tuesdays")} />
                                                    <FormSelectOption value="wed" label={_("Wednesdays")} />
                                                    <FormSelectOption value="thu" label={_("Thursdays")} />
                                                    <FormSelectOption value="fri" label={_("Fridays")} />
                                                    <FormSelectOption value="sat" label={_("Saturdays")} />
                                                    <FormSelectOption value="sun" label={_("Sundays")} />
                                                </FormSelect>
                                                {timePicker(idx)}
                                            </>}
                                            {repeat === "monthly" && <>
                                                <FormSelect value={repeatPatterns[idx].day}
                                                            className="month-days"
                                                            onChange={(_, day) => setRepeatPatterns(old => {
                                                                const arr = [...old];
                                                                arr[idx].day = day;
                                                                return arr;
                                                            })}
                                                            aria-label={_("Repeat monthly")}>
                                                    {[_("1st"), _("2nd"), _("3rd"), _("4th"), _("5th"),
                                                        _("6th"), _("7th"), _("8th"), _("9th"), _("10th"),
                                                        _("11th"), _("12th"), _("13th"), _("14th"), _("15th"),
                                                        _("16th"), _("17th"), _("18th"), _("19th"), _("20th"),
                                                        _("21th"), _("22th"), _("23th"), _("24th"), _("25th"),
                                                        _("26th"), _("27th"), _("28th"), _("29th"), _("30th"), _("31st")
                                                    ].map((day, index) => <FormSelectOption key={day} value={index + 1} label={day} />)}
                                                </FormSelect>
                                                {timePicker(idx)}
                                            </>}
                                            {repeat === "yearly" && <>
                                                <DatePicker aria-label={_("Pick date")}
                                                            buttonAriaLabel={_("Toggle date picker")}
                                                            locale={timeformat.dateFormatLang()}
                                                            weekStart={timeformat.firstDayOfWeek()}
                                                            onChange={(_, str, data) => setRepeatPatterns(old => {
                                                                const arr = [...old];
                                                                arr[idx].date = str;
                                                                return arr;
                                                            })}
                                                            appendTo={() => document.body}
                                                            value={repeatPatterns[idx].date || ""} />
                                                {timePicker(idx)}
                                            </>}
                                            {repeat !== "no" && <FlexItem align={{ default: 'alignRight' }}>
                                                <InputGroup>
                                                    <Button icon={<MinusIcon />} aria-label={_("Remove")}
                                                            variant="secondary"
                                                            isDisabled={repeatPatterns.length === 1}
                                                        onClick={() => setRepeatPatterns(old => old.filter((item, item_idx) => idx !== item_idx))} />
                                                    <Button icon={<PlusIcon />} aria-label={_("Add")}
                                                            variant="secondary"
                                                            onClick={() => {
                                                                if (repeat === "minutely")
                                                                    setRepeatPatterns(old => [...old, { key: repeatPatterns.length, second: "0" }]);
                                                                else if (repeat === "hourly")
                                                                    setRepeatPatterns(old => [...old, { key: repeatPatterns.length, minute: "0" }]);
                                                                else if (repeat === "daily")
                                                                    setRepeatPatterns(old => [...old, { key: repeatPatterns.length, time: "00:00" }]);
                                                                else if (repeat === "weekly")
                                                                    setRepeatPatterns(old => [...old, { key: repeatPatterns.length, day: "mon", time: "00:00" }]);
                                                                else if (repeat === "monthly")
                                                                    setRepeatPatterns(old => [...old, { key: repeatPatterns.length, day: 1, time: "00:00" }]);
                                                                else if (repeat === "yearly")
                                                                    setRepeatPatterns(old => [...old, { key: repeatPatterns.length, date: undefined, time: "00:00" }]);
                                                            }} />
                                                </InputGroup>
                                            </FlexItem>}
                                        </Flex>
                                        <FormHelper helperTextInvalid={helperTextInvalid} />
                                    </FormGroup>
                                );
                            })}
                        </>}
                    </FormGroup>
                </Form>
            </ModalBody>
            <ModalFooter>
                <Button variant='primary'
                        id="timer-save-button"
                        isLoading={inProgress}
                        isDisabled={inProgress}
                        onClick={onSubmit}>
                    {_("Save")}
                </Button>
                <Button id="timer-dialog-close-button" variant='link' onClick={Dialogs.close}>
                    {_("Cancel")}
                </Button>
            </ModalFooter>
        </Modal>
    );
};