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:
parent
e8f19e6341
commit
9bae14a2cc
@ -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';
|
||||||
|
@ -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 = {
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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 }) => ({
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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;
|
||||||
|
@ -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',
|
||||||
|
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;
|
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 = {
|
||||||
|
Loading…
Reference in New Issue
Block a user