client/app/components/queries/ScheduleDialog.jsx (250 lines of code) (raw):

import React, { useState } from "react"; import PropTypes from "prop-types"; import Modal from "antd/lib/modal"; import DatePicker from "antd/lib/date-picker"; import TimePicker from "antd/lib/time-picker"; import Select from "antd/lib/select"; import Radio from "antd/lib/radio"; import { capitalize, clone, isEqual, omitBy, isNil, isEmpty } from "lodash"; import moment from "moment"; import { secondsToInterval, durationHumanize, pluralize, IntervalEnum, localizeTime } from "@/lib/utils"; import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper"; import { RefreshScheduleType, RefreshScheduleDefault, Moment } from "../proptypes"; import "./ScheduleDialog.css"; const WEEKDAYS_SHORT = moment.weekdaysShort(); const WEEKDAYS_FULL = moment.weekdays(); const DATE_FORMAT = "YYYY-MM-DD"; const HOUR_FORMAT = "HH:mm"; const { Option, OptGroup } = Select; export function TimeEditor(props) { const [time, setTime] = useState(props.defaultValue); const showUtc = time && !time.isUTC(); function onChange(newTime) { setTime(newTime); props.onChange(newTime); } return ( <React.Fragment> <TimePicker allowClear={false} value={time} format={HOUR_FORMAT} minuteStep={5} onChange={onChange} /> {showUtc && ( <span className="utc" data-testid="utc"> ({moment.utc(time).format(HOUR_FORMAT)} UTC) </span> )} </React.Fragment> ); } TimeEditor.propTypes = { defaultValue: Moment, onChange: PropTypes.func.isRequired, }; TimeEditor.defaultProps = { defaultValue: null, }; class ScheduleDialog extends React.Component { static propTypes = { schedule: RefreshScheduleType, refreshOptions: PropTypes.arrayOf(PropTypes.number).isRequired, dialog: DialogPropType.isRequired, }; static defaultProps = { schedule: RefreshScheduleDefault, }; state = this.getState(); getState() { const newSchedule = clone(this.props.schedule || ScheduleDialog.defaultProps.schedule); const { time, interval: seconds, day_of_week: day } = newSchedule; const { interval } = secondsToInterval(seconds); const [hour, minute] = time ? localizeTime(time).split(":") : [null, null]; return { hour, minute, seconds, interval, dayOfWeek: day ? WEEKDAYS_SHORT[WEEKDAYS_FULL.indexOf(day)] : null, newSchedule, }; } get intervals() { const ret = { [IntervalEnum.NEVER]: [], }; this.props.refreshOptions.forEach(seconds => { const { count, interval } = secondsToInterval(seconds); if (!(interval in ret)) { ret[interval] = []; } ret[interval].push([count, seconds]); }); Object.defineProperty(this, "intervals", { value: ret }); // memoize return ret; } set newSchedule(newProps) { this.setState(prevState => ({ newSchedule: Object.assign(prevState.newSchedule, newProps), })); } setTime = time => { this.newSchedule = { time: moment(time) .utc() .format(HOUR_FORMAT), }; }; setInterval = newSeconds => { const { newSchedule } = this.state; const { interval: newInterval } = secondsToInterval(newSeconds); // resets to defaults if (newInterval === IntervalEnum.NEVER) { newSchedule.until = null; } if ([IntervalEnum.NEVER, IntervalEnum.MINUTES, IntervalEnum.HOURS].indexOf(newInterval) !== -1) { newSchedule.time = null; } if (newInterval !== IntervalEnum.WEEKS) { newSchedule.day_of_week = null; } if ( (newInterval === IntervalEnum.DAYS || newInterval === IntervalEnum.WEEKS) && (!this.state.minute || !this.state.hour) ) { newSchedule.time = moment() .hour("00") .minute("15") .utc() .format(HOUR_FORMAT); } if (newInterval === IntervalEnum.WEEKS && !this.state.dayOfWeek) { newSchedule.day_of_week = WEEKDAYS_FULL[0]; } newSchedule.interval = newSeconds; const [hour, minute] = newSchedule.time ? localizeTime(newSchedule.time).split(":") : [null, null]; this.setState({ interval: newInterval, seconds: newSeconds, hour, minute, dayOfWeek: newSchedule.day_of_week ? WEEKDAYS_SHORT[WEEKDAYS_FULL.indexOf(newSchedule.day_of_week)] : null, }); this.newSchedule = newSchedule; }; setScheduleUntil = (_, date) => { this.newSchedule = { until: date }; }; setWeekday = e => { const dayOfWeek = e.target.value; this.setState({ dayOfWeek }); this.newSchedule = { day_of_week: dayOfWeek ? WEEKDAYS_FULL[WEEKDAYS_SHORT.indexOf(dayOfWeek)] : null, }; }; setUntilToggle = e => { const date = e.target.value ? moment().format(DATE_FORMAT) : null; this.setScheduleUntil(null, date); }; save() { const { newSchedule } = this.state; const hasChanged = () => { const newCompact = omitBy(newSchedule, isNil); const oldCompact = omitBy(this.props.schedule, isNil); return !isEqual(newCompact, oldCompact); }; // save if changed if (hasChanged()) { if (newSchedule.interval) { this.props.dialog.close(clone(newSchedule)); } else { this.props.dialog.close(null); } } this.props.dialog.dismiss(); } render() { const { dialog } = this.props; const { interval, minute, hour, seconds, newSchedule: { until }, } = this.state; return ( <Modal {...dialog.props} title="Refresh Schedule" className="schedule" onOk={() => this.save()}> <div className="schedule-component"> <h5>Refresh every</h5> <div data-testid="interval"> <Select className="input" value={seconds} onChange={this.setInterval} dropdownMatchSelectWidth={false}> <Option value={null} key="never"> Never </Option> {Object.keys(this.intervals) .filter(int => !isEmpty(this.intervals[int])) .map(int => ( <OptGroup label={capitalize(pluralize(int))} key={int}> {this.intervals[int].map(([cnt, secs]) => ( <Option value={secs} key={`${int}-${cnt}`}> {durationHumanize(secs)} </Option> ))} </OptGroup> ))} </Select> </div> </div> {[IntervalEnum.DAYS, IntervalEnum.WEEKS].indexOf(interval) !== -1 ? ( <div className="schedule-component"> <h5>On time</h5> <div data-testid="time"> <TimeEditor defaultValue={ hour ? moment() .hour(hour) .minute(minute) : null } onChange={this.setTime} /> </div> </div> ) : null} {IntervalEnum.WEEKS === interval ? ( <div className="schedule-component"> <h5>On day</h5> <div data-testid="weekday"> <Radio.Group size="medium" defaultValue={this.state.dayOfWeek} onChange={this.setWeekday}> {WEEKDAYS_SHORT.map(day => ( <Radio.Button value={day} key={day} className="input"> {day[0]} </Radio.Button> ))} </Radio.Group> </div> </div> ) : null} {interval !== IntervalEnum.NEVER ? ( <div className="schedule-component"> <h5>Ends</h5> <div className="ends" data-testid="ends"> <Radio.Group size="medium" value={!!until} onChange={this.setUntilToggle}> <Radio value={false}>Never</Radio> <Radio value>On</Radio> </Radio.Group> {until ? ( <DatePicker size="small" className="datepicker" value={moment(until)} allowClear={false} format={DATE_FORMAT} onChange={this.setScheduleUntil} /> ) : null} </div> </div> ) : null} </Modal> ); } } export default wrapDialog(ScheduleDialog);