Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const API = {
// Repos
REPOS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/repos`,
REPOS_LIST: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/list`,
GET_REPO: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/get`,
INIT_REPO: (projectName: IProject['project_name']) => `${API.PROJECTS.REPOS(projectName)}/init`,

// Runs
RUNS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/runs`,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export { default as HelpPanel } from '@cloudscape-design/components/help-panel';
export type { HelpPanelProps } from '@cloudscape-design/components/help-panel';
export { default as TextContent } from '@cloudscape-design/components/text-content';
export { default as Toggle } from '@cloudscape-design/components/toggle';
export type { ToggleProps } from '@cloudscape-design/components/toggle';
export { default as Modal } from '@cloudscape-design/components/modal';
export { default as TutorialPanel } from '@cloudscape-design/components/tutorial-panel';
export type { TutorialPanelProps } from '@cloudscape-design/components/tutorial-panel';
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/libs/repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
function bufferToHex(buffer: ArrayBuffer): string {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}

export async function slugify(prefix: string, unique_key: string, hash_size: number = 8): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(unique_key);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const fullHash = bufferToHex(hashBuffer);
return `${prefix}-${fullHash.substring(0, hash_size)}`;
}

export function getRepoName(url: string): string {
const cleaned = url
.replace(/^https?:\/\//i, '')
.replace(/:\/(\S*)/, '')
.replace(/\/+$/, '')
.replace(/\.git$/, '');
const parts = cleaned.split('/').filter(Boolean);
return parts.length ? parts[parts.length - 1] : '';
}

export function getPathWithoutProtocol(url: string): string {
return url.replace(/^https?:\/\//i, '');
}

export function getRepoUrlWithOutDir(url: string): string {
const parsedUrl = url.match(/^([^:]+(?::[^:]+)?)/)?.[1];

return parsedUrl ?? url;
}

export function getRepoDirFromUrl(url: string): string | undefined {
const dirName = url.replace(/^https?:\/\//i, '').match(/:\/(\S*)/)?.[1];

return dirName ? `/${dirName}` : undefined;
}
24 changes: 23 additions & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,10 +474,32 @@
"offer": "Offer",
"offer_description": "Select an offer for the dev environment.",
"name": "Name",
"name_description": "The name of the run. If not specified, the name will be generated automatically.",
"name_description": "The name of the run, e.g. 'my-dev-env'",
"name_constraint": "If not specified, generated automatically",
"name_placeholder": "Optional",
"ide": "IDE",
"ide_description": "Select which IDE would you like to use with the dev environment.",
"docker": "Docker",
"docker_image": "Image",
"docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'",
"docker_image_constraint": "The image must be public",
"docker_image_placeholder": "Required",
"python": "Python",
"python_description": "The version of Python, e.g. '3.12'",
"python_placeholder": "Optional",
"repo": "Repo",
"working_dir": "Working dir",
"working_dir_description": "The absolute path to the working directory inside the container, e.g. '/home/user/project'",
"working_dir_placeholder": "Optional",
"working_dir_constraint": "By default, set to '/workflow'",
"repo_url": "URL",
"repo_url_description": "A URL of a Git repository, e.g. 'https://github.com/user/repo'",
"repo_url_constraint": "The repo must be public",
"repo_url_placeholder": "Required",
"repo_path": "Path",
"repo_path_description": "The path inside the container to clone the repository, e.g. '/home/user/project'",
"repo_path_placeholder": "Optional",
"repo_path_constraint": "By default, set to '/workflow'",
"config": "Configuration file",
"config_description": "Review the configuration file and adjust it if needed. Click Info for examples.",
"success_notification": "The run is submitted!"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React from 'react';

import { RequestParam } from '../../../libs/filters';

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

const rangeSeparator = '..';

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

export const renderRangeJSX = (range: { min?: number; max?: number }) => {
if (typeof range.min === 'number' && typeof range.max === 'number' && range.max != range.min) {
return (
<>
{round(range.min)}
<span className={styles.greyText}>{rangeSeparator}</span>
{round(range.max)}
</>
);
}

return range.min?.toString() ?? range.max?.toString();
};

export const rangeToObject = (range: RequestParam): { min?: number; max?: number } | undefined => {
if (!range) return;

Expand Down
39 changes: 23 additions & 16 deletions frontend/src/pages/Offers/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { Cards, CardsProps, Link, MultiselectCSD, PropertyFilter, StatusIndicator } from 'components';
import { Cards, CardsProps, MultiselectCSD, PropertyFilter, StatusIndicator } from 'components';

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

import { useEmptyMessages } from './hooks/useEmptyMessages';
import { useFilters } from './hooks/useFilters';
import { convertMiBToGB, rangeToObject, renderRange, round } from './helpers';
import { convertMiBToGB, rangeToObject, renderRange, renderRangeJSX, round } from './helpers';

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

Expand Down Expand Up @@ -132,31 +132,37 @@ export const OfferList: React.FC<OfferListProps> = ({ withSearchParams, onChange
const sections = [
{
id: 'memory_mib',
header: t('offer.memory_mib'),
content: (gpu: IGpu) => `${round(convertMiBToGB(gpu.memory_mib))}GB`,
// header: t('offer.memory_mib'),
content: (gpu: IGpu) => (
<div>
{round(convertMiBToGB(gpu.memory_mib))}GB
<span className={styles.greyText}>:</span>
{renderRange(gpu.count)}
</div>
),
width: 50,
},
{
id: 'price',
header: t('offer.price'),
content: (gpu: IGpu) => <span className={styles.greenText}>{renderRange(gpu.price) ?? '-'}</span>,
width: 50,
},
{
id: 'count',
header: t('offer.count'),
content: (gpu: IGpu) => renderRange(gpu.count) ?? '-',
// header: t('offer.price'),
content: (gpu: IGpu) => <span className={styles.greenText}>${renderRangeJSX(gpu.price) ?? '-'}</span>,
width: 50,
},
// {
// id: 'count',
// header: t('offer.count'),
// content: (gpu: IGpu) => renderRange(gpu.count) ?? '-',
// width: 50,
// },
!groupByBackend && {
id: 'backends',
header: t('offer.backend_plural'),
// header: t('offer.backend_plural'),
content: (gpu: IGpu) => gpu.backends?.join(', ') ?? '-',
width: 50,
},
groupByBackend && {
id: 'backend',
header: t('offer.backend'),
// header: t('offer.backend'),
content: (gpu: IGpu) => gpu.backend ?? '-',
width: 50,
},
Expand All @@ -168,7 +174,7 @@ export const OfferList: React.FC<OfferListProps> = ({ withSearchParams, onChange
// },
{
id: 'spot',
header: t('offer.spot'),
// header: t('offer.spot'),
content: (gpu: IGpu) => gpu.spot.join(', ') ?? '-',
width: 50,
},
Expand All @@ -189,9 +195,10 @@ export const OfferList: React.FC<OfferListProps> = ({ withSearchParams, onChange
<Cards
{...collectionProps}
{...props}
entireCardClickable
items={items}
cardDefinition={{
header: (gpu) => <Link>{gpu.name}</Link>,
header: (gpu) => gpu.name,
sections,
}}
loading={isLoading || isFetching}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/Offers/List/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
}
}

.greyText {
color: awsui.$color-text-status-inactive;
}

.greenText {
color: awsui.$color-text-status-success;
}
16 changes: 16 additions & 0 deletions frontend/src/pages/Runs/CreateDevEnvironment/constants.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react';

import { IRunEnvironmentFormKeys } from './types';
export const CONFIG_INFO = {
header: <h2>Credits history</h2>,
body: (
Expand All @@ -7,3 +9,17 @@ export const CONFIG_INFO = {
</>
),
};

export const FORM_FIELD_NAMES = {
offer: 'offer',
name: 'name',
ide: 'ide',
config_yaml: 'config_yaml',
docker: 'docker',
image: 'image',
python: 'python',
repo_enabled: 'repo_enabled',
repo_url: 'repo_url',
repo_path: 'repo_path',
working_dir: 'working_dir',
} as const satisfies Record<IRunEnvironmentFormKeys, IRunEnvironmentFormKeys>;

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useMemo } from 'react';
import jsYaml from 'js-yaml';

import { convertMiBToGB, renderRange, round } from 'pages/Offers/List/helpers';

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

export type UseGenerateYamlArgs = {
formValues: IRunEnvironmentFormValues;
};

export const useGenerateYaml = ({ formValues }: UseGenerateYamlArgs) => {
return useMemo(() => {
if (!formValues.offer || !formValues.ide) {
return '';
}

const { name, ide, image, python, offer, docker, repo_url, repo_path, working_dir } = formValues;

return jsYaml.dump({
type: 'dev-environment',
...(name ? { name } : {}),
ide,
...(docker ? { docker } : {}),
...(image ? { image } : {}),
...(python ? { python } : {}),

resources: {
gpu: `${offer.name}:${round(convertMiBToGB(offer.memory_mib))}GB:${renderRange(offer.count)}`,
},

...(repo_url || repo_path
? {
repos: [[repo_url?.trim(), repo_path?.trim()].filter(Boolean).join(':')],
}
: {}),

...(working_dir ? { working_dir } : {}),
backends: offer.backends,
spot_policy: 'auto',
});
}, [
formValues.name,
formValues.ide,
formValues.offer,
formValues.python,
formValues.image,
formValues.repo_url,
formValues.repo_path,
formValues.working_dir,
]);
};
Loading