mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: filter by feature type (#7273)
This commit is contained in:
parent
257cd5513f
commit
fef77c1fde
@ -2,8 +2,10 @@ import { screen } from '@testing-library/react';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { FeatureOverviewCell as makeFeatureOverviewCell } from './FeatureOverviewCell';
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
test('Display full overview information', () => {
|
||||
const FeatureOverviewCell = makeFeatureOverviewCell(() => {});
|
||||
const FeatureOverviewCell = makeFeatureOverviewCell(noOp, noOp);
|
||||
|
||||
render(
|
||||
<FeatureOverviewCell
|
||||
@ -40,7 +42,7 @@ test('Display full overview information', () => {
|
||||
});
|
||||
|
||||
test('Display minimal overview information', () => {
|
||||
const FeatureOverviewCell = makeFeatureOverviewCell(() => {});
|
||||
const FeatureOverviewCell = makeFeatureOverviewCell(noOp, noOp);
|
||||
|
||||
render(
|
||||
<FeatureOverviewCell
|
||||
|
@ -197,7 +197,8 @@ const PrimaryFeatureInfo: FC<{
|
||||
searchQuery: string;
|
||||
type: string;
|
||||
dependencyType: string;
|
||||
}> = ({ project, feature, type, searchQuery, dependencyType }) => {
|
||||
onTypeClick: (type: string) => void;
|
||||
}> = ({ project, feature, type, searchQuery, dependencyType, onTypeClick }) => {
|
||||
const { featureTypes } = useFeatureTypes();
|
||||
const IconComponent = getFeatureTypeIcons(type);
|
||||
const typeName = featureTypes.find(
|
||||
@ -207,7 +208,14 @@ const PrimaryFeatureInfo: FC<{
|
||||
|
||||
const TypeIcon = () => (
|
||||
<HtmlTooltip arrow title={title} describeChild>
|
||||
<IconComponent sx={(theme) => ({ fontSize: theme.spacing(2) })} />
|
||||
<IconComponent
|
||||
data-testid='feature-type-icon'
|
||||
sx={(theme) => ({
|
||||
cursor: 'pointer',
|
||||
fontSize: theme.spacing(2),
|
||||
})}
|
||||
onClick={() => onTypeClick(type)}
|
||||
/>
|
||||
</HtmlTooltip>
|
||||
);
|
||||
|
||||
@ -259,7 +267,10 @@ const SecondaryFeatureInfo: FC<{
|
||||
};
|
||||
|
||||
export const FeatureOverviewCell =
|
||||
(onClick: (tag: string) => void): FC<IFeatureNameCellProps> =>
|
||||
(
|
||||
onTagClick: (tag: string) => void,
|
||||
onFlagTypeClick: (type: string) => void,
|
||||
): FC<IFeatureNameCellProps> =>
|
||||
({ row }) => {
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
@ -271,12 +282,13 @@ export const FeatureOverviewCell =
|
||||
searchQuery={searchQuery}
|
||||
type={row.original.type || ''}
|
||||
dependencyType={row.original.dependencyType || ''}
|
||||
onTypeClick={onFlagTypeClick}
|
||||
/>
|
||||
<SecondaryFeatureInfo
|
||||
description={row.original.description || ''}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
<Tags tags={row.original.tags} onClick={onClick} />
|
||||
<Tags tags={row.original.tags} onClick={onTagClick} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
@ -2,15 +2,19 @@ import { render } from 'utils/testRenderer';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ProjectFeatureToggles } from './ProjectFeatureToggles';
|
||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import { BATCH_SELECTED_COUNT } from 'utils/testIds';
|
||||
|
||||
const server = testServerSetup();
|
||||
|
||||
const setupApi = () => {
|
||||
const features = [
|
||||
{ name: 'featureA', tags: [{ type: 'backend', value: 'sdk' }] },
|
||||
{ name: 'featureB' },
|
||||
{
|
||||
name: 'featureA',
|
||||
tags: [{ type: 'backend', value: 'sdk' }],
|
||||
type: 'operational',
|
||||
},
|
||||
{ name: 'featureB', type: 'release' },
|
||||
];
|
||||
testServerRoute(server, '/api/admin/search/features', {
|
||||
features,
|
||||
@ -89,3 +93,29 @@ test('filters by tag', async () => {
|
||||
await screen.findByText('include');
|
||||
expect(screen.getAllByText('backend:sdk')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('filters by flag type', async () => {
|
||||
setupApi();
|
||||
render(
|
||||
<Routes>
|
||||
<Route
|
||||
path={'/projects/:projectId'}
|
||||
element={
|
||||
<ProjectFeatureToggles
|
||||
environments={['development', 'production']}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>,
|
||||
{
|
||||
route: '/projects/default',
|
||||
},
|
||||
);
|
||||
await screen.findByText('featureA');
|
||||
const [icon] = await screen.getAllByTestId('feature-type-icon');
|
||||
|
||||
fireEvent.click(icon);
|
||||
|
||||
await screen.findByText('Flag type');
|
||||
await screen.findByText('Operational');
|
||||
});
|
||||
|
@ -35,7 +35,10 @@ import { useRowActions } from './hooks/useRowActions';
|
||||
import { useSelectedData } from './hooks/useSelectedData';
|
||||
import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { useProjectFeatureSearch } from './useProjectFeatureSearch';
|
||||
import {
|
||||
useProjectFeatureSearch,
|
||||
useProjectFeatureSearchActions,
|
||||
} from './useProjectFeatureSearch';
|
||||
|
||||
interface IPaginatedProjectFeatureTogglesProps {
|
||||
environments: string[];
|
||||
@ -62,9 +65,15 @@ export const ProjectFeatureToggles = ({
|
||||
setTableState,
|
||||
} = useProjectFeatureSearch(projectId);
|
||||
|
||||
const { onFlagTypeClick, onTagClick } = useProjectFeatureSearchActions(
|
||||
tableState,
|
||||
setTableState,
|
||||
);
|
||||
|
||||
const filterState = {
|
||||
tag: tableState.tag,
|
||||
createdAt: tableState.createdAt,
|
||||
type: tableState.type,
|
||||
};
|
||||
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
@ -93,24 +102,6 @@ export const ProjectFeatureToggles = ({
|
||||
|
||||
const featureLifecycleEnabled = useUiFlag('featureLifecycle');
|
||||
|
||||
const onTagClick = (tag: string) => {
|
||||
if (
|
||||
tableState.tag &&
|
||||
tableState.tag.values.length > 0 &&
|
||||
!tableState.tag.values.includes(tag)
|
||||
) {
|
||||
setTableState({
|
||||
tag: {
|
||||
operator: tableState.tag.operator,
|
||||
values: [...tableState.tag.values, tag],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!tableState.tag) {
|
||||
setTableState({ tag: { operator: 'INCLUDE', values: [tag] } });
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
@ -162,7 +153,7 @@ export const ProjectFeatureToggles = ({
|
||||
columnHelper.accessor('name', {
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
cell: FeatureOverviewCell(onTagClick),
|
||||
cell: FeatureOverviewCell(onTagClick, onFlagTypeClick),
|
||||
enableHiding: false,
|
||||
meta: {
|
||||
width: '50%',
|
||||
|
@ -45,6 +45,20 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
|
||||
filterKey: 'createdAt',
|
||||
dateOperators: ['IS_ON_OR_AFTER', 'IS_BEFORE'],
|
||||
},
|
||||
{
|
||||
label: 'Flag type',
|
||||
icon: 'flag',
|
||||
options: [
|
||||
{ label: 'Release', value: 'release' },
|
||||
{ label: 'Experiment', value: 'experiment' },
|
||||
{ label: 'Operational', value: 'operational' },
|
||||
{ label: 'Kill switch', value: 'kill-switch' },
|
||||
{ label: 'Permission', value: 'permission' },
|
||||
],
|
||||
filterKey: 'type',
|
||||
singularOperators: ['IS', 'IS_NOT'],
|
||||
pluralOperators: ['IS_ANY_OF', 'IS_NONE_OF'],
|
||||
},
|
||||
];
|
||||
|
||||
setAvailableFilters(availableFilters);
|
||||
|
@ -16,6 +16,10 @@ import {
|
||||
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
||||
import mapValues from 'lodash.mapvalues';
|
||||
|
||||
type Attribute =
|
||||
| { key: 'tag'; operator: 'INCLUDE' }
|
||||
| { key: 'type'; operator: 'IS' };
|
||||
|
||||
export const useProjectFeatureSearch = (
|
||||
projectId: string,
|
||||
storageKey = 'project-overview-v2',
|
||||
@ -31,6 +35,7 @@ export const useProjectFeatureSearch = (
|
||||
columns: ArrayParam,
|
||||
tag: FilterItemParam,
|
||||
createdAt: FilterItemParam,
|
||||
type: FilterItemParam,
|
||||
};
|
||||
const [tableState, setTableState] = usePersistentTableState(
|
||||
`${storageKey}-${projectId}`,
|
||||
@ -61,3 +66,42 @@ export const useProjectFeatureSearch = (
|
||||
setTableState,
|
||||
};
|
||||
};
|
||||
|
||||
export const useProjectFeatureSearchActions = (
|
||||
tableState: ReturnType<typeof useProjectFeatureSearch>['tableState'],
|
||||
setTableState: ReturnType<typeof useProjectFeatureSearch>['setTableState'],
|
||||
) => {
|
||||
const onAttributeClick = (attribute: Attribute, value: string) => {
|
||||
const attributeState = tableState[attribute.key];
|
||||
|
||||
if (
|
||||
attributeState &&
|
||||
attributeState.values.length > 0 &&
|
||||
!attributeState.values.includes(value)
|
||||
) {
|
||||
setTableState({
|
||||
[attribute.key]: {
|
||||
operator: attributeState.operator,
|
||||
values: [...attributeState.values, value],
|
||||
},
|
||||
});
|
||||
} else if (!attributeState) {
|
||||
setTableState({
|
||||
[attribute.key]: {
|
||||
operator: attribute.operator,
|
||||
values: [value],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onTagClick = (tag: string) =>
|
||||
onAttributeClick({ key: 'tag', operator: 'INCLUDE' }, tag);
|
||||
const onFlagTypeClick = (type: string) =>
|
||||
onAttributeClick({ key: 'type', operator: 'IS' }, type);
|
||||
|
||||
return {
|
||||
onFlagTypeClick,
|
||||
onTagClick,
|
||||
};
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ export type SearchFeaturesParams = {
|
||||
/**
|
||||
* The list of feature types to filter by
|
||||
*/
|
||||
type?: string[];
|
||||
type?: string;
|
||||
/**
|
||||
* The list of feature tags to filter by. Feature tag has to specify a type and a value joined with a colon.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user