Skip to content

Commit a49d34c

Browse files
[UI] Run Wizard. Added docker, python and repos fields (#3252)
* [UI] Run Wizard. Added docker, python and repos fields * [UI] Run Wizard. Added docker, python and repos fields * [UI] Run Wizard. Added docker, python and repos fields * [UI] Run Wizard. Added docker, python and repos fields * [UI] Run Wizard. Added docker, python and repos fields - [x] `Connect` section is empty when provisioning - [x] Add `--logs` to the `dstack attach` command * [UI] Run Wizard. Added repo initialization * [UI] Run Wizard. Added working dir, refactoring getting repo * [UI] Run Wizard. Added working dir, refactoring getting repo Minor text edits * [UI] Run Wizard. Added working dir, refactoring getting repo Minor styling edits * [UI] Run Wizard. Await initializing repo * [UI] Run Wizard. Styles was fixed --------- Co-authored-by: peterschmidt85 <[email protected]>
1 parent 24defbb commit a49d34c

File tree

16 files changed

+549
-144
lines changed

16 files changed

+549
-144
lines changed

frontend/src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export const API = {
7070
// Repos
7171
REPOS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/repos`,
7272
REPOS_LIST: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/list`,
73+
GET_REPO: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/get`,
74+
INIT_REPO: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/init`,
7375

7476
// Runs
7577
RUNS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/runs`,

frontend/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export { default as HelpPanel } from '@cloudscape-design/components/help-panel';
4646
export type { HelpPanelProps } from '@cloudscape-design/components/help-panel';
4747
export { default as TextContent } from '@cloudscape-design/components/text-content';
4848
export { default as Toggle } from '@cloudscape-design/components/toggle';
49+
export type { ToggleProps } from '@cloudscape-design/components/toggle';
4950
export { default as Modal } from '@cloudscape-design/components/modal';
5051
export { default as TutorialPanel } from '@cloudscape-design/components/tutorial-panel';
5152
export type { TutorialPanelProps } from '@cloudscape-design/components/tutorial-panel';

frontend/src/libs/repo.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
function bufferToHex(buffer: ArrayBuffer): string {
2+
return Array.from(new Uint8Array(buffer))
3+
.map((b) => b.toString(16).padStart(2, '0'))
4+
.join('');
5+
}
6+
7+
export async function slugify(prefix: string, unique_key: string, hash_size: number = 8): Promise<string> {
8+
const encoder = new TextEncoder();
9+
const data = encoder.encode(unique_key);
10+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
11+
const fullHash = bufferToHex(hashBuffer);
12+
return `${prefix}-${fullHash.substring(0, hash_size)}`;
13+
}
14+
15+
export function getRepoName(url: string): string {
16+
const cleaned = url
17+
.replace(/^https?:\/\//i, '')
18+
.replace(/:\/(\S*)/, '')
19+
.replace(/\/+$/, '')
20+
.replace(/\.git$/, '');
21+
const parts = cleaned.split('/').filter(Boolean);
22+
return parts.length ? parts[parts.length - 1] : '';
23+
}
24+
25+
export function getPathWithoutProtocol(url: string): string {
26+
return url.replace(/^https?:\/\//i, '');
27+
}
28+
29+
export function getRepoUrlWithOutDir(url: string): string {
30+
const parsedUrl = url.match(/^([^:]+(?::[^:]+)?)/)?.[1];
31+
32+
return parsedUrl ?? url;
33+
}
34+
35+
export function getRepoDirFromUrl(url: string): string | undefined {
36+
const dirName = url.replace(/^https?:\/\//i, '').match(/:\/(\S*)/)?.[1];
37+
38+
return dirName ? `/${dirName}` : undefined;
39+
}

frontend/src/locale/en.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,10 +476,32 @@
476476
"offer": "Offer",
477477
"offer_description": "Select an offer for the dev environment.",
478478
"name": "Name",
479-
"name_description": "The name of the run. If not specified, the name will be generated automatically.",
479+
"name_description": "The name of the run, e.g. 'my-dev-env'",
480+
"name_constraint": "If not specified, generated automatically",
480481
"name_placeholder": "Optional",
481482
"ide": "IDE",
482483
"ide_description": "Select which IDE would you like to use with the dev environment.",
484+
"docker": "Docker",
485+
"docker_image": "Image",
486+
"docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'",
487+
"docker_image_constraint": "The image must be public",
488+
"docker_image_placeholder": "Required",
489+
"python": "Python",
490+
"python_description": "The version of Python, e.g. '3.12'",
491+
"python_placeholder": "Optional",
492+
"repo": "Repo",
493+
"working_dir": "Working dir",
494+
"working_dir_description": "The absolute path to the working directory inside the container, e.g. '/home/user/project'",
495+
"working_dir_placeholder": "Optional",
496+
"working_dir_constraint": "By default, set to '/workflow'",
497+
"repo_url": "URL",
498+
"repo_url_description": "A URL of a Git repository, e.g. 'https://github.com/user/repo'",
499+
"repo_url_constraint": "The repo must be public",
500+
"repo_url_placeholder": "Required",
501+
"repo_path": "Path",
502+
"repo_path_description": "The path inside the container to clone the repository, e.g. '/home/user/project'",
503+
"repo_path_placeholder": "Optional",
504+
"repo_path_constraint": "By default, set to '/workflow'",
483505
"config": "Configuration file",
484506
"config_description": "Review the configuration file and adjust it if needed. Click Info for examples.",
485507
"success_notification": "The run is submitted!"

frontend/src/pages/Offers/List/helpers.ts renamed to frontend/src/pages/Offers/List/helpers.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import React from 'react';
2+
13
import { RequestParam } from '../../../libs/filters';
24

5+
import styles from './styles.module.scss';
6+
37
const rangeSeparator = '..';
48

59
export function convertMiBToGB(mib: number) {
@@ -46,6 +50,20 @@ export const renderRange = (range: { min?: number; max?: number }) => {
4650
return range.min?.toString() ?? range.max?.toString();
4751
};
4852

53+
export const renderRangeJSX = (range: { min?: number; max?: number }) => {
54+
if (typeof range.min === 'number' && typeof range.max === 'number' && range.max != range.min) {
55+
return (
56+
<>
57+
{round(range.min)}
58+
<span className={styles.greyText}>{rangeSeparator}</span>
59+
{round(range.max)}
60+
</>
61+
);
62+
}
63+
64+
return range.min?.toString() ?? range.max?.toString();
65+
};
66+
4967
export const rangeToObject = (range: RequestParam): { min?: number; max?: number } | undefined => {
5068
if (!range) return;
5169

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

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import React, { useEffect, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33

4-
import { Cards, CardsProps, Link, MultiselectCSD, PropertyFilter, StatusIndicator } from 'components';
4+
import { Cards, CardsProps, MultiselectCSD, PropertyFilter, StatusIndicator } from 'components';
55

66
import { useCollection } from 'hooks';
77
import { useGetGpusListQuery } from 'services/gpu';
88

99
import { useEmptyMessages } from './hooks/useEmptyMessages';
1010
import { useFilters } from './hooks/useFilters';
11-
import { convertMiBToGB, rangeToObject, renderRange, round } from './helpers';
11+
import { convertMiBToGB, rangeToObject, renderRange, renderRangeJSX, round } from './helpers';
1212

1313
import styles from './styles.module.scss';
1414

@@ -132,31 +132,37 @@ export const OfferList: React.FC<OfferListProps> = ({ withSearchParams, onChange
132132
const sections = [
133133
{
134134
id: 'memory_mib',
135-
header: t('offer.memory_mib'),
136-
content: (gpu: IGpu) => `${round(convertMiBToGB(gpu.memory_mib))}GB`,
135+
// header: t('offer.memory_mib'),
136+
content: (gpu: IGpu) => (
137+
<div>
138+
{round(convertMiBToGB(gpu.memory_mib))}GB
139+
<span className={styles.greyText}>:</span>
140+
{renderRange(gpu.count)}
141+
</div>
142+
),
137143
width: 50,
138144
},
139145
{
140146
id: 'price',
141-
header: t('offer.price'),
142-
content: (gpu: IGpu) => <span className={styles.greenText}>{renderRange(gpu.price) ?? '-'}</span>,
143-
width: 50,
144-
},
145-
{
146-
id: 'count',
147-
header: t('offer.count'),
148-
content: (gpu: IGpu) => renderRange(gpu.count) ?? '-',
147+
// header: t('offer.price'),
148+
content: (gpu: IGpu) => <span className={styles.greenText}>${renderRangeJSX(gpu.price) ?? '-'}</span>,
149149
width: 50,
150150
},
151+
// {
152+
// id: 'count',
153+
// header: t('offer.count'),
154+
// content: (gpu: IGpu) => renderRange(gpu.count) ?? '-',
155+
// width: 50,
156+
// },
151157
!groupByBackend && {
152158
id: 'backends',
153-
header: t('offer.backend_plural'),
159+
// header: t('offer.backend_plural'),
154160
content: (gpu: IGpu) => gpu.backends?.join(', ') ?? '-',
155161
width: 50,
156162
},
157163
groupByBackend && {
158164
id: 'backend',
159-
header: t('offer.backend'),
165+
// header: t('offer.backend'),
160166
content: (gpu: IGpu) => gpu.backend ?? '-',
161167
width: 50,
162168
},
@@ -168,7 +174,7 @@ export const OfferList: React.FC<OfferListProps> = ({ withSearchParams, onChange
168174
// },
169175
{
170176
id: 'spot',
171-
header: t('offer.spot'),
177+
// header: t('offer.spot'),
172178
content: (gpu: IGpu) => gpu.spot.join(', ') ?? '-',
173179
width: 50,
174180
},
@@ -189,9 +195,10 @@ export const OfferList: React.FC<OfferListProps> = ({ withSearchParams, onChange
189195
<Cards
190196
{...collectionProps}
191197
{...props}
198+
entireCardClickable
192199
items={items}
193200
cardDefinition={{
194-
header: (gpu) => <Link>{gpu.name}</Link>,
201+
header: (gpu) => gpu.name,
195202
sections,
196203
}}
197204
loading={isLoading || isFetching}

frontend/src/pages/Offers/List/styles.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
}
1818
}
1919

20+
.greyText {
21+
color: awsui.$color-text-status-inactive;
22+
}
23+
2024
.greenText {
2125
color: awsui.$color-text-status-success;
2226
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import React from 'react';
2+
3+
import { IRunEnvironmentFormKeys } from './types';
24
export const CONFIG_INFO = {
35
header: <h2>Credits history</h2>,
46
body: (
@@ -7,3 +9,17 @@ export const CONFIG_INFO = {
79
</>
810
),
911
};
12+
13+
export const FORM_FIELD_NAMES = {
14+
offer: 'offer',
15+
name: 'name',
16+
ide: 'ide',
17+
config_yaml: 'config_yaml',
18+
docker: 'docker',
19+
image: 'image',
20+
python: 'python',
21+
repo_enabled: 'repo_enabled',
22+
repo_url: 'repo_url',
23+
repo_path: 'repo_path',
24+
working_dir: 'working_dir',
25+
} as const satisfies Record<IRunEnvironmentFormKeys, IRunEnvironmentFormKeys>;

frontend/src/pages/Runs/CreateDevEnvironment/helpers/getRunSpecFromYaml.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useMemo } from 'react';
2+
import jsYaml from 'js-yaml';
3+
4+
import { convertMiBToGB, renderRange, round } from 'pages/Offers/List/helpers';
5+
6+
import { IRunEnvironmentFormValues } from '../types';
7+
8+
export type UseGenerateYamlArgs = {
9+
formValues: IRunEnvironmentFormValues;
10+
};
11+
12+
export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => {
13+
return useMemo(() => {
14+
if (!formValues.offer || !formValues.ide) {
15+
return '';
16+
}
17+
18+
const { name, ide, image, python, offer, docker, repo_url, repo_path, working_dir } = formValues;
19+
20+
return jsYaml.dump({
21+
type: 'dev-environment',
22+
...(name ? { name } : {}),
23+
ide,
24+
...(docker ? { docker } : {}),
25+
...(image ? { image } : {}),
26+
...(python ? { python } : {}),
27+
28+
resources: {
29+
gpu: `${offer.name}:${round(convertMiBToGB(offer.memory_mib))}GB:${renderRange(offer.count)}`,
30+
},
31+
32+
...(repo_url || repo_path
33+
? {
34+
repos: [[repo_url?.trim(), repo_path?.trim()].filter(Boolean).join(':')],
35+
}
36+
: {}),
37+
38+
...(working_dir ? { working_dir } : {}),
39+
backends: offer.backends,
40+
spot_policy: 'auto',
41+
});
42+
}, [
43+
formValues.name,
44+
formValues.ide,
45+
formValues.offer,
46+
formValues.python,
47+
formValues.image,
48+
formValues.repo_url,
49+
formValues.repo_path,
50+
formValues.working_dir,
51+
]);
52+
};

0 commit comments

Comments
 (0)