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 @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/layouts/AppLayout/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
28 changes: 23 additions & 5 deletions frontend/src/libs/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,54 @@ export const tokensToSearchParams = <RequestParamsKeys extends string>(
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 = <RequestParamsKeys extends string>({
tokens,
arrayFieldKeys,
}: {
tokens: PropertyFilterProps.Query['tokens'];
arrayFieldKeys?: RequestParamsKeys[];
}) => {
return tokens.reduce<Record<RequestParamsKeys, string | string[]>>(
return tokens.reduce<Record<RequestParamsKeys, RequestParam | string[]>>(
(acc, token) => {
const propertyKey = token.propertyKey as RequestParamsKeys;

if (!propertyKey) {
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<RequestParamsKeys, string>,
{} as Record<RequestParamsKeys, RequestParam>,
);
};

Expand Down
22 changes: 21 additions & 1 deletion frontend/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
"billing": "Billing",
"resources": "Resources",
"volumes": "Volumes",
"instances": "Instances"
"instances": "Instances",
"offers": "Offers"
},

"backend": {
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/pages/Instances/List/hooks/useFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => {
const { projectOptions } = useProjectFilter({ localStorePrefix });

const [propertyFilterQuery, setPropertyFilterQuery] = useState<PropertyFilterProps.Query>(() => {
console.log(requestParamsToTokens<RequestParamsKeys>({ searchParams, filterKeys }));

return requestParamsToTokens<RequestParamsKeys>({ searchParams, filterKeys });
});

Expand Down
74 changes: 74 additions & 0 deletions frontend/src/pages/Offers/List/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<string>();
const backends = new Set<string>();
const counts = new Set<string>();

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;
};
47 changes: 47 additions & 0 deletions frontend/src/pages/Offers/List/hooks/useEmptyMessages.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ListEmptyMessage
title={t('offer.empty_message_title_select_project')}
message={t('offer.empty_message_text_select_project')}
></ListEmptyMessage>
);
}

return (
<ListEmptyMessage title={t('offer.empty_message_title')} message={t('offer.empty_message_text')}>
<Button disabled={isDisabledClearFilter} onClick={clearFilter}>
{t('common.clearFilter')}
</Button>
</ListEmptyMessage>
);
}, [clearFilter, isDisabledClearFilter]);

const renderNoMatchMessage = useCallback<() => React.ReactNode>(() => {
return (
<ListEmptyMessage title={t('offer.nomatch_message_title')} message={t('offer.nomatch_message_text')}>
<Button disabled={isDisabledClearFilter} onClick={clearFilter}>
{t('common.clearFilter')}
</Button>
</ListEmptyMessage>
);
}, [clearFilter, isDisabledClearFilter]);

return { renderEmptyMessage, renderNoMatchMessage } as const;
};
Loading
Loading