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

feat: clickable tags in project overview (#7263)

This commit is contained in:
Mateusz Kwasniewski 2024-06-04 11:08:38 +02:00 committed by GitHub
parent 927f911c62
commit 75529f465d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 124 additions and 39 deletions

View File

@ -1,8 +1,10 @@
import { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { FeatureOverviewCell } from './FeatureOverviewCell';
import { FeatureOverviewCell as makeFeatureOverviewCell } from './FeatureOverviewCell';
test('Display full overview information', () => {
const FeatureOverviewCell = makeFeatureOverviewCell(() => {});
render(
<FeatureOverviewCell
row={{
@ -38,6 +40,8 @@ test('Display full overview information', () => {
});
test('Display minimal overview information', () => {
const FeatureOverviewCell = makeFeatureOverviewCell(() => {});
render(
<FeatureOverviewCell
row={{

View File

@ -1,4 +1,4 @@
import type { FC } from 'react';
import type { FC, ReactElement } from 'react';
import type { FeatureSearchResponseSchema } from '../../../../../openapi';
import { Box, styled } from '@mui/material';
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
@ -32,7 +32,7 @@ const StyledFeatureLink = styled(Link)({
},
});
const Tag = styled('div')(({ theme }) => ({
const Tag = styled('button')(({ theme }) => ({
marginRight: theme.spacing(0.5),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
@ -42,6 +42,9 @@ const Tag = styled('div')(({ theme }) => ({
textWrap: 'nowrap',
maxWidth: '250px',
padding: theme.spacing(0.25, 0.5),
cursor: 'pointer',
background: 'inherit',
color: 'inherit',
}));
const CappedDescription: FC<{ text: string; searchQuery: string }> = ({
@ -73,16 +76,15 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({
);
};
const CappedTag: FC<{ tag: string }> = ({ tag }) => {
const CappedTag: FC<{ tag: string; children: ReactElement }> = ({
tag,
children,
}) => {
return (
<ConditionallyRender
condition={tag.length > 30}
show={
<HtmlTooltip title={tag}>
<Tag>{tag}</Tag>
</HtmlTooltip>
}
elseShow={<Tag>{tag}</Tag>}
show={<HtmlTooltip title={tag}>{children}</HtmlTooltip>}
elseShow={children}
/>
);
};
@ -135,27 +137,55 @@ const FeatureName: FC<{
);
};
const RestTags: FC<{ tags: string[] }> = ({ tags }) => {
const RestTags: FC<{ tags: string[]; onClick: (tag: string) => void }> = ({
tags,
onClick,
}) => {
return (
<HtmlTooltip title={tags.map((tag) => <div key={tag}>{tag}</div>)}>
<Tag>{tags.length} more...</Tag>
<HtmlTooltip
title={tags.map((tag) => (
<Box
sx={{ cursor: 'pointer' }}
onClick={() => onClick(tag)}
key={tag}
>
{tag}
</Box>
))}
>
<Tag sx={{ cursor: 'initial' }}>{tags.length} more...</Tag>
</HtmlTooltip>
);
};
const Tags: FC<{ tags: FeatureSearchResponseSchema['tags'] }> = ({ tags }) => {
const Tags: FC<{
tags: FeatureSearchResponseSchema['tags'];
onClick: (tag: string) => void;
}> = ({ tags, onClick }) => {
const [tag1, tag2, tag3, ...restTags] = (tags || []).map(
({ type, value }) => `${type}:${value}`,
);
return (
<TagsContainer>
{tag1 && <CappedTag tag={tag1} />}
{tag2 && <CappedTag tag={tag2} />}
{tag3 && <CappedTag tag={tag3} />}
{tag1 && (
<CappedTag tag={tag1}>
<Tag onClick={() => onClick(tag1)}>{tag1}</Tag>
</CappedTag>
)}
{tag2 && (
<CappedTag tag={tag2}>
<Tag onClick={() => onClick(tag2)}>{tag2}</Tag>
</CappedTag>
)}
{tag3 && (
<CappedTag tag={tag3}>
<Tag onClick={() => onClick(tag3)}>{tag3}</Tag>
</CappedTag>
)}
<ConditionallyRender
condition={restTags.length > 0}
show={<RestTags tags={restTags} />}
show={<RestTags tags={restTags} onClick={onClick} />}
/>
</TagsContainer>
);
@ -228,23 +258,25 @@ const SecondaryFeatureInfo: FC<{
);
};
export const FeatureOverviewCell: FC<IFeatureNameCellProps> = ({ row }) => {
const { searchQuery } = useSearchHighlightContext();
export const FeatureOverviewCell =
(onClick: (tag: string) => void): FC<IFeatureNameCellProps> =>
({ row }) => {
const { searchQuery } = useSearchHighlightContext();
return (
<Container>
<PrimaryFeatureInfo
project={row.original.project || ''}
feature={row.original.name}
searchQuery={searchQuery}
type={row.original.type || ''}
dependencyType={row.original.dependencyType || ''}
/>
<SecondaryFeatureInfo
description={row.original.description || ''}
searchQuery={searchQuery}
/>
<Tags tags={row.original.tags} />
</Container>
);
};
return (
<Container>
<PrimaryFeatureInfo
project={row.original.project || ''}
feature={row.original.name}
searchQuery={searchQuery}
type={row.original.type || ''}
dependencyType={row.original.dependencyType || ''}
/>
<SecondaryFeatureInfo
description={row.original.description || ''}
searchQuery={searchQuery}
/>
<Tags tags={row.original.tags} onClick={onClick} />
</Container>
);
};

View File

@ -8,12 +8,18 @@ import { BATCH_SELECTED_COUNT } from 'utils/testIds';
const server = testServerSetup();
const setupApi = () => {
const features = [{ name: 'featureA' }, { name: 'featureB' }];
const features = [
{ name: 'featureA', tags: [{ type: 'backend', value: 'sdk' }] },
{ name: 'featureB' },
];
testServerRoute(server, '/api/admin/search/features', {
features,
total: features.length,
});
testServerRoute(server, '/api/admin/ui-config', {});
testServerRoute(server, '/api/admin/tags', {
tags: [{ type: 'backend', value: 'sdk' }],
});
};
test('selects project features', async () => {
@ -58,3 +64,28 @@ test('selects project features', async () => {
selectFeatureA.click();
expect(screen.queryByTestId(BATCH_SELECTED_COUNT)).not.toBeInTheDocument();
});
test('filters by tag', async () => {
setupApi();
render(
<Routes>
<Route
path={'/projects/:projectId'}
element={
<ProjectFeatureToggles
environments={['development', 'production']}
/>
}
/>
</Routes>,
{
route: '/projects/default',
},
);
const tag = await screen.findByText('backend:sdk');
tag.click();
await screen.findByText('include');
expect(screen.getAllByText('backend:sdk')).toHaveLength(2);
});

View File

@ -93,6 +93,24 @@ 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({
@ -144,7 +162,7 @@ export const ProjectFeatureToggles = ({
columnHelper.accessor('name', {
id: 'name',
header: 'Name',
cell: FeatureOverviewCell,
cell: FeatureOverviewCell(onTagClick),
enableHiding: false,
meta: {
width: '50%',