From 4f207f18e6aa64ac923b9d46967cf04dd7ef28a7 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Tue, 12 Dec 2023 22:50:49 +0200 Subject: [PATCH] feat: filters for project overview (#5620) --- .../FeatureToggleFilters.tsx | 5 +- .../FeatureToggleListTable.tsx | 6 +- .../ExperimentalProjectTable.tsx | 40 ++-- .../ProjectOverviewFilters.tsx | 183 ++++++++++++++++++ 4 files changed, 216 insertions(+), 18 deletions(-) create mode 100644 frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ProjectOverviewFilters.tsx diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx index cbc09b469d..3b162a50c0 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx @@ -10,7 +10,6 @@ import { FilterItemParams, } from 'component/common/FilterItem/FilterItem'; import useAllTags from 'hooks/api/getters/useAllTags/useAllTags'; -import { FILTER_ITEM } from 'utils/testIds'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'flex', @@ -19,7 +18,7 @@ const StyledBox = styled(Box)(({ theme }) => ({ flexWrap: 'wrap', })); -export type FeatureTogglesListFilters = { +type FeatureTogglesListFilters = { project?: FilterItemParams | null | undefined; tag?: FilterItemParams | null | undefined; state?: FilterItemParams | null | undefined; @@ -32,7 +31,7 @@ interface IFeatureToggleFiltersProps { onChange: (value: FeatureTogglesListFilters) => void; } -export interface IFilterItem { +interface IFilterItem { label: string; options: { label: string; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index b10109b901..355b40ee23 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -75,7 +75,7 @@ export const FeatureToggleListTable: VFC = () => { const { setToastApiError } = useToast(); const { uiConfig } = useUiConfig(); - const config = { + const stateConfig = { offset: withDefault(NumberParam, 0), limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), query: StringParam, @@ -90,7 +90,7 @@ export const FeatureToggleListTable: VFC = () => { }; const [tableState, setTableState] = usePersistentTableState( 'features-list-table', - config, + stateConfig, ); const { @@ -100,7 +100,7 @@ export const FeatureToggleListTable: VFC = () => { refetch: refetchFeatures, initialLoad, } = useFeatureSearch( - mapValues(encodeQueryParams(config, tableState), (value) => + mapValues(encodeQueryParams(stateConfig, tableState), (value) => value ? `${value}` : undefined, ), ); diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx index f103fb6c07..39d07905e4 100644 --- a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx +++ b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectTable/ExperimentalProjectTable.tsx @@ -73,12 +73,16 @@ import { } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; import mapValues from 'lodash.mapvalues'; import { usePersistentTableState } from 'hooks/usePersistentTableState'; -import { BooleansStringParam } from 'utils/serializeQueryParams'; +import { + BooleansStringParam, + FilterItemParam, +} from 'utils/serializeQueryParams'; import { NumberParam, StringParam, ArrayParam, withDefault, + encodeQueryParams, } from 'use-query-params'; import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; import { createColumnHelper, useReactTable } from '@tanstack/react-table'; @@ -86,6 +90,7 @@ import { withTableState } from 'utils/withTableState'; import { type FeatureSearchResponseSchema } from 'openapi'; import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; import { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell'; +import { ProjectOverviewFilters } from './ProjectOverviewFilters'; interface IExperimentalProjectFeatureTogglesProps { environments: IProject['environments']; @@ -105,22 +110,29 @@ export const ExperimentalProjectFeatureToggles = ({ storageKey = 'project-feature-toggles', }: IExperimentalProjectFeatureTogglesProps) => { const projectId = useRequiredPathParam('projectId'); + const stateConfig = { + offset: withDefault(NumberParam, 0), + limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), + query: StringParam, + favoritesFirst: withDefault(BooleansStringParam, true), + sortBy: withDefault(StringParam, 'createdAt'), + sortOrder: withDefault(StringParam, 'desc'), + columns: ArrayParam, + tag: FilterItemParam, + createdAt: FilterItemParam, + }; const [tableState, setTableState] = usePersistentTableState( `${storageKey}-${projectId}`, - { - offset: withDefault(NumberParam, 0), - limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), - query: StringParam, - favoritesFirst: withDefault(BooleansStringParam, true), - sortBy: withDefault(StringParam, 'createdAt'), - sortOrder: withDefault(StringParam, 'desc'), - columns: ArrayParam, - }, + stateConfig, ); const { features, total, refetch, loading, initialLoad } = useFeatureSearch( - mapValues({ ...tableState, projectId }, (value) => - value ? `${value}` : undefined, + mapValues( + { + ...encodeQueryParams(stateConfig, tableState), + project: `IS:${projectId}`, + }, + (value) => (value ? `${value}` : undefined), ), { refreshInterval, @@ -366,6 +378,10 @@ export const ExperimentalProjectFeatureToggles = ({ aria-busy={loading} aria-live='polite' > + ({ + display: 'flex', + padding: theme.spacing(2, 3), + gap: theme.spacing(1), + flexWrap: 'wrap', +})); + +type FeatureTogglesListFilters = { + tag?: FilterItemParams | null | undefined; + createdAt?: FilterItemParams | null | undefined; +}; + +interface IFeatureToggleFiltersProps { + state: FeatureTogglesListFilters; + onChange: (value: FeatureTogglesListFilters) => void; +} + +export interface IFilterItem { + label: string; + options: { + label: string; + value: string; + }[]; + filterKey: keyof FeatureTogglesListFilters; + singularOperators: [string, ...string[]]; + pluralOperators: [string, ...string[]]; +} + +export const ProjectOverviewFilters: VFC = ({ + state, + onChange, +}) => { + const { tags } = useAllTags(); + + const [availableFilters, setAvailableFilters] = useState([]); + const [unselectedFilters, setUnselectedFilters] = useState([]); + const [selectedFilters, setSelectedFilters] = useState([]); + + const deselectFilter = (label: string) => { + const newSelectedFilters = selectedFilters.filter((f) => f !== label); + const newUnselectedFilters = [...unselectedFilters, label].sort(); + + setSelectedFilters(newSelectedFilters); + setUnselectedFilters(newUnselectedFilters); + }; + + const mergeArraysKeepingOrder = ( + firstArray: string[], + secondArray: string[], + ): string[] => { + const elementsSet = new Set(firstArray); + + secondArray.forEach((element) => { + if (!elementsSet.has(element)) { + firstArray.push(element); + } + }); + + return firstArray; + }; + + useEffect(() => { + const tagsOptions = (tags || []).map((tag) => ({ + label: `${tag.type}:${tag.value}`, + value: `${tag.type}:${tag.value}`, + })); + + const availableFilters: IFilterItem[] = [ + { + label: 'Tags', + options: tagsOptions, + filterKey: 'tag', + singularOperators: ['INCLUDE', 'DO_NOT_INCLUDE'], + pluralOperators: [ + 'INCLUDE_ALL_OF', + 'INCLUDE_ANY_OF', + 'EXCLUDE_IF_ANY_OF', + 'EXCLUDE_ALL', + ], + }, + ]; + + setAvailableFilters(availableFilters); + }, [JSON.stringify(tags)]); + + useEffect(() => { + const fieldsMapping = [ + { + stateField: 'tag', + label: 'Tags', + }, + { + stateField: 'createdAt', + label: 'Created date', + }, + ]; + + const newSelectedFilters = fieldsMapping + .filter((field) => + Boolean( + state[field.stateField as keyof FeatureTogglesListFilters], + ), + ) + .map((field) => field.label); + const newUnselectedFilters = fieldsMapping + .filter( + (field) => + !state[field.stateField as keyof FeatureTogglesListFilters], + ) + .map((field) => field.label) + .sort(); + + setSelectedFilters( + mergeArraysKeepingOrder(selectedFilters, newSelectedFilters), + ); + setUnselectedFilters(newUnselectedFilters); + }, [JSON.stringify(state)]); + + const hasAvailableFilters = unselectedFilters.length > 0; + return ( + + {selectedFilters.map((selectedFilter) => { + if (selectedFilter === 'Created date') { + return ( + onChange({ createdAt: value })} + operators={['IS_ON_OR_AFTER', 'IS_BEFORE']} + onChipClose={() => deselectFilter('Created date')} + /> + ); + } + + const filter = availableFilters.find( + (filter) => filter.label === selectedFilter, + ); + + if (!filter) { + return null; + } + + return ( + + onChange({ [filter.filterKey]: value }) + } + singularOperators={filter.singularOperators} + pluralOperators={filter.pluralOperators} + onChipClose={() => deselectFilter(filter.label)} + /> + ); + })} + + + } + /> + + ); +};