Skip to content

Commit ec6cf01

Browse files
[UI] A prototype of the "Connect" section to show on the running dev environment page (WIP) (#3184)
* [UI] A prototype of the "Connect" section to show on the running dev environment page (WIP) * [UI] A prototype of the "Connect" section to show on the running dev environment page (WIP) * [UI] A prototype of the "Connect" section to show on the running dev environment page (WIP) --------- Co-authored-by: Oleg Vavilov <vavilovolegik@gmail.com>
1 parent 05d1ad1 commit ec6cf01

3 files changed

Lines changed: 265 additions & 1 deletion

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import React, { FC } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
4+
import {
5+
Alert,
6+
Box,
7+
Button,
8+
Code,
9+
Container,
10+
ExpandableSection,
11+
Header,
12+
Popover,
13+
SpaceBetween,
14+
StatusIndicator,
15+
Tabs,
16+
Wizard,
17+
} from 'components';
18+
19+
import { copyToClipboard } from 'libs';
20+
21+
import styles from './styles.module.scss';
22+
23+
const UvInstallCommand = 'uv tool install dstack -U';
24+
const PipInstallCommand = 'pip install dstack -U';
25+
26+
export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) => {
27+
const { t } = useTranslation();
28+
29+
const getAttachCommand = (runData: IRun) => {
30+
const attachCommand = `dstack attach ${runData.run_spec.run_name}`;
31+
32+
const copyAttachCommand = () => {
33+
copyToClipboard(attachCommand);
34+
};
35+
36+
return [attachCommand, copyAttachCommand] as const;
37+
};
38+
39+
const getSSHCommand = (runData: IRun) => {
40+
const sshCommand = `ssh ${runData.run_spec.run_name}`;
41+
42+
const copySSHCommand = () => {
43+
copyToClipboard(sshCommand);
44+
};
45+
46+
return [sshCommand, copySSHCommand] as const;
47+
};
48+
49+
const [activeStepIndex, setActiveStepIndex] = React.useState(0);
50+
const [attachCommand, copyAttachCommand] = getAttachCommand(run);
51+
const [sshCommand, copySSHCommand] = getSSHCommand(run);
52+
53+
const openInIDEUrl = `${run.run_spec.configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}/${run.run_spec.working_dir || 'workflow'}`;
54+
55+
return (
56+
<Container>
57+
<Header variant="h2">Connect</Header>
58+
59+
{run.status === 'running' && (
60+
<Wizard
61+
i18nStrings={{
62+
stepNumberLabel: (stepNumber) => `Step ${stepNumber}`,
63+
collapsedStepsLabel: (stepNumber, stepsCount) => `Step ${stepNumber} of ${stepsCount}`,
64+
skipToButtonLabel: (step) => `Skip to ${step.title}`,
65+
navigationAriaLabel: 'Steps',
66+
previousButton: 'Previous',
67+
nextButton: 'Next',
68+
optional: 'required',
69+
}}
70+
onNavigate={({ detail }) => setActiveStepIndex(detail.requestedStepIndex)}
71+
activeStepIndex={activeStepIndex}
72+
onSubmit={() => window.open(openInIDEUrl, '_blank')}
73+
submitButtonText="Open in VS Code"
74+
allowSkipTo
75+
steps={[
76+
{
77+
title: 'Attach',
78+
content: (
79+
<SpaceBetween size="s">
80+
<Box>To access this run, first you need to attach to it.</Box>
81+
<div className={styles.codeWrapper}>
82+
<Code className={styles.code}>{attachCommand}</Code>
83+
84+
<div className={styles.copy}>
85+
<Popover
86+
dismissButton={false}
87+
position="top"
88+
size="small"
89+
triggerType="custom"
90+
content={<StatusIndicator type="success">{t('common.copied')}</StatusIndicator>}
91+
>
92+
<Button
93+
formAction="none"
94+
iconName="copy"
95+
variant="normal"
96+
onClick={copyAttachCommand}
97+
/>
98+
</Popover>
99+
</div>
100+
</div>
101+
102+
<ExpandableSection headerText="No CLI installed?">
103+
<SpaceBetween size="s">
104+
<Box />
105+
<Box>To use dstack, install the CLI on your local machine.</Box>
106+
107+
<Tabs
108+
variant="container"
109+
tabs={[
110+
{
111+
label: 'uv',
112+
id: 'uv',
113+
content: (
114+
<>
115+
<div className={styles.codeWrapper}>
116+
<Code className={styles.code}>{UvInstallCommand}</Code>
117+
118+
<div className={styles.copy}>
119+
<Popover
120+
dismissButton={false}
121+
position="top"
122+
size="small"
123+
triggerType="custom"
124+
content={
125+
<StatusIndicator type="success">
126+
{t('common.copied')}
127+
</StatusIndicator>
128+
}
129+
>
130+
<Button
131+
formAction="none"
132+
iconName="copy"
133+
variant="normal"
134+
onClick={() =>
135+
copyToClipboard(UvInstallCommand)
136+
}
137+
/>
138+
</Popover>
139+
</div>
140+
</div>
141+
</>
142+
),
143+
},
144+
{
145+
label: 'pip',
146+
id: 'pip',
147+
content: (
148+
<>
149+
<div className={styles.codeWrapper}>
150+
<Code className={styles.code}>{PipInstallCommand}</Code>
151+
152+
<div className={styles.copy}>
153+
<Popover
154+
dismissButton={false}
155+
position="top"
156+
size="small"
157+
triggerType="custom"
158+
content={
159+
<StatusIndicator type="success">
160+
{t('common.copied')}
161+
</StatusIndicator>
162+
}
163+
>
164+
<Button
165+
formAction="none"
166+
iconName="copy"
167+
variant="normal"
168+
onClick={() =>
169+
copyToClipboard(PipInstallCommand)
170+
}
171+
/>
172+
</Popover>
173+
</div>
174+
</div>
175+
</>
176+
),
177+
},
178+
]}
179+
/>
180+
</SpaceBetween>
181+
</ExpandableSection>
182+
</SpaceBetween>
183+
),
184+
isOptional: true,
185+
},
186+
{
187+
title: 'Open',
188+
description: 'After the CLI is attached, you can open the dev environment in VS Code.',
189+
content: (
190+
<SpaceBetween size="s">
191+
<Button
192+
variant="primary"
193+
external={true}
194+
onClick={() => window.open(openInIDEUrl, '_blank')}
195+
>
196+
Open in VS Code
197+
</Button>
198+
199+
<ExpandableSection headerText="Need plain SSH?">
200+
<SpaceBetween size="s">
201+
<Box />
202+
<div className={styles.codeWrapper}>
203+
<Code className={styles.code}>{sshCommand}</Code>
204+
205+
<div className={styles.copy}>
206+
<Popover
207+
dismissButton={false}
208+
position="top"
209+
size="small"
210+
triggerType="custom"
211+
content={
212+
<StatusIndicator type="success">
213+
{t('common.copied')}
214+
</StatusIndicator>
215+
}
216+
>
217+
<Button
218+
formAction="none"
219+
iconName="copy"
220+
variant="normal"
221+
onClick={() => copySSHCommand()}
222+
/>
223+
</Popover>
224+
</div>
225+
</div>
226+
</SpaceBetween>
227+
</ExpandableSection>
228+
</SpaceBetween>
229+
),
230+
isOptional: true,
231+
},
232+
]}
233+
/>
234+
)}
235+
236+
{run.status === 'running' && (
237+
<SpaceBetween size="s">
238+
<Box />
239+
<Alert type="info">Waiting for the run to start.</Alert>
240+
</SpaceBetween>
241+
)}
242+
</Container>
243+
);
244+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.codeWrapper {
2+
position: relative;
3+
4+
.code {
5+
padding: 16px 12px;
6+
}
7+
8+
.copy {
9+
position: absolute;
10+
top: 10px;
11+
right: 8px;
12+
}
13+
}

frontend/src/pages/Runs/Details/RunDetails/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import { Box, ColumnLayout, Container, Header, Loader, NavigateLink, StatusIndic
88

99
import { DATE_TIME_FORMAT } from 'consts';
1010
import { getRunError, getRunPriority, getRunStatusMessage, getStatusIconColor, getStatusIconType } from 'libs/run';
11+
import { ROUTES } from 'routes';
1112
import { useGetRunQuery } from 'services/run';
1213

1314
import { finishedRunStatuses } from 'pages/Runs/constants';
15+
import { runIsStopped } from 'pages/Runs/utils';
1416

15-
import { ROUTES } from '../../../../routes';
1617
import {
1718
getRunListItemBackend,
1819
getRunListItemInstanceId,
@@ -23,6 +24,7 @@ import {
2324
getRunListItemSpot,
2425
} from '../../List/helpers';
2526
import { JobList } from '../Jobs/List';
27+
import { ConnectToRunWithDevEnvConfiguration } from './ConnectToRunWithDevEnvConfiguration';
2628

2729
export const RunDetails = () => {
2830
const { t } = useTranslation();
@@ -49,6 +51,7 @@ export const RunDetails = () => {
4951
const status = finishedRunStatuses.includes(runData.status)
5052
? (runData.latest_job_submission?.status ?? runData.status)
5153
: runData.status;
54+
5255
const terminationReason = finishedRunStatuses.includes(runData.status)
5356
? runData.latest_job_submission?.termination_reason
5457
: null;
@@ -168,6 +171,10 @@ export const RunDetails = () => {
168171
)}
169172
</Container>
170173

174+
{runData.run_spec.configuration.type === 'dev-environment' && !runIsStopped(runData.status) && (
175+
<ConnectToRunWithDevEnvConfiguration run={runData} />
176+
)}
177+
171178
{runData.jobs.length > 1 && (
172179
<JobList
173180
projectName={paramProjectName}

0 commit comments

Comments
 (0)