1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: filters for project overview (#5620)

This commit is contained in:
Jaanus Sellin 2023-12-12 22:50:49 +02:00 committed by GitHub
parent d632690203
commit 4f207f18e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 216 additions and 18 deletions

View File

@ -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;

View File

@ -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,
),
);

View File

@ -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,9 +110,7 @@ export const ExperimentalProjectFeatureToggles = ({
storageKey = 'project-feature-toggles',
}: IExperimentalProjectFeatureTogglesProps) => {
const projectId = useRequiredPathParam('projectId');
const [tableState, setTableState] = usePersistentTableState(
`${storageKey}-${projectId}`,
{
const stateConfig = {
offset: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
query: StringParam,
@ -115,12 +118,21 @@ export const ExperimentalProjectFeatureToggles = ({
sortBy: withDefault(StringParam, 'createdAt'),
sortOrder: withDefault(StringParam, 'desc'),
columns: ArrayParam,
},
tag: FilterItemParam,
createdAt: FilterItemParam,
};
const [tableState, setTableState] = usePersistentTableState(
`${storageKey}-${projectId}`,
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'
>
<ProjectOverviewFilters
onChange={setTableState}
state={tableState}
/>
<SearchHighlightProvider value={tableState.query || ''}>
<PaginatedTable
tableInstance={table}

View File

@ -0,0 +1,183 @@
import { useEffect, useState, VFC } from 'react';
import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FilterDateItem } from 'component/common/FilterDateItem/FilterDateItem';
import {
FilterItem,
FilterItemParams,
} from 'component/common/FilterItem/FilterItem';
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
import AddFilterButton from 'component/feature/FeatureToggleList/FeatureToggleFilters/AddFilterButton/AddFilterButton';
const StyledBox = styled(Box)(({ theme }) => ({
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<IFeatureToggleFiltersProps> = ({
state,
onChange,
}) => {
const { tags } = useAllTags();
const [availableFilters, setAvailableFilters] = useState<IFilterItem[]>([]);
const [unselectedFilters, setUnselectedFilters] = useState<string[]>([]);
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
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 (
<StyledBox>
{selectedFilters.map((selectedFilter) => {
if (selectedFilter === 'Created date') {
return (
<FilterDateItem
label={'Created date'}
state={state.createdAt}
onChange={(value) => 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 (
<FilterItem
key={filter.label}
label={filter.label}
state={state[filter.filterKey]}
options={filter.options}
onChange={(value) =>
onChange({ [filter.filterKey]: value })
}
singularOperators={filter.singularOperators}
pluralOperators={filter.pluralOperators}
onChipClose={() => deselectFilter(filter.label)}
/>
);
})}
<ConditionallyRender
condition={hasAvailableFilters}
show={
<AddFilterButton
visibleOptions={unselectedFilters}
setVisibleOptions={setUnselectedFilters}
hiddenOptions={selectedFilters}
setHiddenOptions={setSelectedFilters}
/>
}
/>
</StyledBox>
);
};