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)}
+ />
+ );
+ })}
+
+
+ }
+ />
+
+ );
+};