Skip to content

Commit

Permalink
Easy create: Create job schedule by minute, hour, day, weekday, week,…
Browse files Browse the repository at this point in the history
… or month (#111)

Adds easy scheduling options with validation
  • Loading branch information
JasonWeill authored Oct 10, 2022
1 parent 776d6d5 commit e2f5628
Show file tree
Hide file tree
Showing 7 changed files with 548 additions and 91 deletions.
29 changes: 21 additions & 8 deletions src/components/create-schedule-options.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,13 +20,17 @@ export type CreateScheduleOptionsProps = {
id: string;
model: ICreateJobModel;
handleModelChange: (model: ICreateJobModel) => void;
createType: string;
handleScheduleIntervalChange: (event: SelectChangeEvent<string>) => void;
handleScheduleWeekDayChange: (event: SelectChangeEvent<string>) => void;
handleScheduleMonthDayChange: (event: ChangeEvent<HTMLInputElement>) => void;
handleScheduleTimeChange: (event: ChangeEvent<HTMLInputElement>) => void;
handleScheduleMinuteChange: (event: ChangeEvent<HTMLInputElement>) => void;
handleCreateTypeChange: (
event: React.ChangeEvent<HTMLInputElement>,
event: ChangeEvent<HTMLInputElement>,
value: string
) => void;
schedule?: string;
handleScheduleChange: (event: ChangeEvent) => void;
handleScheduleChange: (event: ChangeEvent<HTMLInputElement>) => void;
timezone?: string;
handleTimezoneChange: (newValue: string | null) => void;
errors: Scheduler.ErrorsType;
Expand All @@ -40,7 +50,7 @@ export function CreateScheduleOptions(
<RadioGroup
aria-labelledby={labelId}
name={props.name}
value={props.createType}
value={props.model.createType}
onChange={props.handleCreateTypeChange}
>
<FormControlLabel
Expand All @@ -54,14 +64,17 @@ export function CreateScheduleOptions(
label={trans.__('Run on a schedule')}
/>
</RadioGroup>
{props.createType === 'JobDefinition' && (
{props.model.createType === 'JobDefinition' && (
<ScheduleInputs
idPrefix={`${props.id}-definition-`}
schedule={props.schedule}
model={props.model}
handleModelChange={props.handleModelChange}
handleScheduleIntervalChange={props.handleScheduleIntervalChange}
handleScheduleWeekDayChange={props.handleScheduleWeekDayChange}
handleScheduleMonthDayChange={props.handleScheduleMonthDayChange}
handleScheduleTimeChange={props.handleScheduleTimeChange}
handleScheduleMinuteChange={props.handleScheduleMinuteChange}
handleScheduleChange={props.handleScheduleChange}
timezone={props.timezone}
handleTimezoneChange={props.handleTimezoneChange}
errors={props.errors}
handleErrorsChange={props.handleErrorsChange}
Expand Down
3 changes: 2 additions & 1 deletion src/components/job-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ function RefillButton(props: {
outputPath: props.job.output_prefix,
environment: props.job.runtime_environment_name,
parameters: jobParameters,
createType: 'Job'
createType: 'Job',
scheduleInterval: 'weekday'
};

// Convert the list of output formats, if any, into a list for the initial state
Expand Down
266 changes: 198 additions & 68 deletions src/components/schedule-inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,44 @@ import React, { ChangeEvent } from 'react';
import cronstrue from 'cronstrue';
import tzdata from 'tzdata';

import { Autocomplete, Button, TextField } from '@mui/material';
import {
Autocomplete,
FormControl,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
TextField
} from '@mui/material';

import { useTranslator } from '../hooks';
import { ICreateJobModel } from '../model';
import { Scheduler } from '../tokens';

import { Cluster } from './cluster';

export type ScheduleInputsProps = {
idPrefix: string;
model: ICreateJobModel;
handleModelChange: (model: ICreateJobModel) => void;
schedule?: string;
handleScheduleChange: (event: ChangeEvent) => void;
timezone?: string;
handleScheduleIntervalChange: (event: SelectChangeEvent<string>) => void;
handleScheduleWeekDayChange: (event: SelectChangeEvent<string>) => void;
handleScheduleMonthDayChange: (event: ChangeEvent<HTMLInputElement>) => void;
handleScheduleTimeChange: (event: ChangeEvent<HTMLInputElement>) => void;
handleScheduleMinuteChange: (event: ChangeEvent<HTMLInputElement>) => void;
handleScheduleChange: (event: ChangeEvent<HTMLInputElement>) => void;
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');

Expand All @@ -32,81 +50,193 @@ 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 && !props.errors['schedule']) {
cronString = cronstrue.toString(props.model.schedule);
}
} catch (e) {
// Do nothing; let the errors or nothing display instead
}

const presetButton = (label: string, schedule: string) => {
return (
<Button
onClick={e => {
props.handleModelChange({
...props.model,
schedule: schedule
});
}}
>
{label}
</Button>
);
};
// 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;

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 * *'
if (hours === 0) {
return trans.__('%1:%2 AM', hours, displayMinutes);
} else if (hours === 12) {
return trans.__('%1:%2 PM', hours, displayMinutes);
} else if (hours > 12) {
return trans.__('%1:%2 PM', hours - 12, displayMinutes);
} else {
return trans.__('%1:%2 AM', hours, displayMinutes);
}
];
};

const intervalLabelId = `${props.idPrefix}interval-label`;
const intervalLabelText = trans.__('Interval');

const dayOfWeekLabelId = `${props.idPrefix}dayofweek-label`;
const dayOfWeekText = trans.__('Day of the week');

const monthDayHelperText =
props.model.scheduleMonthDay !== undefined &&
props.model.scheduleMonthDay > 28
? trans.__(
'The job will not run in months with fewer than %1 days',
props.model.scheduleMonthDay
)
: '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 = (
<Autocomplete
id={`${props.idPrefix}timezone`}
options={timezones}
value={props.model.timezone ?? null}
onChange={(
event: React.SyntheticEvent<Element, Event>,
newValue: string | null
) => {
props.handleTimezoneChange(newValue);
}}
renderInput={(params: any) => (
<TextField
{...params}
name="timezone"
label={timezoneLabel}
variant="outlined"
/>
)}
/>
);

const timePicker = (
<TextField
label={trans.__('Time')}
value={
props.model.scheduleTimeInput ??
formatTime(
props.model.scheduleHour ?? 0,
props.model.scheduleMinute ?? 0
)
}
onChange={props.handleScheduleTimeChange}
error={!!props.errors['scheduleTime']}
helperText={props.errors['scheduleTime'] || timeHelperText}
/>
);

return (
<>
<Cluster gap={4}>
{presets.map(preset => presetButton(preset.label, preset.schedule))}
</Cluster>
<TextField
label={trans.__('Cron expression')}
variant="outlined"
onChange={props.handleScheduleChange}
value={props.schedule ?? ''}
id={`${props.idPrefix}schedule`}
name="schedule"
error={!!props.errors['schedule']}
helperText={props.errors['schedule'] || cronString}
/>
<Autocomplete
id={`${props.idPrefix}timezone`}
options={timezones}
value={props.timezone ?? null}
onChange={(
event: React.SyntheticEvent<Element, Event>,
newValue: string | null
) => {
props.handleTimezoneChange(newValue);
}}
renderInput={(params: any) => (
<FormControl>
<InputLabel id={intervalLabelId}>{intervalLabelText}</InputLabel>
<Select
labelId={intervalLabelId}
label={intervalLabelText}
variant="outlined"
id={`${props.idPrefix}interval`}
name="interval"
value={props.model.scheduleInterval}
onChange={props.handleScheduleIntervalChange}
>
<MenuItem value={'minute'}>{trans.__('Minute')}</MenuItem>
<MenuItem value={'hour'}>{trans.__('Hour')}</MenuItem>
<MenuItem value={'day'}>{trans.__('Day')}</MenuItem>
<MenuItem value={'week'}>{trans.__('Week')}</MenuItem>
<MenuItem value={'weekday'}>{trans.__('Weekday')}</MenuItem>
<MenuItem value={'month'}>{trans.__('Month')}</MenuItem>
<MenuItem value={'custom'}>{trans.__('Custom schedule')}</MenuItem>
</Select>
</FormControl>
{props.model.scheduleInterval === 'hour' && (
<>
<TextField
label={trans.__('Minutes past the hour')}
value={
props.model.scheduleMinuteInput ??
props.model.scheduleHourMinute ??
0
}
onChange={props.handleScheduleMinuteChange}
error={!!props.errors['scheduleHourMinute']}
helperText={props.errors['scheduleHourMinute'] || trans.__('0–59')}
/>
</>
)}
{props.model.scheduleInterval === 'week' && (
<>
<FormControl>
<InputLabel id={dayOfWeekLabelId}>{dayOfWeekText}</InputLabel>
<Select
labelId={dayOfWeekLabelId}
label={dayOfWeekText}
variant="outlined"
id={`${props.idPrefix}dayOfWeek`}
name="dayOfWeek"
value={props.model.scheduleWeekDay ?? '1'}
onChange={props.handleScheduleWeekDayChange}
>
<MenuItem value={'1'}>{trans.__('Monday')}</MenuItem>
<MenuItem value={'2'}>{trans.__('Tuesday')}</MenuItem>
<MenuItem value={'3'}>{trans.__('Wednesday')}</MenuItem>
<MenuItem value={'4'}>{trans.__('Thursday')}</MenuItem>
<MenuItem value={'5'}>{trans.__('Friday')}</MenuItem>
<MenuItem value={'6'}>{trans.__('Saturday')}</MenuItem>
<MenuItem value={'0'}>{trans.__('Sunday')}</MenuItem>
</Select>
</FormControl>
{timePicker}
{timezonePicker}
</>
)}
{(props.model.scheduleInterval === 'weekday' ||
props.model.scheduleInterval === 'day') && (
<>
{timePicker}
{timezonePicker}
</>
)}
{props.model.scheduleInterval === 'month' && (
<>
<TextField
label={trans.__('Day of the month')}
value={
props.model.scheduleMonthDayInput ??
props.model.scheduleMonthDay ??
''
}
onChange={props.handleScheduleMonthDayChange}
error={!!props.errors['scheduleMonthDay']}
helperText={props.errors['scheduleMonthDay'] || monthDayHelperText}
/>
{timePicker}
{timezonePicker}
</>
)}
{props.model.scheduleInterval === 'custom' && (
<>
<TextField
{...params}
name="timezone"
label={timezoneLabel}
label={trans.__('Cron expression')}
variant="outlined"
onChange={props.handleScheduleChange}
value={props.model.schedule ?? ''}
id={`${props.idPrefix}schedule`}
name="schedule"
error={!!props.errors['schedule']}
helperText={props.errors['schedule'] || cronString}
/>
)}
/>
{timezonePicker}
</>
)}
</>
);
}
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ async function activatePlugin(
jobName: fileName,
outputPath: '',
environment: '',
createType: 'Job'
createType: 'Job',
scheduleInterval: 'weekday'
};

model.createJobModel = newModel;
Expand Down
Loading

0 comments on commit e2f5628

Please sign in to comment.