mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
feat: feature overview cell (#6697)
This commit is contained in:
parent
a6643e4721
commit
a3ddefaf6d
@ -0,0 +1,60 @@
|
|||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { render } from 'utils/testRenderer';
|
||||||
|
import { FeatureOverviewCell } from './FeatureOverviewCell';
|
||||||
|
|
||||||
|
test('Display full overview information', () => {
|
||||||
|
render(
|
||||||
|
<FeatureOverviewCell
|
||||||
|
row={{
|
||||||
|
original: {
|
||||||
|
name: 'my_feature',
|
||||||
|
tags: [
|
||||||
|
{ type: 'simple', value: 'value1' },
|
||||||
|
{ type: 'simple', value: 'value2' },
|
||||||
|
{ type: 'simple', value: 'value3' },
|
||||||
|
{ type: 'simple', value: 'value4' },
|
||||||
|
],
|
||||||
|
description: 'My description',
|
||||||
|
type: 'release',
|
||||||
|
dependencyType: 'child',
|
||||||
|
project: 'my_project',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('my_feature')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('My description')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('child')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('simple:value1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('simple:value2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('simple:value3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('1 more...')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('simple:value4')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('link')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'/projects/my_project/features/my_feature',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Display minimal overview information', () => {
|
||||||
|
render(
|
||||||
|
<FeatureOverviewCell
|
||||||
|
row={{
|
||||||
|
original: {
|
||||||
|
name: 'my_feature',
|
||||||
|
tags: [],
|
||||||
|
description: '',
|
||||||
|
type: '',
|
||||||
|
project: 'my_project',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('my_feature')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('link')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'/projects/my_project/features/my_feature',
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,234 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import type { FeatureSearchResponseSchema } from '../../../../../openapi';
|
||||||
|
import { Box, styled, Tooltip } from '@mui/material';
|
||||||
|
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||||
|
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||||
|
import { useSearchHighlightContext } from '../../SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { Highlighter } from '../../../Highlighter/Highlighter';
|
||||||
|
import { StyledDescription, StyledTitle } from '../LinkCell/LinkCell.styles';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Badge } from '../../../Badge/Badge';
|
||||||
|
import { HtmlTooltip } from '../../../HtmlTooltip/HtmlTooltip';
|
||||||
|
import { ConditionallyRender } from '../../../ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
interface IFeatureNameCellProps {
|
||||||
|
row: {
|
||||||
|
original: Pick<
|
||||||
|
FeatureSearchResponseSchema,
|
||||||
|
| 'name'
|
||||||
|
| 'description'
|
||||||
|
| 'project'
|
||||||
|
| 'tags'
|
||||||
|
| 'type'
|
||||||
|
| 'dependencyType'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledFeatureLink = styled(Link)({
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover, &:focus': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Tag = styled('div')(({ theme }) => ({
|
||||||
|
marginRight: theme.spacing(0.5),
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textWrap: 'nowrap',
|
||||||
|
maxWidth: '250px',
|
||||||
|
padding: theme.spacing(0.25, 0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CappedDescription: FC<{ text: string; searchQuery: string }> = ({
|
||||||
|
text,
|
||||||
|
searchQuery,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(text && text.length > 40)}
|
||||||
|
show={
|
||||||
|
<HtmlTooltip
|
||||||
|
title={
|
||||||
|
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||||
|
}
|
||||||
|
placement='bottom-start'
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<StyledDescription data-loading>
|
||||||
|
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||||
|
</StyledDescription>
|
||||||
|
</HtmlTooltip>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<StyledDescription data-loading>
|
||||||
|
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||||
|
</StyledDescription>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(0.5),
|
||||||
|
margin: theme.spacing(1, 0, 1, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FeatureNameAndType = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
color: theme.palette.primary.dark,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const TagsContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: theme.spacing(0.25),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const DependencyBadge = styled(Badge)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(0.5),
|
||||||
|
border: 0,
|
||||||
|
textTransform: 'capitalize',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FeatureName: FC<{
|
||||||
|
project: string;
|
||||||
|
feature: string;
|
||||||
|
searchQuery: string;
|
||||||
|
}> = ({ project, feature, searchQuery }) => {
|
||||||
|
return (
|
||||||
|
<Box sx={(theme) => ({ fontWeight: theme.typography.fontWeightBold })}>
|
||||||
|
<StyledFeatureLink to={`/projects/${project}/features/${feature}`}>
|
||||||
|
<StyledTitle
|
||||||
|
data-loading
|
||||||
|
style={{
|
||||||
|
WebkitLineClamp: 1,
|
||||||
|
lineClamp: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Highlighter search={searchQuery}>{feature}</Highlighter>
|
||||||
|
</StyledTitle>
|
||||||
|
</StyledFeatureLink>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RestTags: FC<{ tags: string[] }> = ({ tags }) => {
|
||||||
|
return (
|
||||||
|
<HtmlTooltip title={tags.map((tag) => <div key={tag}>{tag}</div>)}>
|
||||||
|
<Tag>{tags.length} more...</Tag>
|
||||||
|
</HtmlTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tags: FC<{ tags: FeatureSearchResponseSchema['tags'] }> = ({ tags }) => {
|
||||||
|
const [tag1, tag2, tag3, ...restTags] = (tags || []).map(
|
||||||
|
({ type, value }) => `${type}:${value}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TagsContainer>
|
||||||
|
{tag1 && <Tag>{tag1}</Tag>}
|
||||||
|
{tag2 && <Tag>{tag2}</Tag>}
|
||||||
|
{tag3 && <Tag>{tag3}</Tag>}
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={restTags.length > 0}
|
||||||
|
show={<RestTags tags={restTags} />}
|
||||||
|
/>
|
||||||
|
</TagsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PrimaryFeatureInfo: FC<{
|
||||||
|
project: string;
|
||||||
|
feature: string;
|
||||||
|
searchQuery: string;
|
||||||
|
type: string;
|
||||||
|
}> = ({ project, feature, type, searchQuery }) => {
|
||||||
|
const { featureTypes } = useFeatureTypes();
|
||||||
|
const IconComponent = getFeatureTypeIcons(type);
|
||||||
|
const typeName = featureTypes.find(
|
||||||
|
(featureType) => featureType.id === type,
|
||||||
|
)?.name;
|
||||||
|
const title = `This is a "${typeName || type}" flag`;
|
||||||
|
|
||||||
|
const TypeIcon = () => (
|
||||||
|
<Tooltip arrow title={title} describeChild>
|
||||||
|
<IconComponent
|
||||||
|
sx={(theme) => ({ fontSize: theme.spacing(2) })}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureNameAndType>
|
||||||
|
<TypeIcon />
|
||||||
|
<FeatureName
|
||||||
|
project={project}
|
||||||
|
feature={feature}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
/>
|
||||||
|
</FeatureNameAndType>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SecondaryFeatureInfo: FC<{
|
||||||
|
dependencyType: string;
|
||||||
|
description: string;
|
||||||
|
searchQuery: string;
|
||||||
|
}> = ({ dependencyType, description, searchQuery }) => {
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(dependencyType) || Boolean(description)}
|
||||||
|
show={
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({ display: 'flex', gap: theme.spacing(1) })}
|
||||||
|
>
|
||||||
|
<DependencyBadge
|
||||||
|
color={
|
||||||
|
dependencyType === 'parent'
|
||||||
|
? 'warning'
|
||||||
|
: 'secondary'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dependencyType}
|
||||||
|
</DependencyBadge>
|
||||||
|
<CappedDescription
|
||||||
|
text={description}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeatureOverviewCell: FC<IFeatureNameCellProps> = ({ row }) => {
|
||||||
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<PrimaryFeatureInfo
|
||||||
|
project={row.original.project || ''}
|
||||||
|
feature={row.original.name}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
type={row.original.type || ''}
|
||||||
|
/>
|
||||||
|
<SecondaryFeatureInfo
|
||||||
|
description={row.original.description || ''}
|
||||||
|
dependencyType={row.original.dependencyType || ''}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
/>
|
||||||
|
<Tags tags={row.original.tags} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
@ -3,7 +3,6 @@
|
|||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* See `gen:api` script in package.json
|
* See `gen:api` script in package.json
|
||||||
*/
|
*/
|
||||||
import type { FeatureSearchResponseSchemaDependenciesItem } from './featureSearchResponseSchemaDependenciesItem';
|
|
||||||
import type { FeatureSearchEnvironmentSchema } from './featureSearchEnvironmentSchema';
|
import type { FeatureSearchEnvironmentSchema } from './featureSearchEnvironmentSchema';
|
||||||
import type { FeatureSearchResponseSchemaStrategiesItem } from './featureSearchResponseSchemaStrategiesItem';
|
import type { FeatureSearchResponseSchemaStrategiesItem } from './featureSearchResponseSchemaStrategiesItem';
|
||||||
import type { TagSchema } from './tagSchema';
|
import type { TagSchema } from './tagSchema';
|
||||||
@ -22,7 +21,7 @@ export interface FeatureSearchResponseSchema {
|
|||||||
/** The date the feature was created */
|
/** The date the feature was created */
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
/** The list of parent dependencies. This is an experimental field and may change. */
|
/** The list of parent dependencies. This is an experimental field and may change. */
|
||||||
dependencies?: FeatureSearchResponseSchemaDependenciesItem[];
|
dependencyType?: string;
|
||||||
/** Detailed description of the feature */
|
/** Detailed description of the feature */
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
/** `true` if the feature is enabled, otherwise `false`. */
|
/** `true` if the feature is enabled, otherwise `false`. */
|
||||||
|
Loading…
Reference in New Issue
Block a user