diff --git a/frontend/src/libs/filters.test.ts b/frontend/src/libs/filters.test.ts new file mode 100644 index 000000000..1efeee989 --- /dev/null +++ b/frontend/src/libs/filters.test.ts @@ -0,0 +1,36 @@ +import { getTokenAwareNamePatternFilterRequestParams } from './filters'; + +describe('filters helpers', () => { + test('loads the full list when reopening an existing token value', () => { + expect( + getTokenAwareNamePatternFilterRequestParams({ + filteringText: 'main', + limit: 100, + propertyKey: 'project_name', + tokens: [{ propertyKey: 'project_name', operator: '=', value: 'main' }], + }), + ).toEqual({ limit: 100 }); + }); + + test('keeps the typed text when the value is being edited', () => { + expect( + getTokenAwareNamePatternFilterRequestParams({ + filteringText: 'mai', + limit: 100, + propertyKey: 'project_name', + tokens: [{ propertyKey: 'project_name', operator: '=', value: 'main' }], + }), + ).toEqual({ limit: 100, name_pattern: 'mai' }); + }); + + test('ignores matching values from other properties', () => { + expect( + getTokenAwareNamePatternFilterRequestParams({ + filteringText: 'main', + limit: 100, + propertyKey: 'project_name', + tokens: [{ propertyKey: 'username', operator: '=', value: 'main' }], + }), + ).toEqual({ limit: 100, name_pattern: 'main' }); + }); +}); diff --git a/frontend/src/libs/filters.ts b/frontend/src/libs/filters.ts index 8690734dd..13b360537 100644 --- a/frontend/src/libs/filters.ts +++ b/frontend/src/libs/filters.ts @@ -21,6 +21,31 @@ export const tokensToSearchParams = ( export type RequestParam = string | { min: number } | { max: number }; +export const getNamePatternFilterRequestParams = (filteringText: string, limit: number) => { + return { + ...(filteringText ? { name_pattern: filteringText } : {}), + limit, + }; +}; + +export const getTokenAwareNamePatternFilterRequestParams = ({ + filteringText, + limit, + propertyKey, + tokens, +}: { + filteringText: string; + limit: number; + propertyKey: PropertyKey; + tokens: PropertyFilterProps.Query['tokens']; +}) => { + const matchingExistingToken = tokens.some((token) => { + return token.propertyKey === propertyKey && typeof token.value === 'string' && token.value === filteringText; + }); + + return getNamePatternFilterRequestParams(matchingExistingToken ? '' : filteringText, limit); +}; + const convertTokenValueToRequestParam = (token: PropertyFilterProps.Query['tokens'][number]): RequestParam => { const { value, operator } = token; diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts index 0f70d541c..d160caa17 100644 --- a/frontend/src/pages/Events/List/hooks/useFilters.ts +++ b/frontend/src/pages/Events/List/hooks/useFilters.ts @@ -4,7 +4,13 @@ import { omit } from 'lodash'; import type { PropertyFilterProps } from 'components'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; +import { + EMPTY_QUERY, + getNamePatternFilterRequestParams, + requestParamsToTokens, + tokensToRequestParams, + tokensToSearchParams, +} from 'libs/filters'; import { useLazyGetProjectsQuery } from 'services/project'; import { useLazyGetUserListQuery } from 'services/user'; @@ -340,14 +346,10 @@ export const useFilters = ({ const handleLoadItems: PropertyFilterProps['onLoadItems'] = async ({ detail: { filteringProperty, filteringText } }) => { setDynamicFilteringOptions([]); - if (!filteringText.length) { - return Promise.resolve(); - } - setFilteringStatusType('loading'); if (filteringProperty?.key === filterKeys.TARGET_PROJECTS || filteringProperty?.key === filterKeys.WITHIN_PROJECTS) { - await getProjects({ name_pattern: filteringText, limit }) + await getProjects(getNamePatternFilterRequestParams(filteringText, limit)) .unwrap() .then(({ data }) => data.map(({ project_name, project_id }) => ({ @@ -361,7 +363,7 @@ export const useFilters = ({ } if (filteringProperty?.key === filterKeys.TARGET_USERS || filteringProperty?.key === filterKeys.ACTORS) { - await getUsers({ name_pattern: filteringText, limit }) + await getUsers(getNamePatternFilterRequestParams(filteringText, limit)) .unwrap() .then(({ data }) => data.map(({ username, id }) => ({ diff --git a/frontend/src/pages/Fleets/List/hooks.tsx b/frontend/src/pages/Fleets/List/hooks.tsx index 255675743..73286d247 100644 --- a/frontend/src/pages/Fleets/List/hooks.tsx +++ b/frontend/src/pages/Fleets/List/hooks.tsx @@ -9,7 +9,13 @@ import { Button, ListEmptyMessage, NavigateLink, StatusIndicator, TableProps } f import { DATE_TIME_FORMAT } from 'consts'; import { useLocalStorageState } from 'hooks'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; +import { + EMPTY_QUERY, + getNamePatternFilterRequestParams, + requestParamsToTokens, + tokensToRequestParams, + tokensToSearchParams, +} from 'libs/filters'; import { formatFleetBackend, formatFleetResources, @@ -190,14 +196,10 @@ export const useFilters = () => { const handleLoadItems: PropertyFilterProps['onLoadItems'] = async ({ detail: { filteringProperty, filteringText } }) => { setDynamicFilteringOptions([]); - if (!filteringText.length) { - return Promise.resolve(); - } - setFilteringStatusType('loading'); if (filteringProperty?.key === filterKeys.PROJECT_NAME) { - await getProjects({ name_pattern: filteringText, limit }) + await getProjects(getNamePatternFilterRequestParams(filteringText, limit)) .unwrap() .then(({ data }) => data.map(({ project_name }) => ({ diff --git a/frontend/src/pages/Instances/List/hooks/useFilters.ts b/frontend/src/pages/Instances/List/hooks/useFilters.ts index c7dfcecf5..4239ef50c 100644 --- a/frontend/src/pages/Instances/List/hooks/useFilters.ts +++ b/frontend/src/pages/Instances/List/hooks/useFilters.ts @@ -5,7 +5,13 @@ import { ToggleProps } from '@cloudscape-design/components'; import type { PropertyFilterProps } from 'components'; import { useLocalStorageState } from 'hooks'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; +import { + EMPTY_QUERY, + getNamePatternFilterRequestParams, + requestParamsToTokens, + tokensToRequestParams, + tokensToSearchParams, +} from 'libs/filters'; import { useLazyGetProjectsQuery } from 'services/project'; type RequestParamsKeys = keyof Pick; @@ -85,16 +91,10 @@ export const useFilters = () => { const handleLoadItems: PropertyFilterProps['onLoadItems'] = async ({ detail: { filteringProperty, filteringText } }) => { setDynamicFilteringOptions([]); - console.log({ filteringProperty, filteringText }); - - if (!filteringText.length) { - return Promise.resolve(); - } - setFilteringStatusType('loading'); if (filteringProperty?.key === filterKeys.PROJECT_NAMES) { - await getProjects({ name_pattern: filteringText, limit }) + await getProjects(getNamePatternFilterRequestParams(filteringText, limit)) .unwrap() .then(({ data }) => data.map(({ project_name }) => ({ diff --git a/frontend/src/pages/Models/List/hooks.tsx b/frontend/src/pages/Models/List/hooks.tsx index 3f1449de6..7994b6e78 100644 --- a/frontend/src/pages/Models/List/hooks.tsx +++ b/frontend/src/pages/Models/List/hooks.tsx @@ -7,7 +7,13 @@ import type { PropertyFilterProps } from 'components'; import { Button, ListEmptyMessage, NavigateLink, TableProps } from 'components'; import { DATE_TIME_FORMAT } from 'consts'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; +import { + EMPTY_QUERY, + getNamePatternFilterRequestParams, + requestParamsToTokens, + tokensToRequestParams, + tokensToSearchParams, +} from 'libs/filters'; import { ROUTES } from 'routes'; import { useLazyGetProjectsQuery } from 'services/project'; import { useLazyGetUserListQuery } from 'services/user'; @@ -184,14 +190,10 @@ export const useFilters = () => { const handleLoadItems: PropertyFilterProps['onLoadItems'] = async ({ detail: { filteringProperty, filteringText } }) => { setFilteringOptions([]); - if (!filteringText.length) { - return Promise.resolve(); - } - setFilteringStatusType('loading'); if (filteringProperty?.key === filterKeys.PROJECT_NAME) { - await getProjects({ name_pattern: filteringText, limit }) + await getProjects(getNamePatternFilterRequestParams(filteringText, limit)) .unwrap() .then(({ data }) => data.map(({ project_name }) => ({ @@ -203,7 +205,7 @@ export const useFilters = () => { } if (filteringProperty?.key === filterKeys.USER_NAME) { - await getUsers({ name_pattern: filteringText, limit }) + await getUsers(getNamePatternFilterRequestParams(filteringText, limit)) .unwrap() .then(({ data }) => data.map(({ username }) => ({ diff --git a/frontend/src/pages/Offers/List/helpers.tsx b/frontend/src/pages/Offers/List/helpers.tsx index 37d89782d..531eb0544 100644 --- a/frontend/src/pages/Offers/List/helpers.tsx +++ b/frontend/src/pages/Offers/List/helpers.tsx @@ -40,6 +40,14 @@ export const getPropertyFilterOptions = (gpus: IGpu[]) => { }; }; +export const getFleetFilterValue = (fleet: IFleet, selectedProjectName?: string) => { + if (selectedProjectName && fleet.project_name === selectedProjectName) { + return fleet.name; + } + + return `${fleet.project_name}/${fleet.name}`; +}; + export const round = (number: number) => Math.round(number * 100) / 100; export const renderRange = (range: { min?: number; max?: number }) => { diff --git a/frontend/src/pages/Offers/List/hooks/useFilters.ts b/frontend/src/pages/Offers/List/hooks/useFilters.ts index ba5aa3129..5d331aa6d 100644 --- a/frontend/src/pages/Offers/List/hooks/useFilters.ts +++ b/frontend/src/pages/Offers/List/hooks/useFilters.ts @@ -5,20 +5,31 @@ import type { MultiselectProps, PropertyFilterProps } from 'components'; import { EMPTY_QUERY, + getTokenAwareNamePatternFilterRequestParams, requestParamsToArray, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams, } from 'libs/filters'; +import { useLazyGetProjectFleetsQuery } from 'services/fleet'; import { useGetProjectsQuery, useLazyGetProjectsQuery } from 'services/project'; -import { getPropertyFilterOptions } from '../helpers'; +import { getFleetFilterValue, getPropertyFilterOptions } from '../helpers'; -type RequestParamsKeys = 'project_name' | 'gpu_name' | 'gpu_count' | 'gpu_memory' | 'backend' | 'spot_policy' | 'group_by'; +type RequestParamsKeys = + | 'project_name' + | 'gpu_name' + | 'gpu_count' + | 'gpu_memory' + | 'backend' + | 'fleet' + | 'spot_policy' + | 'group_by'; export type UseFiltersArgs = { gpus: IGpu[]; withSearchParams?: boolean; + showFleetFilter?: boolean; permanentFilters?: Partial>; defaultFilters?: Partial>; }; @@ -29,10 +40,11 @@ export const filterKeys: Record = { GPU_COUNT: 'gpu_count', GPU_MEMORY: 'gpu_memory', BACKEND: 'backend', + FLEET: 'fleet', SPOT_POLICY: 'spot_policy', }; -const multipleChoiceKeys: RequestParamsKeys[] = ['gpu_name', 'backend']; +const multipleChoiceKeys: RequestParamsKeys[] = ['gpu_name', 'backend', 'fleet']; const spotPolicyOptions = [ { @@ -80,6 +92,12 @@ const filteringProperties = [ propertyLabel: 'Backend', groupValuesLabel: 'Backend values', }, + { + key: filterKeys.FLEET, + operators: ['='], + propertyLabel: 'Fleet', + groupValuesLabel: 'Fleet values', + }, { key: filterKeys.SPOT_POLICY, operators: ['='], @@ -93,13 +111,21 @@ const defaultGroupByOptions = [{ ...gpuFilterOption }, { label: 'Backend', value const groupByRequestParamName: RequestParamsKeys = 'group_by'; const limit = 100; -export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = {}, defaultFilters }: UseFiltersArgs) => { +export const useFilters = ({ + gpus, + withSearchParams = true, + showFleetFilter = false, + permanentFilters = {}, + defaultFilters, +}: UseFiltersArgs) => { const [searchParams, setSearchParams] = useSearchParams(); const [dynamicFilteringOptions, setDynamicFilteringOptions] = useState([]); const [filteringStatusType, setFilteringStatusType] = useState(); const [getProjects] = useLazyGetProjectsQuery(); + const [getProjectFleets] = useLazyGetProjectFleetsQuery(); const { data: projectsData } = useGetProjectsQuery({ limit: 1 }); const projectNameIsChecked = useRef(false); + const prevSelectedProjectName = useRef(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => { const queryFromSearchParams = requestParamsToTokens({ @@ -107,8 +133,18 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { filterKeys, defaultFilterValues: defaultFilters, }); - if (queryFromSearchParams.tokens.length > 0) { - return queryFromSearchParams; + + const tokens = showFleetFilter + ? queryFromSearchParams.tokens + : queryFromSearchParams.tokens.filter((token) => token.propertyKey !== filterKeys.FLEET); + + const query = { + ...queryFromSearchParams, + tokens, + }; + + if (query.tokens.length > 0) { + return query; } return EMPTY_QUERY; @@ -179,8 +215,14 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { const filteringPropertiesForShowing = useMemo(() => { const permanentFilterKeys = Object.keys(permanentFilters); - return filteringProperties.filter(({ key }) => !permanentFilterKeys.includes(key)); - }, [permanentFilters]); + return filteringProperties.filter(({ key }) => { + if (key === filterKeys.FLEET && !showFleetFilter) { + return false; + } + + return !permanentFilterKeys.includes(key); + }); + }, [permanentFilters, showFleetFilter]); const setSearchParamsHandle = ({ tokens, @@ -252,17 +294,26 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { }; }, [propertyFilterQuery, permanentFilters]); + const selectedProjectName = useMemo(() => { + const projectName = filteringRequestParams['project_name']; + + return typeof projectName === 'string' ? projectName : undefined; + }, [filteringRequestParams]); + const handleLoadItems: PropertyFilterProps['onLoadItems'] = async ({ detail: { filteringProperty, filteringText } }) => { setDynamicFilteringOptions([]); - if (!filteringText.length) { - return Promise.resolve(); - } - setFilteringStatusType('loading'); if (filteringProperty?.key === filterKeys.PROJECT_NAME) { - await getProjects({ name_pattern: filteringText, limit }) + await getProjects( + getTokenAwareNamePatternFilterRequestParams({ + filteringText, + limit, + propertyKey: filterKeys.PROJECT_NAME, + tokens: propertyFilterQuery.tokens, + }), + ) .unwrap() .then(({ data }) => data.map(({ project_name }) => ({ @@ -273,6 +324,24 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { .then(setDynamicFilteringOptions); } + if (showFleetFilter && filteringProperty?.key === filterKeys.FLEET && selectedProjectName) { + await getProjectFleets({ + projectName: selectedProjectName, + includeImported: true, + }) + .unwrap() + .then((fleets) => + fleets + .map((fleet) => ({ + propertyKey: filterKeys.FLEET, + value: getFleetFilterValue(fleet, selectedProjectName), + })) + .filter(({ value }) => value.toLowerCase().includes(filteringText.toLowerCase())) + .slice(0, limit), + ) + .then(setDynamicFilteringOptions); + } + setFilteringStatusType(undefined); }; @@ -296,6 +365,24 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { } }, [projectsData]); + useEffect(() => { + const prevProjectName = prevSelectedProjectName.current; + prevSelectedProjectName.current = selectedProjectName; + + if (!showFleetFilter || prevProjectName === selectedProjectName) { + return; + } + + if (!propertyFilterQuery.tokens.some((token) => token.propertyKey === filterKeys.FLEET)) { + return; + } + + onChangePropertyFilterHandle({ + tokens: propertyFilterQuery.tokens.filter((token) => token.propertyKey !== filterKeys.FLEET), + operation: propertyFilterQuery.operation, + }); + }, [propertyFilterQuery, selectedProjectName, showFleetFilter]); + return { filteringRequestParams, clearFilter, diff --git a/frontend/src/pages/Offers/List/index.tsx b/frontend/src/pages/Offers/List/index.tsx index 15658e422..60b2ed147 100644 --- a/frontend/src/pages/Offers/List/index.tsx +++ b/frontend/src/pages/Offers/List/index.tsx @@ -16,6 +16,7 @@ const getRequestParams = ({ project_name, gpu_name, backend, + fleet, gpu_count, gpu_memory, spot_policy, @@ -24,6 +25,7 @@ const getRequestParams = ({ project_name: string; gpu_name?: string[]; backend?: string[]; + fleet?: string[]; gpu_count?: string; gpu_memory?: string; spot_policy?: TSpot; @@ -59,6 +61,7 @@ const getRequestParams = ({ files: [], setup: [], ...(backend?.length ? { backends: backend as TBackendType[] } : {}), + ...(fleet?.length ? { fleets: fleet } : {}), }, profile: { name: 'default', default: false }, ssh_key_pub: '(dummy)', @@ -70,16 +73,20 @@ type OfferListProps = Pick void; onChangeBackendFilter?: (backends: string[]) => void; + onChangeFleetFilter?: (fleets: string[]) => void; }; export const OfferList: React.FC = ({ withSearchParams, + showFleetFilter, disabled, onChangeProjectName, onChangeBackendFilter, + onChangeFleetFilter, permanentFilters, defaultFilters, ...props @@ -108,7 +115,7 @@ export const OfferList: React.FC = ({ onChangeGroupBy, filteringStatusType, handleLoadItems, - } = useFilters({ gpus: data?.gpus ?? [], withSearchParams, permanentFilters, defaultFilters }); + } = useFilters({ gpus: data?.gpus ?? [], withSearchParams, showFleetFilter, permanentFilters, defaultFilters }); useEffect(() => { setRequestParams( @@ -134,6 +141,14 @@ export const OfferList: React.FC = ({ onChangeBackendFilter?.(backendValues); }, [filteringRequestParams.backend]); + useEffect(() => { + const fleet = filteringRequestParams.fleet; + const fleetValues = fleet + ? (Array.isArray(fleet) ? fleet : [fleet]).filter((value): value is string => typeof value === 'string') + : []; + onChangeFleetFilter?.(fleetValues); + }, [filteringRequestParams.fleet]); + const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({ clearFilter, projectNameSelected: Boolean(requestParams?.['project_name']), diff --git a/frontend/src/pages/Offers/ListPage.tsx b/frontend/src/pages/Offers/ListPage.tsx index e8cd939f4..17e27c96e 100644 --- a/frontend/src/pages/Offers/ListPage.tsx +++ b/frontend/src/pages/Offers/ListPage.tsx @@ -17,5 +17,7 @@ export const ListPage: React.FC = () => { }, ]); - return {t('offer.title')}} />; + return ( + {t('offer.title')}} /> + ); }; diff --git a/frontend/src/pages/Runs/Launch/helpers/templateResources.test.ts b/frontend/src/pages/Runs/Launch/helpers/templateResources.test.ts index 7fa227036..7feb69b18 100644 --- a/frontend/src/pages/Runs/Launch/helpers/templateResources.test.ts +++ b/frontend/src/pages/Runs/Launch/helpers/templateResources.test.ts @@ -60,4 +60,19 @@ describe('templateResources', () => { spot_policy: 'auto', }); }); + + test('adds fleet defaults', () => { + const template = makeTemplate({ + type: 'task', + resources: { + gpu: 'H100:1', + }, + fleets: ['team-a', 'other-project/team-b'], + }); + + expect(getTemplateOfferDefaultFilters(template)).toMatchObject({ + gpu_name: 'H100', + fleet: ['team-a', 'other-project/team-b'], + }); + }); }); diff --git a/frontend/src/pages/Runs/Launch/helpers/templateResources.ts b/frontend/src/pages/Runs/Launch/helpers/templateResources.ts index af1963f25..cc20d6d72 100644 --- a/frontend/src/pages/Runs/Launch/helpers/templateResources.ts +++ b/frontend/src/pages/Runs/Launch/helpers/templateResources.ts @@ -8,6 +8,7 @@ type TGpuFilterDefaults = { type TOfferFilterDefaults = TGpuFilterDefaults & { backend?: string | string[]; + fleet?: string | string[]; spot_policy?: string; }; @@ -106,12 +107,20 @@ export const getTemplateOfferDefaultFilters = (template?: ITemplate): TOfferFilt configuration.backends.every((backend) => typeof backend === 'string') ? (configuration.backends as string[]) : undefined; + const fleets = + configuration && + typeof configuration === 'object' && + Array.isArray(configuration.fleets) && + configuration.fleets.every((fleet) => typeof fleet === 'string') + ? (configuration.fleets as string[]) + : undefined; if (typeof gpu === 'number') { return { ...(gpu > 0 ? { gpu_count: String(gpu) } : {}), ...(spotPolicy ? { spot_policy: spotPolicy } : {}), ...(backends?.length ? { backend: backends } : {}), + ...(fleets?.length ? { fleet: fleets } : {}), }; } @@ -131,6 +140,7 @@ export const getTemplateOfferDefaultFilters = (template?: ITemplate): TOfferFilt ...(gpuNames && gpuNames.length > 1 ? { gpu_name: gpuNames } : {}), ...(spotPolicy ? { spot_policy: spotPolicy } : {}), ...(backends?.length ? { backend: backends } : {}), + ...(fleets?.length ? { fleet: fleets } : {}), }; } @@ -158,5 +168,6 @@ export const getTemplateOfferDefaultFilters = (template?: ITemplate): TOfferFilt ...(gpuMemory ? { gpu_memory: gpuMemory } : {}), ...(spotPolicy ? { spot_policy: spotPolicy } : {}), ...(backends?.length ? { backend: backends } : {}), + ...(fleets?.length ? { fleet: fleets } : {}), }; }; diff --git a/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts b/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts index 8af880cf7..bff0dfc7e 100644 --- a/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts +++ b/frontend/src/pages/Runs/Launch/hooks/useGenerateYaml.ts @@ -11,9 +11,17 @@ export type UseGenerateYamlArgs = { envParam?: TTemplateParam; hasResourcesParam?: boolean; backends?: string[]; + fleets?: string[]; }; -export const useGenerateYaml = ({ formValues, configuration, envParam, hasResourcesParam, backends }: UseGenerateYamlArgs) => { +export const useGenerateYaml = ({ + formValues, + configuration, + envParam, + hasResourcesParam, + backends, + fleets, +}: UseGenerateYamlArgs) => { return useMemo(() => { const { name, ide, image, python, offer, repo_url, repo_path, working_dir, password, gpu_enabled } = formValues; const gpuEnabled = gpu_enabled === true; @@ -54,6 +62,7 @@ export const useGenerateYaml = ({ formValues, configuration, envParam, hasResour }, ...(backends && backends.length > 0 ? { backends } : {}), + ...(fleets && fleets.length > 0 ? { fleets } : {}), ...(offer.spot.length === 1 ? { spot_policy: offer.spot[0] } : {}), ...(offer.spot.length > 1 ? { spot_policy: 'auto' } : {}), } @@ -85,5 +94,5 @@ export const useGenerateYaml = ({ formValues, configuration, envParam, hasResour }, { lineWidth: -1 }, ); - }, [formValues, configuration, envParam, hasResourcesParam, backends]); + }, [formValues, configuration, envParam, hasResourcesParam, backends, fleets]); }; diff --git a/frontend/src/pages/Runs/Launch/index.tsx b/frontend/src/pages/Runs/Launch/index.tsx index 23b6acc29..c70ee317a 100644 --- a/frontend/src/pages/Runs/Launch/index.tsx +++ b/frontend/src/pages/Runs/Launch/index.tsx @@ -68,6 +68,7 @@ export const Launch: React.FC = () => { const [selectedOffers, setSelectedOffers] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(); const [selectedBackends, setSelectedBackends] = useState([]); + const [selectedFleets, setSelectedFleets] = useState([]); const { projectOptions, isLoadingProjectOptions } = useProjectFilter({ localStorePrefix: 'run-env-list-projects' }); const [applyRun, { isLoading: isApplying }] = useApplyRunMutation(); @@ -263,6 +264,7 @@ export const Launch: React.FC = () => { envParam, hasResourcesParam, backends: selectedBackends, + fleets: selectedFleets, }); useEffect(() => { @@ -371,9 +373,11 @@ export const Launch: React.FC = () => { selectionType="single" disabled={!formValues.gpu_enabled} withSearchParams={false} + showFleetFilter selectedItems={selectedOffers} onSelectionChange={onChangeOffer} onChangeBackendFilter={setSelectedBackends} + onChangeFleetFilter={setSelectedFleets} permanentFilters={{ project_name: formValues.project ?? '' }} defaultFilters={defaultOfferFilters} header={ diff --git a/frontend/src/pages/Runs/List/hooks/useFilters.ts b/frontend/src/pages/Runs/List/hooks/useFilters.ts index c1af16160..7aac60969 100644 --- a/frontend/src/pages/Runs/List/hooks/useFilters.ts +++ b/frontend/src/pages/Runs/List/hooks/useFilters.ts @@ -5,7 +5,13 @@ import { ToggleProps } from '@cloudscape-design/components'; import type { PropertyFilterProps } from 'components'; import { useLocalStorageState } from 'hooks'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; +import { + EMPTY_QUERY, + getTokenAwareNamePatternFilterRequestParams, + requestParamsToTokens, + tokensToRequestParams, + tokensToSearchParams, +} from 'libs/filters'; import { useLazyGetProjectsQuery } from 'services/project'; import { useLazyGetUserListQuery } from 'services/user'; @@ -86,14 +92,17 @@ export const useFilters = () => { const handleLoadItems: PropertyFilterProps['onLoadItems'] = async ({ detail: { filteringProperty, filteringText } }) => { setFilteringOptions([]); - if (!filteringText.length) { - return Promise.resolve(); - } - setFilteringStatusType('loading'); if (filteringProperty?.key === filterKeys.PROJECT_NAME) { - await getProjects({ name_pattern: filteringText, limit }) + await getProjects( + getTokenAwareNamePatternFilterRequestParams({ + filteringText, + limit, + propertyKey: filterKeys.PROJECT_NAME, + tokens: propertyFilterQuery.tokens, + }), + ) .unwrap() .then(({ data }) => data.map(({ project_name }) => ({ @@ -105,7 +114,14 @@ export const useFilters = () => { } if (filteringProperty?.key === filterKeys.USER_NAME) { - await getUsers({ name_pattern: filteringText, limit }) + await getUsers( + getTokenAwareNamePatternFilterRequestParams({ + filteringText, + limit, + propertyKey: filterKeys.USER_NAME, + tokens: propertyFilterQuery.tokens, + }), + ) .unwrap() .then(({ data }) => data.map(({ username }) => ({ diff --git a/frontend/src/pages/Volumes/List/hooks.tsx b/frontend/src/pages/Volumes/List/hooks.tsx index a969cfc87..b7119f841 100644 --- a/frontend/src/pages/Volumes/List/hooks.tsx +++ b/frontend/src/pages/Volumes/List/hooks.tsx @@ -10,7 +10,13 @@ import { Button, ListEmptyMessage, NavigateLink, StatusIndicator } from 'compone import { DATE_TIME_FORMAT } from 'consts'; import { useLocalStorageState, useNotifications } from 'hooks'; import { getServerError } from 'libs'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; +import { + EMPTY_QUERY, + getNamePatternFilterRequestParams, + requestParamsToTokens, + tokensToRequestParams, + tokensToSearchParams, +} from 'libs/filters'; import { getStatusIconType } from 'libs/volumes'; import { ROUTES } from 'routes'; import { useLazyGetProjectsQuery } from 'services/project'; @@ -188,14 +194,10 @@ export const useFilters = () => { const handleLoadItems: PropertyFilterProps['onLoadItems'] = async ({ detail: { filteringProperty, filteringText } }) => { setDynamicFilteringOptions([]); - if (!filteringText.length) { - return Promise.resolve(); - } - setFilteringStatusType('loading'); if (filteringProperty?.key === filterKeys.PROJECT_NAME) { - await getProjects({ name_pattern: filteringText, limit }) + await getProjects(getNamePatternFilterRequestParams(filteringText, limit)) .unwrap() .then(({ data }) => data.map(({ project_name }) => ({ diff --git a/frontend/src/services/fleet.ts b/frontend/src/services/fleet.ts index fa723d7d2..ccae62c1b 100644 --- a/frontend/src/services/fleet.ts +++ b/frontend/src/services/fleet.ts @@ -25,11 +25,18 @@ export const fleetApi = createApi({ result ? [...result.map(({ name }) => ({ type: 'Fleet' as const, id: name })), 'Fleets'] : ['Fleets'], }), - getProjectFleets: builder.query({ - query: ({ projectName }) => { + getProjectFleets: builder.query({ + query: ({ projectName, includeImported }) => { return { url: API.PROJECTS.FLEETS(projectName), method: 'POST', + ...(typeof includeImported === 'boolean' + ? { + body: { + include_imported: includeImported, + }, + } + : {}), }; }, @@ -84,6 +91,8 @@ export const fleetApi = createApi({ export const { useGetFleetsQuery, useLazyGetFleetsQuery, + useGetProjectFleetsQuery, + useLazyGetProjectFleetsQuery, useDeleteFleetMutation, useGetFleetDetailsQuery, useApplyFleetMutation, diff --git a/frontend/src/types/gpu.d.ts b/frontend/src/types/gpu.d.ts index 4ee39a34d..5a9f98046 100644 --- a/frontend/src/types/gpu.d.ts +++ b/frontend/src/types/gpu.d.ts @@ -44,6 +44,7 @@ declare interface ITaskConfigurationQueryParams { files?: Array; setup?: string[]; backends?: TBackendType[]; + fleets?: string[]; regions?: string[]; availability_zones?: string[]; instance_types?: string[];