diff --git a/react/src/helper/index.tsx b/react/src/helper/index.tsx index 1b96160aa..27469ada3 100644 --- a/react/src/helper/index.tsx +++ b/react/src/helper/index.tsx @@ -3,6 +3,8 @@ import { Image } from '../components/ImageEnvironmentSelectFormItems'; import { EnvironmentImage } from '../components/ImageList'; import { useSuspendedBackendaiClient } from '../hooks'; import { SorterResult } from 'antd/es/table/interface'; +import { Duration } from 'dayjs/plugin/duration'; +import { TFunction } from 'i18next'; import _ from 'lodash'; export const newLineToBrElement = ( @@ -407,3 +409,16 @@ export function preserveDotStartCase(str: string) { // Replace the placeholder back with periods return startCased.replace(new RegExp(placeholder, 'g'), '.'); } + +export function formatDuration(duration: Duration, t: TFunction) { + // We need to `t` function to translate the time unit in React render phase + return [ + duration.weeks() ? `${duration.weeks()} ${t('time.week')}` : '', + duration.days() ? `${duration.days()} ${t('time.day')}` : '', + duration.hours() ? `${duration.hours()} ${t('time.hour')}` : '', + duration.minutes() ? `${duration.minutes()} ${t('time.min')}` : '', + duration.seconds() ? `${duration.seconds()} ${t('time.sec')}` : '', + ] + .filter(Boolean) + .join(' '); +} diff --git a/react/src/pages/SessionLauncherPage.tsx b/react/src/pages/SessionLauncherPage.tsx index 8f6102571..778dd63a9 100644 --- a/react/src/pages/SessionLauncherPage.tsx +++ b/react/src/pages/SessionLauncherPage.tsx @@ -38,6 +38,7 @@ import VFolderTableFormItem, { } from '../components/VFolderTableFormItem'; import { compareNumberWithUnits, + formatDuration, generateRandomString, getImageFullName, iSizeToSize, @@ -140,6 +141,7 @@ interface CreateSessionInfo { kernelName: string; sessionName: string; architecture: string; + batchTimeout?: string; config: SessionConfig; } @@ -149,6 +151,9 @@ interface SessionLauncherValue { enabled: boolean; scheduleDate?: string; command?: string; + timeoutEnabled?: boolean; + timeout?: string; + timeoutUnit?: string; }; allocationPreset: string; envvars: EnvVarFormListValue[]; @@ -194,6 +199,7 @@ const SessionLauncherPage = () => { const supportExtendedImageInfo = baiClient?.supports('extended-image-info') ?? false; + const supportBatchTimeout = baiClient?.supports('batch-timeout') ?? false; const [isStartingSession, setIsStartingSession] = useState(false); const INITIAL_FORM_VALUES: DeepPartial = useMemo( @@ -210,6 +216,11 @@ const SessionLauncherPage = () => { enabled: false, command: undefined, scheduleDate: undefined, + ...(supportBatchTimeout && { + timeoutEnabled: false, + timeout: undefined, + timeoutUnit: 's', + }), }, envvars: [], // set default_session_environment only if set @@ -224,6 +235,7 @@ const SessionLauncherPage = () => { [ baiClient._config?.default_session_environment, currentGlobalResourceGroup, + supportBatchTimeout, ], ); const StepParam = withDefault(NumberParam, 0); @@ -445,6 +457,14 @@ const SessionLauncherPage = () => { kernelName, architecture, sessionName: sessionName, + ...(supportBatchTimeout && + values?.batch?.timeoutEnabled && + !_.isUndefined(values?.batch?.timeout) + ? { + batchTimeout: + _.toString(values.batch.timeout) + values?.batch?.timeoutUnit, + } + : undefined), config: { ...(baiClient.supports('agent-select') && !baiClient?._config?.hideAgents && @@ -533,6 +553,7 @@ const SessionLauncherPage = () => { sessionInfo.config, 30000, sessionInfo.architecture, + sessionInfo.batchTimeout, ) .then((res: { created: boolean; status: string }) => { // // When session is already created with the same name, the status code @@ -841,146 +862,306 @@ const SessionLauncherPage = () => { - prev.batch.scheduleDate !== next.batch.scheduleDate - } - > - {() => { - const scheduleDate = form.getFieldValue([ - 'batch', - 'scheduleDate', - ]); - return ( - { - const scheduleDate = form.getFieldValue([ - 'batch', - 'scheduleDate', - ]); - if (scheduleDate) { - if (dayjs(scheduleDate).isBefore(dayjs())) { - if ( - form.getFieldError([ - 'batch', - 'scheduleDate', - ]).length === 0 - ) { - form.validateFields([ - ['batch', 'scheduleDate'], - ]); - } - return undefined; - } else { - return dayjs(scheduleDate).fromNow(); - } - } else { - return undefined; + noStyle + dependencies={[['batch', 'scheduleDate']]} + > + {() => { + const scheduleDate = form.getFieldValue([ + 'batch', + 'scheduleDate', + ]); + return ( + { + const scheduleDate = form.getFieldValue([ + 'batch', + 'scheduleDate', + ]); + if (scheduleDate) { + if (dayjs(scheduleDate).isBefore(dayjs())) { + if ( + form.getFieldError([ + 'batch', + 'scheduleDate', + ]).length === 0 + ) { + form.validateFields([ + ['batch', 'scheduleDate'], + ]); } - }} - triggerKey={ - scheduleDate ? scheduleDate : 'none' + return undefined; + } else { + return dayjs(scheduleDate).fromNow(); } - /> - ); - }} - - } - > - - - { - if ( - e.target.checked && - _.isEmpty( - form.getFieldValue(['batch', 'scheduleDate']), - ) - ) { - form.setFieldValue( - ['batch', 'scheduleDate'], - dayjs().add(2, 'minutes').toISOString(), - ); - } else if (e.target.checked === false) { - form.setFieldValue( - ['batch', 'scheduleDate'], - undefined, - ); + } else { + return undefined; } - form.validateFields([['batch', 'scheduleDate']]); }} - > - {t('session.launcher.Enable')} - - - { - return ( - // @ts-ignore - prev.batch?.enabled !== next.batch?.enabled - ); - }} - > - {() => { - const disabled = - form.getFieldValue('batch')?.enabled !== true; - return ( - <> + triggerKey={scheduleDate ? scheduleDate : 'none'} + render={(time) => { + return ( { - if ( - value && - dayjs(value).isBefore(dayjs()) - ) { - return Promise.reject( - t( - 'session.launcher.StartTimeMustBeInTheFuture', - ), - ); - } - return Promise.resolve(); - }, - }, - ]} + label={t('session.launcher.SessionStartTime')} + extra={time} > - { - return value.isBefore( - dayjs().startOf('day'), - ); - }} - /> - - {/* + - - */} - - ); - }} - - + { + if ( + e.target.checked && + _.isEmpty( + form.getFieldValue([ + 'batch', + 'scheduleDate', + ]), + ) + ) { + form.setFieldValue( + ['batch', 'scheduleDate'], + dayjs() + .add(2, 'minutes') + .toISOString(), + ); + } else if ( + e.target.checked === false + ) { + form.setFieldValue( + ['batch', 'scheduleDate'], + undefined, + ); + } + form.validateFields([ + ['batch', 'scheduleDate'], + ]); + }} + > + {t('session.launcher.Enable')} + + + { + return ( + // @ts-ignore + prev.batch?.enabled !== + next.batch?.enabled + ); + }} + > + {() => { + const disabled = + form.getFieldValue('batch') + ?.enabled !== true; + return ( + <> + { + if ( + value && + dayjs(value).isBefore( + dayjs(), + ) + ) { + return Promise.reject( + t( + 'session.launcher.StartTimeMustBeInTheFuture', + ), + ); + } + return Promise.resolve(); + }, + }, + ]} + > + { + return value.isBefore( + dayjs().startOf('day'), + ); + }} + /> + + {/* + + */} + + ); + }} + + + + ); + }} + /> + ); + }} + + {supportBatchTimeout ? ( + + {() => { + const timeout = form.getFieldValue([ + 'batch', + 'timeout', + ]); + const unit = form.getFieldValue([ + 'batch', + 'timeoutUnit', + ]); + + const timeDuration = dayjs.duration( + timeout, + unit ?? 's', + ); + + const formattedDuration = formatDuration( + timeDuration, + t, + ); + + const durationText = + !_.isNull(timeout) && _.toFinite(timeout) > 0 + ? formattedDuration + : null; + return ( + + + + { + if (e.target.checked === false) { + form.setFieldValue( + ['batch', 'timeout'], + undefined, + ); + } + form.validateFields([ + ['batch', 'timeout'], + ]); + }} + > + {t('session.launcher.Enable')} + + + + {() => { + const disabled = + form.getFieldValue([ + 'batch', + 'timeoutEnabled', + ]) !== true; + return ( + <> + + +