Skip to content

Commit de850ef

Browse files
Add GPU toggle, env password handling, and wizard polish
Resources step: GPU toggle (off by default) inside OfferList header with disabled prop to skip API/hide content when off. Generates resources.gpu:0 when disabled, passes backends only from user filter. Env params: respect $random-password, password input with copy-before- proceed validation, info panel, dynamic env variable name in YAML. Also: fix useFilters permanentFilters dep, add onChangeBackendFilter callback, FormToggle errorText support, configuration info panel with concept links. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5311ca0 commit de850ef

10 files changed

Lines changed: 67 additions & 33 deletions

File tree

frontend/src/components/form/Toogle/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const FormToggle = <T extends FieldValues>({
2323
onChange: onChangeProp,
2424
toggleDescription,
2525
toggleInfo,
26+
errorText: externalErrorText,
2627
...props
2728
}: FormToggleProps<T>) => {
2829
return (
@@ -40,7 +41,7 @@ export const FormToggle = <T extends FieldValues>({
4041
stretch={stretch}
4142
constraintText={constraintText}
4243
secondaryControl={secondaryControl}
43-
errorText={error?.message}
44+
errorText={error?.message || externalErrorText}
4445
>
4546
{leftContent}
4647

frontend/src/components/form/Toogle/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { FormFieldProps } from '@cloudscape-design/components/form-field';
44
import { ToggleProps } from '@cloudscape-design/components/toggle';
55

66
export type FormToggleProps<T extends FieldValues> = Omit<ToggleProps, 'value' | 'checked' | 'name'> &
7-
Omit<FormFieldProps, 'errorText'> &
7+
FormFieldProps &
88
Pick<ControllerProps<T>, 'control' | 'name' | 'rules' | 'defaultValue'> & {
99
toggleDescription?: ReactNode;
1010
leftContent?: ReactNode;

frontend/src/locale/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@
486486
"template_loading": "Loading options",
487487
"template_placeholder": "Select a project to select a template",
488488
"template_card_type": "Type",
489+
"gpu": "GPU",
490+
"gpu_description": "Enable to select a GPU offer. Disable to run without a GPU.",
489491
"offer": "Offer",
490492
"offer_description": "Select an offer for the run.",
491493
"name": "Name",

frontend/src/pages/Offers/List/hooks/useFilters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = {
241241
...params,
242242
...permanentFilters,
243243
};
244-
}, [propertyFilterQuery]);
244+
}, [propertyFilterQuery, permanentFilters]);
245245

246246
useEffect(() => {
247247
if (!projectNameIsChecked.current && projectOptions.length) {

frontend/src/pages/Offers/List/index.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,16 @@ const getRequestParams = ({
6969
type OfferListProps = Pick<CardsProps, 'variant' | 'header' | 'onSelectionChange' | 'selectedItems' | 'selectionType'> &
7070
Pick<UseFiltersArgs, 'permanentFilters' | 'defaultFilters'> & {
7171
withSearchParams?: boolean;
72+
disabled?: boolean;
7273
onChangeProjectName?: (value: string) => void;
74+
onChangeBackendFilter?: (backends: string[]) => void;
7375
};
7476

7577
export const OfferList: React.FC<OfferListProps> = ({
7678
withSearchParams,
79+
disabled,
7780
onChangeProjectName,
81+
onChangeBackendFilter,
7882
permanentFilters,
7983
defaultFilters,
8084
...props
@@ -87,7 +91,7 @@ export const OfferList: React.FC<OfferListProps> = ({
8791
// @ts-expect-error
8892
requestParams,
8993
{
90-
skip: !requestParams || !requestParams['project_name'] || !requestParams['group_by']?.length,
94+
skip: disabled || !requestParams || !requestParams['project_name'] || !requestParams['group_by']?.length,
9195
},
9296
);
9397

@@ -118,6 +122,11 @@ export const OfferList: React.FC<OfferListProps> = ({
118122
onChangeProjectName?.(filteringRequestParams.project_name ?? '');
119123
}, [filteringRequestParams.project_name]);
120124

125+
useEffect(() => {
126+
const backend = filteringRequestParams.backend;
127+
onChangeBackendFilter?.(backend ? (Array.isArray(backend) ? backend : [backend]) : []);
128+
}, [filteringRequestParams.backend]);
129+
121130
const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({
122131
clearFilter,
123132
projectNameSelected: Boolean(requestParams?.['project_name']),
@@ -208,15 +217,15 @@ export const OfferList: React.FC<OfferListProps> = ({
208217
{...collectionProps}
209218
{...props}
210219
entireCardClickable
211-
items={items}
220+
items={disabled ? [] : items}
212221
cardDefinition={{
213222
header: (gpu) => gpu.name,
214223
sections,
215224
}}
216-
loading={isLoading || isFetching}
225+
loading={!disabled && (isLoading || isFetching)}
217226
loadingText={t('common.loading')}
218227
stickyHeader={true}
219-
filter={
228+
filter={disabled ? undefined : (
220229
<div className={styles.selectFilters}>
221230
<div className={styles.propertyFilter}>
222231
<PropertyFilter
@@ -247,7 +256,7 @@ export const OfferList: React.FC<OfferListProps> = ({
247256
/>
248257
</div>
249258
</div>
250-
}
259+
)}
251260
/>
252261
);
253262
};

frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const PASSWORD_INFO = {
5151
export const FORM_FIELD_NAMES = {
5252
project: 'project',
5353
template: 'template',
54+
gpu_enabled: 'gpu_enabled',
5455
offer: 'offer',
5556
name: 'name',
5657
ide: 'ide',

frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ export type UseGenerateYamlArgs = {
99
formValues: IRunEnvironmentFormValues;
1010
configuration?: ITemplate['configuration'];
1111
envParam?: TTemplateParam;
12+
backends?: string[];
1213
};
1314

14-
export const useGenerateYaml = ({ formValues, configuration, envParam }: UseGenerateYamlArgs) => {
15+
export const useGenerateYaml = ({ formValues, configuration, envParam, backends }: UseGenerateYamlArgs) => {
1516
return useMemo(() => {
16-
const { name, ide, image, python, offer, docker, repo_url, repo_path, working_dir, password } = formValues;
17+
const { name, ide, image, python, offer, docker, repo_url, repo_path, working_dir, password, gpu_enabled } =
18+
formValues;
19+
const gpuEnabled = gpu_enabled === true;
1720

1821
const envEntries: string[] = [];
1922
if (envParam?.name && password) {
@@ -33,17 +36,18 @@ export const useGenerateYaml = ({ formValues, configuration, envParam }: UseGene
3336
...(python ? { python } : {}),
3437
...(envEntries.length > 0 ? { env: envEntries } : {}),
3538

36-
...(offer
39+
...(gpuEnabled && offer
3740
? {
3841
resources: {
3942
gpu: `${offer.name}:${round(convertMiBToGB(offer.memory_mib))}GB:${renderRange(offer.count)}`,
4043
},
4144

42-
backends: offer.backends,
45+
...(backends && backends.length > 0 ? { backends } : {}),
4346
...(offer.spot.length === 1 ? { spot_policy: offer.spot[0] } : {}),
4447
...(offer.spot.length > 1 ? { spot_policy: 'auto' } : {}),
4548
}
4649
: {}),
50+
...(!gpuEnabled ? { resources: { gpu: 0 } } : {}),
4751

4852
...(repo_url || repo_path
4953
? {
@@ -53,5 +57,5 @@ export const useGenerateYaml = ({ formValues, configuration, envParam }: UseGene
5357

5458
...(working_dir ? { working_dir } : {}),
5559
});
56-
}, [formValues, configuration, envParam]);
60+
}, [formValues, configuration, envParam, backends]);
5761
};

frontend/src/pages/Runs/CreateDevEnvironment/hooks/useValidationResolver.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ export const useYupValidationResolver = (template?: ITemplate) => {
3333
case 'resources':
3434
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
3535
// @ts-expect-error
36-
schema['offer'] = yup.object().required(requiredFieldError);
36+
schema['offer'] = yup.object().when('gpu_enabled', {
37+
is: true,
38+
then: yup.object().required(requiredFieldError),
39+
});
3740
break;
3841

3942
case 'python_or_docker':

frontend/src/pages/Runs/CreateDevEnvironment/index.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
FormField,
1414
FormSelect,
1515
FormSelectProps,
16+
FormToggle,
1617
InfoLink,
1718
SpaceBetween,
1819
Wizard,
@@ -64,6 +65,7 @@ export const CreateDevEnvironment: React.FC = () => {
6465
const [activeStepIndex, setActiveStepIndex] = useState(0);
6566
const [selectedOffers, setSelectedOffers] = useState<IGpu[]>([]);
6667
const [selectedTemplate, setSelectedTemplate] = useState<ITemplate | undefined>();
68+
const [selectedBackends, setSelectedBackends] = useState<string[]>([]);
6769
const { projectOptions, isLoadingProjectOptions } = useProjectFilter({ localStorePrefix: 'run-env-list-projects' });
6870

6971
const [applyRun, { isLoading: isApplying }] = useApplyRunMutation();
@@ -234,7 +236,12 @@ export const CreateDevEnvironment: React.FC = () => {
234236
};
235237

236238
const envParam = selectedTemplate?.parameters?.find((p) => p.type === 'env');
237-
const yaml = useGenerateYaml({ formValues, configuration: selectedTemplate?.configuration, envParam });
239+
const yaml = useGenerateYaml({
240+
formValues,
241+
configuration: selectedTemplate?.configuration,
242+
envParam,
243+
backends: selectedBackends,
244+
});
238245

239246
useEffect(() => {
240247
setValue(FORM_FIELD_NAMES.config_yaml, yaml);
@@ -324,24 +331,30 @@ export const CreateDevEnvironment: React.FC = () => {
324331
{
325332
title: 'Resources',
326333
content: (
327-
<>
328-
<FormField
329-
label={t('runs.dev_env.wizard.offer')}
330-
description={t('runs.dev_env.wizard.offer_description')}
331-
errorText={formState.errors.offer?.message}
332-
/>
333-
334-
{formState.errors.offer?.message && <br />}
335-
336-
<OfferList
337-
selectionType="single"
338-
withSearchParams={false}
339-
selectedItems={selectedOffers}
340-
onSelectionChange={onChangeOffer}
341-
permanentFilters={{ project_name: formValues.project ?? '' }}
342-
defaultFilters={{ spot_policy: 'on-demand' }}
343-
/>
344-
</>
334+
<OfferList
335+
selectionType="single"
336+
disabled={!formValues.gpu_enabled}
337+
withSearchParams={false}
338+
selectedItems={selectedOffers}
339+
onSelectionChange={onChangeOffer}
340+
onChangeBackendFilter={setSelectedBackends}
341+
permanentFilters={{ project_name: formValues.project ?? '' }}
342+
defaultFilters={{ spot_policy: 'on-demand' }}
343+
header={
344+
<FormToggle
345+
control={control}
346+
defaultValue={false}
347+
toggleLabel={t('runs.dev_env.wizard.gpu')}
348+
toggleDescription={t('runs.dev_env.wizard.gpu_description')}
349+
errorText={
350+
formValues.gpu_enabled
351+
? formState.errors.offer?.message
352+
: undefined
353+
}
354+
name={FORM_FIELD_NAMES.gpu_enabled}
355+
/>
356+
}
357+
/>
345358
),
346359
},
347360

frontend/src/pages/Runs/CreateDevEnvironment/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface IRunEnvironmentFormValues {
22
project: IProject['project_name'];
33
template: string[];
4+
gpu_enabled?: boolean;
45
offer: IGpu;
56
name: string;
67
ide: 'cursor' | 'vscode' | 'windsurf' | 'coder';

0 commit comments

Comments
 (0)