From ce4735a7c205fb0766fc1d38152631626ca9cec7 Mon Sep 17 00:00:00 2001 From: peterschmidt85 Date: Sat, 28 Feb 2026 01:00:36 +0100 Subject: [PATCH 1/2] [UI] Add Connect section for tasks with ports and fix launch wizard issues Add a Connect component for task runs that expose ports, with a two-step wizard (Attach + Open). Single port shows a simple button; multiple ports use a ButtonDropdown to select and open any forwarded port. Also fix two launch wizard issues: - Add `ports` to the supported YAML fields whitelist - Stop setting `docker: true` when selecting a Docker image (unrelated field) - Fix `IJobSpec.app_specs` type from singular to array - Show `-` for empty configuration path on run details Made-with: Cursor --- .../RunDetails/ConnectToTaskRun/index.tsx | 254 ++++++++++++++++++ .../pages/Runs/Details/RunDetails/index.tsx | 8 +- .../components/ParamsWizardStep/index.tsx | 2 - .../Runs/Launch/hooks/useGenerateYaml.ts | 1 - .../Launch/hooks/useGetRunSpecFromYaml.ts | 1 + frontend/src/types/run.d.ts | 2 +- 6 files changed, 263 insertions(+), 5 deletions(-) create mode 100644 frontend/src/pages/Runs/Details/RunDetails/ConnectToTaskRun/index.tsx diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToTaskRun/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/ConnectToTaskRun/index.tsx new file mode 100644 index 000000000..de9a8cc6c --- /dev/null +++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToTaskRun/index.tsx @@ -0,0 +1,254 @@ +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + Alert, + Box, + Button, + ButtonDropdown, + Code, + Container, + ExpandableSection, + Header, + Popover, + SpaceBetween, + StatusIndicator, + Tabs, + Wizard, +} from 'components'; + +import { copyToClipboard } from 'libs'; + +import { useConfigProjectCliCommand } from 'pages/Project/hooks/useConfigProjectCliComand'; + +import styles from '../ConnectToRunWithDevEnvConfiguration/styles.module.scss'; + +const UvInstallCommand = 'uv tool install dstack -U'; +const PipInstallCommand = 'pip install dstack -U'; + +const getPort = (spec: IAppSpec): number => spec.map_to_port ?? spec.port; + +export const ConnectToTaskRun: FC<{ run: IRun }> = ({ run }) => { + const { t } = useTranslation(); + + const attachCommand = `dstack attach ${run.run_spec.run_name} --logs`; + const appSpecs = run.jobs[0]?.job_spec?.app_specs ?? []; + + const [activeStepIndex, setActiveStepIndex] = React.useState(0); + const [selectedPort, setSelectedPort] = React.useState(() => getPort(appSpecs[0])); + const [configCliCommand, copyCliCommand] = useConfigProjectCliCommand({ projectName: run.project_name }); + + const openPort = (port: number) => window.open(`http://127.0.0.1:${port}`, '_blank'); + + return ( + +
Connect
+ + {run.status === 'running' && ( + `Step ${stepNumber}`, + collapsedStepsLabel: (stepNumber, stepsCount) => `Step ${stepNumber} of ${stepsCount}`, + skipToButtonLabel: (step) => `Skip to ${step.title}`, + navigationAriaLabel: 'Steps', + previousButton: 'Previous', + nextButton: 'Next', + optional: 'required', + }} + onNavigate={({ detail }) => setActiveStepIndex(detail.requestedStepIndex)} + activeStepIndex={activeStepIndex} + onSubmit={() => openPort(selectedPort)} + submitButtonText={appSpecs.length === 1 ? 'Open port' : `Open port ${selectedPort}`} + allowSkipTo + steps={[ + { + title: 'Attach', + content: ( + + To access this run, first you need to attach to it. +
+ {attachCommand} + +
+ {t('common.copied')}} + > +
+
+ + + + + To use dstack, install the CLI on your local machine. + + +
+ {UvInstallCommand} + +
+ + {t('common.copied')} + + } + > +
+
+ + ), + }, + { + label: 'pip', + id: 'pip', + content: ( + <> +
+ {PipInstallCommand} + +
+ + {t('common.copied')} + + } + > +
+
+ + ), + }, + ]} + /> + + And then configure the project. + +
+ {configCliCommand} + +
+ + {t('common.copied')} + + } + > +
+
+
+
+
+ ), + isOptional: true, + }, + { + title: 'Open', + description: 'After the CLI is attached, you can open the forwarded ports.', + content: ( + + {appSpecs.length === 1 ? ( + + ) : ( + openPort(selectedPort), + }} + items={appSpecs.map((spec) => { + const port = getPort(spec); + + return { + id: String(port), + text: `Port ${port}`, + external: true, + }; + })} + onItemClick={({ detail }) => { + const port = Number(detail.id); + setSelectedPort(port); + openPort(port); + }} + /> + )} + + ), + isOptional: true, + }, + ]} + /> + )} + + {run.status !== 'running' && ( + + + Waiting for the run to start. + + )} +
+ ); +}; diff --git a/frontend/src/pages/Runs/Details/RunDetails/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/index.tsx index 9a34b6af7..6eb80db63 100644 --- a/frontend/src/pages/Runs/Details/RunDetails/index.tsx +++ b/frontend/src/pages/Runs/Details/RunDetails/index.tsx @@ -36,6 +36,7 @@ import { EventsList } from '../Events/List'; import { JobList } from '../Jobs/List'; import { ConnectToRunWithDevEnvConfiguration } from './ConnectToRunWithDevEnvConfiguration'; import { ConnectToServiceRun } from './ConnectToServiceRun'; +import { ConnectToTaskRun } from './ConnectToTaskRun'; export const RunDetails = () => { const { t } = useTranslation(); @@ -102,7 +103,7 @@ export const RunDetails = () => {
{t('projects.run.configuration')} -
{runData.run_spec.configuration_path}
+
{runData.run_spec.configuration_path || '-'}
@@ -211,6 +212,11 @@ export const RunDetails = () => { )} + {runData.run_spec.configuration.type === 'task' && !runIsStopped(runData.status) && + (runData.jobs[0]?.job_spec?.app_specs?.length ?? 0) > 0 && ( + + )} + {runData.jobs.length > 1 && ( = ({ formMethods, if (detail.activeTabId === DockerPythonTabs.PYTHON) { setValue(FORM_FIELD_NAMES.image, ''); } - - setValue(FORM_FIELD_NAMES.docker, detail.activeTabId === DockerPythonTabs.DOCKER); }; const defaultPassword = generateSecurePassword(20); diff --git a/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts b/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts index def3962aa..f153a43e6 100644 --- a/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts +++ b/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts @@ -32,7 +32,6 @@ export const useGenerateYaml = ({ formValues, configuration, envParam, backends ...(name ? { name } : {}), ...(ide ? { ide } : {}), - ...(docker ? { docker } : {}), ...(image ? { image } : {}), ...(python ? { python } : {}), ...(envEntries.length > 0 ? { env: envEntries } : {}), diff --git a/frontend/src/pages/Runs/Launch/hooks/useGetRunSpecFromYaml.ts b/frontend/src/pages/Runs/Launch/hooks/useGetRunSpecFromYaml.ts index 4d7b6d2cf..3af1e3345 100644 --- a/frontend/src/pages/Runs/Launch/hooks/useGetRunSpecFromYaml.ts +++ b/frontend/src/pages/Runs/Launch/hooks/useGetRunSpecFromYaml.ts @@ -36,6 +36,7 @@ const supportedFields: (keyof TDevEnvironmentConfiguration | keyof TServiceConfi 'repos', 'auth', 'commands', + 'ports', 'port', 'gateway', 'https', diff --git a/frontend/src/types/run.d.ts b/frontend/src/types/run.d.ts index 928a02280..b72312daf 100644 --- a/frontend/src/types/run.d.ts +++ b/frontend/src/types/run.d.ts @@ -241,7 +241,7 @@ declare interface IJobProbe { } declare interface IJobSpec { - app_specs?: IAppSpec; + app_specs?: IAppSpec[]; commands: string[]; env?: { [key: string]: string }; home_dir?: string; From a27e242a450a5df7413bda3337733ddfc9d3fc40 Mon Sep 17 00:00:00 2001 From: Andrey Cheptsov Date: Wed, 4 Mar 2026 14:58:54 +0100 Subject: [PATCH 2/2] [UI] Refine Connect wizard interactions Address PR feedback by making Connect panels collapsible across run types, using Done to collapse wizard flows, and fixing task Open-step behavior when map_to_port is unset. Made-with: Cursor --- .../pages/Project/Details/Settings/index.tsx | 2 +- .../index.tsx | 40 +++--- .../RunDetails/ConnectToServiceRun/index.tsx | 85 ++++++++++-- .../RunDetails/ConnectToTaskRun/index.tsx | 125 ++++++++++-------- frontend/src/pages/Runs/Launch/constants.tsx | 10 +- .../Runs/Launch/hooks/useGenerateYaml.ts | 3 +- frontend/src/pages/Runs/Launch/types.ts | 1 - 7 files changed, 165 insertions(+), 101 deletions(-) diff --git a/frontend/src/pages/Project/Details/Settings/index.tsx b/frontend/src/pages/Project/Details/Settings/index.tsx index 7d2b9bd3f..45e5bb3a9 100644 --- a/frontend/src/pages/Project/Details/Settings/index.tsx +++ b/frontend/src/pages/Project/Details/Settings/index.tsx @@ -230,7 +230,7 @@ export const ProjectSettings: React.FC = () => { onNavigate={({ detail }) => setActiveStepIndex(detail.requestedStepIndex)} activeStepIndex={activeStepIndex} onSubmit={() => setIsExpandedCliSection(false)} - submitButtonText="Dismiss" + submitButtonText="Done" allowSkipTo={true} steps={[ { diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx index 54d03c388..bcd618a0d 100644 --- a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx +++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx @@ -1,20 +1,7 @@ import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { - Alert, - Box, - Button, - Code, - Container, - ExpandableSection, - Header, - Popover, - SpaceBetween, - StatusIndicator, - Tabs, - Wizard, -} from 'components'; +import { Alert, Box, Button, Code, ExpandableSection, Popover, SpaceBetween, StatusIndicator, Tabs, Wizard } from 'components'; import { copyToClipboard } from 'libs'; @@ -28,6 +15,7 @@ const PipInstallCommand = 'pip install dstack -U'; export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) => { const { t } = useTranslation(); + const [isExpandedConnectSection, setIsExpandedConnectSection] = React.useState(true); const getAttachCommand = (runData: IRun) => { const attachCommand = `dstack attach ${runData.run_spec.run_name} --logs`; @@ -62,9 +50,19 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) const [configCliCommand, copyCliCommand] = useConfigProjectCliCommand({ projectName: run.project_name }); return ( - -
Connect
- + setIsExpandedConnectSection(detail.expanded)} + headerActions={ + - ) : ( - openPort(selectedPort), - }} - items={appSpecs.map((spec) => { - const port = getPort(spec); - - return { - id: String(port), - text: `Port ${port}`, - external: true, - }; - })} - onItemClick={({ detail }) => { - const port = Number(detail.id); - setSelectedPort(port); - openPort(port); - }} - /> - )} - - ), - isOptional: true, - }, + ...(mappedAppSpecs.length > 0 + ? [ + { + title: 'Open', + description: 'After the CLI is attached, use the forwarded localhost URLs.', + content: ( + + {mappedAppSpecs.map((spec) => { + const mappedPort = getMappedPort(spec)!; + const localUrl = `http://127.0.0.1:${mappedPort}`; + + return ( + + {t('common.copied')} + + } + > +