From 8a5d292dfb87550c0ab9b7aa9a37ae40ba53e87c Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Fri, 31 Oct 2025 23:19:36 +0300 Subject: [PATCH 1/9] [UI] Run Wizard. Added docker, python and repos fields --- frontend/src/components/index.ts | 1 + frontend/src/locale/en.json | 13 +++ .../hooks/useGenerateYaml.ts | 55 ++++++++++++ .../pages/Runs/CreateDevEnvironment/index.tsx | 83 +++++++++++++------ .../pages/Runs/CreateDevEnvironment/types.ts | 5 ++ 5 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index bfe834152..70d240a25 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -46,6 +46,7 @@ export { default as HelpPanel } from '@cloudscape-design/components/help-panel'; export type { HelpPanelProps } from '@cloudscape-design/components/help-panel'; export { default as TextContent } from '@cloudscape-design/components/text-content'; export { default as Toggle } from '@cloudscape-design/components/toggle'; +export type { ToggleProps } from '@cloudscape-design/components/toggle'; export { default as Modal } from '@cloudscape-design/components/modal'; export { default as TutorialPanel } from '@cloudscape-design/components/tutorial-panel'; export type { TutorialPanelProps } from '@cloudscape-design/components/tutorial-panel'; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 7d32a7545..b58322528 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -478,6 +478,19 @@ "name_placeholder": "Optional", "ide": "IDE", "ide_description": "Select which IDE would you like to use with the dev environment.", + "docker": "Docker", + "docker_image": "Docker image", + "docker_image_description": "The Docker image", + "docker_image_placeholder": "Optional", + "python": "Python", + "python_description": "Version of python", + "python_placeholder": "Optional", + "repo": "Repo", + "repo_description": "Repository URL", + "repo_placeholder": "Optional", + "repo_local_path": "Repo local path", + "repo_local_path_description": "Repo local path name", + "repo_local_path_placeholder": "Optional", "config": "Configuration file", "config_description": "Review the configuration file and adjust it if needed. Click Info for examples.", "success_notification": "The run is submitted!" diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts new file mode 100644 index 000000000..183745f58 --- /dev/null +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts @@ -0,0 +1,55 @@ +import { useMemo } from 'react'; +import jsYaml from 'js-yaml'; + +import { convertMiBToGB, renderRange, round } from 'pages/Offers/List/helpers'; + +import { IRunEnvironmentFormValues } from '../types'; + +export type UseGenerateYamlArgs = { + formValues: IRunEnvironmentFormValues; +}; + +export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { + return useMemo(() => { + if (!formValues.offer || !formValues.ide) { + return ''; + } + + const { name, ide, docker, image, python, offer, repo, repo_local_path } = formValues; + + return jsYaml.dump({ + type: 'dev-environment', + ...(name ? { name } : {}), + ide, + ...(docker ? { docker } : {}), + ...(image ? { image } : {}), + ...(python ? { python } : {}), + + resources: { + gpu: `${offer.name}:${round(convertMiBToGB(offer.memory_mib))}GB:${renderRange(offer.count)}`, + }, + + ...(repo || repo_local_path + ? { + repos: [ + { + ...(repo ? { url: repo } : {}), + ...(repo_local_path ? { local_path: repo_local_path } : {}), + }, + ], + } + : {}), + + backends: offer.backends, + spot_policy: 'auto', + }); + }, [ + formValues.name, + formValues.ide, + formValues.offer, + formValues.python, + formValues.image, + formValues.repo, + formValues.repo_local_path, + ]); +}; diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index bab507f8a..1a2529e19 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -7,7 +7,8 @@ import * as yup from 'yup'; import { Box, Link, WizardProps } from '@cloudscape-design/components'; import { CardsProps } from '@cloudscape-design/components/cards'; -import { Container, FormCodeEditor, FormField, FormInput, FormSelect, SpaceBetween, Wizard } from 'components'; +import type { ToggleProps } from 'components'; +import { Container, FormCodeEditor, FormField, FormInput, FormSelect, SpaceBetween, Toggle, Wizard } from 'components'; import { useBreadcrumbs, useNotifications } from 'hooks'; import { getServerError } from 'libs'; @@ -18,6 +19,7 @@ import { OfferList } from 'pages/Offers/List'; import { convertMiBToGB, renderRange, round } from 'pages/Offers/List/helpers'; import { getRunSpecFromYaml } from './helpers/getRunSpecFromYaml'; +import { useGenerateYaml } from './hooks/useGenerateYaml'; import { IRunEnvironmentFormValues } from './types'; @@ -161,6 +163,16 @@ export const CreateDevEnvironment: React.FC = () => { onNavigate({ requestedStepIndex, reason }); }; + const toggleDocker: ToggleProps['onChange'] = ({ detail }) => { + setValue('docker', detail.checked); + + if (detail.checked) { + setValue('python', ''); + } else { + setValue('image', ''); + } + }; + const onChangeOffer: CardsProps['onSelectionChange'] = ({ detail }) => { const newSelectedOffers = detail?.selectedItems ?? []; setSelectedOffers(newSelectedOffers); @@ -227,31 +239,13 @@ export const CreateDevEnvironment: React.FC = () => { } }; - useEffect(() => { - if (!formValues.offer || !formValues.ide) { - return; - } - - setValue( - 'config_yaml', - `type: dev-environment -${`${ - formValues.name - ? `name: ${formValues.name} - -` - : '' -}`}ide: ${formValues.ide} + const yaml = useGenerateYaml({ formValues }); -resources: - gpu: ${formValues.offer.name}:${round(convertMiBToGB(formValues.offer.memory_mib))}GB:${renderRange(formValues.offer.count)} + console.log(yaml); -backends: [${formValues.offer.backends?.join(', ')}] - -spot_policy: auto - `, - ); - }, [formValues.name, formValues.ide, formValues.offer]); + useEffect(() => { + setValue('config_yaml', yaml); + }, [yaml]); return (
@@ -304,6 +298,7 @@ spot_policy: auto name="name" disabled={loading} /> + + + + {t('runs.dev_env.wizard.docker')} + + + + + + + + + ), diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/types.ts b/frontend/src/pages/Runs/CreateDevEnvironment/types.ts index 74e5da6bb..7e214650c 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/types.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/types.ts @@ -3,4 +3,9 @@ export interface IRunEnvironmentFormValues { name: string; ide: 'cursor' | 'vscode'; config_yaml: string; + docker: boolean; + image?: string; + python?: string; + repo?: string; + repo_local_path?: string; } From 5fc07fac96658c3366240a6b741eed15e4120e4a Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Sat, 1 Nov 2025 11:56:57 +0300 Subject: [PATCH 2/9] [UI] Run Wizard. Added docker, python and repos fields --- frontend/src/pages/Runs/CreateDevEnvironment/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index 1a2529e19..e34bbbabc 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -241,8 +241,6 @@ export const CreateDevEnvironment: React.FC = () => { const yaml = useGenerateYaml({ formValues }); - console.log(yaml); - useEffect(() => { setValue('config_yaml', yaml); }, [yaml]); From 3458b141a2c43de7dc14b7fa8a7cb70c075f4b23 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Wed, 5 Nov 2025 23:43:56 +0300 Subject: [PATCH 3/9] [UI] Run Wizard. Added docker, python and repos fields --- frontend/src/locale/en.json | 5 +- .../hooks/useGenerateYaml.ts | 11 +- .../pages/Runs/CreateDevEnvironment/index.tsx | 124 ++++++++++++------ .../pages/Runs/CreateDevEnvironment/types.ts | 2 +- 4 files changed, 91 insertions(+), 51 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index b58322528..fe1f34a07 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -486,8 +486,9 @@ "python_description": "Version of python", "python_placeholder": "Optional", "repo": "Repo", - "repo_description": "Repository URL", - "repo_placeholder": "Optional", + "repo_url": "URL", + "repo_url_description": "Repository URL", + "repo_url_placeholder": "Optional", "repo_local_path": "Repo local path", "repo_local_path_description": "Repo local path name", "repo_local_path_placeholder": "Optional", diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts index 183745f58..199a63e09 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts @@ -15,25 +15,24 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { return ''; } - const { name, ide, docker, image, python, offer, repo, repo_local_path } = formValues; + const { name, ide, image, python, offer, repo_url, repo_local_path } = formValues; return jsYaml.dump({ type: 'dev-environment', ...(name ? { name } : {}), ide, - ...(docker ? { docker } : {}), - ...(image ? { image } : {}), + ...(image ? { docker: true, image } : {}), ...(python ? { python } : {}), resources: { gpu: `${offer.name}:${round(convertMiBToGB(offer.memory_mib))}GB:${renderRange(offer.count)}`, }, - ...(repo || repo_local_path + ...(repo_url || repo_local_path ? { repos: [ { - ...(repo ? { url: repo } : {}), + ...(repo_url ? { url: repo_url } : {}), ...(repo_local_path ? { local_path: repo_local_path } : {}), }, ], @@ -49,7 +48,7 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { formValues.offer, formValues.python, formValues.image, - formValues.repo, + formValues.repo_url, formValues.repo_local_path, ]); }; diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index e34bbbabc..859e29013 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -7,8 +7,8 @@ import * as yup from 'yup'; import { Box, Link, WizardProps } from '@cloudscape-design/components'; import { CardsProps } from '@cloudscape-design/components/cards'; -import type { ToggleProps } from 'components'; -import { Container, FormCodeEditor, FormField, FormInput, FormSelect, SpaceBetween, Toggle, Wizard } from 'components'; +import type { TabsProps, ToggleProps } from 'components'; +import { Container, FormCodeEditor, FormField, FormInput, FormSelect, SpaceBetween, Tabs, Toggle, Wizard } from 'components'; import { useBreadcrumbs, useNotifications } from 'hooks'; import { getServerError } from 'libs'; @@ -39,6 +39,11 @@ const ideOptions = [ }, ]; +enum DockerPythonTabs { + DOCKER = 'docker', + PYTHON = 'python', +} + const envValidationSchema = yup.object({ offer: yup.object().required(requiredFieldError), name: yup.string().matches(/^[a-z][a-z0-9-]{1,40}$/, namesFieldError), @@ -89,6 +94,8 @@ export const CreateDevEnvironment: React.FC = () => { const navigate = useNavigate(); const [pushNotification] = useNotifications(); const [activeStepIndex, setActiveStepIndex] = useState(0); + const [isEnabledRepo, setIsEnabledRepo] = useState(false); + const [activeTab, setActiveTab] = useState(DockerPythonTabs.DOCKER); const [selectedOffers, setSelectedOffers] = useState([]); const [selectedProject, setSelectedProject] = useState( () => searchParams.get('project_name') ?? null, @@ -163,12 +170,21 @@ export const CreateDevEnvironment: React.FC = () => { onNavigate({ requestedStepIndex, reason }); }; - const toggleDocker: ToggleProps['onChange'] = ({ detail }) => { - setValue('docker', detail.checked); + const toggleRepo: ToggleProps['onChange'] = ({ detail }) => { + setIsEnabledRepo(detail.checked); + + if (!detail.checked) { + setValue('repo_url', ''); + setValue('repo_local_path', ''); + } + }; - if (detail.checked) { + const onChangeTab: TabsProps['onChange'] = ({ detail }) => { + if (detail.activeTabId === DockerPythonTabs.DOCKER) { setValue('python', ''); - } else { + } + + if (detail.activeTabId === DockerPythonTabs.PYTHON) { setValue('image', ''); } }; @@ -306,45 +322,69 @@ export const CreateDevEnvironment: React.FC = () => { disabled={loading} /> - - {t('runs.dev_env.wizard.docker')} - - - - - + + + ), + }, + { + label: t('runs.dev_env.wizard.python'), + id: DockerPythonTabs.PYTHON, + content: ( +
+ +
+ ), + }, + ]} /> - + + {t('runs.dev_env.wizard.repo')} + - + {isEnabledRepo && ( + <> + + + + + )} ), diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/types.ts b/frontend/src/pages/Runs/CreateDevEnvironment/types.ts index 7e214650c..cdae05494 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/types.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/types.ts @@ -6,6 +6,6 @@ export interface IRunEnvironmentFormValues { docker: boolean; image?: string; python?: string; - repo?: string; + repo_url?: string; repo_local_path?: string; } From edf409603f3593a97bd7778aa10268d2234651f5 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Thu, 6 Nov 2025 22:36:27 +0300 Subject: [PATCH 4/9] [UI] Run Wizard. Added docker, python and repos fields --- frontend/src/locale/en.json | 4 +-- .../hooks/useGenerateYaml.ts | 5 +-- .../pages/Runs/CreateDevEnvironment/index.tsx | 32 ++++++++++++------- .../pages/Runs/CreateDevEnvironment/types.ts | 1 + 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index fe1f34a07..a67c9ed97 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -481,14 +481,14 @@ "docker": "Docker", "docker_image": "Docker image", "docker_image_description": "The Docker image", - "docker_image_placeholder": "Optional", + "docker_image_placeholder": "Required", "python": "Python", "python_description": "Version of python", "python_placeholder": "Optional", "repo": "Repo", "repo_url": "URL", "repo_url_description": "Repository URL", - "repo_url_placeholder": "Optional", + "repo_url_placeholder": "Required", "repo_local_path": "Repo local path", "repo_local_path_description": "Repo local path name", "repo_local_path_placeholder": "Optional", diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts index 199a63e09..5e8a14c8e 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts @@ -15,13 +15,14 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { return ''; } - const { name, ide, image, python, offer, repo_url, repo_local_path } = formValues; + const { name, ide, image, python, offer, docker, repo_url, repo_local_path } = formValues; return jsYaml.dump({ type: 'dev-environment', ...(name ? { name } : {}), ide, - ...(image ? { docker: true, image } : {}), + ...(docker ? { docker } : {}), + ...(image ? { image } : {}), ...(python ? { python } : {}), resources: { diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index 859e29013..9d597d246 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -16,7 +16,6 @@ import { ROUTES } from 'routes'; import { useApplyRunMutation } from 'services/run'; import { OfferList } from 'pages/Offers/List'; -import { convertMiBToGB, renderRange, round } from 'pages/Offers/List/helpers'; import { getRunSpecFromYaml } from './helpers/getRunSpecFromYaml'; import { useGenerateYaml } from './hooks/useGenerateYaml'; @@ -27,6 +26,7 @@ import styles from './styles.module.scss'; const requiredFieldError = 'This is required field'; const namesFieldError = 'Only latin characters, dashes, and digits'; +const urlFormatError = 'Only URLs'; const ideOptions = [ { @@ -49,6 +49,16 @@ const envValidationSchema = yup.object({ name: yup.string().matches(/^[a-z][a-z0-9-]{1,40}$/, namesFieldError), ide: yup.string().required(requiredFieldError), config_yaml: yup.string().required(requiredFieldError), + + image: yup.string().when('docker', { + is: true, + then: yup.string().required(requiredFieldError), + }), + + repo_url: yup.string().when('repo_enabled', { + is: true, + then: yup.string().url(urlFormatError).required(requiredFieldError), + }), }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -94,8 +104,6 @@ export const CreateDevEnvironment: React.FC = () => { const navigate = useNavigate(); const [pushNotification] = useNotifications(); const [activeStepIndex, setActiveStepIndex] = useState(0); - const [isEnabledRepo, setIsEnabledRepo] = useState(false); - const [activeTab, setActiveTab] = useState(DockerPythonTabs.DOCKER); const [selectedOffers, setSelectedOffers] = useState([]); const [selectedProject, setSelectedProject] = useState( () => searchParams.get('project_name') ?? null, @@ -121,6 +129,8 @@ export const CreateDevEnvironment: React.FC = () => { resolver, defaultValues: { ide: 'cursor', + docker: true, + repo_enabled: false, }, }); const { handleSubmit, control, trigger, setValue, watch, formState, getValues } = formMethods; @@ -134,16 +144,14 @@ export const CreateDevEnvironment: React.FC = () => { return await trigger(['offer']); }; - const validateName = async () => { - return await trigger(['name', 'ide']); + const validateSecondStep = async () => { + return await trigger(['name', 'ide', 'docker', 'image', 'python', 'repo_enabled', 'repo_url', 'repo_local_path']); }; const validateConfig = async () => { return await trigger(['config_yaml']); }; - const emptyValidator = async () => Promise.resolve(true); - const onNavigate = ({ requestedStepIndex, reason, @@ -151,7 +159,7 @@ export const CreateDevEnvironment: React.FC = () => { requestedStepIndex: number; reason: WizardProps.NavigationReason; }) => { - const stepValidators = [validateOffer, validateName, validateConfig, emptyValidator]; + const stepValidators = [validateOffer, validateSecondStep, validateConfig]; if (reason === 'next') { stepValidators[activeStepIndex]?.().then((isValid) => { @@ -171,7 +179,7 @@ export const CreateDevEnvironment: React.FC = () => { }; const toggleRepo: ToggleProps['onChange'] = ({ detail }) => { - setIsEnabledRepo(detail.checked); + setValue('repo_enabled', detail.checked); if (!detail.checked) { setValue('repo_url', ''); @@ -187,6 +195,8 @@ export const CreateDevEnvironment: React.FC = () => { if (detail.activeTabId === DockerPythonTabs.PYTHON) { setValue('image', ''); } + + setValue('docker', detail.activeTabId === DockerPythonTabs.DOCKER); }; const onChangeOffer: CardsProps['onSelectionChange'] = ({ detail }) => { @@ -360,11 +370,11 @@ export const CreateDevEnvironment: React.FC = () => { ]} /> - + {t('runs.dev_env.wizard.repo')} - {isEnabledRepo && ( + {formValues.repo_enabled && ( <> Date: Wed, 12 Nov 2025 15:21:18 +0100 Subject: [PATCH 5/9] [UI] Run Wizard. Added docker, python and repos fields - [x] `Connect` section is empty when provisioning - [x] Add `--logs` to the `dstack attach` command --- .../RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx index 36bfa2379..4176996ea 100644 --- a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx +++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx @@ -27,7 +27,7 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) const { t } = useTranslation(); const getAttachCommand = (runData: IRun) => { - const attachCommand = `dstack attach ${runData.run_spec.run_name}`; + const attachCommand = `dstack attach ${runData.run_spec.run_name} --logs`; const copyAttachCommand = () => { copyToClipboard(attachCommand); @@ -233,7 +233,7 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) /> )} - {run.status === 'running' && ( + {run.status !== 'running' && ( Waiting for the run to start. From 449e584c6f074d6abb227e1ea365fc30e7a3acad Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Wed, 19 Nov 2025 21:38:48 +0300 Subject: [PATCH 6/9] [UI] Run Wizard. Added repo initialization --- frontend/src/api.ts | 2 + frontend/src/libs/repo.ts | 33 ++++ .../helpers/getRunSpecFromYaml.ts | 71 --------- .../hooks/useGetRunSpecFromYaml.ts | 146 ++++++++++++++++++ .../pages/Runs/CreateDevEnvironment/index.tsx | 47 +++--- frontend/src/services/repo.ts | 41 +++++ frontend/src/store.ts | 3 + frontend/src/types/repo.d.ts | 38 +++-- 8 files changed, 273 insertions(+), 108 deletions(-) create mode 100644 frontend/src/libs/repo.ts delete mode 100644 frontend/src/pages/Runs/CreateDevEnvironment/helpers/getRunSpecFromYaml.ts create mode 100644 frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts create mode 100644 frontend/src/services/repo.ts diff --git a/frontend/src/api.ts b/frontend/src/api.ts index b0266e74e..aace68a15 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -70,6 +70,8 @@ export const API = { // Repos REPOS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/repos`, REPOS_LIST: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/list`, + GET_REPO: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/get`, + INIT_REPO: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/init`, // Runs RUNS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/runs`, diff --git a/frontend/src/libs/repo.ts b/frontend/src/libs/repo.ts new file mode 100644 index 000000000..40354e97b --- /dev/null +++ b/frontend/src/libs/repo.ts @@ -0,0 +1,33 @@ +function bufferToHex(buffer: ArrayBuffer): string { + return Array.from(new Uint8Array(buffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +export async function slugify(prefix: string, unique_key: string, hash_size: number = 8): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(unique_key); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const fullHash = bufferToHex(hashBuffer); + return `${prefix}-${fullHash.substring(0, hash_size)}`; +} + +export function getRepoName(url: string): string { + const cleaned = url + .replace(/^https?:\/\//i, '') + .replace(/:\/(\S*)/, '') + .replace(/\/+$/, '') + .replace(/\.git$/, ''); + const parts = cleaned.split('/').filter(Boolean); + return parts.length ? parts[parts.length - 1] : ''; +} + +export function getPathWithoutProtocol(url: string): string { + return url.replace(/^https?:\/\//i, ''); +} + +export function getRepoDirFromUrl(url: string): string | undefined { + const dirName = url.replace(/^https?:\/\//i, '').match(/:\/(\S*)/)?.[1]; + + return dirName ? `/${dirName}` : undefined; +} diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/helpers/getRunSpecFromYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/helpers/getRunSpecFromYaml.ts deleted file mode 100644 index a4d029c44..000000000 --- a/frontend/src/pages/Runs/CreateDevEnvironment/helpers/getRunSpecFromYaml.ts +++ /dev/null @@ -1,71 +0,0 @@ -import jsYaml from 'js-yaml'; - -import { getRunSpecConfigurationResources } from './getRunSpecConfigurationResources'; - -// TODO add next fields: volumes, repos, -const supportedFields: (keyof TDevEnvironmentConfiguration)[] = [ - 'type', - 'init', - 'inactivity_duration', - 'image', - 'user', - 'privileged', - 'entrypoint', - 'working_dir', - 'registry_auth', - 'python', - 'nvcc', - 'env', - 'docker', - 'backends', - 'regions', - 'instance_types', - 'spot_policy', - 'retry', - 'max_duration', - 'max_price', - 'idle_duration', - 'utilization_policy', - 'fleets', -]; - -export const getRunSpecFromYaml = async (yaml: string) => { - let parsedYaml; - - try { - parsedYaml = (await jsYaml.load(yaml)) as { [key: string]: unknown }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { - throw new Error(`Invalid YAML`); - } - - const { name, ...otherFields } = parsedYaml; - - const runSpec: TRunSpec = { - run_name: name as string, - configuration: {} as TDevEnvironmentConfiguration, - }; - - Object.keys(otherFields).forEach((fieldName) => { - switch (fieldName) { - case 'ide': - runSpec.configuration.ide = otherFields[fieldName] as TIde; - break; - case 'resources': - runSpec.configuration.resources = getRunSpecConfigurationResources(otherFields[fieldName]); - break; - default: - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - if (!supportedFields.includes(fieldName)) { - throw new Error(`Unsupported field: ${fieldName}`); - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - runSpec.configuration[fieldName] = otherFields[fieldName]; - return {}; - } - }); - - return runSpec; -}; diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts new file mode 100644 index 000000000..4cfa885c6 --- /dev/null +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts @@ -0,0 +1,146 @@ +import { useCallback } from 'react'; +import jsYaml from 'js-yaml'; + +import { useNotifications } from 'hooks'; +import { useInitRepoMutation, useLazyGetRepoQuery } from 'services/repo'; + +import { getPathWithoutProtocol, getRepoDirFromUrl, getRepoName, slugify } from '../../../../libs/repo'; +import { getRunSpecConfigurationResources } from '../helpers/getRunSpecConfigurationResources'; + +// TODO add next fields: volumes, repos, +const supportedFields: (keyof TDevEnvironmentConfiguration)[] = [ + 'type', + 'init', + 'inactivity_duration', + 'image', + 'user', + 'privileged', + 'entrypoint', + 'working_dir', + 'registry_auth', + 'python', + 'nvcc', + 'env', + 'docker', + 'backends', + 'regions', + 'instance_types', + 'spot_policy', + 'retry', + 'max_duration', + 'max_price', + 'idle_duration', + 'utilization_policy', + 'fleets', + 'repos', +]; + +export const useGetRunSpecFromYaml = ({ projectName = '' }) => { + const [pushNotification] = useNotifications(); + const [getRepo] = useLazyGetRepoQuery(); + const [initRepo] = useInitRepoMutation(); + + const getRepoData = useCallback( + async (repos: TEnvironmentConfigurationRepo[]) => { + const [firstRepo] = repos; + + if (!firstRepo || !firstRepo.url) { + return {}; + } + + const prefix = getRepoName(firstRepo.url); + const uniqKey = getPathWithoutProtocol(firstRepo.url); + const repoId = await slugify(prefix, uniqKey); + const repoDir = getRepoDirFromUrl(firstRepo.url); + + try { + await getRepo({ project_name: projectName, repo_id: repoId, include_creds: true }).unwrap(); + } catch (_) { + initRepo({ + project_name: projectName, + repo_id: repoId, + repo_info: { + repo_type: 'remote', + repo_name: prefix, + }, + repo_creds: { clone_url: firstRepo.url, private_key: null, oauth_token: null }, + }) + .unwrap() + .catch(console.error); + } + + return { + repo_id: repoId, + repo_data: { + repo_type: 'remote', + repo_name: prefix, + repo_branch: firstRepo.branch ?? null, + repo_hash: firstRepo.hash ?? null, + repo_config_name: null, + repo_config_email: null, + }, + repo_code_hash: null, + repo_dir: repoDir ?? null, + }; + }, + [projectName, getRepo, initRepo], + ); + + const getRunSpecFromYaml = useCallback( + async (yaml: string) => { + let parsedYaml; + + try { + parsedYaml = (await jsYaml.load(yaml)) as { [key: string]: unknown }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + pushNotification({ + type: 'error', + content: 'Invalid YAML', + }); + + window.scrollTo(0, 0); + + throw new Error('Invalid YAML'); + } + + const { name, ...otherFields } = parsedYaml; + + const runSpec: TRunSpec = { + run_name: name as string, + configuration: {} as TDevEnvironmentConfiguration, + }; + + for (const fieldName of Object.keys(otherFields)) { + switch (fieldName) { + case 'ide': + runSpec.configuration.ide = otherFields[fieldName] as TIde; + break; + case 'resources': + runSpec.configuration.resources = getRunSpecConfigurationResources(otherFields[fieldName]); + break; + case 'repos': { + const repoData = await getRepoData(otherFields['repos'] as TEnvironmentConfigurationRepo[]); + Object.assign(runSpec, repoData); + break; + } + default: + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + if (!supportedFields.includes(fieldName)) { + throw new Error(`Unsupported field: ${fieldName}`); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + runSpec.configuration[fieldName] = otherFields[fieldName]; + break; + } + } + + return runSpec; + }, + [pushNotification, getRepoData], + ); + + return [getRunSpecFromYaml]; +}; diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index 9d597d246..92b0d5827 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -17,8 +17,8 @@ import { useApplyRunMutation } from 'services/run'; import { OfferList } from 'pages/Offers/List'; -import { getRunSpecFromYaml } from './helpers/getRunSpecFromYaml'; import { useGenerateYaml } from './hooks/useGenerateYaml'; +import { useGetRunSpecFromYaml } from './hooks/useGetRunSpecFromYaml'; import { IRunEnvironmentFormValues } from './types'; @@ -57,7 +57,10 @@ const envValidationSchema = yup.object({ repo_url: yup.string().when('repo_enabled', { is: true, - then: yup.string().url(urlFormatError).required(requiredFieldError), + then: yup + .string() + .matches(/^(https?):\/\/([^\s\/?#]+)((?:\/[^\s?#]*)*)(?::\/(.*))?$/i, urlFormatError) + .required(requiredFieldError), }), }); @@ -109,6 +112,8 @@ export const CreateDevEnvironment: React.FC = () => { () => searchParams.get('project_name') ?? null, ); + const [getRunSpecFromYaml] = useGetRunSpecFromYaml({ projectName: selectedProject }); + const [applyRun, { isLoading: isApplying }] = useApplyRunMutation(); const loading = isApplying; @@ -129,7 +134,7 @@ export const CreateDevEnvironment: React.FC = () => { resolver, defaultValues: { ide: 'cursor', - docker: true, + docker: false, repo_enabled: false, }, }); @@ -218,16 +223,8 @@ export const CreateDevEnvironment: React.FC = () => { try { runSpec = await getRunSpecFromYaml(config_yaml); - } catch (error) { - pushNotification({ - type: 'error', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - content: error?.message, - }); - - window.scrollTo(0, 0); - + } catch (e) { + console.log('parse transaction error:', e); return; } @@ -336,32 +333,32 @@ export const CreateDevEnvironment: React.FC = () => { onChange={onChangeTab} tabs={[ { - label: t('runs.dev_env.wizard.docker'), - id: DockerPythonTabs.DOCKER, + label: t('runs.dev_env.wizard.python'), + id: DockerPythonTabs.PYTHON, content: (
), }, { - label: t('runs.dev_env.wizard.python'), - id: DockerPythonTabs.PYTHON, + label: t('runs.dev_env.wizard.docker'), + id: DockerPythonTabs.DOCKER, content: (
diff --git a/frontend/src/services/repo.ts b/frontend/src/services/repo.ts new file mode 100644 index 000000000..47a68bd6d --- /dev/null +++ b/frontend/src/services/repo.ts @@ -0,0 +1,41 @@ +import { API } from 'api'; +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +import fetchBaseQueryHeaders from 'libs/fetchBaseQueryHeaders'; + +import { TGetRepoResponse, TInitRepoRequestParams } from '../types/repo'; + +export const repoApi = createApi({ + reducerPath: 'repoApi', + baseQuery: fetchBaseQuery({ + prepareHeaders: fetchBaseQueryHeaders, + }), + + tagTypes: ['Repos'], + + endpoints: (builder) => ({ + getRepo: builder.query({ + query: ({ project_name, ...body }) => { + return { + url: API.PROJECTS.GET_REPO(project_name), + body, + method: 'POST', + }; + }, + + providesTags: (result) => (result ? [{ type: 'Secrets' as const, id: result.repo_id }, 'Repos'] : ['Repos']), + }), + + initRepo: builder.mutation({ + query: ({ project_name, ...body }) => ({ + url: API.PROJECTS.INIT_REPO(project_name), + method: 'POST', + body, + }), + + invalidatesTags: () => ['Secrets'], + }), + }), +}); + +export const { useLazyGetRepoQuery, useInitRepoMutation } = repoApi; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 326f113ad..8465544d4 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -9,6 +9,7 @@ import { gatewayApi } from 'services/gateway'; import { instanceApi } from 'services/instance'; import { mainApi } from 'services/mainApi'; import { projectApi } from 'services/project'; +import { repoApi } from 'services/repo'; import { runApi } from 'services/run'; import { secretApi } from 'services/secrets'; import { serverApi } from 'services/server'; @@ -35,6 +36,7 @@ export const store = configureStore({ [volumeApi.reducerPath]: volumeApi.reducer, [secretApi.reducerPath]: secretApi.reducer, [gpuApi.reducerPath]: gpuApi.reducer, + [repoApi.reducerPath]: repoApi.reducer, [mainApi.reducerPath]: mainApi.reducer, }, @@ -54,6 +56,7 @@ export const store = configureStore({ .concat(volumeApi.middleware) .concat(secretApi.middleware) .concat(gpuApi.middleware) + .concat(repoApi.middleware) .concat(mainApi.middleware), }); diff --git a/frontend/src/types/repo.d.ts b/frontend/src/types/repo.d.ts index f90a7f8d7..3108d7e0a 100644 --- a/frontend/src/types/repo.d.ts +++ b/frontend/src/types/repo.d.ts @@ -4,25 +4,39 @@ enum RepoTypeEnum { } declare interface IRemoteRunRepoData { - repo_type: 'remote' - repo_name: string - repo_branch?: string - repo_hash?: string - repo_diff?: string - repo_config_name?: string - repo_config_email?: string + repo_type: 'remote'; + repo_name: string; + repo_branch?: string; + repo_hash?: string; + repo_diff?: string; + repo_config_name?: string; + repo_config_email?: string; } declare interface ILocalRunRepoData { - repo_type: 'local' - repo_dir: string + repo_type: 'local'; + repo_dir: string; } declare interface VirtualRunRepoData { - repo_type: 'virtual' + repo_type: 'virtual'; +} + +declare interface RepoCreds { + clone_url: string; + private_key: string | null; + oauth_token: string | null; } declare interface IRepo { - repo_id: string, - repo_info: IRemoteRunRepoData | ILocalRunRepoData | VirtualRunRepoData + repo_id: string; + repo_info: IRemoteRunRepoData | ILocalRunRepoData | VirtualRunRepoData; } + +declare type TGetRepoResponse = IRepo & RepoCreds; + +declare type TInitRepoRequestParams = { + repo_id: string; + repo_info: IRemoteRunRepoData; + repo_creds: RepoCreds; +}; From 3dafb08267ad332441331abdd319d6d742b88b52 Mon Sep 17 00:00:00 2001 From: Oleg Vavilov Date: Wed, 26 Nov 2025 23:57:32 +0300 Subject: [PATCH 7/9] [UI] Run Wizard. Added working dir, refactoring getting repo --- frontend/src/libs/repo.ts | 6 ++++ frontend/src/locale/en.json | 9 ++++-- .../Runs/CreateDevEnvironment/constants.tsx | 16 ++++++++++ .../hooks/useGenerateYaml.ts | 15 ++++------ .../hooks/useGetRunSpecFromYaml.ts | 19 ++++++------ .../pages/Runs/CreateDevEnvironment/index.tsx | 30 ++++++++++++++----- .../pages/Runs/CreateDevEnvironment/types.ts | 5 +++- 7 files changed, 71 insertions(+), 29 deletions(-) diff --git a/frontend/src/libs/repo.ts b/frontend/src/libs/repo.ts index 40354e97b..8f82f6f37 100644 --- a/frontend/src/libs/repo.ts +++ b/frontend/src/libs/repo.ts @@ -26,6 +26,12 @@ export function getPathWithoutProtocol(url: string): string { return url.replace(/^https?:\/\//i, ''); } +export function getRepoUrlWithOutDir(url: string): string { + const parsedUrl = url.match(/^([^:]+(?::[^:]+)?)/)?.[1]; + + return parsedUrl ?? url; +} + export function getRepoDirFromUrl(url: string): string | undefined { const dirName = url.replace(/^https?:\/\//i, '').match(/:\/(\S*)/)?.[1]; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index a67c9ed97..15d9beb41 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -486,12 +486,15 @@ "python_description": "Version of python", "python_placeholder": "Optional", "repo": "Repo", + "working_dir": "Working dir", + "working_dir_description": "Working dir", + "working_dir_placeholder": "Optional", "repo_url": "URL", "repo_url_description": "Repository URL", "repo_url_placeholder": "Required", - "repo_local_path": "Repo local path", - "repo_local_path_description": "Repo local path name", - "repo_local_path_placeholder": "Optional", + "repo_path": "Repo path", + "repo_path_description": "Repo path name", + "repo_path_placeholder": "Optional", "config": "Configuration file", "config_description": "Review the configuration file and adjust it if needed. Click Info for examples.", "success_notification": "The run is submitted!" diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx index f8ae58895..15593b6df 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx @@ -1,4 +1,6 @@ import React from 'react'; + +import { IRunEnvironmentFormKeys } from './types'; export const CONFIG_INFO = { header:

Credits history

, body: ( @@ -7,3 +9,17 @@ export const CONFIG_INFO = { ), }; + +export const FORM_FIELD_NAMES = { + offer: 'offer', + name: 'name', + ide: 'ide', + config_yaml: 'config_yaml', + docker: 'docker', + image: 'image', + python: 'python', + repo_enabled: 'repo_enabled', + repo_url: 'repo_url', + repo_path: 'repo_path', + working_dir: 'working_dir', +} as const satisfies Record; diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts index 5e8a14c8e..a693985fc 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGenerateYaml.ts @@ -15,7 +15,7 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { return ''; } - const { name, ide, image, python, offer, docker, repo_url, repo_local_path } = formValues; + const { name, ide, image, python, offer, docker, repo_url, repo_path, working_dir } = formValues; return jsYaml.dump({ type: 'dev-environment', @@ -29,17 +29,13 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { gpu: `${offer.name}:${round(convertMiBToGB(offer.memory_mib))}GB:${renderRange(offer.count)}`, }, - ...(repo_url || repo_local_path + ...(repo_url || repo_path ? { - repos: [ - { - ...(repo_url ? { url: repo_url } : {}), - ...(repo_local_path ? { local_path: repo_local_path } : {}), - }, - ], + repos: [[repo_url?.trim(), repo_path?.trim()].filter(Boolean).join(':')], } : {}), + ...(working_dir ? { working_dir } : {}), backends: offer.backends, spot_policy: 'auto', }); @@ -50,6 +46,7 @@ export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => { formValues.python, formValues.image, formValues.repo_url, - formValues.repo_local_path, + formValues.repo_path, + formValues.working_dir, ]); }; diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts index 4cfa885c6..649db570b 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/hooks/useGetRunSpecFromYaml.ts @@ -4,7 +4,7 @@ import jsYaml from 'js-yaml'; import { useNotifications } from 'hooks'; import { useInitRepoMutation, useLazyGetRepoQuery } from 'services/repo'; -import { getPathWithoutProtocol, getRepoDirFromUrl, getRepoName, slugify } from '../../../../libs/repo'; +import { getPathWithoutProtocol, getRepoDirFromUrl, getRepoName, getRepoUrlWithOutDir, slugify } from '../../../../libs/repo'; import { getRunSpecConfigurationResources } from '../helpers/getRunSpecConfigurationResources'; // TODO add next fields: volumes, repos, @@ -41,17 +41,18 @@ export const useGetRunSpecFromYaml = ({ projectName = '' }) => { const [initRepo] = useInitRepoMutation(); const getRepoData = useCallback( - async (repos: TEnvironmentConfigurationRepo[]) => { + async (repos: string[]) => { const [firstRepo] = repos; - if (!firstRepo || !firstRepo.url) { + if (!firstRepo) { return {}; } - const prefix = getRepoName(firstRepo.url); - const uniqKey = getPathWithoutProtocol(firstRepo.url); + const repoUrlWithoutDir = getRepoUrlWithOutDir(firstRepo); + const prefix = getRepoName(repoUrlWithoutDir); + const uniqKey = getPathWithoutProtocol(repoUrlWithoutDir); const repoId = await slugify(prefix, uniqKey); - const repoDir = getRepoDirFromUrl(firstRepo.url); + const repoDir = getRepoDirFromUrl(firstRepo); try { await getRepo({ project_name: projectName, repo_id: repoId, include_creds: true }).unwrap(); @@ -63,7 +64,7 @@ export const useGetRunSpecFromYaml = ({ projectName = '' }) => { repo_type: 'remote', repo_name: prefix, }, - repo_creds: { clone_url: firstRepo.url, private_key: null, oauth_token: null }, + repo_creds: { clone_url: repoUrlWithoutDir, private_key: null, oauth_token: null }, }) .unwrap() .catch(console.error); @@ -74,8 +75,8 @@ export const useGetRunSpecFromYaml = ({ projectName = '' }) => { repo_data: { repo_type: 'remote', repo_name: prefix, - repo_branch: firstRepo.branch ?? null, - repo_hash: firstRepo.hash ?? null, + repo_branch: null, + repo_hash: null, repo_config_name: null, repo_config_email: null, }, diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index 92b0d5827..d5dab2c85 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -19,14 +19,16 @@ import { OfferList } from 'pages/Offers/List'; import { useGenerateYaml } from './hooks/useGenerateYaml'; import { useGetRunSpecFromYaml } from './hooks/useGetRunSpecFromYaml'; +import { FORM_FIELD_NAMES } from './constants'; -import { IRunEnvironmentFormValues } from './types'; +import { IRunEnvironmentFormKeys, IRunEnvironmentFormValues } from './types'; import styles from './styles.module.scss'; const requiredFieldError = 'This is required field'; const namesFieldError = 'Only latin characters, dashes, and digits'; const urlFormatError = 'Only URLs'; +const workingDirFormatError = 'Use absolute path'; const ideOptions = [ { @@ -49,6 +51,7 @@ const envValidationSchema = yup.object({ name: yup.string().matches(/^[a-z][a-z0-9-]{1,40}$/, namesFieldError), ide: yup.string().required(requiredFieldError), config_yaml: yup.string().required(requiredFieldError), + working_dir: yup.string().matches(/^\//, workingDirFormatError), image: yup.string().when('docker', { is: true, @@ -150,7 +153,11 @@ export const CreateDevEnvironment: React.FC = () => { }; const validateSecondStep = async () => { - return await trigger(['name', 'ide', 'docker', 'image', 'python', 'repo_enabled', 'repo_url', 'repo_local_path']); + const secondStepFields = Object.keys(FORM_FIELD_NAMES).filter( + (fieldName) => !['offer', 'config_yaml'].includes(fieldName), + ) as IRunEnvironmentFormKeys[]; + + return await trigger(secondStepFields); }; const validateConfig = async () => { @@ -188,7 +195,7 @@ export const CreateDevEnvironment: React.FC = () => { if (!detail.checked) { setValue('repo_url', ''); - setValue('repo_local_path', ''); + setValue('repo_path', ''); } }; @@ -367,6 +374,15 @@ export const CreateDevEnvironment: React.FC = () => { ]} /> + + {t('runs.dev_env.wizard.repo')} @@ -383,11 +399,11 @@ export const CreateDevEnvironment: React.FC = () => { /> diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/types.ts b/frontend/src/pages/Runs/CreateDevEnvironment/types.ts index 6a69e05e3..020d40e59 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/types.ts +++ b/frontend/src/pages/Runs/CreateDevEnvironment/types.ts @@ -8,5 +8,8 @@ export interface IRunEnvironmentFormValues { python?: string; repo_enabled?: boolean; repo_url?: string; - repo_local_path?: string; + repo_path?: string; + working_dir?: string; } + +export type IRunEnvironmentFormKeys = keyof Required; From 96fc9d6fd38374f8c03fcb22dac299ff09a91d79 Mon Sep 17 00:00:00 2001 From: peterschmidt85 Date: Tue, 2 Dec 2025 19:59:30 +0100 Subject: [PATCH 8/9] [UI] Run Wizard. Added working dir, refactoring getting repo Minor text edits --- frontend/src/locale/en.json | 21 ++++++++++++------- .../pages/Runs/CreateDevEnvironment/index.tsx | 11 +++++++--- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 15d9beb41..d4334bbfc 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -474,27 +474,32 @@ "offer": "Offer", "offer_description": "Select an offer for the dev environment.", "name": "Name", - "name_description": "The name of the run. If not specified, the name will be generated automatically.", + "name_description": "The name of the run, e.g. 'my-dev-env'", + "name_constraint": "If not specified, generated automatically", "name_placeholder": "Optional", "ide": "IDE", "ide_description": "Select which IDE would you like to use with the dev environment.", "docker": "Docker", - "docker_image": "Docker image", - "docker_image_description": "The Docker image", + "docker_image": "Image", + "docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'", + "docker_image_constraint": "The image must be public", "docker_image_placeholder": "Required", "python": "Python", - "python_description": "Version of python", + "python_description": "The version of Python, e.g. '3.12'", "python_placeholder": "Optional", "repo": "Repo", "working_dir": "Working dir", - "working_dir_description": "Working dir", + "working_dir_description": "The absolute path to the working directory inside the container, e.g. '/home/user/project'", "working_dir_placeholder": "Optional", + "working_dir_constraint": "By default, set to '/workflow'", "repo_url": "URL", - "repo_url_description": "Repository URL", + "repo_url_description": "A URL of a Git repository, e.g. 'https://github.com/user/repo'", + "repo_url_constraint": "The repo must be public", "repo_url_placeholder": "Required", - "repo_path": "Repo path", - "repo_path_description": "Repo path name", + "repo_path": "Path", + "repo_path_description": "The path inside the container to clone the repository, e.g. '/home/user/project'", "repo_path_placeholder": "Optional", + "repo_path_constraint": "By default, set to '/workflow'", "config": "Configuration file", "config_description": "Review the configuration file and adjust it if needed. Click Info for examples.", "success_notification": "The run is submitted!" diff --git a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx index d5dab2c85..567304675 100644 --- a/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx +++ b/frontend/src/pages/Runs/CreateDevEnvironment/index.tsx @@ -25,10 +25,10 @@ import { IRunEnvironmentFormKeys, IRunEnvironmentFormValues } from './types'; import styles from './styles.module.scss'; -const requiredFieldError = 'This is required field'; +const requiredFieldError = 'This is a required field'; const namesFieldError = 'Only latin characters, dashes, and digits'; const urlFormatError = 'Only URLs'; -const workingDirFormatError = 'Use absolute path'; +const workingDirFormatError = 'Must be an absolute path'; const ideOptions = [ { @@ -321,6 +321,7 @@ export const CreateDevEnvironment: React.FC = () => { { { { { Date: Tue, 2 Dec 2025 20:41:23 +0100 Subject: [PATCH 9/9] [UI] Run Wizard. Added working dir, refactoring getting repo Minor styling edits --- frontend/src/pages/Offers/List/index.tsx | 31 ++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/Offers/List/index.tsx b/frontend/src/pages/Offers/List/index.tsx index a0814b91a..462562569 100644 --- a/frontend/src/pages/Offers/List/index.tsx +++ b/frontend/src/pages/Offers/List/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Cards, CardsProps, Link, MultiselectCSD, PropertyFilter, StatusIndicator } from 'components'; +import { Cards, CardsProps, MultiselectCSD, PropertyFilter, StatusIndicator } from 'components'; import { useCollection } from 'hooks'; import { useGetGpusListQuery } from 'services/gpu'; @@ -132,31 +132,31 @@ export const OfferList: React.FC = ({ withSearchParams, onChange const sections = [ { id: 'memory_mib', - header: t('offer.memory_mib'), - content: (gpu: IGpu) => `${round(convertMiBToGB(gpu.memory_mib))}GB`, + // header: t('offer.memory_mib'), + content: (gpu: IGpu) => `${round(convertMiBToGB(gpu.memory_mib))}GB:${renderRange(gpu.count)}`, width: 50, }, { id: 'price', - header: t('offer.price'), - content: (gpu: IGpu) => {renderRange(gpu.price) ?? '-'}, - width: 50, - }, - { - id: 'count', - header: t('offer.count'), - content: (gpu: IGpu) => renderRange(gpu.count) ?? '-', + // header: t('offer.price'), + content: (gpu: IGpu) => ${renderRange(gpu.price) ?? '-'}, width: 50, }, + // { + // id: 'count', + // header: t('offer.count'), + // content: (gpu: IGpu) => renderRange(gpu.count) ?? '-', + // width: 50, + // }, !groupByBackend && { id: 'backends', - header: t('offer.backend_plural'), + // header: t('offer.backend_plural'), content: (gpu: IGpu) => gpu.backends?.join(', ') ?? '-', width: 50, }, groupByBackend && { id: 'backend', - header: t('offer.backend'), + // header: t('offer.backend'), content: (gpu: IGpu) => gpu.backend ?? '-', width: 50, }, @@ -168,7 +168,7 @@ export const OfferList: React.FC = ({ withSearchParams, onChange // }, { id: 'spot', - header: t('offer.spot'), + // header: t('offer.spot'), content: (gpu: IGpu) => gpu.spot.join(', ') ?? '-', width: 50, }, @@ -189,9 +189,10 @@ export const OfferList: React.FC = ({ withSearchParams, onChange {gpu.name}, + header: (gpu) => gpu.name, sections, }} loading={isLoading || isFetching}