mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: add tags filter (#5584)
This commit is contained in:
parent
e8f19e6341
commit
9bae14a2cc
@ -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';
|
||||
|
@ -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 = {
|
||||
|
@ -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<IFeatureToggleFiltersProps> = ({
|
||||
}) => {
|
||||
const { projects } = useProjects();
|
||||
const { segments } = useSegments();
|
||||
const { tags } = useAllTags();
|
||||
|
||||
const stateOptions = [
|
||||
{
|
||||
@ -81,6 +84,10 @@ export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
|
||||
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<IFeatureToggleFiltersProps> = ({
|
||||
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<IFeatureToggleFiltersProps> = ({
|
||||
];
|
||||
|
||||
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<IFeatureToggleFiltersProps> = ({
|
||||
const hasAvailableFilters = Object.values(visibleFilters).some(
|
||||
(value) => !value,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledBox>
|
||||
{availableFilters.map(
|
||||
|
@ -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,
|
||||
|
@ -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 }) => ({
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
37
frontend/src/hooks/api/getters/useAllTags/useAllTags.ts
Normal file
37
frontend/src/hooks/api/getters/useAllTags/useAllTags.ts
Normal 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;
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user