1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +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 { screen } from '@testing-library/react';
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { FeatureOverviewCell } from './FeatureOverviewCell'; import { FeatureOverviewCell as makeFeatureOverviewCell } from './FeatureOverviewCell';
test('Display full overview information', () => { test('Display full overview information', () => {
const FeatureOverviewCell = makeFeatureOverviewCell(() => {});
render( render(
<FeatureOverviewCell <FeatureOverviewCell
row={{ row={{
@ -38,6 +40,8 @@ test('Display full overview information', () => {
}); });
test('Display minimal overview information', () => { test('Display minimal overview information', () => {
const FeatureOverviewCell = makeFeatureOverviewCell(() => {});
render( render(
<FeatureOverviewCell <FeatureOverviewCell
row={{ row={{

View File

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

View File

@ -8,12 +8,18 @@ import { BATCH_SELECTED_COUNT } from 'utils/testIds';
const server = testServerSetup(); const server = testServerSetup();
const setupApi = () => { 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', { testServerRoute(server, '/api/admin/search/features', {
features, features,
total: features.length, total: features.length,
}); });
testServerRoute(server, '/api/admin/ui-config', {}); testServerRoute(server, '/api/admin/ui-config', {});
testServerRoute(server, '/api/admin/tags', {
tags: [{ type: 'backend', value: 'sdk' }],
});
}; };
test('selects project features', async () => { test('selects project features', async () => {
@ -58,3 +64,28 @@ test('selects project features', async () => {
selectFeatureA.click(); selectFeatureA.click();
expect(screen.queryByTestId(BATCH_SELECTED_COUNT)).not.toBeInTheDocument(); 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 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( const columns = useMemo(
() => [ () => [
columnHelper.display({ columnHelper.display({
@ -144,7 +162,7 @@ export const ProjectFeatureToggles = ({
columnHelper.accessor('name', { columnHelper.accessor('name', {
id: 'name', id: 'name',
header: 'Name', header: 'Name',
cell: FeatureOverviewCell, cell: FeatureOverviewCell(onTagClick),
enableHiding: false, enableHiding: false,
meta: { meta: {
width: '50%', width: '50%',