1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02:00
unleash.unleash/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx
2024-06-05 10:05:41 +02:00

359 lines
10 KiB
TypeScript

import type { FC, ReactElement } from 'react';
import type { FeatureSearchResponseSchema } from '../../../../../openapi';
import { Box, IconButton, styled } 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 'component/common/ConditionallyRender/ConditionallyRender';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
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('button')(({ 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),
cursor: 'pointer',
background: 'inherit',
color: 'inherit',
}));
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>
<Highlighter search={searchQuery}>{text}</Highlighter>
</StyledDescription>
</HtmlTooltip>
}
elseShow={
<StyledDescription>
<Highlighter search={searchQuery}>{text}</Highlighter>
</StyledDescription>
}
/>
);
};
const CappedTag: FC<{ tag: string; children: ReactElement }> = ({
tag,
children,
}) => {
return (
<ConditionallyRender
condition={tag.length > 30}
show={<HtmlTooltip title={tag}>{children}</HtmlTooltip>}
elseShow={children}
/>
);
};
const Container = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
margin: theme.spacing(1.25, 0, 1, 0),
paddingLeft: theme.spacing(2),
}));
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
style={{
WebkitLineClamp: 1,
lineClamp: 1,
}}
>
<Highlighter search={searchQuery}>{feature}</Highlighter>
</StyledTitle>
</StyledFeatureLink>
</Box>
);
};
const RestTags: FC<{ tags: string[]; onClick: (tag: string) => void }> = ({
tags,
onClick,
}) => {
return (
<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'];
onClick: (tag: string) => void;
}> = ({ tags, onClick }) => {
const [tag1, tag2, tag3, ...restTags] = (tags || []).map(
({ type, value }) => `${type}:${value}`,
);
return (
<TagsContainer>
{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} onClick={onClick} />}
/>
</TagsContainer>
);
};
const StyledDependencyLink = styled(Link)({
display: 'block',
});
const DependencyPreview: FC<{ feature: string; project: string }> = ({
feature,
project,
}) => {
const { feature: fetchedFeature } = useFeature(project, feature);
const children = fetchedFeature.children;
const parents = fetchedFeature.dependencies;
if (children.length > 0) {
return (
<>
<Box>Children</Box>
{children.map((child) => (
<StyledDependencyLink
to={`/projects/${project}/features/${child}`}
key={child}
>
{child}
</StyledDependencyLink>
))}
</>
);
} else if (parents[0]) {
const parentFeature = parents[0].feature;
return (
<>
<Box>Parent</Box>
<Link to={`/projects/${project}/features/${parentFeature}`}>
{parentFeature}
</Link>
</>
);
}
return <>Loading...</>;
};
export const PrimaryFeatureInfo: FC<{
project: string;
feature: string;
searchQuery: string;
type: string;
dependencyType: string;
onTypeClick: (type: string) => void;
delay?: number;
}> = ({
project,
feature,
type,
searchQuery,
dependencyType,
onTypeClick,
delay = 500,
}) => {
const { featureTypes } = useFeatureTypes();
const IconComponent = getFeatureTypeIcons(type);
const typeName = featureTypes.find(
(featureType) => featureType.id === type,
)?.name;
const title = `${typeName || type} flag`;
const TypeIcon = () => (
<HtmlTooltip arrow title={title} describeChild>
<IconButton
sx={{ p: 0 }}
aria-label={`add ${type} flag to filter`}
onClick={() => onTypeClick(type)}
>
<IconComponent
sx={(theme) => ({ fontSize: theme.spacing(2) })}
data-testid='feature-type-icon'
/>
</IconButton>
</HtmlTooltip>
);
return (
<FeatureNameAndType data-loading>
<TypeIcon />
<FeatureName
project={project}
feature={feature}
searchQuery={searchQuery}
/>
<ConditionallyRender
condition={Boolean(dependencyType)}
show={
<HtmlTooltip
title={
<DependencyPreview
feature={feature}
project={project}
/>
}
enterDelay={delay}
enterNextDelay={delay}
>
<DependencyBadge
color={
dependencyType === 'parent'
? 'warning'
: 'secondary'
}
>
{dependencyType}
</DependencyBadge>
</HtmlTooltip>
}
/>
</FeatureNameAndType>
);
};
const SecondaryFeatureInfo: FC<{
description: string;
searchQuery: string;
}> = ({ description, searchQuery }) => {
return (
<ConditionallyRender
condition={Boolean(description)}
show={
<Box
sx={(theme) => ({ display: 'flex', gap: theme.spacing(1) })}
>
<CappedDescription
text={description}
searchQuery={searchQuery}
/>
</Box>
}
/>
);
};
export const FeatureOverviewCell =
(
onTagClick: (tag: string) => void,
onFlagTypeClick: (type: 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 || ''}
onTypeClick={onFlagTypeClick}
/>
<SecondaryFeatureInfo
description={row.original.description || ''}
searchQuery={searchQuery}
/>
<Tags tags={row.original.tags} onClick={onTagClick} />
</Container>
);
};