diff --git a/src/handler.ts b/src/handler.ts index c852fab8..590bd483 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -363,6 +363,9 @@ export namespace Scheduler { compute_type?: string; schedule?: string; timezone?: string; + maxRetryAttempts: number; + maxRunTime: number; + maxWaitTime: number; } export interface IUpdateJobDefinition { @@ -389,6 +392,9 @@ export namespace Scheduler { create_time: number; update_time: number; active: boolean; + maxRetryAttempts: number; + maxRunTime: number; + maxWaitTime: number; } export interface IEmailNotifications { @@ -415,6 +421,9 @@ export namespace Scheduler { output_filename_template?: string; output_formats?: string[]; compute_type?: string; + maxRetryAttempts: number; + maxRunTime: number; + maxWaitTime: number; } export interface ICreateJobFromDefinition { diff --git a/src/mainviews/create-job.tsx b/src/mainviews/create-job.tsx index 9b2ea81c..efa891b4 100644 --- a/src/mainviews/create-job.tsx +++ b/src/mainviews/create-job.tsx @@ -20,7 +20,7 @@ import { Scheduler, SchedulerService } from '../handler'; import { useTranslator } from '../hooks'; import { ICreateJobModel, IJobParameter, JobsView } from '../model'; import { Scheduler as SchedulerTokens } from '../tokens'; -import { NameError } from '../util/job-name-validation'; +import { NameError, MaxRetryAttemptsError, MaxRunTimeError, MaxWaitTimeError } from '../util/job-name-validation'; import { caretDownIcon } from '@jupyterlab/ui-components'; @@ -191,6 +191,26 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { } }; + const handleNumericInputChange = (event: ChangeEvent) => { + const target = event.target; + + const parameterNameIdx = parameterNameMatch(target.name); + const parameterValueIdx = parameterValueMatch(target.name); + const newParams = props.model.parameters || []; + + if (parameterNameIdx !== null) { + newParams[parameterNameIdx].name = target.value; + props.handleModelChange({ ...props.model, parameters: newParams }); + } else if (parameterValueIdx !== null) { + newParams[parameterValueIdx].value = +target.value; + props.handleModelChange({ ...props.model, parameters: newParams }); + } else { + const value = parseInt(target.value); + const name = target.name; + props.handleModelChange({ ...props.model, [name]: isNaN(value)? target.value: value }); + } + }; + const handleSelectChange = (event: SelectChangeEvent) => { const target = event.target; @@ -288,11 +308,11 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { if (jobParameters[name] !== undefined) { console.error( 'Parameter ' + - name + - ' already set to ' + - jobParameters[name] + - ' and is about to be set again to ' + - value + name + + ' already set to ' + + jobParameters[name] + + ' and is about to be set again to ' + + value ); } else { jobParameters[name] = value; @@ -319,7 +339,10 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { compute_type: props.model.computeType, idempotency_token: props.model.idempotencyToken, tags: props.model.tags, - runtime_environment_parameters: props.model.runtimeEnvironmentParameters + runtime_environment_parameters: props.model.runtimeEnvironmentParameters, + maxRetryAttempts: props.model.maxRetryAttempts, + maxRunTime: props.model.maxRunTime, + maxWaitTime: props.model.maxWaitTime, }; if (props.model.parameters !== undefined) { @@ -364,7 +387,10 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { tags: props.model.tags, runtime_environment_parameters: props.model.runtimeEnvironmentParameters, schedule: props.model.schedule, - timezone: props.model.timezone + timezone: props.model.timezone, + maxRetryAttempts: props.model.maxRetryAttempts, + maxRunTime: props.model.maxRunTime, + maxWaitTime: props.model.maxWaitTime, }; if (props.model.parameters !== undefined) { @@ -495,6 +521,7 @@ export function CreateJob(props: ICreateJobProps): JSX.Element { environmentList={environmentList} value={props.model.environment} /> + +
+ { + // Validate name + setErrors({ + ...errors, + maxRetryAttempts: MaxRetryAttemptsError(e.target.value, trans) + }); + handleNumericInputChange(e as ChangeEvent); + }} + error={!!errors['maxRetryAttempts']} + helperText={errors['maxRetryAttempts'] ?? ''} + value={props.model.maxRetryAttempts} + id={`${formPrefix}maxRetryAttempts`} + name="maxRetryAttempts" + /> +
+
+ { + // Validate name + setErrors({ + ...errors, + maxRunTime: MaxRunTimeError(e.target.value, trans) + }); + handleNumericInputChange(e as ChangeEvent); + }} + error={!!errors['maxRunTime']} + helperText={errors['maxRunTime'] ?? ''} + value={props.model.maxRunTime} + id={`${formPrefix}maxRunTime`} + name="maxRunTime" + /> +
+
+ { + // Validate name + setErrors({ + ...errors, + maxWaitTime: MaxWaitTimeError(e.target.value, trans) + }); + handleNumericInputChange(e as ChangeEvent); + }} + error={!!errors['maxWaitTime']} + helperText={errors['maxWaitTime'] ?? ''} + value={props.model.maxWaitTime} + id={`${formPrefix}maxWaitTime`} + name="maxWaitTime" + /> +
30) { + return trans.__('Max retry attempts must be between 1 and 30'); + } + + return ''; +} + +export function MaxRunTimeError(maxRunTime: string, trans: TranslationBundle): string { + // Check for blank + if (maxRunTime === '') { + return trans.__('You must specify max run time'); + } + + const integerValue = parseInt(maxRunTime) + if (isNaN(integerValue)) { + return trans.__( + 'Max run time must be an integer' + ); + } + + // Check for length. + if (integerValue < 1) { + return trans.__('Max run time must be greater than 1'); + } + + return ''; +} + +export function MaxWaitTimeError(maxWaitTime: string, trans: TranslationBundle): string { + // Check for blank + if (maxWaitTime === '') { + return trans.__('You must specify max run time'); + } + const integerValue = parseInt(maxWaitTime) + if (isNaN(integerValue)) { + return trans.__( + 'Max wait time must be an integer' + ); + } + + // Check for length. + if (integerValue < 1) { + return trans.__('Max wait time must be greater than 1'); + } + + return ''; +}