1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

feat: add tags filter (#5584)

This commit is contained in:
Jaanus Sellin 2023-12-11 14:10:03 +02:00 committed by GitHub
parent e8f19e6341
commit 9bae14a2cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 79 additions and 20 deletions

View File

@ -9,7 +9,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import { VFC } from 'react'; 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 { formatApiPath } from '../../../../utils/formatPath';
import { PageContent } from '../../../common/PageContent/PageContent'; import { PageContent } from '../../../common/PageContent/PageContent';
import { PageHeader } from '../../../common/PageHeader/PageHeader'; import { PageHeader } from '../../../common/PageHeader/PageHeader';

View File

@ -1,6 +1,5 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { vi } from 'vitest';
import { FilterItemParams } from '../FilterItem/FilterItem'; import { FilterItemParams } from '../FilterItem/FilterItem';
import { FilterDateItem, IFilterDateItemProps } from './FilterDateItem'; import { FilterDateItem, IFilterDateItemProps } from './FilterDateItem';
@ -23,10 +22,6 @@ const setup = (initialState: FilterItemParams) => {
return recordedChanges; return recordedChanges;
}; };
afterEach(() => {
vi.restoreAllMocks();
});
describe('FilterDateItem Component', () => { describe('FilterDateItem Component', () => {
it('renders initial state correctly', async () => { it('renders initial state correctly', async () => {
const mockState = { const mockState = {

View File

@ -9,6 +9,7 @@ import {
FilterItem, FilterItem,
FilterItemParams, FilterItemParams,
} from 'component/common/FilterItem/FilterItem'; } from 'component/common/FilterItem/FilterItem';
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
const StyledBox = styled(Box)(({ theme }) => ({ const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
@ -18,6 +19,7 @@ const StyledBox = styled(Box)(({ theme }) => ({
export type FeatureTogglesListFilters = { export type FeatureTogglesListFilters = {
project?: FilterItemParams | null | undefined; project?: FilterItemParams | null | undefined;
tag?: FilterItemParams | null | undefined;
state?: FilterItemParams | null | undefined; state?: FilterItemParams | null | undefined;
segment?: FilterItemParams | null | undefined; segment?: FilterItemParams | null | undefined;
createdAt?: FilterItemParams | null | undefined; createdAt?: FilterItemParams | null | undefined;
@ -49,6 +51,7 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
}) => { }) => {
const { projects } = useProjects(); const { projects } = useProjects();
const { segments } = useSegments(); const { segments } = useSegments();
const { tags } = useAllTags();
const stateOptions = [ const stateOptions = [
{ {
@ -81,6 +84,10 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
label: segment.name, label: segment.name,
value: segment.name, value: segment.name,
})); }));
const tagsOptions = (tags || []).map((tag) => ({
label: `${tag.type}:${tag.value}`,
value: `${tag.type}:${tag.value}`,
}));
const availableFilters: IFilterItem[] = [ const availableFilters: IFilterItem[] = [
{ {
@ -97,6 +104,18 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
singularOperators: ['IS', 'IS_NOT'], singularOperators: ['IS', 'IS_NOT'],
pluralOperators: ['IS_ANY_OF', 'IS_NONE_OF'], 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', label: 'Segment',
options: segmentsOptions, options: segmentsOptions,
@ -112,12 +131,17 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
]; ];
setAvailableFilters(availableFilters); setAvailableFilters(availableFilters);
}, [JSON.stringify(projects), JSON.stringify(segments)]); }, [
JSON.stringify(projects),
JSON.stringify(segments),
JSON.stringify(tags),
]);
useEffect(() => { useEffect(() => {
const filterVisibility: IFilterVisibility = { const filterVisibility: IFilterVisibility = {
State: Boolean(state.state), State: Boolean(state.state),
Project: Boolean(state.project), Project: Boolean(state.project),
Tags: Boolean(state.tag),
Segment: Boolean(state.segment), Segment: Boolean(state.segment),
'Created date': Boolean(state.createdAt), 'Created date': Boolean(state.createdAt),
}; };
@ -127,7 +151,6 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
const hasAvailableFilters = Object.values(visibleFilters).some( const hasAvailableFilters = Object.values(visibleFilters).some(
(value) => !value, (value) => !value,
); );
return ( return (
<StyledBox> <StyledBox>
{availableFilters.map( {availableFilters.map(

View File

@ -83,6 +83,7 @@ export const FeatureToggleListTable: VFC = () => {
sortBy: withDefault(StringParam, 'createdAt'), sortBy: withDefault(StringParam, 'createdAt'),
sortOrder: withDefault(StringParam, 'desc'), sortOrder: withDefault(StringParam, 'desc'),
project: FilterItemParam, project: FilterItemParam,
tag: FilterItemParam,
state: FilterItemParam, state: FilterItemParam,
segment: FilterItemParam, segment: FilterItemParam,
createdAt: FilterItemParam, createdAt: FilterItemParam,

View File

@ -4,9 +4,9 @@ import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { EnvironmentVariantsTable } from './EnvironmentVariantsTable/EnvironmentVariantsTable'; import { EnvironmentVariantsTable } from './EnvironmentVariantsTable/EnvironmentVariantsTable';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Badge } from 'component/common/Badge/Badge'; import { Badge } from 'component/common/Badge/Badge';
import { useRequiredPathParam } from '../../../../../../hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useVariantsFromScheduledRequests } from './useVariantsFromScheduledRequests'; import { useVariantsFromScheduledRequests } from './useVariantsFromScheduledRequests';
import { ChangesScheduledBadge } from '../../../../../changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge'; import { ChangesScheduledBadge } from 'component/changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
const StyledCard = styled('div')(({ theme }) => ({ const StyledCard = styled('div')(({ theme }) => ({

View File

@ -35,7 +35,7 @@ import { useThemeMode } from 'hooks/useThemeMode';
import { Notifications } from 'component/common/Notifications/Notifications'; import { Notifications } from 'component/common/Notifications/Notifications';
import { useAdminRoutes } from 'component/admin/useAdminRoutes'; import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton'; import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from '../../../hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
const StyledHeader = styled(AppBar)(({ theme }) => ({ const StyledHeader = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,

View File

@ -22,7 +22,7 @@ import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable
import { AdvancedPlaygroundResponseSchema } from 'openapi'; import { AdvancedPlaygroundResponseSchema } from 'openapi';
import { createLocalStorage } from 'utils/createLocalStorage'; import { createLocalStorage } from 'utils/createLocalStorage';
import { BadRequestError } from 'utils/apiUtils'; import { BadRequestError } from 'utils/apiUtils';
import { usePlausibleTracker } from '../../../hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
const StyledAlert = styled(Alert)(({ theme }) => ({ const StyledAlert = styled(Alert)(({ theme }) => ({
marginBottom: theme.spacing(3), marginBottom: theme.spacing(3),

View File

@ -5,7 +5,6 @@ import {
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi'; import { PlaygroundFeatureSchema, PlaygroundRequestSchema } from 'openapi';
import { Alert } from '@mui/material'; import { Alert } from '@mui/material';
import { useUiFlag } from '../../../../../../hooks/useUiFlag';
interface PlaygroundResultFeatureStrategyListProps { interface PlaygroundResultFeatureStrategyListProps {
feature: PlaygroundFeatureSchema; feature: PlaygroundFeatureSchema;

View File

@ -8,7 +8,6 @@ import {
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem'; import { FeatureStrategyItem } from './StrategyItem/FeatureStrategyItem';
import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator'; import { StrategySeparator } from 'component/common/StrategySeparator/StrategySeparator';
import { useUiFlag } from '../../../../../../../hooks/useUiFlag';
const StyledAlertWrapper = styled('div')(({ theme }) => ({ const StyledAlertWrapper = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',

View File

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

View File

@ -49,13 +49,18 @@ const decodeFilterItem = (
return undefined; return undefined;
} }
const [operator, values = ''] = input.split(':'); const pattern =
if (!operator) return undefined; /^(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(','); if (match) {
return splitValues.length > 0 return {
? { operator, values: splitValues } operator: match[1],
: undefined; values: match[2].split(','),
};
}
return undefined;
}; };
export const FilterItemParam = { export const FilterItemParam = {