Skip to content

Commit 449e584

Browse files
committed
[UI] Run Wizard. Added repo initialization
1 parent b8b6e47 commit 449e584

8 files changed

Lines changed: 273 additions & 108 deletions

File tree

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/libs/repo.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 getRepoDirFromUrl(url: string): string | undefined {
30+
const dirName = url.replace(/^https?:\/\//i, '').match(/:\/(\S*)/)?.[1];
31+
32+
return dirName ? `/${dirName}` : undefined;
33+
}

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

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { useCallback } from 'react';
2+
import jsYaml from 'js-yaml';
3+
4+
import { useNotifications } from 'hooks';
5+
import { useInitRepoMutation, useLazyGetRepoQuery } from 'services/repo';
6+
7+
import { getPathWithoutProtocol, getRepoDirFromUrl, getRepoName, slugify } from '../../../../libs/repo';
8+
import { getRunSpecConfigurationResources } from '../helpers/getRunSpecConfigurationResources';
9+
10+
// TODO add next fields: volumes, repos,
11+
const supportedFields: (keyof TDevEnvironmentConfiguration)[] = [
12+
'type',
13+
'init',
14+
'inactivity_duration',
15+
'image',
16+
'user',
17+
'privileged',
18+
'entrypoint',
19+
'working_dir',
20+
'registry_auth',
21+
'python',
22+
'nvcc',
23+
'env',
24+
'docker',
25+
'backends',
26+
'regions',
27+
'instance_types',
28+
'spot_policy',
29+
'retry',
30+
'max_duration',
31+
'max_price',
32+
'idle_duration',
33+
'utilization_policy',
34+
'fleets',
35+
'repos',
36+
];
37+
38+
export const useGetRunSpecFromYaml = ({ projectName = '' }) => {
39+
const [pushNotification] = useNotifications();
40+
const [getRepo] = useLazyGetRepoQuery();
41+
const [initRepo] = useInitRepoMutation();
42+
43+
const getRepoData = useCallback(
44+
async (repos: TEnvironmentConfigurationRepo[]) => {
45+
const [firstRepo] = repos;
46+
47+
if (!firstRepo || !firstRepo.url) {
48+
return {};
49+
}
50+
51+
const prefix = getRepoName(firstRepo.url);
52+
const uniqKey = getPathWithoutProtocol(firstRepo.url);
53+
const repoId = await slugify(prefix, uniqKey);
54+
const repoDir = getRepoDirFromUrl(firstRepo.url);
55+
56+
try {
57+
await getRepo({ project_name: projectName, repo_id: repoId, include_creds: true }).unwrap();
58+
} catch (_) {
59+
initRepo({
60+
project_name: projectName,
61+
repo_id: repoId,
62+
repo_info: {
63+
repo_type: 'remote',
64+
repo_name: prefix,
65+
},
66+
repo_creds: { clone_url: firstRepo.url, private_key: null, oauth_token: null },
67+
})
68+
.unwrap()
69+
.catch(console.error);
70+
}
71+
72+
return {
73+
repo_id: repoId,
74+
repo_data: {
75+
repo_type: 'remote',
76+
repo_name: prefix,
77+
repo_branch: firstRepo.branch ?? null,
78+
repo_hash: firstRepo.hash ?? null,
79+
repo_config_name: null,
80+
repo_config_email: null,
81+
},
82+
repo_code_hash: null,
83+
repo_dir: repoDir ?? null,
84+
};
85+
},
86+
[projectName, getRepo, initRepo],
87+
);
88+
89+
const getRunSpecFromYaml = useCallback(
90+
async (yaml: string) => {
91+
let parsedYaml;
92+
93+
try {
94+
parsedYaml = (await jsYaml.load(yaml)) as { [key: string]: unknown };
95+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
96+
} catch (_) {
97+
pushNotification({
98+
type: 'error',
99+
content: 'Invalid YAML',
100+
});
101+
102+
window.scrollTo(0, 0);
103+
104+
throw new Error('Invalid YAML');
105+
}
106+
107+
const { name, ...otherFields } = parsedYaml;
108+
109+
const runSpec: TRunSpec = {
110+
run_name: name as string,
111+
configuration: {} as TDevEnvironmentConfiguration,
112+
};
113+
114+
for (const fieldName of Object.keys(otherFields)) {
115+
switch (fieldName) {
116+
case 'ide':
117+
runSpec.configuration.ide = otherFields[fieldName] as TIde;
118+
break;
119+
case 'resources':
120+
runSpec.configuration.resources = getRunSpecConfigurationResources(otherFields[fieldName]);
121+
break;
122+
case 'repos': {
123+
const repoData = await getRepoData(otherFields['repos'] as TEnvironmentConfigurationRepo[]);
124+
Object.assign(runSpec, repoData);
125+
break;
126+
}
127+
default:
128+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
129+
// @ts-expect-error
130+
if (!supportedFields.includes(fieldName)) {
131+
throw new Error(`Unsupported field: ${fieldName}`);
132+
}
133+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
134+
// @ts-expect-error
135+
runSpec.configuration[fieldName] = otherFields[fieldName];
136+
break;
137+
}
138+
}
139+
140+
return runSpec;
141+
},
142+
[pushNotification, getRepoData],
143+
);
144+
145+
return [getRunSpecFromYaml];
146+
};

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

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import { useApplyRunMutation } from 'services/run';
1717

1818
import { OfferList } from 'pages/Offers/List';
1919

20-
import { getRunSpecFromYaml } from './helpers/getRunSpecFromYaml';
2120
import { useGenerateYaml } from './hooks/useGenerateYaml';
21+
import { useGetRunSpecFromYaml } from './hooks/useGetRunSpecFromYaml';
2222

2323
import { IRunEnvironmentFormValues } from './types';
2424

@@ -57,7 +57,10 @@ const envValidationSchema = yup.object({
5757

5858
repo_url: yup.string().when('repo_enabled', {
5959
is: true,
60-
then: yup.string().url(urlFormatError).required(requiredFieldError),
60+
then: yup
61+
.string()
62+
.matches(/^(https?):\/\/([^\s\/?#]+)((?:\/[^\s?#]*)*)(?::\/(.*))?$/i, urlFormatError)
63+
.required(requiredFieldError),
6164
}),
6265
});
6366

@@ -109,6 +112,8 @@ export const CreateDevEnvironment: React.FC = () => {
109112
() => searchParams.get('project_name') ?? null,
110113
);
111114

115+
const [getRunSpecFromYaml] = useGetRunSpecFromYaml({ projectName: selectedProject });
116+
112117
const [applyRun, { isLoading: isApplying }] = useApplyRunMutation();
113118

114119
const loading = isApplying;
@@ -129,7 +134,7 @@ export const CreateDevEnvironment: React.FC = () => {
129134
resolver,
130135
defaultValues: {
131136
ide: 'cursor',
132-
docker: true,
137+
docker: false,
133138
repo_enabled: false,
134139
},
135140
});
@@ -218,16 +223,8 @@ export const CreateDevEnvironment: React.FC = () => {
218223

219224
try {
220225
runSpec = await getRunSpecFromYaml(config_yaml);
221-
} catch (error) {
222-
pushNotification({
223-
type: 'error',
224-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
225-
// @ts-expect-error
226-
content: error?.message,
227-
});
228-
229-
window.scrollTo(0, 0);
230-
226+
} catch (e) {
227+
console.log('parse transaction error:', e);
231228
return;
232229
}
233230

@@ -336,32 +333,32 @@ export const CreateDevEnvironment: React.FC = () => {
336333
onChange={onChangeTab}
337334
tabs={[
338335
{
339-
label: t('runs.dev_env.wizard.docker'),
340-
id: DockerPythonTabs.DOCKER,
336+
label: t('runs.dev_env.wizard.python'),
337+
id: DockerPythonTabs.PYTHON,
341338
content: (
342339
<div>
343340
<FormInput
344-
label={t('runs.dev_env.wizard.docker_image')}
345-
description={t('runs.dev_env.wizard.docker_image_description')}
346-
placeholder={t('runs.dev_env.wizard.docker_image_placeholder')}
341+
label={t('runs.dev_env.wizard.python')}
342+
description={t('runs.dev_env.wizard.python_description')}
343+
placeholder={t('runs.dev_env.wizard.python_placeholder')}
347344
control={control}
348-
name="image"
345+
name="python"
349346
disabled={loading}
350347
/>
351348
</div>
352349
),
353350
},
354351
{
355-
label: t('runs.dev_env.wizard.python'),
356-
id: DockerPythonTabs.PYTHON,
352+
label: t('runs.dev_env.wizard.docker'),
353+
id: DockerPythonTabs.DOCKER,
357354
content: (
358355
<div>
359356
<FormInput
360-
label={t('runs.dev_env.wizard.python')}
361-
description={t('runs.dev_env.wizard.python_description')}
362-
placeholder={t('runs.dev_env.wizard.python_placeholder')}
357+
label={t('runs.dev_env.wizard.docker_image')}
358+
description={t('runs.dev_env.wizard.docker_image_description')}
359+
placeholder={t('runs.dev_env.wizard.docker_image_placeholder')}
363360
control={control}
364-
name="python"
361+
name="image"
365362
disabled={loading}
366363
/>
367364
</div>

0 commit comments

Comments
 (0)