From b2d87663fb0948ef196c8534b8a433c98f7e49b5 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 15:19:21 -0700 Subject: [PATCH 01/22] WIP: weekday scheduler --- src/components/create-schedule-options.tsx | 19 ++- src/components/job-row.tsx | 3 +- src/components/schedule-inputs.tsx | 130 +++++++++++++++------ src/index.tsx | 3 +- src/mainviews/create-job.tsx | 44 ++++++- src/mainviews/detail-view/job-detail.tsx | 3 +- src/model.ts | 16 ++- 7 files changed, 170 insertions(+), 48 deletions(-) diff --git a/src/components/create-schedule-options.tsx b/src/components/create-schedule-options.tsx index 891f690e..e95d64b6 100644 --- a/src/components/create-schedule-options.tsx +++ b/src/components/create-schedule-options.tsx @@ -1,6 +1,12 @@ import React, { ChangeEvent } from 'react'; -import { FormControlLabel, InputLabel, Radio, RadioGroup } from '@mui/material'; +import { + FormControlLabel, + InputLabel, + Radio, + RadioGroup, + SelectChangeEvent +} from '@mui/material'; import Stack from '@mui/system/Stack'; import { useTranslator } from '../hooks'; @@ -14,7 +20,8 @@ export type CreateScheduleOptionsProps = { id: string; model: ICreateJobModel; handleModelChange: (model: ICreateJobModel) => void; - createType: string; + handleScheduleIntervalChange: (event: SelectChangeEvent) => void; + handleScheduleTimeChange: (event: ChangeEvent) => void; handleCreateTypeChange: ( event: React.ChangeEvent, value: string @@ -40,7 +47,7 @@ export function CreateScheduleOptions( - {props.createType === 'JobDefinition' && ( + {props.model.createType === 'JobDefinition' && ( void; - schedule?: string; + handleScheduleIntervalChange: (event: SelectChangeEvent) => void; + handleScheduleTimeChange: (event: ChangeEvent) => void; handleScheduleChange: (event: ChangeEvent) => void; - timezone?: string; handleTimezoneChange: (newValue: string | null) => void; errors: Scheduler.ErrorsType; handleErrorsChange: (errors: Scheduler.ErrorsType) => void; }; +// Converts hours and minutes to hh:mm format +function formatTime(hours: number, minutes: number): string { + return ( + (hours < 10 ? '0' + hours : hours) + + ':' + + (minutes < 10 ? '0' + minutes : minutes) + ); +} + export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { const trans = useTranslator('jupyterlab'); @@ -32,8 +50,8 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { let cronString; try { - if (props.schedule !== undefined && !props.errors['schedule']) { - cronString = cronstrue.toString(props.schedule); + if (props.model.schedule !== undefined && !props.errors['schedule']) { + cronString = cronstrue.toString(props.model.schedule); } } catch (e) { // Do nothing; let the errors or nothing display instead @@ -73,40 +91,84 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { } ]; + const labelId = `${props.idPrefix}every-label`; + const labelText = trans.__('Every'); + + const timezonePicker = ( + , + newValue: string | null + ) => { + props.handleTimezoneChange(newValue); + }} + renderInput={(params: any) => ( + + )} + /> + ); + return ( <> - - {presets.map(preset => presetButton(preset.label, preset.schedule))} - - - , - newValue: string | null - ) => { - props.handleTimezoneChange(newValue); - }} - renderInput={(params: any) => ( + + {labelText} + + + {props.model.scheduleInterval === 'weekday' && ( + <> + + {timezonePicker} + + )} + {props.model.scheduleInterval === 'custom' && ( + <> + + {presets.map(preset => presetButton(preset.label, preset.schedule))} + - )} - /> + {timezonePicker} + + )} ); } diff --git a/src/index.tsx b/src/index.tsx index ea1f4964..a76fb02e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -205,7 +205,8 @@ async function activatePlugin( jobName: fileName, outputPath: '', environment: '', - createType: 'Job' + createType: 'Job', + scheduleInterval: 'custom' }; model.createJobModel = newModel; diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index 8bd4b6ed..62b18744 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -195,6 +195,45 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { // If no change in checkedness, don't do anything }; + const handleScheduleTimeChange = (event: ChangeEvent) => { + const value = (event.target as HTMLInputElement).value; + // Allow h:mm or hh:mm + const timeRegex = /^(\d\d?):(\d\d)$/; + const timeResult = timeRegex.exec(value); + + let hours, minutes; + + if (timeResult) { + setErrors({ + ...errors, + scheduleTime: '' + }); + + hours = parseInt(timeResult[1]); + minutes = parseInt(timeResult[2]); + } else { + setErrors({ + ...errors, + scheduleTime: trans.__('Time must be in hh:mm format') + }); + } + + props.handleModelChange({ + ...props.model, + scheduleTimeInput: value, + scheduleHour: hours, + scheduleMinute: minutes + }); + }; + + const handleScheduleIntervalChange = (event: SelectChangeEvent) => { + // TODO: Set the schedule (in cron format) based on the new interval + props.handleModelChange({ + ...props.model, + scheduleInterval: event.target.value + }); + }; + const handleScheduleOptionsChange = ( event: React.ChangeEvent, value: string @@ -490,11 +529,10 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { id={`${formPrefix}createType`} model={props.model} handleModelChange={props.handleModelChange} - createType={props.model.createType} + handleScheduleIntervalChange={handleScheduleIntervalChange} + handleScheduleTimeChange={handleScheduleTimeChange} handleCreateTypeChange={handleScheduleOptionsChange} - schedule={props.model.schedule} handleScheduleChange={handleScheduleChange} - timezone={props.model.timezone} handleTimezoneChange={handleTimezoneChange} errors={errors} handleErrorsChange={setErrors} diff --git a/src/mainviews/detail-view/job-detail.tsx b/src/mainviews/detail-view/job-detail.tsx index 280c1453..c7ea97e5 100644 --- a/src/mainviews/detail-view/job-detail.tsx +++ b/src/mainviews/detail-view/job-detail.tsx @@ -61,7 +61,8 @@ export function JobDetail(props: IJobDetailProps): JSX.Element { outputPath: props.model.outputPrefix ?? '', environment: props.model.environment, parameters: props.model.parameters, - createType: 'Job' + createType: 'Job', + scheduleInterval: 'custom' }; props.setCreateJobModel(initialState); diff --git a/src/model.ts b/src/model.ts index 7eeb66d9..6bf5c9d4 100644 --- a/src/model.ts +++ b/src/model.ts @@ -94,6 +94,16 @@ export interface ICreateJobModel { schedule?: string; // String for timezone in tz database format timezone?: string; + // "Easy scheduling" inputs + // Intervals: 'minute' | 'hour' | 'day' | 'week' | 'weekday' | 'month' | 'custom' + scheduleInterval: string; + // Form input values for time and minutes + scheduleTimeInput?: string; + scheduleMinuteInput?: string; + scheduleMinute?: number; + scheduleHour?: number; + scheduleMonthDay?: number; + scheduleWeekDay?: number; } export interface IListJobsModel { @@ -156,7 +166,8 @@ export function convertDescribeJobtoJobDetail( createTime: dj.create_time, updateTime: dj.update_time, startTime: dj.start_time, - endTime: dj.end_time + endTime: dj.end_time, + scheduleInterval: 'custom' }; } @@ -284,7 +295,8 @@ namespace Private { inputFile: '', outputPath: '', environment: '', - createType: 'Job' + createType: 'Job', + scheduleInterval: 'custom' }; } } From d53b8508f2d67fffe2fef0f7a5a9d5a75281100e Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 15:34:59 -0700 Subject: [PATCH 02/22] Persist time change in schedule --- src/mainviews/create-job.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index 62b18744..48640b9b 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -202,6 +202,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const timeResult = timeRegex.exec(value); let hours, minutes; + let schedule = props.model.schedule; if (timeResult) { setErrors({ @@ -211,6 +212,13 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { hours = parseInt(timeResult[1]); minutes = parseInt(timeResult[2]); + + // Compose a new schedule in cron format + switch (props.model.scheduleInterval) { + case 'weekday': + schedule = `${minutes} ${hours} * * MON-FRI`; + break; + } } else { setErrors({ ...errors, @@ -220,6 +228,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { props.handleModelChange({ ...props.model, + schedule: schedule, scheduleTimeInput: value, scheduleHour: hours, scheduleMinute: minutes From fae182c7924c78b498e4f35675e7a2176d66860b Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 15:38:20 -0700 Subject: [PATCH 03/22] Default schedule: Weekdays --- src/components/job-row.tsx | 2 +- src/index.tsx | 2 +- src/mainviews/detail-view/job-detail.tsx | 2 +- src/model.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/job-row.tsx b/src/components/job-row.tsx index f22103f0..f8622be1 100644 --- a/src/components/job-row.tsx +++ b/src/components/job-row.tsx @@ -93,7 +93,7 @@ function RefillButton(props: { environment: props.job.runtime_environment_name, parameters: jobParameters, createType: 'Job', - scheduleInterval: 'custom' + scheduleInterval: 'weekday' }; // Convert the list of output formats, if any, into a list for the initial state diff --git a/src/index.tsx b/src/index.tsx index a76fb02e..7670d4e6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -206,7 +206,7 @@ async function activatePlugin( outputPath: '', environment: '', createType: 'Job', - scheduleInterval: 'custom' + scheduleInterval: 'weekday' }; model.createJobModel = newModel; diff --git a/src/mainviews/detail-view/job-detail.tsx b/src/mainviews/detail-view/job-detail.tsx index c7ea97e5..3a67dd21 100644 --- a/src/mainviews/detail-view/job-detail.tsx +++ b/src/mainviews/detail-view/job-detail.tsx @@ -62,7 +62,7 @@ export function JobDetail(props: IJobDetailProps): JSX.Element { environment: props.model.environment, parameters: props.model.parameters, createType: 'Job', - scheduleInterval: 'custom' + scheduleInterval: 'weekday' }; props.setCreateJobModel(initialState); diff --git a/src/model.ts b/src/model.ts index 6bf5c9d4..a9073a1d 100644 --- a/src/model.ts +++ b/src/model.ts @@ -167,7 +167,7 @@ export function convertDescribeJobtoJobDetail( updateTime: dj.update_time, startTime: dj.start_time, endTime: dj.end_time, - scheduleInterval: 'custom' + scheduleInterval: 'weekday' }; } @@ -296,7 +296,7 @@ namespace Private { outputPath: '', environment: '', createType: 'Job', - scheduleInterval: 'custom' + scheduleInterval: 'weekday' }; } } From 0dee96ea4248eb8f7f2079b9d65138c0c6b0e288 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 15:47:06 -0700 Subject: [PATCH 04/22] Fix custom schedule interval --- src/model.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/model.ts b/src/model.ts index a9073a1d..284df6f4 100644 --- a/src/model.ts +++ b/src/model.ts @@ -202,7 +202,8 @@ export function convertDescribeDefinitiontoDefinition( createTime: dj.create_time, updateTime: dj.update_time, schedule: dj.schedule, - timezone: dj.timezone + timezone: dj.timezone, + scheduleInterval: 'custom' }; } From 8cc819973a612abaf84fe7e88a076df4d6bb7a77 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 15:57:53 -0700 Subject: [PATCH 05/22] Update schedule on interval change --- src/mainviews/create-job.tsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index 48640b9b..12b4761b 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -236,9 +236,31 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { }; const handleScheduleIntervalChange = (event: SelectChangeEvent) => { - // TODO: Set the schedule (in cron format) based on the new interval + // Set the schedule (in cron format) based on the new interval + let schedule = props.model.schedule; + + switch (props.model.scheduleInterval) { + case 'minute': + schedule = '* * * * *'; // every minute + break; + case 'hour': + schedule = `${props.model.scheduleMinute ?? '*'} * * * *`; + break; + case 'day': + schedule = `${props.model.scheduleMinute ?? '*'} ${ + props.model.scheduleHour ?? '*' + } * * *`; + break; + case 'weekday': + schedule = `${props.model.scheduleMinute ?? '*'} ${ + props.model.scheduleHour ?? '*' + } * * MON-FRI`; + break; + } + props.handleModelChange({ ...props.model, + schedule: schedule, scheduleInterval: event.target.value }); }; From 76c7ef0bb469a737a1ed5ba1d7ad8ea16f06d7f7 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 16:08:44 -0700 Subject: [PATCH 06/22] Adds minute and day to dropdown; fix serialization --- src/components/schedule-inputs.tsx | 5 ++++- src/mainviews/create-job.tsx | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index ce4108d4..d99b5683 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -129,11 +129,14 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { value={props.model.scheduleInterval} onChange={props.handleScheduleIntervalChange} > + {trans.__('Minute')} + {trans.__('Day')} {trans.__('Weekday')} {trans.__('Custom schedule')} - {props.model.scheduleInterval === 'weekday' && ( + {(props.model.scheduleInterval === 'weekday' || + props.model.scheduleInterval === 'day') && ( <> Date: Fri, 7 Oct 2022 16:27:24 -0700 Subject: [PATCH 07/22] Strengthen alidation, minute change --- src/components/create-schedule-options.tsx | 2 + src/components/schedule-inputs.tsx | 15 +++++ src/mainviews/create-job.tsx | 67 ++++++++++++++++++++-- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/components/create-schedule-options.tsx b/src/components/create-schedule-options.tsx index e95d64b6..3a19d539 100644 --- a/src/components/create-schedule-options.tsx +++ b/src/components/create-schedule-options.tsx @@ -22,6 +22,7 @@ export type CreateScheduleOptionsProps = { handleModelChange: (model: ICreateJobModel) => void; handleScheduleIntervalChange: (event: SelectChangeEvent) => void; handleScheduleTimeChange: (event: ChangeEvent) => void; + handleScheduleMinuteChange: (event: ChangeEvent) => void; handleCreateTypeChange: ( event: React.ChangeEvent, value: string @@ -68,6 +69,7 @@ export function CreateScheduleOptions( handleModelChange={props.handleModelChange} handleScheduleIntervalChange={props.handleScheduleIntervalChange} handleScheduleTimeChange={props.handleScheduleTimeChange} + handleScheduleMinuteChange={props.handleScheduleMinuteChange} handleScheduleChange={props.handleScheduleChange} handleTimezoneChange={props.handleTimezoneChange} errors={props.errors} diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index d99b5683..c10c4dfa 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -26,6 +26,7 @@ export type ScheduleInputsProps = { handleModelChange: (model: ICreateJobModel) => void; handleScheduleIntervalChange: (event: SelectChangeEvent) => void; handleScheduleTimeChange: (event: ChangeEvent) => void; + handleScheduleMinuteChange: (event: ChangeEvent) => void; handleScheduleChange: (event: ChangeEvent) => void; handleTimezoneChange: (newValue: string | null) => void; errors: Scheduler.ErrorsType; @@ -130,11 +131,25 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { onChange={props.handleScheduleIntervalChange} > {trans.__('Minute')} + {trans.__('Hour')} {trans.__('Day')} {trans.__('Weekday')} {trans.__('Custom schedule')} + {props.model.scheduleInterval === 'hour' && ( + <> + + + )} {(props.model.scheduleInterval === 'weekday' || props.model.scheduleInterval === 'day') && ( <> diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index ae8148a3..f0a36e33 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -201,20 +201,34 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const timeRegex = /^(\d\d?):(\d\d)$/; const timeResult = timeRegex.exec(value); - let hours, minutes; + let hours = props.model.scheduleHour; + let minutes = props.model.scheduleMinute; let schedule = props.model.schedule; if (timeResult) { + hours = parseInt(timeResult[1]); + minutes = parseInt(timeResult[2]); + } + + if ( + timeResult && + hours !== undefined && + hours >= 0 && + hours <= 23 && + minutes !== undefined && + minutes >= 0 && + minutes <= 59 + ) { setErrors({ ...errors, scheduleTime: '' }); - hours = parseInt(timeResult[1]); - minutes = parseInt(timeResult[2]); - // Compose a new schedule in cron format switch (props.model.scheduleInterval) { + case 'day': + schedule = `${minutes} ${hours} * * *`; + break; case 'weekday': schedule = `${minutes} ${hours} * * MON-FRI`; break; @@ -235,6 +249,50 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { }); }; + const handleScheduleMinuteChange = (event: ChangeEvent) => { + const value = (event.target as HTMLInputElement).value; + const minuteRegex = /^(\d\d?)$/; + const minuteResult = minuteRegex.exec(value); + + let minutes = props.model.scheduleMinute; + let schedule = props.model.schedule; + + if (minuteResult) { + minutes = parseInt(minuteResult[1]); + } + + if ( + minuteResult && + minutes !== undefined && + minutes >= 0 && + minutes <= 59 + ) { + setErrors({ + ...errors, + scheduleMinute: '' + }); + + // Compose a new schedule in cron format + switch (props.model.scheduleInterval) { + case 'hour': + schedule = `${minutes} * * * *`; + break; + } + } else { + setErrors({ + ...errors, + scheduleMinute: trans.__('Minute must be between 0 and 59') + }); + } + + props.handleModelChange({ + ...props.model, + schedule: schedule, + scheduleMinuteInput: value, + scheduleMinute: minutes + }); + }; + const handleScheduleIntervalChange = (event: SelectChangeEvent) => { // Set the schedule (in cron format) based on the new interval let schedule = props.model.schedule; @@ -562,6 +620,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { handleModelChange={props.handleModelChange} handleScheduleIntervalChange={handleScheduleIntervalChange} handleScheduleTimeChange={handleScheduleTimeChange} + handleScheduleMinuteChange={handleScheduleMinuteChange} handleCreateTypeChange={handleScheduleOptionsChange} handleScheduleChange={handleScheduleChange} handleTimezoneChange={handleTimezoneChange} From 39a39dad1de81a4fd6e8e45fb848eae97640f517 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 16:57:12 -0700 Subject: [PATCH 08/22] Adds day-of-week picker --- src/components/create-schedule-options.tsx | 2 + src/components/schedule-inputs.tsx | 54 ++++++++++++++++++++-- src/mainviews/create-job.tsx | 23 +++++++++ src/model.ts | 2 +- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/components/create-schedule-options.tsx b/src/components/create-schedule-options.tsx index 3a19d539..318fed3a 100644 --- a/src/components/create-schedule-options.tsx +++ b/src/components/create-schedule-options.tsx @@ -21,6 +21,7 @@ export type CreateScheduleOptionsProps = { model: ICreateJobModel; handleModelChange: (model: ICreateJobModel) => void; handleScheduleIntervalChange: (event: SelectChangeEvent) => void; + handleScheduleWeekDayChange: (event: SelectChangeEvent) => void; handleScheduleTimeChange: (event: ChangeEvent) => void; handleScheduleMinuteChange: (event: ChangeEvent) => void; handleCreateTypeChange: ( @@ -68,6 +69,7 @@ export function CreateScheduleOptions( model={props.model} handleModelChange={props.handleModelChange} handleScheduleIntervalChange={props.handleScheduleIntervalChange} + handleScheduleWeekDayChange={props.handleScheduleWeekDayChange} handleScheduleTimeChange={props.handleScheduleTimeChange} handleScheduleMinuteChange={props.handleScheduleMinuteChange} handleScheduleChange={props.handleScheduleChange} diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index c10c4dfa..70f4e539 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -25,6 +25,7 @@ export type ScheduleInputsProps = { model: ICreateJobModel; handleModelChange: (model: ICreateJobModel) => void; handleScheduleIntervalChange: (event: SelectChangeEvent) => void; + handleScheduleWeekDayChange: (event: SelectChangeEvent) => void; handleScheduleTimeChange: (event: ChangeEvent) => void; handleScheduleMinuteChange: (event: ChangeEvent) => void; handleScheduleChange: (event: ChangeEvent) => void; @@ -92,8 +93,11 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { } ]; - const labelId = `${props.idPrefix}every-label`; - const labelText = trans.__('Every'); + const everyLabelId = `${props.idPrefix}every-label`; + const everyLabelText = trans.__('Every'); + + const dayOfWeekLabelId = `${props.idPrefix}dayofweek-label`; + const dayOfWeekText = trans.__('Day of the week'); const timezonePicker = ( - {labelText} + {everyLabelText} @@ -150,6 +156,44 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { /> )} + {props.model.scheduleInterval === 'week' && ( + <> + + {dayOfWeekText} + + + + {timezonePicker} + + )} {(props.model.scheduleInterval === 'weekday' || props.model.scheduleInterval === 'day') && ( <> diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index f0a36e33..0219a8db 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -293,6 +293,23 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { }); }; + const handleScheduleWeekDayChange = (event: SelectChangeEvent) => { + // Days of the week are numbered 0 (Sunday) through 6 (Saturday) + const value = (event.target as HTMLSelectElement).value; + + let schedule = props.model.schedule; + + schedule = `${props.model.scheduleMinute ?? '0'} ${ + props.model.scheduleHour ?? '0' + } * * ${value}`; + + props.handleModelChange({ + ...props.model, + schedule: schedule, + scheduleWeekDay: value + }); + }; + const handleScheduleIntervalChange = (event: SelectChangeEvent) => { // Set the schedule (in cron format) based on the new interval let schedule = props.model.schedule; @@ -309,6 +326,11 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { props.model.scheduleHour ?? '0' } * * *`; break; + case 'week': + schedule = `${props.model.scheduleMinute ?? '0'} ${ + props.model.scheduleHour ?? '0' + } * * ${props.model.scheduleWeekDay ?? '1'}`; + break; case 'weekday': schedule = `${props.model.scheduleMinute ?? '0'} ${ props.model.scheduleHour ?? '0' @@ -619,6 +641,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { model={props.model} handleModelChange={props.handleModelChange} handleScheduleIntervalChange={handleScheduleIntervalChange} + handleScheduleWeekDayChange={handleScheduleWeekDayChange} handleScheduleTimeChange={handleScheduleTimeChange} handleScheduleMinuteChange={handleScheduleMinuteChange} handleCreateTypeChange={handleScheduleOptionsChange} diff --git a/src/model.ts b/src/model.ts index 284df6f4..47002386 100644 --- a/src/model.ts +++ b/src/model.ts @@ -103,7 +103,7 @@ export interface ICreateJobModel { scheduleMinute?: number; scheduleHour?: number; scheduleMonthDay?: number; - scheduleWeekDay?: number; + scheduleWeekDay?: string; } export interface IListJobsModel { From 8558e84a28ca6c2797796cd0324f27457df81274 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 17:20:07 -0700 Subject: [PATCH 09/22] Adds month picker --- src/components/create-schedule-options.tsx | 2 + src/components/schedule-inputs.tsx | 41 +++++++++++++++- src/mainviews/create-job.tsx | 57 +++++++++++++++++++++- src/model.ts | 1 + 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/components/create-schedule-options.tsx b/src/components/create-schedule-options.tsx index 318fed3a..445898cc 100644 --- a/src/components/create-schedule-options.tsx +++ b/src/components/create-schedule-options.tsx @@ -22,6 +22,7 @@ export type CreateScheduleOptionsProps = { handleModelChange: (model: ICreateJobModel) => void; handleScheduleIntervalChange: (event: SelectChangeEvent) => void; handleScheduleWeekDayChange: (event: SelectChangeEvent) => void; + handleScheduleMonthDayChange: (event: ChangeEvent) => void; handleScheduleTimeChange: (event: ChangeEvent) => void; handleScheduleMinuteChange: (event: ChangeEvent) => void; handleCreateTypeChange: ( @@ -70,6 +71,7 @@ export function CreateScheduleOptions( handleModelChange={props.handleModelChange} handleScheduleIntervalChange={props.handleScheduleIntervalChange} handleScheduleWeekDayChange={props.handleScheduleWeekDayChange} + handleScheduleMonthDayChange={props.handleScheduleMonthDayChange} handleScheduleTimeChange={props.handleScheduleTimeChange} handleScheduleMinuteChange={props.handleScheduleMinuteChange} handleScheduleChange={props.handleScheduleChange} diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index 70f4e539..f5648cd2 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -26,6 +26,7 @@ export type ScheduleInputsProps = { handleModelChange: (model: ICreateJobModel) => void; handleScheduleIntervalChange: (event: SelectChangeEvent) => void; handleScheduleWeekDayChange: (event: SelectChangeEvent) => void; + handleScheduleMonthDayChange: (event: ChangeEvent) => void; handleScheduleTimeChange: (event: ChangeEvent) => void; handleScheduleMinuteChange: (event: ChangeEvent) => void; handleScheduleChange: (event: ChangeEvent) => void; @@ -99,6 +100,15 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { const dayOfWeekLabelId = `${props.idPrefix}dayofweek-label`; const dayOfWeekText = trans.__('Day of the week'); + const monthDayHelperText = + props.model.scheduleMonthDay !== undefined && + props.model.scheduleMonthDay > 28 + ? trans.__( + 'Will not execute in months with fewer than %1 days', + props.model.scheduleMonthDay + ) + : '1–31'; + const timezonePicker = ( {trans.__('Monday')} @@ -213,6 +223,35 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { {timezonePicker} )} + {props.model.scheduleInterval === 'month' && ( + <> + + + {timezonePicker} + + )} {props.model.scheduleInterval === 'custom' && ( <> diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index 0219a8db..e3f923ea 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -293,6 +293,52 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { }); }; + const handleScheduleMonthDayChange = (event: ChangeEvent) => { + const value = (event.target as HTMLInputElement).value; + const monthDayRegex = /^(\d\d?)$/; + const monthDayResult = monthDayRegex.exec(value); + + let monthDay = props.model.scheduleMonthDay; + let schedule = props.model.schedule; + + if (monthDayResult) { + monthDay = parseInt(monthDayResult[1]); + } + + if ( + monthDayResult && + monthDay !== undefined && + monthDay >= 1 && + monthDay <= 31 + ) { + setErrors({ + ...errors, + scheduleMonthDay: '' + }); + + // Compose a new schedule in cron format + switch (props.model.scheduleInterval) { + case 'month': + schedule = `${props.model.scheduleMinute ?? 0} ${ + props.model.scheduleHour ?? 0 + } ${monthDay} * *`; + break; + } + } else { + setErrors({ + ...errors, + scheduleMonthDay: trans.__('Day of the month must be between 1 and 31') + }); + } + + props.handleModelChange({ + ...props.model, + schedule: schedule, + scheduleMonthDayInput: value, + scheduleMonthDay: monthDay + }); + }; + const handleScheduleWeekDayChange = (event: SelectChangeEvent) => { // Days of the week are numbered 0 (Sunday) through 6 (Saturday) const value = (event.target as HTMLSelectElement).value; @@ -313,6 +359,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const handleScheduleIntervalChange = (event: SelectChangeEvent) => { // Set the schedule (in cron format) based on the new interval let schedule = props.model.schedule; + let dayOfWeek = props.model.scheduleWeekDay; switch (props.model.scheduleInterval) { case 'minute': @@ -330,18 +377,25 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { schedule = `${props.model.scheduleMinute ?? '0'} ${ props.model.scheduleHour ?? '0' } * * ${props.model.scheduleWeekDay ?? '1'}`; + dayOfWeek ??= '1'; // Default to Monday break; case 'weekday': schedule = `${props.model.scheduleMinute ?? '0'} ${ props.model.scheduleHour ?? '0' } * * MON-FRI`; break; + case 'month': + schedule = `${props.model.scheduleMinute ?? '0'} ${ + props.model.scheduleHour ?? '0' + } ${props.model.scheduleMonthDay ?? '1'} * *`; + break; } props.handleModelChange({ ...props.model, schedule: schedule, - scheduleInterval: event.target.value + scheduleInterval: event.target.value, + scheduleWeekDay: dayOfWeek }); }; @@ -641,6 +695,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { model={props.model} handleModelChange={props.handleModelChange} handleScheduleIntervalChange={handleScheduleIntervalChange} + handleScheduleMonthDayChange={handleScheduleMonthDayChange} handleScheduleWeekDayChange={handleScheduleWeekDayChange} handleScheduleTimeChange={handleScheduleTimeChange} handleScheduleMinuteChange={handleScheduleMinuteChange} diff --git a/src/model.ts b/src/model.ts index 47002386..7a95e053 100644 --- a/src/model.ts +++ b/src/model.ts @@ -102,6 +102,7 @@ export interface ICreateJobModel { scheduleMinuteInput?: string; scheduleMinute?: number; scheduleHour?: number; + scheduleMonthDayInput?: string; scheduleMonthDay?: number; scheduleWeekDay?: string; } From b01edbd042e9f4006e0a0b8780259a3474e5ff48 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 17:33:04 -0700 Subject: [PATCH 10/22] Time helper text for 24-hour format --- src/components/schedule-inputs.tsx | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index f5648cd2..2697254f 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -44,6 +44,17 @@ function formatTime(hours: number, minutes: number): string { ); } +// Converts 24-hour hh:mm format to 12-hour hh:mm AM/PM format +function twentyFourToTwelveHourTime(hours: number, minutes: number): string { + if (hours === 12) { + return `${hours}:${minutes} PM`; + } else if (hours > 12) { + return `${hours - 12}:${minutes} PM`; + } else { + return `${hours}:${minutes} AM`; + } +} + export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { const trans = useTranslator('jupyterlab'); @@ -109,6 +120,16 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { ) : '1–31'; + const timeHelperText = + !props.errors['scheduleTime'] && + props.model.scheduleHour !== undefined && + props.model.scheduleMinute !== undefined + ? twentyFourToTwelveHourTime( + props.model.scheduleHour, + props.model.scheduleMinute + ) + : trans.__('00:00–23:59'); + const timezonePicker = ( {timezonePicker} @@ -218,7 +239,7 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { } onChange={props.handleScheduleTimeChange} error={!!props.errors['scheduleTime']} - helperText={props.errors['scheduleTime'] || trans.__('00:00–23:59')} + helperText={props.errors['scheduleTime'] || timeHelperText} /> {timezonePicker} @@ -247,7 +268,7 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { } onChange={props.handleScheduleTimeChange} error={!!props.errors['scheduleTime']} - helperText={props.errors['scheduleTime'] || trans.__('00:00–23:59')} + helperText={props.errors['scheduleTime'] || timeHelperText} /> {timezonePicker} From 12989fcd9b1f0b6cbb692581075b5cf8a5b14f6f Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Fri, 7 Oct 2022 17:34:47 -0700 Subject: [PATCH 11/22] Refactor timePicker --- src/components/schedule-inputs.tsx | 58 ++++++++++-------------------- 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index 2697254f..db9f413f 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -152,6 +152,22 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { /> ); + const timePicker = ( + + ); + return ( <> @@ -209,38 +225,14 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { {trans.__('Sunday')} - + {timePicker} {timezonePicker} )} {(props.model.scheduleInterval === 'weekday' || props.model.scheduleInterval === 'day') && ( <> - + {timePicker} {timezonePicker} )} @@ -257,19 +249,7 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { error={!!props.errors['scheduleMonthDay']} helperText={props.errors['scheduleMonthDay'] || monthDayHelperText} /> - + {timePicker} {timezonePicker} )} From 44522d0897b073cb33e5377eab37408460b5f362 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Sat, 8 Oct 2022 07:20:17 -0700 Subject: [PATCH 12/22] Adds zero hours --- src/components/schedule-inputs.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index db9f413f..97817fe8 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -44,17 +44,6 @@ function formatTime(hours: number, minutes: number): string { ); } -// Converts 24-hour hh:mm format to 12-hour hh:mm AM/PM format -function twentyFourToTwelveHourTime(hours: number, minutes: number): string { - if (hours === 12) { - return `${hours}:${minutes} PM`; - } else if (hours > 12) { - return `${hours - 12}:${minutes} PM`; - } else { - return `${hours}:${minutes} AM`; - } -} - export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { const trans = useTranslator('jupyterlab'); @@ -86,6 +75,19 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { ); }; + // Converts 24-hour hh:mm format to 12-hour hh:mm AM/PM format + const twentyFourToTwelveHourTime = (hours: number, minutes: number) => { + if (hours === 0) { + return trans.__('%1:%2 AM', hours, minutes); + } else if (hours === 12) { + return trans.__('%1:%2 PM', hours, minutes); + } else if (hours > 12) { + return trans.__('%1:%2 PM', hours - 12, minutes); + } else { + return trans.__('%1:%2 AM', hours, minutes); + } + }; + const presets = [ { label: trans.__('Every day'), From 86940601d49810dd5ca1c8e0b9c032a2a4fe9568 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Mon, 10 Oct 2022 08:25:48 -0700 Subject: [PATCH 13/22] Splits isolated minute for validation --- src/components/schedule-inputs.tsx | 8 +++++--- src/mainviews/create-job.tsx | 10 +++++----- src/model.ts | 6 ++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index 97817fe8..90262979 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -197,11 +197,13 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { )} diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index e3f923ea..eefa5c04 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -254,7 +254,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const minuteRegex = /^(\d\d?)$/; const minuteResult = minuteRegex.exec(value); - let minutes = props.model.scheduleMinute; + let minutes = props.model.scheduleHourMinute; let schedule = props.model.schedule; if (minuteResult) { @@ -269,7 +269,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { ) { setErrors({ ...errors, - scheduleMinute: '' + scheduleHourMinute: '' }); // Compose a new schedule in cron format @@ -281,7 +281,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { } else { setErrors({ ...errors, - scheduleMinute: trans.__('Minute must be between 0 and 59') + scheduleHourMinute: trans.__('Minute must be between 0 and 59') }); } @@ -289,7 +289,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { ...props.model, schedule: schedule, scheduleMinuteInput: value, - scheduleMinute: minutes + scheduleHourMinute: minutes }); }; @@ -366,7 +366,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { schedule = '* * * * *'; // every minute break; case 'hour': - schedule = `${props.model.scheduleMinute ?? '0'} * * * *`; + schedule = `${props.model.scheduleHourMinute ?? '0'} * * * *`; break; case 'day': schedule = `${props.model.scheduleMinute ?? '0'} ${ diff --git a/src/model.ts b/src/model.ts index 7a95e053..f65012c8 100644 --- a/src/model.ts +++ b/src/model.ts @@ -97,9 +97,11 @@ export interface ICreateJobModel { // "Easy scheduling" inputs // Intervals: 'minute' | 'hour' | 'day' | 'week' | 'weekday' | 'month' | 'custom' scheduleInterval: string; - // Form input values for time and minutes - scheduleTimeInput?: string; + // Minute for an input that only accepts minutes (of the hour) scheduleMinuteInput?: string; + scheduleHourMinute?: number; + // Hour and minute for time inputs + scheduleTimeInput?: string; scheduleMinute?: number; scheduleHour?: number; scheduleMonthDayInput?: string; From 464e427aefaf91c9de37d7a9caf5666a62c2ca8b Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Mon, 10 Oct 2022 09:43:51 -0700 Subject: [PATCH 14/22] Validation on "minute of hour" switch --- src/mainviews/create-job.tsx | 103 +++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 22 deletions(-) diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index eefa5c04..b87abc16 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -249,42 +249,50 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { }); }; - const handleScheduleMinuteChange = (event: ChangeEvent) => { - const value = (event.target as HTMLInputElement).value; + const validateHourMinute = (input: string) => { + const errorMessage = trans.__('Minute must be between 0 and 59'); const minuteRegex = /^(\d\d?)$/; - const minuteResult = minuteRegex.exec(value); - - let minutes = props.model.scheduleHourMinute; - let schedule = props.model.schedule; + const minuteResult = minuteRegex.exec(input); + let minutes = null; if (minuteResult) { minutes = parseInt(minuteResult[1]); } - if ( - minuteResult && - minutes !== undefined && - minutes >= 0 && - minutes <= 59 - ) { - setErrors({ - ...errors, - scheduleHourMinute: '' - }); + if (minutes === null || minutes === undefined) { + return errorMessage; + } - // Compose a new schedule in cron format + if (minutes >= 0 && minutes <= 59) { + return ''; // No error + } else { + return errorMessage; + } + }; + + const handleScheduleMinuteChange = (event: ChangeEvent) => { + const value = (event.target as HTMLInputElement).value; + + let minutes = props.model.scheduleHourMinute; + let schedule = props.model.schedule; + + const scheduleHourMinuteError = validateHourMinute(value); + + if (!scheduleHourMinuteError) { + minutes = parseInt(props.model.scheduleMinuteInput ?? ''); + // No errors; compose a new schedule in cron format switch (props.model.scheduleInterval) { case 'hour': schedule = `${minutes} * * * *`; break; } - } else { - setErrors({ - ...errors, - scheduleHourMinute: trans.__('Minute must be between 0 and 59') - }); } + setErrors({ + ...errors, + scheduleHourMinute: scheduleHourMinuteError + }); + props.handleModelChange({ ...props.model, schedule: schedule, @@ -357,10 +365,61 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { }; const handleScheduleIntervalChange = (event: SelectChangeEvent) => { + const newInterval = event.target.value; // Set the schedule (in cron format) based on the new interval let schedule = props.model.schedule; let dayOfWeek = props.model.scheduleWeekDay; + // On switch, validate only the needed fields, and remove errors for unneeded fields. + const neededFields: { [key: string]: string[] } = { + minute: [], // No inputs + hour: ['scheduleHourMinute'], + day: ['scheduleMinute', 'scheduleHour'], + week: ['scheduleMinute', 'scheduleHour', 'scheduleWeekDay'], + weekday: ['scheduleMinute', 'scheduleHour'], + month: ['scheduleMinute', 'scheduleHour', 'scheduleMonthDay'], + custom: [] + }; + + const allFields = [ + 'scheduleMinute', + 'scheduleHour', + 'scheduleHourMinute', + 'scheduleWeekDay', + 'scheduleMonthDay' + ]; + + const newErrors = errors; + for (const fieldToValidate of allFields) { + if (neededFields[newInterval].includes(fieldToValidate)) { + // Validate the field. + switch (fieldToValidate) { + case 'scheduleMinute': + break; + case 'scheduleHour': + break; + case 'scheduleHourMinute': + // Don't indicate errors if scheduleMinuteInput is undefined + // (typically on first load) + if (props.model.scheduleMinuteInput !== undefined) { + newErrors[fieldToValidate] = validateHourMinute( + props.model.scheduleMinuteInput + ); + } + break; + case 'scheduleWeekDay': + break; + case 'scheduleMonthDay': + break; + } + } else { + // Clear validation errors. + newErrors[fieldToValidate] = ''; + } + } + + setErrors(newErrors); + switch (props.model.scheduleInterval) { case 'minute': schedule = '* * * * *'; // every minute From 1f43d48f768cb18d890015a5aa95de13328ccda8 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Mon, 10 Oct 2022 10:12:54 -0700 Subject: [PATCH 15/22] Fix up displayMinutes logic --- src/components/schedule-inputs.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index 90262979..f927e861 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -77,14 +77,16 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { // Converts 24-hour hh:mm format to 12-hour hh:mm AM/PM format const twentyFourToTwelveHourTime = (hours: number, minutes: number) => { + const displayMinutes: string = minutes < 10 ? '0' + minutes : '' + minutes; + if (hours === 0) { - return trans.__('%1:%2 AM', hours, minutes); + return trans.__('%1:%2 AM', hours, displayMinutes); } else if (hours === 12) { - return trans.__('%1:%2 PM', hours, minutes); + return trans.__('%1:%2 PM', hours, displayMinutes); } else if (hours > 12) { - return trans.__('%1:%2 PM', hours - 12, minutes); + return trans.__('%1:%2 PM', hours - 12, displayMinutes); } else { - return trans.__('%1:%2 AM', hours, minutes); + return trans.__('%1:%2 AM', hours, displayMinutes); } }; From ff2c5dc86361ae2262a002db657cf0cf273c3ddb Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Mon, 10 Oct 2022 10:13:31 -0700 Subject: [PATCH 16/22] Validation for time input --- src/mainviews/create-job.tsx | 81 +++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index b87abc16..54f522a2 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -195,35 +195,59 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { // If no change in checkedness, don't do anything }; - const handleScheduleTimeChange = (event: ChangeEvent) => { - const value = (event.target as HTMLInputElement).value; + const parseTime = (input: string) => { // Allow h:mm or hh:mm const timeRegex = /^(\d\d?):(\d\d)$/; - const timeResult = timeRegex.exec(value); + const timeResult = timeRegex.exec(input); - let hours = props.model.scheduleHour; - let minutes = props.model.scheduleMinute; - let schedule = props.model.schedule; + let hours; + let minutes; if (timeResult) { hours = parseInt(timeResult[1]); minutes = parseInt(timeResult[2]); } + return [hours, minutes]; + }; + + const validateTime = (input: string) => { + const errorMessage = trans.__('Time must be in hh:mm format'); + + const [hours, minutes] = parseTime(input); + if ( - timeResult && - hours !== undefined && - hours >= 0 && - hours <= 23 && - minutes !== undefined && - minutes >= 0 && - minutes <= 59 + hours === undefined || + minutes === undefined || + hours < 0 || + hours > 23 || + minutes < 0 || + minutes > 59 ) { + return errorMessage; + } + + return ''; + }; + + const handleScheduleTimeChange = (event: ChangeEvent) => { + const value = (event.target as HTMLInputElement).value; + + let hours: number | null | undefined = props.model.scheduleHour; + let minutes: number | null | undefined = props.model.scheduleMinute; + let schedule = props.model.schedule; + + const timeError = validateTime(value); + + if (!timeError) { setErrors({ ...errors, scheduleTime: '' }); + // Parse the time (we expect that neither minutes nor hours will be undefined) + [hours, minutes] = parseTime(value); + // Compose a new schedule in cron format switch (props.model.scheduleInterval) { case 'day': @@ -233,13 +257,13 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { schedule = `${minutes} ${hours} * * MON-FRI`; break; } - } else { - setErrors({ - ...errors, - scheduleTime: trans.__('Time must be in hh:mm format') - }); } + setErrors({ + ...errors, + scheduleTime: timeError + }); + props.handleModelChange({ ...props.model, schedule: schedule, @@ -374,16 +398,15 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const neededFields: { [key: string]: string[] } = { minute: [], // No inputs hour: ['scheduleHourMinute'], - day: ['scheduleMinute', 'scheduleHour'], - week: ['scheduleMinute', 'scheduleHour', 'scheduleWeekDay'], + day: ['scheduleTime'], + week: ['scheduleTime', 'scheduleWeekDay'], weekday: ['scheduleMinute', 'scheduleHour'], - month: ['scheduleMinute', 'scheduleHour', 'scheduleMonthDay'], + month: ['scheduleTime', 'scheduleMonthDay'], custom: [] }; const allFields = [ - 'scheduleMinute', - 'scheduleHour', + 'scheduleTime', 'scheduleHourMinute', 'scheduleWeekDay', 'scheduleMonthDay' @@ -393,14 +416,16 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { for (const fieldToValidate of allFields) { if (neededFields[newInterval].includes(fieldToValidate)) { // Validate the field. + // Skip validation if value in model is undefined; this typically indicates initial load. switch (fieldToValidate) { - case 'scheduleMinute': - break; - case 'scheduleHour': + case 'scheduleTime': + if (props.model.scheduleTimeInput !== undefined) { + newErrors[fieldToValidate] = validateTime( + props.model.scheduleTimeInput + ); + } break; case 'scheduleHourMinute': - // Don't indicate errors if scheduleMinuteInput is undefined - // (typically on first load) if (props.model.scheduleMinuteInput !== undefined) { newErrors[fieldToValidate] = validateHourMinute( props.model.scheduleMinuteInput From e8ea1c25326472a35b61663bc11d2013dca2611f Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Mon, 10 Oct 2022 10:24:08 -0700 Subject: [PATCH 17/22] Validate month-day --- src/mainviews/create-job.tsx | 73 +++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index 54f522a2..2a1c4d16 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -278,20 +278,16 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const minuteRegex = /^(\d\d?)$/; const minuteResult = minuteRegex.exec(input); - let minutes = null; + let minutes; if (minuteResult) { minutes = parseInt(minuteResult[1]); } - if (minutes === null || minutes === undefined) { + if (minutes === undefined || minutes < 0 || minutes > 59) { return errorMessage; } - if (minutes >= 0 && minutes <= 59) { - return ''; // No error - } else { - return errorMessage; - } + return ''; // No error }; const handleScheduleMinuteChange = (event: ChangeEvent) => { @@ -303,7 +299,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const scheduleHourMinuteError = validateHourMinute(value); if (!scheduleHourMinuteError) { - minutes = parseInt(props.model.scheduleMinuteInput ?? ''); + minutes = parseInt(value); // No errors; compose a new schedule in cron format switch (props.model.scheduleInterval) { case 'hour': @@ -325,29 +321,34 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { }); }; - const handleScheduleMonthDayChange = (event: ChangeEvent) => { - const value = (event.target as HTMLInputElement).value; - const monthDayRegex = /^(\d\d?)$/; - const monthDayResult = monthDayRegex.exec(value); + const validateMonthDay = (input: string) => { + const errorMessage = trans.__('Day of the month must be between 1 and 31'); - let monthDay = props.model.scheduleMonthDay; - let schedule = props.model.schedule; + const monthDayRegex = /^(\d\d?)$/; + const monthDayResult = monthDayRegex.exec(input); + let monthDay; if (monthDayResult) { monthDay = parseInt(monthDayResult[1]); } - if ( - monthDayResult && - monthDay !== undefined && - monthDay >= 1 && - monthDay <= 31 - ) { - setErrors({ - ...errors, - scheduleMonthDay: '' - }); + if (monthDay === undefined || monthDay < 1 || monthDay > 31) { + return errorMessage; + } + + return ''; // No error + }; + const handleScheduleMonthDayChange = (event: ChangeEvent) => { + const value = (event.target as HTMLInputElement).value; + + let monthDay = props.model.scheduleMonthDay; + let schedule = props.model.schedule; + + const monthDayError = validateMonthDay(value); + + if (!monthDayError) { + monthDay = parseInt(value); // Compose a new schedule in cron format switch (props.model.scheduleInterval) { case 'month': @@ -356,13 +357,13 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { } ${monthDay} * *`; break; } - } else { - setErrors({ - ...errors, - scheduleMonthDay: trans.__('Day of the month must be between 1 and 31') - }); } + setErrors({ + ...errors, + scheduleMonthDay: monthDayError + }); + props.handleModelChange({ ...props.model, schedule: schedule, @@ -394,13 +395,13 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { let schedule = props.model.schedule; let dayOfWeek = props.model.scheduleWeekDay; - // On switch, validate only the needed fields, and remove errors for unneeded fields. + // On switch, validate only the needed text fields, and remove errors for unneeded fields. const neededFields: { [key: string]: string[] } = { minute: [], // No inputs hour: ['scheduleHourMinute'], day: ['scheduleTime'], - week: ['scheduleTime', 'scheduleWeekDay'], - weekday: ['scheduleMinute', 'scheduleHour'], + week: ['scheduleTime'], + weekday: ['scheduleTime'], month: ['scheduleTime', 'scheduleMonthDay'], custom: [] }; @@ -408,7 +409,6 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { const allFields = [ 'scheduleTime', 'scheduleHourMinute', - 'scheduleWeekDay', 'scheduleMonthDay' ]; @@ -432,9 +432,12 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { ); } break; - case 'scheduleWeekDay': - break; case 'scheduleMonthDay': + if (props.model.scheduleMonthDayInput !== undefined) { + newErrors[fieldToValidate] = validateMonthDay( + props.model.scheduleMonthDayInput + ); + } break; } } else { From 0055ab3a87d45835c0e76b8bc7fa1d3ad45bc9c8 Mon Sep 17 00:00:00 2001 From: Jason Weill Date: Mon, 10 Oct 2022 12:03:26 -0700 Subject: [PATCH 18/22] Removes preset buttons --- src/components/schedule-inputs.tsx | 40 ------------------------------ 1 file changed, 40 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index f927e861..8385908c 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -5,7 +5,6 @@ import tzdata from 'tzdata'; import { Autocomplete, - Button, FormControl, InputLabel, MenuItem, @@ -18,8 +17,6 @@ import { useTranslator } from '../hooks'; import { ICreateJobModel } from '../model'; import { Scheduler } from '../tokens'; -import { Cluster } from './cluster'; - export type ScheduleInputsProps = { idPrefix: string; model: ICreateJobModel; @@ -60,21 +57,6 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { // Do nothing; let the errors or nothing display instead } - const presetButton = (label: string, schedule: string) => { - return ( - - ); - }; - // Converts 24-hour hh:mm format to 12-hour hh:mm AM/PM format const twentyFourToTwelveHourTime = (hours: number, minutes: number) => { const displayMinutes: string = minutes < 10 ? '0' + minutes : '' + minutes; @@ -90,25 +72,6 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { } }; - const presets = [ - { - label: trans.__('Every day'), - schedule: '0 7 * * *' - }, - { - label: trans.__('Every 6 hours'), - schedule: '* */6 * * *' - }, - { - label: trans.__('Every weekday'), - schedule: '0 6 * * MON-FRI' - }, - { - label: trans.__('Every month'), - schedule: '0 5 1 * *' - } - ]; - const everyLabelId = `${props.idPrefix}every-label`; const everyLabelText = trans.__('Every'); @@ -261,9 +224,6 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { )} {props.model.scheduleInterval === 'custom' && ( <> - - {presets.map(preset => presetButton(preset.label, preset.schedule))} - Date: Mon, 10 Oct 2022 12:09:24 -0700 Subject: [PATCH 19/22] Every -> interval --- src/components/schedule-inputs.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/schedule-inputs.tsx b/src/components/schedule-inputs.tsx index 8385908c..16f4fd6d 100644 --- a/src/components/schedule-inputs.tsx +++ b/src/components/schedule-inputs.tsx @@ -72,8 +72,8 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { } }; - const everyLabelId = `${props.idPrefix}every-label`; - const everyLabelText = trans.__('Every'); + const intervalLabelId = `${props.idPrefix}interval-label`; + const intervalLabelText = trans.__('Interval'); const dayOfWeekLabelId = `${props.idPrefix}dayofweek-label`; const dayOfWeekText = trans.__('Day of the week'); @@ -138,13 +138,13 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null { return ( <> - {everyLabelText} + {intervalLabelText}