diff --git a/frontend/src/libs/filters.ts b/frontend/src/libs/filters.ts index 03ae946930..bdf1437fe0 100644 --- a/frontend/src/libs/filters.ts +++ b/frontend/src/libs/filters.ts @@ -1,24 +1,54 @@ import type { PropertyFilterProps } from 'components'; -export const tokensToRequestParams = ( +export const tokensToSearchParams = ( tokens: PropertyFilterProps.Query['tokens'], onlyActive?: boolean, ) => { - const params: Record = tokens.reduce((acc, token) => { + const params = new URLSearchParams(); + + tokens.forEach((token) => { if (token.propertyKey) { - acc[token.propertyKey as RequestParamsKeys] = token.value; + params.append(token.propertyKey as RequestParamsKeys, token.value); } - - return acc; - }, {} as Record); + }); if (onlyActive) { - params['only_active'] = 'true'; + params.append('only_active', 'true'); } return params; }; +export const tokensToRequestParams = ({ + tokens, + arrayFieldKeys, +}: { + tokens: PropertyFilterProps.Query['tokens']; + arrayFieldKeys?: RequestParamsKeys[]; +}) => { + return tokens.reduce>((acc, token) => { + const propertyKey = token.propertyKey as RequestParamsKeys; + + if (!propertyKey) { + return acc; + } + + if (arrayFieldKeys?.includes(propertyKey)) { + if (Array.isArray(acc[propertyKey])) { + acc[propertyKey].push(token.value); + } else { + acc[propertyKey] = [token.value]; + } + + return acc; + } + + acc[propertyKey] = token.value; + + return acc; + }, {} as Record); +}; + export const EMPTY_QUERY: PropertyFilterProps.Query = { tokens: [], operation: 'and', diff --git a/frontend/src/pages/Fleets/Details/index.tsx b/frontend/src/pages/Fleets/Details/index.tsx index 658d61c010..e487f7a2c9 100644 --- a/frontend/src/pages/Fleets/Details/index.tsx +++ b/frontend/src/pages/Fleets/Details/index.tsx @@ -135,7 +135,7 @@ export const FleetDetails: React.FC = () => { {t('fleets.instances.title')}
- + {getFleetInstancesLinkText(data)}
diff --git a/frontend/src/pages/Fleets/List/hooks.tsx b/frontend/src/pages/Fleets/List/hooks.tsx index 4a1a0e3835..b9789fd41b 100644 --- a/frontend/src/pages/Fleets/List/hooks.tsx +++ b/frontend/src/pages/Fleets/List/hooks.tsx @@ -9,7 +9,7 @@ import { Button, ListEmptyMessage, NavigateLink, StatusIndicator, TableProps } f import { DATE_TIME_FORMAT } from 'consts'; import { useProjectFilter } from 'hooks/useProjectFilter'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams } from 'libs/filters'; +import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; import { getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet'; import { ROUTES } from 'routes'; @@ -76,7 +76,7 @@ export const useColumnsDefinitions = () => { id: 'instances', header: t('fleets.instances.title'), cell: (item) => ( - + {getFleetInstancesLinkText(item)} ), @@ -153,7 +153,7 @@ export const useFilters = (localStorePrefix = 'fleet-list-page') => { return !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex); }); - setSearchParams(tokensToRequestParams(filteredTokens, onlyActive)); + setSearchParams(tokensToSearchParams(filteredTokens, onlyActive)); setPropertyFilterQuery({ operation, @@ -164,16 +164,18 @@ export const useFilters = (localStorePrefix = 'fleet-list-page') => { const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { setOnlyActive(detail.checked); - setSearchParams(tokensToRequestParams(propertyFilterQuery.tokens, detail.checked)); + setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); }; const filteringRequestParams = useMemo(() => { - const params = tokensToRequestParams(propertyFilterQuery.tokens); + const params = tokensToRequestParams({ + tokens: propertyFilterQuery.tokens, + }); return { ...params, only_active: onlyActive, - }; + } as Partial; }, [propertyFilterQuery, onlyActive]); const isDisabledClearFilter = !propertyFilterQuery.tokens.length && !onlyActive; diff --git a/frontend/src/pages/Instances/List/hooks/useEmptyMessage.tsx b/frontend/src/pages/Instances/List/hooks/useEmptyMessage.tsx index d2ca88d1f0..a2909c7637 100644 --- a/frontend/src/pages/Instances/List/hooks/useEmptyMessage.tsx +++ b/frontend/src/pages/Instances/List/hooks/useEmptyMessage.tsx @@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next'; import { Button, ListEmptyMessage } from 'components'; export const useEmptyMessages = ({ - clearFilters, + clearFilter, isDisabledClearFilter, }: { - clearFilters?: () => void; + clearFilter?: () => void; isDisabledClearFilter?: boolean; }) => { const { t } = useTranslation(); @@ -18,12 +18,12 @@ export const useEmptyMessages = ({ title={t('fleets.instances.empty_message_title')} message={t('fleets.instances.empty_message_text')} > - ); - }, [clearFilters, isDisabledClearFilter]); + }, [clearFilter, isDisabledClearFilter]); const renderNoMatchMessage = useCallback<() => React.ReactNode>(() => { return ( @@ -31,12 +31,12 @@ export const useEmptyMessages = ({ title={t('fleets.instances.nomatch_message_title')} message={t('fleets.instances.nomatch_message_text')} > - ); - }, [clearFilters, isDisabledClearFilter]); + }, [clearFilter, isDisabledClearFilter]); return { renderEmptyMessage, renderNoMatchMessage } as const; }; diff --git a/frontend/src/pages/Instances/List/hooks/useFilters.ts b/frontend/src/pages/Instances/List/hooks/useFilters.ts index ab5df30f3f..a15a7ca3e0 100644 --- a/frontend/src/pages/Instances/List/hooks/useFilters.ts +++ b/frontend/src/pages/Instances/List/hooks/useFilters.ts @@ -1,44 +1,104 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { ToggleProps } from '@cloudscape-design/components'; + +import type { PropertyFilterProps } from 'components'; -import { useLocalStorageState } from 'hooks/useLocalStorageState'; import { useProjectFilter } from 'hooks/useProjectFilter'; +import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; + +type RequestParamsKeys = keyof Pick; + +const filterKeys: Record = { + PROJECT_NAMES: 'project_names', + FLEET_IDS: 'fleet_ids', +}; export const useFilters = (localStorePrefix = 'instances-list-page') => { const [searchParams, setSearchParams] = useSearchParams(); - const [onlyActive, setOnlyActive] = useLocalStorageState(`${localStorePrefix}-is-active`, false); - const { selectedProject, setSelectedProject, projectOptions } = useProjectFilter({ localStorePrefix }); + const [onlyActive, setOnlyActive] = useState(() => searchParams.get('only_active') === 'true'); + const { projectOptions } = useProjectFilter({ localStorePrefix }); + + const [propertyFilterQuery, setPropertyFilterQuery] = useState(() => { + console.log(requestParamsToTokens({ searchParams, filterKeys })); - const clearFilters = () => { + return requestParamsToTokens({ searchParams, filterKeys }); + }); + + const clearFilter = () => { + setSearchParams({}); setOnlyActive(false); - setSelectedProject(null); + setPropertyFilterQuery(EMPTY_QUERY); + }; - setSearchParams((prev) => { - prev.delete('fleetId'); - return prev; + const filteringOptions = useMemo(() => { + const options: PropertyFilterProps.FilteringOption[] = []; + + projectOptions.forEach(({ value }) => { + if (value) + options.push({ + propertyKey: filterKeys.PROJECT_NAMES, + value, + }); + }); + + return options; + }, [projectOptions]); + + const filteringProperties = [ + { + key: filterKeys.PROJECT_NAMES, + operators: ['='], + propertyLabel: 'Project', + groupValuesLabel: 'Project values', + }, + { + key: filterKeys.FLEET_IDS, + operators: ['='], + propertyLabel: 'Fleet ID', + }, + ]; + + const onChangePropertyFilter: PropertyFilterProps['onChange'] = ({ detail }) => { + const { tokens, operation } = detail; + + setSearchParams(tokensToSearchParams(tokens, onlyActive)); + + setPropertyFilterQuery({ + operation, + tokens, }); }; - const selectedFleet = useMemo(() => { - const fleetName = searchParams.get('fleetId'); + const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { + setOnlyActive(detail.checked); + + setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); + }; - if (fleetName) { - return { label: fleetName, value: fleetName }; - } + const filteringRequestParams = useMemo(() => { + const params = tokensToRequestParams({ + tokens: propertyFilterQuery.tokens, + arrayFieldKeys: [filterKeys.PROJECT_NAMES, filterKeys.FLEET_IDS], + }); - return null; - }, [searchParams]); + return { + ...params, + only_active: onlyActive, + } as Partial; + }, [propertyFilterQuery, onlyActive]); - const isDisabledClearFilter = !selectedProject && !onlyActive && !selectedFleet; + const isDisabledClearFilter = !propertyFilterQuery.tokens.length && !onlyActive; return { - projectOptions, - selectedProject, - setSelectedProject, - selectedFleet, + filteringRequestParams, + clearFilter, + propertyFilterQuery, + onChangePropertyFilter, + filteringOptions, + filteringProperties, onlyActive, - setOnlyActive, - clearFilters, + onChangeOnlyActive, isDisabledClearFilter, } as const; }; diff --git a/frontend/src/pages/Instances/List/index.tsx b/frontend/src/pages/Instances/List/index.tsx index e3d392d158..423ebc77f9 100644 --- a/frontend/src/pages/Instances/List/index.tsx +++ b/frontend/src/pages/Instances/List/index.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, FormField, Header, Loader, SelectCSD, SpaceBetween, Table, Toggle } from 'components'; +import { Button, Header, Loader, PropertyFilter, SpaceBetween, Table, Toggle } from 'components'; import { DEFAULT_TABLE_PAGE_SIZE } from 'consts'; import { useBreadcrumbs, useInfiniteScroll } from 'hooks'; @@ -29,28 +29,20 @@ export const List: React.FC = () => { const { columns } = useColumnsDefinitions(); const { + filteringRequestParams, + clearFilter, + propertyFilterQuery, + onChangePropertyFilter, + filteringOptions, + filteringProperties, onlyActive, - setOnlyActive, + onChangeOnlyActive, isDisabledClearFilter, - clearFilters, - projectOptions, - selectedProject, - setSelectedProject, - selectedFleet, } = useFilters(); - const args = useMemo(() => { - return { - project_names: selectedProject?.value ? [selectedProject.value] : undefined, - only_active: onlyActive, - fleet_ids: selectedFleet?.value ? [selectedFleet.value] : undefined, - limit: DEFAULT_TABLE_PAGE_SIZE, - }; - }, [selectedProject, selectedFleet, onlyActive]); - const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll({ useLazyQuery: useLazyGetInstancesQuery, - args, + args: { ...filteringRequestParams, limit: DEFAULT_TABLE_PAGE_SIZE }, getPaginationParams: (lastInstance) => ({ prev_created_at: lastInstance.created, @@ -60,7 +52,7 @@ export const List: React.FC = () => { const { deleteFleets, isDeleting } = useActions(); - const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({ clearFilters, isDisabledClearFilter }); + const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({ clearFilter, isDisabledClearFilter }); const { items, collectionProps } = useCollection(data, { filtering: { @@ -113,46 +105,28 @@ export const List: React.FC = () => { } filter={
-
- - { - setSelectedProject(event.detail.selectedOption); - }} - placeholder={t('projects.run.project_placeholder')} - expandToViewport={true} - filteringType="auto" - /> - -
- -
- - - +
+
- setOnlyActive(detail.checked)} checked={onlyActive}> + {t('fleets.instances.active_only')}
- -
- -
} footer={} diff --git a/frontend/src/pages/Instances/List/styles.module.scss b/frontend/src/pages/Instances/List/styles.module.scss index a6917fc622..183b03d011 100644 --- a/frontend/src/pages/Instances/List/styles.module.scss +++ b/frontend/src/pages/Instances/List/styles.module.scss @@ -1,20 +1,15 @@ .filters { - --select-width: calc((688px - 3 * 20px) / 2); display: flex; flex-wrap: wrap; gap: 0 20px; - .select { - width: var(--select-width, 30%); + .propertyFilter { + max-width: 640px; + flex-grow: 1; + min-width: 0; } - .activeOnly { display: flex; - align-items: center; - padding-top: 26px; - } - - .clear { - padding-top: 26px; + padding-top: 7px; } } diff --git a/frontend/src/pages/Models/List/hooks.tsx b/frontend/src/pages/Models/List/hooks.tsx index 8188ec7514..912875b394 100644 --- a/frontend/src/pages/Models/List/hooks.tsx +++ b/frontend/src/pages/Models/List/hooks.tsx @@ -8,7 +8,7 @@ import { Button, ListEmptyMessage, NavigateLink, TableProps } from 'components'; import { DATE_TIME_FORMAT } from 'consts'; import { useProjectFilter } from 'hooks/useProjectFilter'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams } from 'libs/filters'; +import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; import { ROUTES } from 'routes'; import { useGetUserListQuery } from 'services/user'; @@ -182,7 +182,7 @@ export const useFilters = (localStorePrefix = 'models-list-page') => { return !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex); }); - setSearchParams(tokensToRequestParams(filteredTokens)); + setSearchParams(tokensToSearchParams(filteredTokens)); setPropertyFilterQuery({ operation, @@ -191,11 +191,9 @@ export const useFilters = (localStorePrefix = 'models-list-page') => { }; const filteringRequestParams = useMemo(() => { - const { only_active, ...params } = tokensToRequestParams(propertyFilterQuery.tokens); - - return { - ...params, - }; + return tokensToRequestParams({ + tokens: propertyFilterQuery.tokens, + }) as Partial; }, [propertyFilterQuery]); return { diff --git a/frontend/src/pages/Runs/List/hooks/useFilters.ts b/frontend/src/pages/Runs/List/hooks/useFilters.ts index 18db5e332b..d675f78ea5 100644 --- a/frontend/src/pages/Runs/List/hooks/useFilters.ts +++ b/frontend/src/pages/Runs/List/hooks/useFilters.ts @@ -5,7 +5,7 @@ import { ToggleProps } from '@cloudscape-design/components'; import type { PropertyFilterProps } from 'components'; import { useProjectFilter } from 'hooks/useProjectFilter'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams } from 'libs/filters'; +import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; import { useGetUserListQuery } from 'services/user'; type Args = { @@ -77,7 +77,7 @@ export const useFilters = ({ localStorePrefix }: Args) => { return !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex); }); - setSearchParams(tokensToRequestParams(filteredTokens, onlyActive)); + setSearchParams(tokensToSearchParams(filteredTokens, onlyActive)); setPropertyFilterQuery({ operation, @@ -88,16 +88,18 @@ export const useFilters = ({ localStorePrefix }: Args) => { const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { setOnlyActive(detail.checked); - setSearchParams(tokensToRequestParams(propertyFilterQuery.tokens, detail.checked)); + setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); }; const filteringRequestParams = useMemo(() => { - const params = tokensToRequestParams(propertyFilterQuery.tokens); + const params = tokensToRequestParams({ + tokens: propertyFilterQuery.tokens, + }); return { ...params, only_active: onlyActive, - }; + } as Partial; }, [propertyFilterQuery, onlyActive]); return { diff --git a/frontend/src/pages/Volumes/List/hooks.tsx b/frontend/src/pages/Volumes/List/hooks.tsx index 8fe5be9817..ce73b3a94b 100644 --- a/frontend/src/pages/Volumes/List/hooks.tsx +++ b/frontend/src/pages/Volumes/List/hooks.tsx @@ -11,7 +11,7 @@ import { DATE_TIME_FORMAT } from 'consts'; import { useNotifications } from 'hooks'; import { useProjectFilter } from 'hooks/useProjectFilter'; import { getServerError } from 'libs'; -import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams } from 'libs/filters'; +import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams, tokensToSearchParams } from 'libs/filters'; import { getStatusIconType } from 'libs/volumes'; import { ROUTES } from 'routes'; import { useDeleteVolumesMutation } from 'services/volume'; @@ -169,7 +169,7 @@ export const useFilters = (localStorePrefix = 'volume-list-page') => { return !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex); }); - setSearchParams(tokensToRequestParams(filteredTokens, onlyActive)); + setSearchParams(tokensToSearchParams(filteredTokens, onlyActive)); setPropertyFilterQuery({ operation, @@ -180,16 +180,18 @@ export const useFilters = (localStorePrefix = 'volume-list-page') => { const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => { setOnlyActive(detail.checked); - setSearchParams(tokensToRequestParams(propertyFilterQuery.tokens, detail.checked)); + setSearchParams(tokensToSearchParams(propertyFilterQuery.tokens, detail.checked)); }; const filteringRequestParams = useMemo(() => { - const params = tokensToRequestParams(propertyFilterQuery.tokens); + const params = tokensToRequestParams({ + tokens: propertyFilterQuery.tokens, + }); return { ...params, only_active: onlyActive, - }; + } as Partial; }, [propertyFilterQuery, onlyActive]); return {