diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 911e138757..19a109d52c 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -6,6 +6,7 @@ export { useConfirmationDialog } from './useConfirmationDialog'; export { useHelpPanel } from './useHelpPanel'; export { usePermissionGuard } from './usePermissionGuard'; export { useInfiniteScroll } from './useInfiniteScroll'; +export { useLocalStorageState } from './useLocalStorageState'; // cloudscape export { useCollection } from '@cloudscape-design/collection-hooks'; diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts index 6413c40392..0f70d541ca 100644 --- a/frontend/src/pages/Events/List/hooks/useFilters.ts +++ b/frontend/src/pages/Events/List/hooks/useFilters.ts @@ -5,8 +5,8 @@ import { omit } from 'lodash'; import type { PropertyFilterProps } from 'components'; import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; -import { useGetProjectsQuery } from 'services/project'; -import { useGetUserListQuery } from 'services/user'; +import { useLazyGetProjectsQuery } from 'services/project'; +import { useLazyGetUserListQuery } from 'services/user'; import { filterLastElementByPrefix } from '../helpers'; @@ -71,42 +71,49 @@ const baseFilteringProperties = [ key: filterKeys.TARGET_USERS, operators: ['='], propertyLabel: 'Target users', - groupValuesLabel: 'Project ids', + groupValuesLabel: 'User ids', }, { key: filterKeys.TARGET_FLEETS, operators: ['='], propertyLabel: 'Target fleet IDs', + groupValuesLabel: 'Fleet ids', }, { key: filterKeys.TARGET_INSTANCES, operators: ['='], propertyLabel: 'Target instance IDs', + groupValuesLabel: 'Instance ids', }, { key: filterKeys.TARGET_RUNS, operators: ['='], propertyLabel: 'Target run IDs', + groupValuesLabel: 'Run ids', }, { key: filterKeys.TARGET_JOBS, operators: ['='], propertyLabel: 'Target job IDs', + groupValuesLabel: 'Job ids', }, { key: filterKeys.TARGET_VOLUMES, operators: ['='], propertyLabel: 'Target volume IDs', + groupValuesLabel: 'Volume ids', }, { key: filterKeys.TARGET_GATEWAYS, operators: ['='], propertyLabel: 'Target gateway IDs', + groupValuesLabel: 'Gateway ids', }, { key: filterKeys.TARGET_SECRETS, operators: ['='], propertyLabel: 'Target secret IDs', + groupValuesLabel: 'Secret ids', }, { @@ -120,12 +127,14 @@ const baseFilteringProperties = [ key: filterKeys.WITHIN_FLEETS, operators: ['='], propertyLabel: 'Within fleet IDs', + groupValuesLabel: 'Fleet ids', }, { key: filterKeys.WITHIN_RUNS, operators: ['='], propertyLabel: 'Within run IDs', + groupValuesLabel: 'Run ids', }, { @@ -139,9 +148,12 @@ const baseFilteringProperties = [ key: filterKeys.ACTORS, operators: ['='], propertyLabel: 'Actors', + groupValuesLabel: 'User names', }, ]; +const limit = 100; + export const useFilters = ({ permanentFilters, withSearchParams, @@ -150,8 +162,10 @@ export const useFilters = ({ withSearchParams?: boolean; }) => { const [searchParams, setSearchParams] = useSearchParams(); - const { data: projectsData, isLoading: isLoadingProjects } = useGetProjectsQuery({}); - const { data: usersData, isLoading: isLoadingUsers } = useGetUserListQuery({}); + const [dynamicFilteringOptions, setDynamicFilteringOptions] = useState([]); + const [filteringStatusType, setFilteringStatusType] = useState(); + const [getProjects] = useLazyGetProjectsQuery(); + const [getUsers] = useLazyGetUserListQuery(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -165,31 +179,7 @@ export const useFilters = ({ }; const filteringOptions = useMemo(() => { - const options: PropertyFilterProps.FilteringOption[] = []; - - projectsData?.data?.forEach(({ project_name }) => { - options.push({ - propertyKey: filterKeys.TARGET_PROJECTS, - value: project_name, - }); - - options.push({ - propertyKey: filterKeys.WITHIN_PROJECTS, - value: project_name, - }); - }); - - usersData?.data?.forEach(({ username }) => { - options.push({ - propertyKey: filterKeys.TARGET_USERS, - value: username, - }); - - options.push({ - propertyKey: filterKeys.ACTORS, - value: username, - }); - }); + const options: PropertyFilterProps.FilteringOption[] = [...dynamicFilteringOptions]; targetTypes?.forEach((targetType) => { options.push({ @@ -199,7 +189,7 @@ export const useFilters = ({ }); return options; - }, [projectsData, usersData]); + }, [dynamicFilteringOptions]); const setSearchParamsHandle = ({ tokens }: { tokens: PropertyFilterProps.Query['tokens'] }) => { const searchParams = tokensToSearchParams(tokens); @@ -293,25 +283,15 @@ export const useFilters = ({ return [paramsFilter, permanentFilter]; }; - const targetProjects = filterParamsWithPermanentFitters(filterKeys.TARGET_PROJECTS) - .map((name: string) => projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id']) - .filter(Boolean); + const targetProjects = filterParamsWithPermanentFitters(filterKeys.TARGET_PROJECTS).filter(Boolean); - const withInProjects = filterParamsWithPermanentFitters(filterKeys.WITHIN_PROJECTS) - .map((name: string) => projectsData?.data?.find(({ project_name }) => project_name === name)?.['project_id']) - .filter(Boolean); + const withInProjects = filterParamsWithPermanentFitters(filterKeys.WITHIN_PROJECTS).filter(Boolean); - const targetUsers = filterParamsWithPermanentFitters(filterKeys.TARGET_USERS) - .map((name: string) => usersData?.data?.find(({ username }) => username === name)?.['id']) - .filter(Boolean); + const targetUsers = filterParamsWithPermanentFitters(filterKeys.TARGET_USERS).filter(Boolean); - const actors = filterParamsWithPermanentFitters(filterKeys.ACTORS) - .map((name: string) => usersData?.data?.find(({ username }) => username === name)?.['id']) - .filter(Boolean); + const actors = filterParamsWithPermanentFitters(filterKeys.ACTORS).filter(Boolean); - const includeTargetTypes = filterParamsWithPermanentFitters(filterKeys.INCLUDE_TARGET_TYPES) - .map((selectedLabel: string) => targetTypes?.find(({ label }) => label === selectedLabel)?.['value']) - .filter(Boolean); + const includeTargetTypes = filterParamsWithPermanentFitters(filterKeys.INCLUDE_TARGET_TYPES).filter(Boolean); const mappedFields = { ...(targetProjects?.length @@ -355,7 +335,47 @@ export const useFilters = ({ ...permanentFilters, ...mappedFields, } as TEventListFilters; - }, [propertyFilterQuery, usersData, projectsData, permanentFilters]); + }, [propertyFilterQuery, permanentFilters]); + + 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 }) + .unwrap() + .then(({ data }) => + data.map(({ project_name, project_id }) => ({ + propertyKey: filteringProperty?.key, + label: project_name, + value: project_id, + hiddenValue: 'test', + })), + ) + .then(setDynamicFilteringOptions); + } + + if (filteringProperty?.key === filterKeys.TARGET_USERS || filteringProperty?.key === filterKeys.ACTORS) { + await getUsers({ name_pattern: filteringText, limit }) + .unwrap() + .then(({ data }) => + data.map(({ username, id }) => ({ + propertyKey: filteringProperty?.key, + label: username, + value: id, + hiddenValue: 'test2', + })), + ) + .then(setDynamicFilteringOptions); + } + + setFilteringStatusType(undefined); + }; return { filteringRequestParams, @@ -364,6 +384,7 @@ export const useFilters = ({ onChangePropertyFilter, filteringOptions, filteringProperties, - isLoadingFilters: isLoadingProjects || isLoadingUsers, + filteringStatusType, + handleLoadItems, } as const; }; diff --git a/frontend/src/pages/Events/List/index.tsx b/frontend/src/pages/Events/List/index.tsx index bfed27f558..5b22d245ad 100644 --- a/frontend/src/pages/Events/List/index.tsx +++ b/frontend/src/pages/Events/List/index.tsx @@ -13,7 +13,7 @@ import { useLazyGetAllEventsQuery } from 'services/events'; import { useColumnsDefinitions } from './hooks/useColumnDefinitions'; import { useFilters } from './hooks/useFilters'; -import styles from '../../Runs/List/styles.module.scss'; +import styles from 'pages/Runs/List/styles.module.scss'; type RenderHeaderArgs = { refreshAction?: () => void; @@ -49,13 +49,13 @@ export const EventList: React.FC = ({ onChangePropertyFilter, filteringOptions, filteringProperties, - isLoadingFilters, + filteringStatusType, + handleLoadItems, } = useFilters({ permanentFilters, withSearchParams }); const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ useLazyQuery: useLazyGetAllEventsQuery, args: { ...filteringRequestParams, limit: DEFAULT_TABLE_PAGE_SIZE }, - skip: isLoadingFilters, getPaginationParams: (lastEvent) => ({ prev_recorded_at: lastEvent.recorded_at, @@ -73,7 +73,7 @@ export const EventList: React.FC = ({ const { columns } = useColumnsDefinitions(); - const loading = isLoadingFilters || isLoading; + const loading = isLoading; return ( = ({ filteringAriaLabel: t('projects.run.filter_property_placeholder'), filteringPlaceholder: t('projects.run.filter_property_placeholder'), operationAndText: 'and', + enteredTextLabel: (value) => `Use: ${value}`, }} filteringOptions={filteringOptions} filteringProperties={filteringProperties} + filteringStatusType={filteringStatusType} + onLoadItems={handleLoadItems} /> diff --git a/frontend/src/pages/Fleets/List/hooks.tsx b/frontend/src/pages/Fleets/List/hooks.tsx index 639d7b8683..bfff69d0ec 100644 --- a/frontend/src/pages/Fleets/List/hooks.tsx +++ b/frontend/src/pages/Fleets/List/hooks.tsx @@ -8,10 +8,19 @@ import type { PropertyFilterProps } from 'components'; import { Button, ListEmptyMessage, NavigateLink, StatusIndicator, TableProps } from 'components'; import { DATE_TIME_FORMAT } from 'consts'; -import { useProjectFilter } from 'hooks/useProjectFilter'; +import { useLocalStorageState } from 'hooks'; import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; -import { formatFleetBackend, formatFleetResources, getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet'; +import { + formatFleetBackend, + formatFleetResources, + getFleetInstancesLinkText, + getFleetPrice, + getFleetStatusIconType, +} from 'libs/fleet'; import { ROUTES } from 'routes'; +import { useLazyGetProjectsQuery } from 'services/project'; + +const limit = 100; export const useEmptyMessages = ({ clearFilter, @@ -115,10 +124,12 @@ const filterKeys: Record = { PROJECT_NAME: 'project_name', }; -export const useFilters = (localStorePrefix = 'fleet-list-page') => { +export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); - const [onlyActive, setOnlyActive] = useState(() => searchParams.get('only_active') === 'true'); - const { projectOptions } = useProjectFilter({ localStorePrefix }); + const [onlyActive, setOnlyActive] = useLocalStorageState('fleet-list-filter-only-active', true); + const [dynamicFilteringOptions, setDynamicFilteringOptions] = useState([]); + const [filteringStatusType, setFilteringStatusType] = useState(); + const [getProjects] = useLazyGetProjectsQuery(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -126,23 +137,12 @@ export const useFilters = (localStorePrefix = 'fleet-list-page') => { const clearFilter = () => { setSearchParams({}); - setOnlyActive(false); setPropertyFilterQuery(EMPTY_QUERY); }; const filteringOptions = useMemo(() => { - const options: PropertyFilterProps.FilteringOption[] = []; - - projectOptions.forEach(({ value }) => { - if (value) - options.push({ - propertyKey: filterKeys.PROJECT_NAME, - value, - }); - }); - - return options; - }, [projectOptions]); + return [...dynamicFilteringOptions]; + }, [dynamicFilteringOptions]); const filteringProperties = [ { @@ -170,8 +170,6 @@ export const useFilters = (localStorePrefix = 'fleet-list-page') => { const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { setOnlyActive(detail.checked); - - setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); }; const filteringRequestParams = useMemo(() => { @@ -187,6 +185,30 @@ export const useFilters = (localStorePrefix = 'fleet-list-page') => { const isDisabledClearFilter = !propertyFilterQuery.tokens.length && !onlyActive; + 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 }) + .unwrap() + .then(({ data }) => + data.map(({ project_name }) => ({ + propertyKey: filterKeys.PROJECT_NAME, + value: project_name, + })), + ) + .then(setDynamicFilteringOptions); + } + + setFilteringStatusType(undefined); + }; + return { filteringRequestParams, clearFilter, @@ -197,5 +219,7 @@ export const useFilters = (localStorePrefix = 'fleet-list-page') => { onlyActive, onChangeOnlyActive, isDisabledClearFilter, + filteringStatusType, + handleLoadItems, } as const; }; diff --git a/frontend/src/pages/Fleets/List/index.tsx b/frontend/src/pages/Fleets/List/index.tsx index 0a29192e0c..7e5ef21cf5 100644 --- a/frontend/src/pages/Fleets/List/index.tsx +++ b/frontend/src/pages/Fleets/List/index.tsx @@ -36,6 +36,8 @@ export const FleetList: React.FC = () => { onlyActive, onChangeOnlyActive, isDisabledClearFilter, + filteringStatusType, + handleLoadItems, } = useFilters(); const projectHavingFleetMap = useCheckingForFleetsInProjects({}); @@ -127,9 +129,12 @@ export const FleetList: React.FC = () => { filteringAriaLabel: t('fleets.filter_property_placeholder'), filteringPlaceholder: t('fleets.filter_property_placeholder'), operationAndText: 'and', + enteredTextLabel: (value) => `Use: ${value}`, }} filteringOptions={filteringOptions} filteringProperties={filteringProperties} + filteringStatusType={filteringStatusType} + onLoadItems={handleLoadItems} /> diff --git a/frontend/src/pages/Instances/List/hooks/useFilters.ts b/frontend/src/pages/Instances/List/hooks/useFilters.ts index 55453c33e4..bb3a5286bc 100644 --- a/frontend/src/pages/Instances/List/hooks/useFilters.ts +++ b/frontend/src/pages/Instances/List/hooks/useFilters.ts @@ -4,8 +4,9 @@ import { ToggleProps } from '@cloudscape-design/components'; import type { PropertyFilterProps } from 'components'; -import { useProjectFilter } from 'hooks/useProjectFilter'; +import { useLocalStorageState } from 'hooks'; import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; +import { useLazyGetProjectsQuery } from 'services/project'; type RequestParamsKeys = keyof Pick; @@ -14,10 +15,14 @@ const filterKeys: Record = { FLEET_IDS: 'fleet_ids', }; -export const useFilters = (localStorePrefix = 'instances-list-page') => { +const limit = 100; + +export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); - const [onlyActive, setOnlyActive] = useState(() => searchParams.get('only_active') === 'true'); - const { projectOptions } = useProjectFilter({ localStorePrefix }); + const [onlyActive, setOnlyActive] = useLocalStorageState('instance-list-filter-only-active', true); + const [dynamicFilteringOptions, setDynamicFilteringOptions] = useState([]); + const [filteringStatusType, setFilteringStatusType] = useState(); + const [getProjects] = useLazyGetProjectsQuery(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => { return requestParamsToTokens({ searchParams, filterKeys }); @@ -25,23 +30,12 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => { const clearFilter = () => { setSearchParams({}); - setOnlyActive(false); setPropertyFilterQuery(EMPTY_QUERY); }; const filteringOptions = useMemo(() => { - const options: PropertyFilterProps.FilteringOption[] = []; - - projectOptions.forEach(({ value }) => { - if (value) - options.push({ - propertyKey: filterKeys.PROJECT_NAMES, - value, - }); - }); - - return options; - }, [projectOptions]); + return [...dynamicFilteringOptions]; + }, [dynamicFilteringOptions]); const filteringProperties = [ { @@ -54,6 +48,7 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => { key: filterKeys.FLEET_IDS, operators: ['='], propertyLabel: 'Fleet ID', + groupValuesLabel: 'Fleet ID values', }, ]; @@ -70,8 +65,6 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => { const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { setOnlyActive(detail.checked); - - setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); }; const filteringRequestParams = useMemo(() => { @@ -88,6 +81,32 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => { const isDisabledClearFilter = !propertyFilterQuery.tokens.length && !onlyActive; + 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 }) + .unwrap() + .then(({ data }) => + data.map(({ project_name }) => ({ + propertyKey: filterKeys.PROJECT_NAMES, + value: project_name, + })), + ) + .then(setDynamicFilteringOptions); + } + + setFilteringStatusType(undefined); + }; + return { filteringRequestParams, clearFilter, @@ -98,5 +117,7 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => { onlyActive, onChangeOnlyActive, isDisabledClearFilter, + filteringStatusType, + handleLoadItems, } as const; }; diff --git a/frontend/src/pages/Instances/List/index.tsx b/frontend/src/pages/Instances/List/index.tsx index 423ebc77f9..a0cd2be951 100644 --- a/frontend/src/pages/Instances/List/index.tsx +++ b/frontend/src/pages/Instances/List/index.tsx @@ -38,6 +38,8 @@ export const List: React.FC = () => { onlyActive, onChangeOnlyActive, isDisabledClearFilter, + filteringStatusType, + handleLoadItems, } = useFilters(); const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ @@ -116,9 +118,12 @@ export const List: React.FC = () => { filteringAriaLabel: t('projects.run.filter_property_placeholder'), filteringPlaceholder: t('projects.run.filter_property_placeholder'), operationAndText: 'and', + enteredTextLabel: (value) => `Use: ${value}`, }} filteringOptions={filteringOptions} filteringProperties={filteringProperties} + filteringStatusType={filteringStatusType} + onLoadItems={handleLoadItems} /> diff --git a/frontend/src/pages/Models/List/hooks.tsx b/frontend/src/pages/Models/List/hooks.tsx index 461bf28a3b..3f1449de66 100644 --- a/frontend/src/pages/Models/List/hooks.tsx +++ b/frontend/src/pages/Models/List/hooks.tsx @@ -7,10 +7,10 @@ import type { PropertyFilterProps } from 'components'; import { Button, ListEmptyMessage, NavigateLink, TableProps } from 'components'; import { DATE_TIME_FORMAT } from 'consts'; -import { useProjectFilter } from 'hooks/useProjectFilter'; import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; import { ROUTES } from 'routes'; -import { useGetUserListQuery } from 'services/user'; +import { useLazyGetProjectsQuery } from 'services/project'; +import { useLazyGetUserListQuery } from 'services/user'; import { getModelGateway } from '../helpers'; @@ -126,10 +126,15 @@ const filterKeys: Record = { USER_NAME: 'username', }; -export const useFilters = (localStorePrefix = 'models-list-page') => { +const limit = 100; + +export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); - const { projectOptions } = useProjectFilter({ localStorePrefix }); - const { data: usersData } = useGetUserListQuery({}); + + const [filteringOptions, setFilteringOptions] = useState([]); + const [filteringStatusType, setFilteringStatusType] = useState(); + const [getProjects] = useLazyGetProjectsQuery(); + const [getUsers] = useLazyGetUserListQuery(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -140,27 +145,6 @@ export const useFilters = (localStorePrefix = 'models-list-page') => { setPropertyFilterQuery(EMPTY_QUERY); }; - const filteringOptions = useMemo(() => { - const options: PropertyFilterProps.FilteringOption[] = []; - - projectOptions.forEach(({ value }) => { - if (value) - options.push({ - propertyKey: filterKeys.PROJECT_NAME, - value, - }); - }); - - usersData?.data?.forEach(({ username }) => { - options.push({ - propertyKey: filterKeys.USER_NAME, - value: username, - }); - }); - - return options; - }, [projectOptions, usersData]); - const filteringProperties = [ { key: filterKeys.PROJECT_NAME, @@ -172,6 +156,7 @@ export const useFilters = (localStorePrefix = 'models-list-page') => { key: filterKeys.USER_NAME, operators: ['='], propertyLabel: 'User', + groupValuesLabel: 'User values', }, ]; @@ -196,6 +181,42 @@ export const useFilters = (localStorePrefix = 'models-list-page') => { }) as Partial; }, [propertyFilterQuery]); + 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 }) + .unwrap() + .then(({ data }) => + data.map(({ project_name }) => ({ + propertyKey: filterKeys.PROJECT_NAME, + value: project_name, + })), + ) + .then(setFilteringOptions); + } + + if (filteringProperty?.key === filterKeys.USER_NAME) { + await getUsers({ name_pattern: filteringText, limit }) + .unwrap() + .then(({ data }) => + data.map(({ username }) => ({ + propertyKey: filterKeys.USER_NAME, + value: username, + })), + ) + .then(setFilteringOptions); + } + + setFilteringStatusType(undefined); + }; + return { filteringRequestParams, clearFilter, @@ -203,5 +224,7 @@ export const useFilters = (localStorePrefix = 'models-list-page') => { onChangePropertyFilter, filteringOptions, filteringProperties, + filteringStatusType, + handleLoadItems, } as const; }; diff --git a/frontend/src/pages/Models/List/index.tsx b/frontend/src/pages/Models/List/index.tsx index f0dffa4cf7..769c8bc105 100644 --- a/frontend/src/pages/Models/List/index.tsx +++ b/frontend/src/pages/Models/List/index.tsx @@ -26,6 +26,8 @@ export const List: React.FC = () => { filteringOptions, filteringProperties, filteringRequestParams, + filteringStatusType, + handleLoadItems, } = useFilters(); useBreadcrumbs([ @@ -98,9 +100,12 @@ export const List: React.FC = () => { filteringAriaLabel: t('projects.run.filter_property_placeholder'), filteringPlaceholder: t('projects.run.filter_property_placeholder'), operationAndText: 'and', + enteredTextLabel: (value) => `Use: ${value}`, }} filteringOptions={filteringOptions} filteringProperties={filteringProperties} + filteringStatusType={filteringStatusType} + onLoadItems={handleLoadItems} /> diff --git a/frontend/src/pages/Offers/List/hooks/useFilters.ts b/frontend/src/pages/Offers/List/hooks/useFilters.ts index 20c95402c0..b44cdfcee0 100644 --- a/frontend/src/pages/Offers/List/hooks/useFilters.ts +++ b/frontend/src/pages/Offers/List/hooks/useFilters.ts @@ -3,7 +3,6 @@ import { useSearchParams } from 'react-router-dom'; import type { MultiselectProps, PropertyFilterProps } from 'components'; -import { useProjectFilter } from 'hooks/useProjectFilter'; import { EMPTY_QUERY, requestParamsToArray, @@ -11,6 +10,7 @@ import { tokensToRequestParams, tokensToSearchParams, } from 'libs/filters'; +import { useGetProjectsQuery, useLazyGetProjectsQuery } from 'services/project'; import { getPropertyFilterOptions } from '../helpers'; @@ -54,43 +54,51 @@ const filteringProperties = [ key: filterKeys.PROJECT_NAME, operators: ['='], propertyLabel: 'Project', + groupValuesLabel: 'Project values', }, { key: filterKeys.GPU_NAME, operators: ['='], propertyLabel: 'GPU name', + groupValuesLabel: 'GPU name values', }, { key: filterKeys.GPU_COUNT, operators: ['<=', '>='], propertyLabel: 'GPU count', + groupValuesLabel: 'GPU count values', }, { key: filterKeys.GPU_MEMORY, operators: ['<=', '>='], propertyLabel: 'GPU memory', + groupValuesLabel: 'GPU memory values', }, { key: filterKeys.BACKEND, operators: ['='], propertyLabel: 'Backend', + groupValuesLabel: 'Backend values', }, { key: filterKeys.SPOT_POLICY, operators: ['='], propertyLabel: 'Spot policy', + groupValuesLabel: 'Spot policy values', }, ]; const gpuFilterOption = { label: 'GPU', value: 'gpu' }; - const defaultGroupByOptions = [{ ...gpuFilterOption }, { label: 'Backend', value: 'backend' }]; - const groupByRequestParamName: RequestParamsKeys = 'group_by'; +const limit = 100; export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = {}, defaultFilters }: UseFiltersArgs) => { const [searchParams, setSearchParams] = useSearchParams(); - const { projectOptions } = useProjectFilter({ localStorePrefix: 'offers-list-projects' }); + const [dynamicFilteringOptions, setDynamicFilteringOptions] = useState([]); + const [filteringStatusType, setFilteringStatusType] = useState(); + const [getProjects] = useLazyGetProjectsQuery(); + const { data: projectsData } = useGetProjectsQuery({ limit: 1 }); const projectNameIsChecked = useRef(false); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => @@ -119,18 +127,10 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { }; const filteringOptions = useMemo(() => { - const options: PropertyFilterProps.FilteringOption[] = [...spotPolicyOptions]; + const options: PropertyFilterProps.FilteringOption[] = [...spotPolicyOptions, ...dynamicFilteringOptions]; 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, @@ -146,7 +146,7 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { }); return options; - }, [gpus]); + }, [gpus, dynamicFilteringOptions]); const groupByOptions: MultiselectProps.Options = useMemo(() => { return defaultGroupByOptions.map((option) => { @@ -243,8 +243,32 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { }; }, [propertyFilterQuery, permanentFilters]); + 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 }) + .unwrap() + .then(({ data }) => + data.map(({ project_name }) => ({ + propertyKey: filterKeys.PROJECT_NAME, + value: project_name, + })), + ) + .then(setDynamicFilteringOptions); + } + + setFilteringStatusType(undefined); + }; + useEffect(() => { - if (!projectNameIsChecked.current && projectOptions.length) { + if (!projectNameIsChecked.current && projectsData?.data?.length) { projectNameIsChecked.current = true; if (!filteringRequestParams['project_name']) { @@ -254,14 +278,14 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { { operator: '=', propertyKey: filterKeys.PROJECT_NAME, - value: projectOptions[0].value, + value: projectsData.data[0].project_name, }, ], operation: 'and', }); } } - }, [projectOptions]); + }, [projectsData]); return { filteringRequestParams, @@ -273,5 +297,7 @@ export const useFilters = ({ gpus, withSearchParams = true, permanentFilters = { groupBy, groupByOptions, onChangeGroupBy, + filteringStatusType, + handleLoadItems, } as const; }; diff --git a/frontend/src/pages/Offers/List/index.tsx b/frontend/src/pages/Offers/List/index.tsx index c594e67f60..a44dbd48f4 100644 --- a/frontend/src/pages/Offers/List/index.tsx +++ b/frontend/src/pages/Offers/List/index.tsx @@ -105,6 +105,8 @@ export const OfferList: React.FC = ({ groupBy, groupByOptions, onChangeGroupBy, + filteringStatusType, + handleLoadItems, } = useFilters({ gpus: data?.gpus ?? [], withSearchParams, permanentFilters, defaultFilters }); useEffect(() => { @@ -239,38 +241,43 @@ export const OfferList: React.FC = ({ loading={!disabled && (isLoading || isFetching)} loadingText={t('common.loading')} stickyHeader={true} - filter={disabled ? undefined : ( -
-
- -
+ filter={ + disabled ? undefined : ( +
+
+ `Use: ${value}`, + }} + filteringOptions={filteringOptions} + filteringProperties={filteringProperties} + filteringStatusType={filteringStatusType} + onLoadItems={handleLoadItems} + /> +
-
- +
+ +
-
- )} + ) + } /> ); }; diff --git a/frontend/src/pages/Project/Details/Events/index.tsx b/frontend/src/pages/Project/Details/Events/index.tsx index df18300c52..f01186cecc 100644 --- a/frontend/src/pages/Project/Details/Events/index.tsx +++ b/frontend/src/pages/Project/Details/Events/index.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; -import { Button, Header, SpaceBetween } from 'components'; +import { Button, Container, Header, Loader, SpaceBetween } from 'components'; import { useBreadcrumbs } from 'hooks'; import { ROUTES } from 'routes'; +import { useGetProjectQuery } from 'services/project'; import { EventList } from 'pages/Events/List'; @@ -14,6 +15,7 @@ export const Events: React.FC = () => { const params = useParams(); const paramProjectName = params.projectName ?? ''; const navigate = useNavigate(); + const { data, isLoading } = useGetProjectQuery({ name: paramProjectName }); useBreadcrumbs([ { @@ -31,9 +33,16 @@ export const Events: React.FC = () => { ]); const goToEventsPage = () => { - navigate(ROUTES.EVENTS.LIST + `?within_projects=${paramProjectName}`); + navigate(ROUTES.EVENTS.LIST + `?within_projects=${data?.project_id}`); }; + if (isLoading || !data) + return ( + + + + ); + return ( { @@ -48,7 +57,7 @@ export const Events: React.FC = () => { /> ); }} - permanentFilters={{ within_projects: [paramProjectName] }} + permanentFilters={{ within_projects: [data.project_id] }} showFilters={false} /> ); diff --git a/frontend/src/pages/Runs/List/hooks/useFilters.ts b/frontend/src/pages/Runs/List/hooks/useFilters.ts index 82f1ca40bd..c1af161607 100644 --- a/frontend/src/pages/Runs/List/hooks/useFilters.ts +++ b/frontend/src/pages/Runs/List/hooks/useFilters.ts @@ -4,13 +4,10 @@ import { ToggleProps } from '@cloudscape-design/components'; import type { PropertyFilterProps } from 'components'; -import { useProjectFilter } from 'hooks/useProjectFilter'; +import { useLocalStorageState } from 'hooks'; import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; -import { useGetUserListQuery } from 'services/user'; - -type Args = { - localStorePrefix: string; -}; +import { useLazyGetProjectsQuery } from 'services/project'; +import { useLazyGetUserListQuery } from 'services/user'; type RequestParamsKeys = keyof Pick; @@ -19,11 +16,15 @@ const filterKeys: Record = { USER_NAME: 'username', }; -export const useFilters = ({ localStorePrefix }: Args) => { +const limit = 100; + +export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); - const [onlyActive, setOnlyActive] = useState(() => searchParams.get('only_active') === 'true'); - const { projectOptions } = useProjectFilter({ localStorePrefix }); - const { data: usersData } = useGetUserListQuery({}); + const [onlyActive, setOnlyActive] = useLocalStorageState('run-list-filter-only-active', true); + const [filteringOptions, setFilteringOptions] = useState([]); + const [filteringStatusType, setFilteringStatusType] = useState(); + const [getProjects] = useLazyGetProjectsQuery(); + const [getUsers] = useLazyGetUserListQuery(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -31,44 +32,26 @@ export const useFilters = ({ localStorePrefix }: Args) => { const clearFilter = () => { setSearchParams({}); - setOnlyActive(false); setPropertyFilterQuery(EMPTY_QUERY); }; - const filteringOptions = useMemo(() => { - const options: PropertyFilterProps.FilteringOption[] = []; - - projectOptions.forEach(({ value }) => { - if (value) - options.push({ - propertyKey: filterKeys.PROJECT_NAME, - value, - }); - }); - - usersData?.data?.forEach(({ username }) => { - options.push({ - propertyKey: filterKeys.USER_NAME, - value: username, - }); - }); - - return options; - }, [projectOptions, usersData]); - - const filteringProperties = [ - { - key: filterKeys.PROJECT_NAME, - operators: ['='], - propertyLabel: 'Project', - groupValuesLabel: 'Project values', - }, - { - key: filterKeys.USER_NAME, - operators: ['='], - propertyLabel: 'User', - }, - ]; + const filteringProperties = useMemo( + () => [ + { + key: filterKeys.PROJECT_NAME, + operators: ['='], + propertyLabel: 'Project', + groupValuesLabel: 'Project values', + }, + { + key: filterKeys.USER_NAME, + operators: ['='], + propertyLabel: 'User', + groupValuesLabel: 'User values', + }, + ], + [], + ); const onChangePropertyFilter: PropertyFilterProps['onChange'] = ({ detail }) => { const { tokens, operation } = detail; @@ -87,8 +70,6 @@ export const useFilters = ({ localStorePrefix }: Args) => { const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { setOnlyActive(detail.checked); - - setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); }; const filteringRequestParams = useMemo(() => { @@ -102,6 +83,42 @@ export const useFilters = ({ localStorePrefix }: Args) => { } as Partial; }, [propertyFilterQuery, onlyActive]); + 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 }) + .unwrap() + .then(({ data }) => + data.map(({ project_name }) => ({ + propertyKey: filterKeys.PROJECT_NAME, + value: project_name, + })), + ) + .then(setFilteringOptions); + } + + if (filteringProperty?.key === filterKeys.USER_NAME) { + await getUsers({ name_pattern: filteringText, limit }) + .unwrap() + .then(({ data }) => + data.map(({ username }) => ({ + propertyKey: filterKeys.USER_NAME, + value: username, + })), + ) + .then(setFilteringOptions); + } + + setFilteringStatusType(undefined); + }; + return { filteringRequestParams, clearFilter, @@ -111,5 +128,7 @@ export const useFilters = ({ localStorePrefix }: Args) => { filteringProperties, onlyActive, onChangeOnlyActive, + filteringStatusType, + handleLoadItems, } as const; }; diff --git a/frontend/src/pages/Runs/List/index.tsx b/frontend/src/pages/Runs/List/index.tsx index 4cd69ffd41..ad4c63ef94 100644 --- a/frontend/src/pages/Runs/List/index.tsx +++ b/frontend/src/pages/Runs/List/index.tsx @@ -47,9 +47,9 @@ export const RunList: React.FC = () => { filteringRequestParams, onlyActive, onChangeOnlyActive, - } = useFilters({ - localStorePrefix: 'administration-run-list-page', - }); + filteringStatusType, + handleLoadItems, + } = useFilters(); const projectHavingFleetMap = useCheckingForFleetsInProjects({}); @@ -188,9 +188,12 @@ export const RunList: React.FC = () => { filteringAriaLabel: t('projects.run.filter_property_placeholder'), filteringPlaceholder: t('projects.run.filter_property_placeholder'), operationAndText: 'and', + enteredTextLabel: (value) => `Use: ${value}`, }} filteringOptions={filteringOptions} filteringProperties={filteringProperties} + filteringStatusType={filteringStatusType} + onLoadItems={handleLoadItems} />
diff --git a/frontend/src/pages/User/Details/Events/index.tsx b/frontend/src/pages/User/Details/Events/index.tsx index 3141d6f33a..be5c174208 100644 --- a/frontend/src/pages/User/Details/Events/index.tsx +++ b/frontend/src/pages/User/Details/Events/index.tsx @@ -2,10 +2,11 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; -import { Button, Header, SegmentedControl, SpaceBetween } from 'components'; +import { Button, Container, Header, Loader, SegmentedControl, SpaceBetween } from 'components'; import { useBreadcrumbs } from 'hooks'; import { ROUTES } from 'routes'; +import { useGetUserQuery } from 'services/user'; import { EventList } from 'pages/Events/List'; @@ -15,6 +16,7 @@ export const Events: React.FC = () => { const paramUserName = params.userName ?? ''; const navigate = useNavigate(); const [filterParamName, setFilterParamName] = useState('actors'); + const { data, isLoading } = useGetUserQuery({ name: paramUserName }); useBreadcrumbs([ { @@ -32,9 +34,16 @@ export const Events: React.FC = () => { ]); const goToEventsPage = () => { - navigate(ROUTES.EVENTS.LIST + `?${filterParamName}=${paramUserName}`); + navigate(ROUTES.EVENTS.LIST + `?${filterParamName}=${data?.id}`); }; + if (isLoading || !data) + return ( + + + + ); + return ( { @@ -57,7 +66,7 @@ export const Events: React.FC = () => { /> ); }} - permanentFilters={{ [filterParamName]: [paramUserName] }} + permanentFilters={{ [filterParamName]: [data.id] }} showFilters={false} /> ); diff --git a/frontend/src/pages/Volumes/List/hooks.tsx b/frontend/src/pages/Volumes/List/hooks.tsx index ce73b3a94b..a969cfc878 100644 --- a/frontend/src/pages/Volumes/List/hooks.tsx +++ b/frontend/src/pages/Volumes/List/hooks.tsx @@ -8,12 +8,12 @@ import type { PropertyFilterProps } from 'components'; import { Button, ListEmptyMessage, NavigateLink, StatusIndicator } from 'components'; import { DATE_TIME_FORMAT } from 'consts'; -import { useNotifications } from 'hooks'; -import { useProjectFilter } from 'hooks/useProjectFilter'; +import { useLocalStorageState, useNotifications } from 'hooks'; import { getServerError } from 'libs'; import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; import { getStatusIconType } from 'libs/volumes'; import { ROUTES } from 'routes'; +import { useLazyGetProjectsQuery } from 'services/project'; import { useDeleteVolumesMutation } from 'services/volume'; export const useVolumesTableEmptyMessages = ({ @@ -122,10 +122,14 @@ const filterKeys: Record = { PROJECT_NAME: 'project_name', }; -export const useFilters = (localStorePrefix = 'volume-list-page') => { +const limit = 100; + +export const useFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); - const [onlyActive, setOnlyActive] = useState(() => searchParams.get('only_active') === 'true'); - const { projectOptions } = useProjectFilter({ localStorePrefix }); + const [onlyActive, setOnlyActive] = useLocalStorageState('volume-list-filter-only-active', true); + const [dynamicFilteringOptions, setDynamicFilteringOptions] = useState([]); + const [filteringStatusType, setFilteringStatusType] = useState(); + const [getProjects] = useLazyGetProjectsQuery(); const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => requestParamsToTokens({ searchParams, filterKeys }), @@ -133,25 +137,14 @@ export const useFilters = (localStorePrefix = 'volume-list-page') => { const clearFilter = () => { setSearchParams({}); - setOnlyActive(false); setPropertyFilterQuery(EMPTY_QUERY); }; const isDisabledClearFilter = !propertyFilterQuery.tokens.length && !onlyActive; - const filteringOptions = useMemo(() => { - const options: PropertyFilterProps.FilteringOption[] = []; - - projectOptions.forEach(({ value }) => { - if (value) - options.push({ - propertyKey: filterKeys.PROJECT_NAME, - value, - }); - }); - - return options; - }, [projectOptions]); + const filteringOptions = useMemo(() => { + return [...dynamicFilteringOptions]; + }, [dynamicFilteringOptions]); const filteringProperties = [ { @@ -179,8 +172,6 @@ export const useFilters = (localStorePrefix = 'volume-list-page') => { const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { setOnlyActive(detail.checked); - - setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); }; const filteringRequestParams = useMemo(() => { @@ -194,6 +185,30 @@ export const useFilters = (localStorePrefix = 'volume-list-page') => { } as Partial; }, [propertyFilterQuery, onlyActive]); + 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 }) + .unwrap() + .then(({ data }) => + data.map(({ project_name }) => ({ + propertyKey: filterKeys.PROJECT_NAME, + value: project_name, + })), + ) + .then(setDynamicFilteringOptions); + } + + setFilteringStatusType(undefined); + }; + return { filteringRequestParams, clearFilter, @@ -204,6 +219,8 @@ export const useFilters = (localStorePrefix = 'volume-list-page') => { onlyActive, onChangeOnlyActive, isDisabledClearFilter, + filteringStatusType, + handleLoadItems, } as const; }; diff --git a/frontend/src/pages/Volumes/List/index.tsx b/frontend/src/pages/Volumes/List/index.tsx index 15229d772e..d51b9f1d5e 100644 --- a/frontend/src/pages/Volumes/List/index.tsx +++ b/frontend/src/pages/Volumes/List/index.tsx @@ -24,6 +24,8 @@ export const VolumeList: React.FC = () => { onlyActive, onChangeOnlyActive, isDisabledClearFilter, + filteringStatusType, + handleLoadItems, } = useFilters(); const { isDeleting, deleteVolumes } = useVolumesDelete(); @@ -125,9 +127,12 @@ export const VolumeList: React.FC = () => { filteringAriaLabel: t('projects.run.filter_property_placeholder'), filteringPlaceholder: t('projects.run.filter_property_placeholder'), operationAndText: 'and', + enteredTextLabel: (value) => `Use: ${value}`, }} filteringOptions={filteringOptions} filteringProperties={filteringProperties} + filteringStatusType={filteringStatusType} + onLoadItems={handleLoadItems} />