diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx new file mode 100644 index 000000000..36bfa2379 --- /dev/null +++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx @@ -0,0 +1,244 @@ +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 { copyToClipboard } from 'libs'; + +import styles from './styles.module.scss'; + +const UvInstallCommand = 'uv tool install dstack -U'; +const PipInstallCommand = 'pip install dstack -U'; + +export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) => { + const { t } = useTranslation(); + + const getAttachCommand = (runData: IRun) => { + const attachCommand = `dstack attach ${runData.run_spec.run_name}`; + + const copyAttachCommand = () => { + copyToClipboard(attachCommand); + }; + + return [attachCommand, copyAttachCommand] as const; + }; + + const getSSHCommand = (runData: IRun) => { + const sshCommand = `ssh ${runData.run_spec.run_name}`; + + const copySSHCommand = () => { + copyToClipboard(sshCommand); + }; + + return [sshCommand, copySSHCommand] as const; + }; + + const [activeStepIndex, setActiveStepIndex] = React.useState(0); + const [attachCommand, copyAttachCommand] = getAttachCommand(run); + const [sshCommand, copySSHCommand] = getSSHCommand(run); + + const openInIDEUrl = `${run.run_spec.configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}/${run.run_spec.working_dir || 'workflow'}`; + + 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={() => window.open(openInIDEUrl, '_blank')} + submitButtonText="Open in VS Code" + 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')} + + } + > +
+
+ + ), + }, + ]} + /> +
+
+
+ ), + isOptional: true, + }, + { + title: 'Open', + description: 'After the CLI is attached, you can open the dev environment in VS Code.', + content: ( + + + + + + +
+ {sshCommand} + +
+ + {t('common.copied')} + + } + > +
+
+
+
+
+ ), + isOptional: true, + }, + ]} + /> + )} + + {run.status === 'running' && ( + + + Waiting for the run to start. + + )} +
+ ); +}; diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/styles.module.scss b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/styles.module.scss new file mode 100644 index 000000000..d01f54b26 --- /dev/null +++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/styles.module.scss @@ -0,0 +1,13 @@ +.codeWrapper { + position: relative; + + .code { + padding: 16px 12px; + } + + .copy { + position: absolute; + top: 10px; + right: 8px; + } +} diff --git a/frontend/src/pages/Runs/Details/RunDetails/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/index.tsx index f5613a288..aab63a3ed 100644 --- a/frontend/src/pages/Runs/Details/RunDetails/index.tsx +++ b/frontend/src/pages/Runs/Details/RunDetails/index.tsx @@ -8,11 +8,12 @@ import { Box, ColumnLayout, Container, Header, Loader, NavigateLink, StatusIndic import { DATE_TIME_FORMAT } from 'consts'; import { getRunError, getRunPriority, getRunStatusMessage, getStatusIconColor, getStatusIconType } from 'libs/run'; +import { ROUTES } from 'routes'; import { useGetRunQuery } from 'services/run'; import { finishedRunStatuses } from 'pages/Runs/constants'; +import { runIsStopped } from 'pages/Runs/utils'; -import { ROUTES } from '../../../../routes'; import { getRunListItemBackend, getRunListItemInstanceId, @@ -23,6 +24,7 @@ import { getRunListItemSpot, } from '../../List/helpers'; import { JobList } from '../Jobs/List'; +import { ConnectToRunWithDevEnvConfiguration } from './ConnectToRunWithDevEnvConfiguration'; export const RunDetails = () => { const { t } = useTranslation(); @@ -49,6 +51,7 @@ export const RunDetails = () => { const status = finishedRunStatuses.includes(runData.status) ? (runData.latest_job_submission?.status ?? runData.status) : runData.status; + const terminationReason = finishedRunStatuses.includes(runData.status) ? runData.latest_job_submission?.termination_reason : null; @@ -168,6 +171,10 @@ export const RunDetails = () => { )} + {runData.run_spec.configuration.type === 'dev-environment' && !runIsStopped(runData.status) && ( + + )} + {runData.jobs.length > 1 && (