From 983534ce3dd1f75cb021ae21c9401a83fedf87e0 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Thu, 29 Jan 2026 22:40:29 +0300 Subject: [PATCH 1/3] [UI] Add Spot policy configuration option to the fleet wizard #3513 --- frontend/src/locale/en.json | 4 ++- .../Fleets/Add/FleetFormFields/constants.tsx | 17 +++++++++++++ .../Fleets/Add/FleetFormFields/index.tsx | 25 +++++++++++++++++-- .../pages/Fleets/Add/FleetFormFields/type.ts | 1 + frontend/src/pages/Fleets/Add/index.tsx | 22 ++++++++++++---- frontend/src/pages/Project/Add/index.tsx | 16 +++++++++--- .../src/pages/Project/CreateWizard/index.tsx | 6 +++-- 7 files changed, 77 insertions(+), 14 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 342ce0cdd..2a13d54a8 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -630,7 +630,9 @@ "max_instances_description": "Required only if you want to set an upper limit", "max_instances_placeholder": "Optional", "idle_duration": "Idle duration", - "idle_duration_description": "Example: '0s', '1m', '1h'" + "idle_duration_description": "Example: '0s', '1m', '1h'", + "spot_policy": "Spot policy", + "spot_policy_description": "Select spot policy" } }, "volume": { diff --git a/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx b/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx index 372c4dcfa..059e0d16d 100644 --- a/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx +++ b/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx @@ -2,6 +2,14 @@ import React from 'react'; import { get } from 'lodash'; import * as yup from 'yup'; +import { FleetFormFields } from './type'; + +export const fleetFormDefaultValues: FleetFormFields = { + min_instances: 0, + idle_duration: '5m', + spot_policy: 'auto', +}; + export const FLEET_MIN_INSTANCES_INFO = { header:

Min number of instances

, body: ( @@ -78,6 +86,15 @@ export const FLEET_IDLE_DURATION_INFO = { ), }; +export const FLEET_SPOT_POLICY_INFO = { + header:

Spot policy

, + body: ( + <> +

Spot policy

+ + ), +}; + const requiredFieldError = 'This is required field'; const numberFieldError = 'This is number field'; diff --git a/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx b/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx index a19e70136..a51f2e394 100644 --- a/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx +++ b/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { FormInput, InfoLink, SpaceBetween } from 'components'; +import { FormInput, FormSelect, InfoLink, SpaceBetween } from 'components'; import { useHelpPanel } from 'hooks'; -import { FLEET_IDLE_DURATION_INFO, FLEET_MAX_INSTANCES_INFO, FLEET_MIN_INSTANCES_INFO } from './constants'; +import { + FLEET_IDLE_DURATION_INFO, + FLEET_MAX_INSTANCES_INFO, + FLEET_MIN_INSTANCES_INFO, + FLEET_SPOT_POLICY_INFO, +} from './constants'; import { FleetFormFieldsProps } from './type'; import type { FieldValues } from 'react-hook-form/dist/types/fields'; @@ -74,6 +79,22 @@ export function FleetFormFields({ name={getFieldNameWitPrefix(`idle_duration`)} disabled={disabledAllFields} /> + + openHelpPanel(FLEET_SPOT_POLICY_INFO)} />} + label={t('fleets.edit.spot_policy')} + constraintText={t('fleets.edit.spot_policy_description')} + control={control} + //eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + name={getFieldNameWitPrefix(`spot_policy`)} + disabled={disabledAllFields} + options={[ + { label: 'auto', value: 'auto' }, + { label: 'on-demand', value: 'on-demand' }, + { label: 'spot', value: 'spot' }, + ]} + /> ); } diff --git a/frontend/src/pages/Fleets/Add/FleetFormFields/type.ts b/frontend/src/pages/Fleets/Add/FleetFormFields/type.ts index 7351a60a6..835ca8373 100644 --- a/frontend/src/pages/Fleets/Add/FleetFormFields/type.ts +++ b/frontend/src/pages/Fleets/Add/FleetFormFields/type.ts @@ -12,4 +12,5 @@ export type FleetFormFields = { min_instances: number; max_instances?: number; idle_duration?: string; + spot_policy: TSpotPolicy; }; diff --git a/frontend/src/pages/Fleets/Add/index.tsx b/frontend/src/pages/Fleets/Add/index.tsx index b661d96cb..d7390fc93 100644 --- a/frontend/src/pages/Fleets/Add/index.tsx +++ b/frontend/src/pages/Fleets/Add/index.tsx @@ -15,7 +15,12 @@ import { useApplyFleetMutation } from 'services/fleet'; import { DEFAULT_FLEET_INFO } from 'pages/Project/constants'; import { useYupValidationResolver } from 'pages/Project/hooks/useYupValidationResolver'; -import { getMaxInstancesValidator, getMinInstancesValidator, idleDurationValidator } from './FleetFormFields/constants'; +import { + fleetFormDefaultValues, + getMaxInstancesValidator, + getMinInstancesValidator, + idleDurationValidator, +} from './FleetFormFields/constants'; import { FleetFormFields } from './FleetFormFields'; import { IFleetWizardForm } from './types'; @@ -33,6 +38,7 @@ const fleetValidationSchema = yup.object({ min_instances: getMinInstancesValidator('max_instances'), max_instances: getMaxInstancesValidator('min_instances'), idle_duration: idleDurationValidator, + spot_policy: yup.string().required(requiredFieldError), }); export const FleetAdd: React.FC = () => { @@ -52,9 +58,8 @@ export const FleetAdd: React.FC = () => { const formMethods = useForm({ resolver, defaultValues: { + ...fleetFormDefaultValues, project_name: paramProjectName, - min_instances: 0, - idle_duration: '5m', }, }); @@ -62,7 +67,7 @@ export const FleetAdd: React.FC = () => { const formValues = watch(); const getFormValuesForFleetApplying = (): IApplyFleetPlanRequestRequest => { - const { min_instances, max_instances, idle_duration, name } = getValues(); + const { min_instances, max_instances, idle_duration, name, spot_policy } = getValues(); return { plan: { @@ -74,6 +79,7 @@ export const FleetAdd: React.FC = () => { ...(max_instances ? { max: max_instances } : {}), }, ...(idle_duration ? { idle_duration } : {}), + spot_policy, }, profile: {}, }, @@ -185,7 +191,13 @@ export const FleetAdd: React.FC = () => { }; const getDefaultFleetSummary = () => { - const summaryFields: Array = ['name', 'min_instances', 'max_instances', 'idle_duration']; + const summaryFields: Array = [ + 'name', + 'min_instances', + 'max_instances', + 'idle_duration', + 'spot_policy', + ]; const result: string[] = []; diff --git a/frontend/src/pages/Project/Add/index.tsx b/frontend/src/pages/Project/Add/index.tsx index 23a9eb0a1..23cbc4c00 100644 --- a/frontend/src/pages/Project/Add/index.tsx +++ b/frontend/src/pages/Project/Add/index.tsx @@ -16,6 +16,7 @@ import { useCreateProjectMutation } from 'services/project'; import { FleetFormFields } from 'pages/Fleets/Add/FleetFormFields'; import { + fleetFormDefaultValues, getMaxInstancesValidator, getMinInstancesValidator, idleDurationValidator, @@ -51,6 +52,7 @@ const projectValidationSchema = yup.object({ is: true, then: idleDurationValidator, }), + spot_policy: yup.string().required(requiredFieldError), }), }); @@ -72,9 +74,8 @@ export const ProjectAdd: React.FC = () => { defaultValues: { is_public: false, fleet: { + ...fleetFormDefaultValues, enable_default: true, - min_instances: 0, - idle_duration: '5m', }, }, }); @@ -93,7 +94,7 @@ export const ProjectAdd: React.FC = () => { const getFormValuesForFleetApplying = (): IApplyFleetPlanRequestRequest => { const { - fleet: { min_instances, max_instances, idle_duration, name }, + fleet: { min_instances, max_instances, idle_duration, name, spot_policy }, } = getValues(); return { @@ -106,6 +107,7 @@ export const ProjectAdd: React.FC = () => { ...(max_instances ? { max: max_instances } : {}), }, ...(idle_duration ? { idle_duration } : {}), + spot_policy, }, profile: {}, }, @@ -231,7 +233,13 @@ export const ProjectAdd: React.FC = () => { }; const getDefaultFleetSummary = () => { - const summaryFields: Array = ['name', 'min_instances', 'max_instances', 'idle_duration']; + const summaryFields: Array = [ + 'name', + 'min_instances', + 'max_instances', + 'idle_duration', + 'spot_policy', + ]; const result: string[] = []; diff --git a/frontend/src/pages/Project/CreateWizard/index.tsx b/frontend/src/pages/Project/CreateWizard/index.tsx index 83cf3804d..ecf2d413e 100644 --- a/frontend/src/pages/Project/CreateWizard/index.tsx +++ b/frontend/src/pages/Project/CreateWizard/index.tsx @@ -30,6 +30,7 @@ import { useCreateWizardProjectMutation } from 'services/project'; import { FleetFormFields } from '../../Fleets/Add/FleetFormFields'; import { + fleetFormDefaultValues, getMaxInstancesValidator, getMinInstancesValidator, idleDurationValidator, @@ -69,6 +70,7 @@ const projectValidationSchema = yup.object({ is: true, then: idleDurationValidator, }), + spot_policy: yup.string().required(requiredFieldError), }), }); @@ -127,9 +129,8 @@ export const CreateProjectWizard: React.FC = () => { defaultValues: { project_type: 'gpu_marketplace', fleet: { + ...fleetFormDefaultValues, enable_default: true, - min_instances: 0, - idle_duration: '5m', }, }, }); @@ -319,6 +320,7 @@ export const CreateProjectWizard: React.FC = () => { 'min_instances', 'max_instances', 'idle_duration', + 'spot_policy', ]; const result: string[] = []; From 04aaa569bae40c25bca6fdcc65ed5faab23656b8 Mon Sep 17 00:00:00 2001 From: peterschmidt85 Date: Thu, 29 Jan 2026 22:18:41 +0100 Subject: [PATCH 2/3] [UI] Minor edits of UI captions --- frontend/src/locale/en.json | 4 ++-- .../pages/Fleets/Add/FleetFormFields/constants.tsx | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 2a13d54a8..e11541edb 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -625,14 +625,14 @@ "name_placeholder": "Optional", "name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.", "min_instances": "Min number of instances", - "min_instances_description": "Set it `0` to provision instances only when required", + "min_instances_description": "Set it '0' to provision instances only when required", "max_instances": "Max number of instances", "max_instances_description": "Required only if you want to set an upper limit", "max_instances_placeholder": "Optional", "idle_duration": "Idle duration", "idle_duration_description": "Example: '0s', '1m', '1h'", "spot_policy": "Spot policy", - "spot_policy_description": "Select spot policy" + "spot_policy_description": "Set it to 'auto' to allow the use of both on-demand and spot instances" } }, "volume": { diff --git a/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx b/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx index 059e0d16d..7904a1774 100644 --- a/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx +++ b/frontend/src/pages/Fleets/Add/FleetFormFields/constants.tsx @@ -90,7 +90,18 @@ export const FLEET_SPOT_POLICY_INFO = { header:

Spot policy

, body: ( <> -

Spot policy

+

+ Some backends may support spot instances, also known as preemptive instances. Such instances come at a + significantly lower price but can be interrupted by the cloud provider at any time. +

+

+ If you set spot_policy to auto, the fleet will allow the use of both types of + instances: on-demand and spot. +

+

+ Note that run configurations must specify their own spot_policy, which by default is always{' '} + on-demand. +

), }; From ed4629cdf1076bf865dbe3b4a77d81763b680de4 Mon Sep 17 00:00:00 2001 From: peterschmidt85 Date: Thu, 29 Jan 2026 22:21:28 +0100 Subject: [PATCH 3/3] [UI] Changed fields order in the fleet wizard --- .../Fleets/Add/FleetFormFields/index.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx b/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx index a51f2e394..4aa04c393 100644 --- a/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx +++ b/frontend/src/pages/Fleets/Add/FleetFormFields/index.tsx @@ -69,17 +69,6 @@ export function FleetFormFields({ type="number" /> - openHelpPanel(FLEET_IDLE_DURATION_INFO)} />} - label={t('fleets.edit.idle_duration')} - constraintText={t('fleets.edit.idle_duration_description')} - control={control} - //eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - name={getFieldNameWitPrefix(`idle_duration`)} - disabled={disabledAllFields} - /> - openHelpPanel(FLEET_SPOT_POLICY_INFO)} />} label={t('fleets.edit.spot_policy')} @@ -95,6 +84,17 @@ export function FleetFormFields({ { label: 'spot', value: 'spot' }, ]} /> + + openHelpPanel(FLEET_IDLE_DURATION_INFO)} />} + label={t('fleets.edit.idle_duration')} + constraintText={t('fleets.edit.idle_duration_description')} + control={control} + //eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + name={getFieldNameWitPrefix(`idle_duration`)} + disabled={disabledAllFields} + /> ); }