mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01: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.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
import type { FeatureSearchResponseSchemaDependenciesItem } from './featureSearchResponseSchemaDependenciesItem';
|
||||
import type { FeatureSearchEnvironmentSchema } from './featureSearchEnvironmentSchema';
|
||||
import type { FeatureSearchResponseSchemaStrategiesItem } from './featureSearchResponseSchemaStrategiesItem';
|
||||
import type { TagSchema } from './tagSchema';
|
||||
@ -22,7 +21,7 @@ export interface FeatureSearchResponseSchema {
|
||||
/** The date the feature was created */
|
||||
createdAt?: string | null;
|
||||
/** The list of parent dependencies. This is an experimental field and may change. */
|
||||
dependencies?: FeatureSearchResponseSchemaDependenciesItem[];
|
||||
dependencyType?: string;
|
||||
/** Detailed description of the feature */
|
||||
description?: string | null;
|
||||
/** `true` if the feature is enabled, otherwise `false`. */
|
||||
|
Loading…
Reference in New Issue
Block a user