diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 7150a2cfcc..661f72ef55 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -105,6 +105,8 @@ export const API = { SECRETS_UPDATE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/secrets/create_or_update`, SECRETS_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/secrets/delete`, + // GPUS + GPUS_LIST: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/gpus/list`, }, BACKENDS: { diff --git a/frontend/src/layouts/AppLayout/hooks.ts b/frontend/src/layouts/AppLayout/hooks.ts index 9028a86218..ef53a0082e 100644 --- a/frontend/src/layouts/AppLayout/hooks.ts +++ b/frontend/src/layouts/AppLayout/hooks.ts @@ -24,6 +24,7 @@ export const useSideNavigation = () => { const generalLinks = [ { type: 'link', text: t('navigation.runs'), href: ROUTES.RUNS.LIST }, + { type: 'link', text: t('navigation.offers'), href: ROUTES.OFFERS.LIST }, { type: 'link', text: t('navigation.models'), href: ROUTES.MODELS.LIST }, { type: 'link', text: t('navigation.fleets'), href: ROUTES.FLEETS.LIST }, { type: 'link', text: t('navigation.instances'), href: ROUTES.INSTANCES.LIST }, diff --git a/frontend/src/libs/filters.ts b/frontend/src/libs/filters.ts index 425caaaef8..d3f06c98df 100644 --- a/frontend/src/libs/filters.ts +++ b/frontend/src/libs/filters.ts @@ -19,6 +19,22 @@ export const tokensToSearchParams = ( return params; }; +export type RequestParam = string | { min: number } | { max: number }; + +const convertTokenValueToRequestParam = (token: PropertyFilterProps.Query['tokens'][number]): RequestParam => { + const { value, operator } = token; + + if (operator === '>=') { + return { min: Number(value) }; + } + + if (operator === '<=') { + return { max: Number(value) }; + } + + return value; +}; + export const tokensToRequestParams = ({ tokens, arrayFieldKeys, @@ -26,7 +42,7 @@ export const tokensToRequestParams = ({ tokens: PropertyFilterProps.Query['tokens']; arrayFieldKeys?: RequestParamsKeys[]; }) => { - return tokens.reduce>( + return tokens.reduce>( (acc, token) => { const propertyKey = token.propertyKey as RequestParamsKeys; @@ -34,21 +50,23 @@ export const tokensToRequestParams = ({ return acc; } + const convertedValue = convertTokenValueToRequestParam(token); + if (arrayFieldKeys?.includes(propertyKey)) { if (Array.isArray(acc[propertyKey])) { - acc[propertyKey].push(token.value); + acc[propertyKey].push(convertedValue as string); } else { - acc[propertyKey] = [token.value]; + acc[propertyKey] = [convertedValue as string]; } return acc; } - acc[propertyKey] = token.value; + acc[propertyKey] = convertedValue; return acc; }, - {} as Record, + {} as Record, ); }; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 15b7c9cc5d..7c40a16bd9 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -78,7 +78,8 @@ "billing": "Billing", "resources": "Resources", "volumes": "Volumes", - "instances": "Instances" + "instances": "Instances", + "offers": "Offers" }, "backend": { @@ -446,6 +447,25 @@ "size": "Size" } }, + "offer": { + "title": "Offers", + "filter_property_placeholder": "Filter offers by properties", + "backend": "Backend", + "backend_plural": "Backends", + "availability": "Availability", + "groupBy": "Group by", + "region": "Region", + "count": "Count", + "price": "$/GPU", + "memory_mib": "Memory", + "spot": "Spot policy", + "empty_message_title_select_project": "Select a project", + "empty_message_text_select_project": "Use the filter above to select a project", + "empty_message_title": "No offers", + "empty_message_text": "No offers to display.", + "nomatch_message_title": "No matches", + "nomatch_message_text": "We can't find a match." + }, "models": { "model_name": "Name", diff --git a/frontend/src/pages/Instances/List/hooks/useFilters.ts b/frontend/src/pages/Instances/List/hooks/useFilters.ts index a15a7ca3e0..55453c33e4 100644 --- a/frontend/src/pages/Instances/List/hooks/useFilters.ts +++ b/frontend/src/pages/Instances/List/hooks/useFilters.ts @@ -20,8 +20,6 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => { const { projectOptions } = useProjectFilter({ localStorePrefix }); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => { - console.log(requestParamsToTokens({ searchParams, filterKeys })); - return requestParamsToTokens({ searchParams, filterKeys }); }); diff --git a/frontend/src/pages/Offers/List/helpers.ts b/frontend/src/pages/Offers/List/helpers.ts new file mode 100644 index 0000000000..7d42a0c37a --- /dev/null +++ b/frontend/src/pages/Offers/List/helpers.ts @@ -0,0 +1,74 @@ +import { RequestParam } from '../../../libs/filters'; + +const rangeSeparator = '..'; + +export function convertMiBToGB(mib: number) { + return mib / 1024; +} + +export const getPropertyFilterOptions = (gpus: IGpu[]) => { + const names = new Set(); + const backends = new Set(); + const counts = new Set(); + + gpus.forEach((gp) => { + names.add(gp.name); + + if (gp.backend) { + backends.add(gp.backend); + } + + if (gp.backends?.length) { + gp.backends.forEach((i) => backends.add(i)); + } + + const countRange = renderRange(gp.count); + + if (gp.count && countRange) { + counts.add(countRange); + } + }); + + return { + names, + backends, + counts, + }; +}; + +export const round = (number: number) => Math.round(number * 100) / 100; + +export const renderRange = (range: { min?: number; max?: number }) => { + if (typeof range.min === 'number' && typeof range.max === 'number' && range.max != range.min) { + return `${round(range.min)}${rangeSeparator}${round(range.max)}`; + } + + return range.min?.toString() ?? range.max?.toString(); +}; + +export const rangeToObject = (range: RequestParam): { min?: number; max?: number } | undefined => { + if (!range) return; + + if (typeof range === 'string') { + const [minString, maxString] = range.split(rangeSeparator); + + const min = Number(minString); + const max = Number(maxString); + + if (!isNaN(min) && !isNaN(max)) { + return { min, max }; + } + + if (!isNaN(min)) { + return { min, max: min }; + } + + if (!isNaN(max)) { + return { min: max, max }; + } + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return range; +}; diff --git a/frontend/src/pages/Offers/List/hooks/useEmptyMessages.tsx b/frontend/src/pages/Offers/List/hooks/useEmptyMessages.tsx new file mode 100644 index 0000000000..f978b8bc69 --- /dev/null +++ b/frontend/src/pages/Offers/List/hooks/useEmptyMessages.tsx @@ -0,0 +1,47 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Button, ListEmptyMessage } from 'components'; + +export const useEmptyMessages = ({ + clearFilter, + isDisabledClearFilter, + projectNameSelected, +}: { + clearFilter?: () => void; + isDisabledClearFilter?: boolean; + projectNameSelected?: boolean; +}) => { + const { t } = useTranslation(); + + const renderEmptyMessage = useCallback<() => React.ReactNode>(() => { + if (!projectNameSelected) { + return ( + + ); + } + + return ( + + + + ); + }, [clearFilter, isDisabledClearFilter]); + + const renderNoMatchMessage = useCallback<() => React.ReactNode>(() => { + return ( + + + + ); + }, [clearFilter, isDisabledClearFilter]); + + return { renderEmptyMessage, renderNoMatchMessage } as const; +}; diff --git a/frontend/src/pages/Offers/List/hooks/useFilters.ts b/frontend/src/pages/Offers/List/hooks/useFilters.ts new file mode 100644 index 0000000000..c3dadccfd9 --- /dev/null +++ b/frontend/src/pages/Offers/List/hooks/useFilters.ts @@ -0,0 +1,181 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import type { PropertyFilterProps } from 'components'; + +import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; + +import { useProjectFilter } from '../../../../hooks/useProjectFilter'; +import { getPropertyFilterOptions } from '../helpers'; + +type Args = { + gpus: IGpu[]; +}; + +type RequestParamsKeys = 'project_name' | 'gpu_name' | 'gpu_count' | 'gpu_memory' | 'backend' | 'spot_policy'; + +export const filterKeys: Record = { + PROJECT_NAME: 'project_name', + GPU_NAME: 'gpu_name', + GPU_COUNT: 'gpu_count', + GPU_MEMORY: 'gpu_memory', + BACKEND: 'backend', + SPOT_POLICY: 'spot_policy', +}; + +const multipleChoiseKeys: RequestParamsKeys[] = ['gpu_name', 'backend']; + +const spotPolicyOptions = [ + { + propertyKey: filterKeys.SPOT_POLICY, + value: 'spot', + }, + { + propertyKey: filterKeys.SPOT_POLICY, + value: 'on-demand', + }, + { + propertyKey: filterKeys.SPOT_POLICY, + value: 'auto', + }, +]; + +export const useFilters = ({ gpus }: Args) => { + const [searchParams, setSearchParams] = useSearchParams(); + const { projectOptions } = useProjectFilter({ localStorePrefix: 'offers-list-projects' }); + const projectNameIsChecked = useRef(false); + + const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => + requestParamsToTokens({ searchParams, filterKeys }), + ); + + const clearFilter = () => { + setSearchParams({}); + setPropertyFilterQuery(EMPTY_QUERY); + }; + + const filteringOptions = useMemo(() => { + const options: PropertyFilterProps.FilteringOption[] = [...spotPolicyOptions]; + + const { names, backends } = getPropertyFilterOptions(gpus); + + projectOptions.forEach(({ value }) => { + if (value) + options.push({ + propertyKey: filterKeys.PROJECT_NAME, + value, + }); + }); + + Array.from(names).forEach((name) => { + options.push({ + propertyKey: filterKeys.GPU_NAME, + value: name, + }); + }); + + Array.from(backends).forEach((backend) => { + options.push({ + propertyKey: filterKeys.BACKEND, + value: backend, + }); + }); + + return options; + }, [gpus]); + + const filteringProperties = [ + { + key: filterKeys.PROJECT_NAME, + operators: ['='], + propertyLabel: 'Project', + }, + { + key: filterKeys.GPU_NAME, + operators: ['='], + propertyLabel: 'GPU name', + }, + { + key: filterKeys.GPU_COUNT, + operators: ['<=', '>='], + propertyLabel: 'GPU count', + }, + { + key: filterKeys.GPU_MEMORY, + operators: ['<=', '>='], + propertyLabel: 'GPU memory', + }, + { + key: filterKeys.BACKEND, + operators: ['='], + propertyLabel: 'Backend', + }, + { + key: filterKeys.SPOT_POLICY, + operators: ['='], + propertyLabel: 'Spot policy', + }, + ]; + + const onChangePropertyFilterHandle = ({ tokens, operation }: PropertyFilterProps.Query) => { + const filteredTokens = tokens.filter((token, tokenIndex) => { + return ( + multipleChoiseKeys.includes(token.propertyKey as RequestParamsKeys) || + !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex) + ); + }); + + setSearchParams(tokensToSearchParams(filteredTokens)); + + setPropertyFilterQuery({ + operation, + tokens: filteredTokens, + }); + }; + + const onChangePropertyFilter: PropertyFilterProps['onChange'] = ({ detail }) => { + onChangePropertyFilterHandle(detail); + }; + + const filteringRequestParams = useMemo(() => { + console.log({ tokens: propertyFilterQuery.tokens }); + + const params = tokensToRequestParams({ + tokens: propertyFilterQuery.tokens, + arrayFieldKeys: multipleChoiseKeys, + }); + + return { + ...params, + } as Partial; + }, [propertyFilterQuery]); + + useEffect(() => { + if (!projectNameIsChecked.current && projectOptions.length) { + projectNameIsChecked.current = true; + + if (!filteringRequestParams['project_name']) { + onChangePropertyFilterHandle({ + tokens: [ + ...propertyFilterQuery.tokens, + { + operator: '=', + propertyKey: filterKeys.PROJECT_NAME, + value: projectOptions[0].value, + }, + ], + operation: 'and', + }); + } + } + }, [projectOptions]); + + return { + filteringRequestParams, + clearFilter, + propertyFilterQuery, + onChangePropertyFilter, + filteringOptions, + filteringProperties, + } as const; +}; diff --git a/frontend/src/pages/Offers/List/index.tsx b/frontend/src/pages/Offers/List/index.tsx new file mode 100644 index 0000000000..f6e9bfb477 --- /dev/null +++ b/frontend/src/pages/Offers/List/index.tsx @@ -0,0 +1,203 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Cards, Header, Link, PropertyFilter, SelectCSD, 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 styles from './styles.module.scss'; + +const gpusFilterOption = { label: 'GPU', value: 'gpu' }; + +const getRequestParams = ({ + project_name, + gpu_name, + backend, + gpu_count, + gpu_memory, + spot_policy, +}: { + project_name: string; + gpu_name?: string[]; + backend?: string[]; + gpu_count?: string; + gpu_memory?: string; + spot_policy?: TSpot; +}): TGpusListQueryParams => { + const gpuCountMinMax = rangeToObject(gpu_count ?? ''); + const gpuMemoryMinMax = rangeToObject(gpu_memory ?? ''); + + return { + project_name: project_name, + run_spec: { + configuration: { + nodes: 1, + ports: [], + commands: [':'], + type: 'task', + privileged: false, + home_dir: '/root', + env: {}, + resources: { + // cpu: { min: 2 }, + // memory: { min: 8.0 }, + // disk: { size: { min: 100.0 } }, + gpu: { + ...(gpu_name?.length ? { name: gpu_name } : {}), + ...(gpuCountMinMax ? { count: gpuCountMinMax } : {}), + ...(gpuMemoryMinMax ? { memory: gpuMemoryMinMax } : {}), + }, + }, + spot_policy, + volumes: [], + files: [], + setup: [], + ...(backend?.length ? { backends: backend } : {}), + }, + profile: { name: 'default', default: false }, + ssh_key_pub: '(dummy)', + }, + }; +}; + +export const OfferList = () => { + const { t } = useTranslation(); + const [requestParams, setRequestParams] = useState(); + + const { data, isLoading, isFetching } = useGetGpusListQuery( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + requestParams, + { + skip: !requestParams || !requestParams['project_name'], + }, + ); + + const { + filteringRequestParams, + clearFilter, + propertyFilterQuery, + onChangePropertyFilter, + filteringOptions, + filteringProperties, + } = useFilters({ gpus: data?.gpus ?? [] }); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setRequestParams(getRequestParams(filteringRequestParams)); + }, [JSON.stringify(filteringRequestParams)]); + + const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({ + clearFilter, + projectNameSelected: Boolean(requestParams?.['project_name']), + }); + + const { items, collectionProps } = useCollection(requestParams?.['project_name'] ? (data?.gpus ?? []) : [], { + filtering: { + empty: renderEmptyMessage(), + noMatch: renderNoMatchMessage(), + }, + selection: {}, + }); + + return ( + {gpu.name}, + sections: [ + { + id: 'memory_mib', + header: t('offer.memory_mib'), + content: (gpu) => `${round(convertMiBToGB(gpu.memory_mib))}GB`, + width: 50, + }, + { + id: 'price', + header: t('offer.price'), + content: (gpu) => {renderRange(gpu.price) ?? '-'}, + width: 50, + }, + { + id: 'count', + header: t('offer.count'), + content: (gpu) => renderRange(gpu.count) ?? '-', + width: 50, + }, + { + id: 'backends', + header: t('offer.backend_plural'), + content: (gpu) => gpu.backends?.join(', ') ?? '-', + width: 50, + }, + // { + // id: 'region', + // header: t('offer.region'), + // content: (gpu) => gpu.region ?? gpu.regions?.join(', ') ?? '-', + // width: 50, + // }, + { + id: 'spot', + header: t('offer.spot'), + content: (gpu) => gpu.spot.join(', ') ?? '-', + width: 50, + }, + { + id: 'availability', + content: (gpu) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + if (gpu.availability === 'not_available') { + return Not Available; + } + }, + width: 50, + }, + ], + }} + loading={isLoading || isFetching} + loadingText={t('common.loading')} + stickyHeader={true} + header={
{t('offer.title')}
} + variant="full-page" + filter={ +
+
+ +
+ +
+ +
+
+ } + /> + ); +}; diff --git a/frontend/src/pages/Offers/List/styles.module.scss b/frontend/src/pages/Offers/List/styles.module.scss new file mode 100644 index 0000000000..f5321171d2 --- /dev/null +++ b/frontend/src/pages/Offers/List/styles.module.scss @@ -0,0 +1,23 @@ +@use '@cloudscape-design/design-tokens/index' as awsui; + +.selectFilters { + display: flex; + flex-wrap: wrap; + gap: 0 20px; + + .filterField { + flex-shrink: 0; + width: 240px; + margin-top: -10px; + } + + .propertyFilter { + max-width: 640px; + flex-grow: 1; + min-width: 0; + } +} + +.greenText { + color: awsui.$color-text-status-success; +} \ No newline at end of file diff --git a/frontend/src/pages/Offers/index.ts b/frontend/src/pages/Offers/index.ts new file mode 100644 index 0000000000..f49062bea0 --- /dev/null +++ b/frontend/src/pages/Offers/index.ts @@ -0,0 +1 @@ +export { OfferList } from './List'; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index cf283d728a..809b086884 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -23,6 +23,7 @@ import { CreditsHistoryAdd, UserAdd, UserDetails, UserEdit, UserList } from 'pag import { UserBilling, UserProjects, UserSettings } from 'pages/User/Details'; import { AuthErrorMessage } from './App/AuthErrorMessage'; +import { OfferList } from './pages/Offers'; import { JobDetails } from './pages/Runs/Details/Jobs/Details/JobDetails'; import { VolumeList } from './pages/Volumes'; import { ROUTES } from './routes'; @@ -136,6 +137,12 @@ export const router = createBrowserRouter([ element: , }, + // Offers + { + path: ROUTES.OFFERS.LIST, + element: , + }, + // Models { path: ROUTES.MODELS.LIST, diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index f529570736..71f77066c0 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -98,6 +98,10 @@ export const ROUTES = { LIST: '/runs', }, + OFFERS: { + LIST: '/offers', + }, + MODELS: { LIST: '/models', DETAILS: { diff --git a/frontend/src/services/gpu.ts b/frontend/src/services/gpu.ts new file mode 100644 index 0000000000..ecb5a60a74 --- /dev/null +++ b/frontend/src/services/gpu.ts @@ -0,0 +1,29 @@ +import { API } from 'api'; +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +import fetchBaseQueryHeaders from 'libs/fetchBaseQueryHeaders'; + +export const gpuApi = createApi({ + reducerPath: 'gpuApi', + baseQuery: fetchBaseQuery({ + prepareHeaders: fetchBaseQueryHeaders, + }), + + tagTypes: ['Gpus'], + + endpoints: (builder) => ({ + getGpusList: builder.query({ + query: ({ project_name, ...body }) => { + return { + url: API.PROJECTS.GPUS_LIST(project_name), + method: 'POST', + body, + }; + }, + + providesTags: ['Gpus'], + }), + }), +}); + +export const { useGetGpusListQuery } = gpuApi; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 7d6afb8cf5..326f113add 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -17,6 +17,8 @@ import { volumeApi } from 'services/volume'; import appReducer from 'App/slice'; +import { gpuApi } from './services/gpu'; + export const store = configureStore({ reducer: { app: appReducer, @@ -32,6 +34,7 @@ export const store = configureStore({ [serverApi.reducerPath]: serverApi.reducer, [volumeApi.reducerPath]: volumeApi.reducer, [secretApi.reducerPath]: secretApi.reducer, + [gpuApi.reducerPath]: gpuApi.reducer, [mainApi.reducerPath]: mainApi.reducer, }, @@ -50,6 +53,7 @@ export const store = configureStore({ .concat(serverApi.middleware) .concat(volumeApi.middleware) .concat(secretApi.middleware) + .concat(gpuApi.middleware) .concat(mainApi.middleware), }); diff --git a/frontend/src/types/backend.d.ts b/frontend/src/types/backend.d.ts index 470063d847..b41d796cee 100644 --- a/frontend/src/types/backend.d.ts +++ b/frontend/src/types/backend.d.ts @@ -1,42 +1,46 @@ -declare type TBackendType = | "aws" - |"azure" - |"cudo" - |"datacrunch" - |"dstack" - |"gcp" - |"kubernetes" - |"lambda" - |"local" - |"nebius" - |"remote" - |"oci" - |"runpod" - |"tensordock" - |"vastai"; +declare type TBackendType = + | 'aws' + | 'azure' + | 'cudo' + | 'datacrunch' + | 'dstack' + | 'gcp' + | 'kubernetes' + | 'lambda' + | 'local' + | 'nebius' + | 'remote' + | 'oci' + | 'runpod' + | 'tensordock' + | 'vastai' + | 'cloudrift' + | 'hotaisle' + | 'vultr'; -declare type TBackendValueField = { - selected?: TSelectedType, - values: TValuesType +declare type TBackendValueField = { + selected?: TSelectedType; + values: TValuesType; } | null; -declare type TBackendValuesResponse = IAwsBackendValues & IAzureBackendValues & IGCPBackendValues & ILambdaBackendValues +declare type TBackendValuesResponse = IAwsBackendValues & IAzureBackendValues & IGCPBackendValues & ILambdaBackendValues; declare interface IBackendLocal { - type: 'local', - path: string + type: 'local'; + path: string; } declare interface IBackendDstack { - type: 'dstack', - path: string + type: 'dstack'; + path: string; } -declare type TBackendConfig = IBackendAWS | IBackendAzure | IBackendGCP | IBackendLambda | IBackendLocal | IBackendDstack +declare type TBackendConfig = IBackendAWS | IBackendAzure | IBackendGCP | IBackendLambda | IBackendLocal | IBackendDstack; declare interface IBackendConfigYaml { - config_yaml: string, + config_yaml: string; } declare interface IProjectBackend { - name: string - config: TBackendConfig + name: string; + config: TBackendConfig; } diff --git a/frontend/src/types/gpu.ts b/frontend/src/types/gpu.ts new file mode 100644 index 0000000000..d46dac4447 --- /dev/null +++ b/frontend/src/types/gpu.ts @@ -0,0 +1,92 @@ +declare type TAvailability = 'unknown' | 'available' | 'not_available' | 'no_quota' | 'no_balance' | 'idle' | 'busy'; + +declare type TSpot = 'spot' | 'on-demand' | 'auto'; + +declare type TRange = { + min: number; + max?: number; +}; + +declare interface IPortMappingRequest { + local_port?: number; + container_port: number; +} + +declare interface IGPUSpecRequest { + vendor?: string; + name?: string[]; + count?: TRange | number | string; + memory?: TRange | number | string; + total_memory?: TRange | number | string; + compute_capability?: any[]; +} + +declare interface IResourcesSpecRequest { + cpu?: TRange | number | string; + memory?: TRange | number | string; + shm_size?: number | string; + gpu?: IGPUSpecRequest | number | string; + disk?: { size: TRange | number | string } | number | string; +} + +declare interface ITaskConfigurationQueryParams { + nodes?: number; + ports?: Array; + commands?: string[]; + type: 'task'; + resources?: IResourcesSpecRequest; + privileged?: boolean; + home_dir?: string; + env?: string[] | object; + volumes?: Array; + docker?: boolean; + files?: Array; + setup?: string[]; + backends?: TBackendType[]; + regions?: string[]; + availability_zones?: string[]; + instance_types?: string[]; + reservation?: string; + spot_policy?: TSpot; + max_price?: number; +} + +declare interface IGpu { + name: string; + memory_mib: number; + vendor: string; + availability: TAvailability[]; + spot: TSpot[]; + count: { + min: number; + max: number; + }; + price: { + min: number; + max: number; + }; + backends?: TBackendType[]; + backend?: TBackendType; + regions?: string[]; + region?: string; +} + +declare type TGpusListQueryParams = { + project_name: string; + run_spec: { + group_gy?: string; + spot?: string | boolean; + gpu_vendor?: string; + gpu_count?: number; + gpu_memory?: number; + gpu_name?: string; + backends?: TBackendType[]; + configuration: ITaskConfigurationQueryParams; + profile?: { name: string; default?: boolean }; + ssh_key_pub: string; + }; +}; + +declare type TGpusListQueryResponse = { + gpus: IGpu[]; +};