Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easy create: Create job schedule by minute, hour, day, weekday, week, or month #111

Merged
merged 23 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 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,7 +20,11 @@ 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) => void;
handleScheduleTimeChange: (event: ChangeEvent) => void;
handleScheduleMinuteChange: (event: ChangeEvent) => void;
handleCreateTypeChange: (
event: React.ChangeEvent<HTMLInputElement>,
value: string
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
238 changes: 204 additions & 34 deletions src/components/schedule-inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import React, { ChangeEvent } from 'react';
import cronstrue from 'cronstrue';
import tzdata from 'tzdata';

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

import { useTranslator } from '../hooks';
import { ICreateJobModel } from '../model';
Expand All @@ -15,14 +24,26 @@ export type ScheduleInputsProps = {
idPrefix: string;
model: ICreateJobModel;
handleModelChange: (model: ICreateJobModel) => void;
schedule?: string;
handleScheduleIntervalChange: (event: SelectChangeEvent<string>) => void;
handleScheduleWeekDayChange: (event: SelectChangeEvent<string>) => void;
handleScheduleMonthDayChange: (event: ChangeEvent) => void;
handleScheduleTimeChange: (event: ChangeEvent) => void;
handleScheduleMinuteChange: (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');

Expand All @@ -32,8 +53,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']) {
JasonWeill marked this conversation as resolved.
Show resolved Hide resolved
cronString = cronstrue.toString(props.model.schedule);
}
} catch (e) {
// Do nothing; let the errors or nothing display instead
Expand All @@ -54,6 +75,21 @@ 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, 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 presets = [
{
label: trans.__('Every day'),
Expand All @@ -73,40 +109,174 @@ export function ScheduleInputs(props: ScheduleInputsProps): JSX.Element | null {
}
];

const everyLabelId = `${props.idPrefix}every-label`;
const everyLabelText = trans.__('Every');

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 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={everyLabelId}>{everyLabelText}</InputLabel>
<Select
labelId={everyLabelId}
label={everyLabelText}
variant="outlined"
id={`${props.idPrefix}every`}
name="every"
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' && (
<>
<Cluster gap={4}>
{presets.map(preset => presetButton(preset.label, preset.schedule))}
</Cluster>
<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