From 9bae14a2cc37405400aad54859dbfc6f393f22f7 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Mon, 11 Dec 2023 14:10:03 +0200 Subject: [PATCH] feat: add tags filter (#5584) --- .../InstanceStats/InstanceStats.tsx | 2 +- .../FilterDateItem/FilterDateItem.test.tsx | 5 --- .../FeatureToggleFilters.tsx | 27 +++++++++++++- .../FeatureToggleListTable.tsx | 1 + .../EnvironmentVariantsCard.tsx | 4 +- frontend/src/component/menu/Header/Header.tsx | 2 +- .../Playground/AdvancedPlayground.tsx | 2 +- .../PlaygroundResultFeatureStrategyList.tsx | 1 - .../playgroundResultStrategyLists.tsx | 1 - .../api/getters/useAllTags/useAllTags.ts | 37 +++++++++++++++++++ frontend/src/utils/serializeQueryParams.ts | 17 ++++++--- 11 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 frontend/src/hooks/api/getters/useAllTags/useAllTags.ts diff --git a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx index 6d84687f09..240831f9ed 100644 --- a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx +++ b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx @@ -9,7 +9,7 @@ import { } from '@mui/material'; import { Box } from '@mui/system'; import { VFC } from 'react'; -import { useInstanceStats } from '../../../../hooks/api/getters/useInstanceStats/useInstanceStats'; +import { useInstanceStats } from 'hooks/api/getters/useInstanceStats/useInstanceStats'; import { formatApiPath } from '../../../../utils/formatPath'; import { PageContent } from '../../../common/PageContent/PageContent'; import { PageHeader } from '../../../common/PageHeader/PageHeader'; diff --git a/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx b/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx index 6e81795a7f..86d85b895b 100644 --- a/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx +++ b/frontend/src/component/common/FilterDateItem/FilterDateItem.test.tsx @@ -1,6 +1,5 @@ import { screen } from '@testing-library/react'; import { render } from 'utils/testRenderer'; -import { vi } from 'vitest'; import { FilterItemParams } from '../FilterItem/FilterItem'; import { FilterDateItem, IFilterDateItemProps } from './FilterDateItem'; @@ -23,10 +22,6 @@ const setup = (initialState: FilterItemParams) => { return recordedChanges; }; -afterEach(() => { - vi.restoreAllMocks(); -}); - describe('FilterDateItem Component', () => { it('renders initial state correctly', async () => { const mockState = { diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx index 55fe341d34..73ed6db791 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleFilters/FeatureToggleFilters.tsx @@ -9,6 +9,7 @@ import { FilterItem, FilterItemParams, } from 'component/common/FilterItem/FilterItem'; +import useAllTags from 'hooks/api/getters/useAllTags/useAllTags'; const StyledBox = styled(Box)(({ theme }) => ({ display: 'flex', @@ -18,6 +19,7 @@ const StyledBox = styled(Box)(({ theme }) => ({ export type FeatureTogglesListFilters = { project?: FilterItemParams | null | undefined; + tag?: FilterItemParams | null | undefined; state?: FilterItemParams | null | undefined; segment?: FilterItemParams | null | undefined; createdAt?: FilterItemParams | null | undefined; @@ -49,6 +51,7 @@ export const FeatureToggleFilters: VFC = ({ }) => { const { projects } = useProjects(); const { segments } = useSegments(); + const { tags } = useAllTags(); const stateOptions = [ { @@ -81,6 +84,10 @@ export const FeatureToggleFilters: VFC = ({ label: segment.name, value: segment.name, })); + const tagsOptions = (tags || []).map((tag) => ({ + label: `${tag.type}:${tag.value}`, + value: `${tag.type}:${tag.value}`, + })); const availableFilters: IFilterItem[] = [ { @@ -97,6 +104,18 @@ export const FeatureToggleFilters: VFC = ({ singularOperators: ['IS', 'IS_NOT'], pluralOperators: ['IS_ANY_OF', 'IS_NONE_OF'], }, + { + label: 'Tags', + options: tagsOptions, + filterKey: 'tag', + singularOperators: ['INCLUDE', 'DO_NOT_INCLUDE'], + pluralOperators: [ + 'INCLUDE_ALL_OF', + 'INCLUDE_ANY_OF', + 'EXCLUDE_IF_ANY_OF', + 'EXCLUDE_ALL', + ], + }, { label: 'Segment', options: segmentsOptions, @@ -112,12 +131,17 @@ export const FeatureToggleFilters: VFC = ({ ]; setAvailableFilters(availableFilters); - }, [JSON.stringify(projects), JSON.stringify(segments)]); + }, [ + JSON.stringify(projects), + JSON.stringify(segments), + JSON.stringify(tags), + ]); useEffect(() => { const filterVisibility: IFilterVisibility = { State: Boolean(state.state), Project: Boolean(state.project), + Tags: Boolean(state.tag), Segment: Boolean(state.segment), 'Created date': Boolean(state.createdAt), }; @@ -127,7 +151,6 @@ export const FeatureToggleFilters: VFC = ({ const hasAvailableFilters = Object.values(visibleFilters).some( (value) => !value, ); - return ( {availableFilters.map( diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 5705609141..b10109b901 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -83,6 +83,7 @@ export const FeatureToggleListTable: VFC = () => { sortBy: withDefault(StringParam, 'createdAt'), sortOrder: withDefault(StringParam, 'desc'), project: FilterItemParam, + tag: FilterItemParam, state: FilterItemParam, segment: FilterItemParam, createdAt: FilterItemParam, diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx index 9b7b296625..bf6904f901 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx @@ -4,9 +4,9 @@ import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { EnvironmentVariantsTable } from './EnvironmentVariantsTable/EnvironmentVariantsTable'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { Badge } from 'component/common/Badge/Badge'; -import { useRequiredPathParam } from '../../../../../../hooks/useRequiredPathParam'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useVariantsFromScheduledRequests } from './useVariantsFromScheduledRequests'; -import { ChangesScheduledBadge } from '../../../../../changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge'; +import { ChangesScheduledBadge } from 'component/changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge'; import { Box } from '@mui/system'; const StyledCard = styled('div')(({ theme }) => ({ diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx index c34afabc79..29ac970f51 100644 --- a/frontend/src/component/menu/Header/Header.tsx +++ b/frontend/src/component/menu/Header/Header.tsx @@ -35,7 +35,7 @@ import { useThemeMode } from 'hooks/useThemeMode'; import { Notifications } from 'component/common/Notifications/Notifications'; import { useAdminRoutes } from 'component/admin/useAdminRoutes'; import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton'; -import { useUiFlag } from '../../../hooks/useUiFlag'; +import { useUiFlag } from 'hooks/useUiFlag'; const StyledHeader = styled(AppBar)(({ theme }) => ({ backgroundColor: theme.palette.background.paper, diff --git a/frontend/src/component/playground/Playground/AdvancedPlayground.tsx b/frontend/src/component/playground/Playground/AdvancedPlayground.tsx index 707e2d990e..403008092a 100644 --- a/frontend/src/component/playground/Playground/AdvancedPlayground.tsx +++ b/frontend/src/component/playground/Playground/AdvancedPlayground.tsx @@ -22,7 +22,7 @@ import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable import { AdvancedPlaygroundResponseSchema } from 'openapi'; import { createLocalStorage } from 'utils/createLocalStorage'; import { BadRequestError } from 'utils/apiUtils'; -import { usePlausibleTracker } from '../../../hooks/usePlausibleTracker'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; const StyledAlert = styled(Alert)(({ theme }) => ({ marginBottom: theme.spacing(3), diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx index d8d7f71759..e49be63702 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/PlaygroundResultFeatureStrategyList.tsx @@ -5,7 +5,6 @@ import { import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi'; import { Alert } from '@mui/material'; -import { useUiFlag } from '../../../../../../hooks/useUiFlag'; interface PlaygroundResultFeatureStrategyListProps { feature: PlaygroundFeatureSchema; diff --git a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx index a22eeed56e..596e78c15a 100644 --- a/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx +++ b/frontend/src/component/playground/Playground/PlaygroundResultsTable/FeatureResultInfoPopoverCell/FeatureStrategyList/StrategyList/playgroundResultStrategyLists.tsx @@ -8,7 +8,6 @@ import { import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; -import { useUiFlag } from '../../../../../../../hooks/useUiFlag'; const StyledAlertWrapper = styled('div')(({ theme }) => ({ display: 'flex', diff --git a/frontend/src/hooks/api/getters/useAllTags/useAllTags.ts b/frontend/src/hooks/api/getters/useAllTags/useAllTags.ts new file mode 100644 index 0000000000..d92cd97b18 --- /dev/null +++ b/frontend/src/hooks/api/getters/useAllTags/useAllTags.ts @@ -0,0 +1,37 @@ +import useSWR, { mutate, SWRConfiguration } from 'swr'; +import { useState, useEffect } from 'react'; +import { formatApiPath } from 'utils/formatPath'; +import { ITag } from 'interfaces/tags'; +import handleErrorResponses from '../httpErrorResponseHandler'; + +const useAllTags = (options: SWRConfiguration = {}) => { + const fetcher = async () => { + const path = formatApiPath(`api/admin/tags`); + const res = await fetch(path, { + method: 'GET', + }).then(handleErrorResponses('Tags')); + return res.json(); + }; + + const KEY = `api/admin/tags`; + + const { data, error } = useSWR<{ tags: ITag[] }>(KEY, fetcher, options); + const [loading, setLoading] = useState(!error && !data); + + const refetch = () => { + mutate(KEY); + }; + + useEffect(() => { + setLoading(!error && !data); + }, [data, error]); + + return { + tags: (data?.tags as ITag[]) || [], + error, + loading, + refetch, + }; +}; + +export default useAllTags; diff --git a/frontend/src/utils/serializeQueryParams.ts b/frontend/src/utils/serializeQueryParams.ts index 3aaf3c500b..4f66b9771e 100644 --- a/frontend/src/utils/serializeQueryParams.ts +++ b/frontend/src/utils/serializeQueryParams.ts @@ -49,13 +49,18 @@ const decodeFilterItem = ( return undefined; } - const [operator, values = ''] = input.split(':'); - if (!operator) return undefined; + const pattern = + /^(IS|IS_NOT|IS_ANY_OF|IS_NONE_OF|INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL|IS_BEFORE|IS_ON_OR_AFTER):(.+)$/; + const match = input.match(pattern); - const splitValues = values.split(','); - return splitValues.length > 0 - ? { operator, values: splitValues } - : undefined; + if (match) { + return { + operator: match[1], + values: match[2].split(','), + }; + } + + return undefined; }; export const FilterItemParam = {