1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

feat: support for passing color in tags

This commit is contained in:
FredrikOseberg 2025-03-19 15:14:56 +01:00
parent aad5a6a1a9
commit 8360e3b6c5
No known key found for this signature in database
GPG Key ID: 282FD8A6D8F9BCF0
9 changed files with 224 additions and 78 deletions

View File

@ -0,0 +1,51 @@
import { styled } from '@mui/material';
import { Chip } from '@mui/material';
import type { ITag } from 'interfaces/tags';
import type { ReactElement } from 'react';
const StyledChip = styled(Chip)<{ $color?: string }>(({ theme, $color }) => ({
borderRadius: theme.spacing(2),
border: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.common.white,
color: theme.palette.text.primary,
'& .MuiChip-label': {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
},
}));
const ColorDot = styled('div')<{ $color: string }>(({ theme, $color }) => ({
width: theme.spacing(1.3),
height: theme.spacing(1.3),
borderRadius: '50%',
backgroundColor: $color,
border: `1px solid ${$color === '#FFFFFF' ? theme.palette.divider : $color}`,
flexShrink: 0,
}));
interface ITagProps {
tag: ITag;
onDelete?: () => void;
deleteIcon?: ReactElement;
}
export const Tag = ({ tag, onDelete, deleteIcon }: ITagProps) => {
const label = `${tag.type}:${tag.value}`;
const showColorDot = tag.color && tag.color !== '#FFFFFF';
const labelContent = (
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{showColorDot && <ColorDot $color={tag.color!} />}
<span>{label}</span>
</span>
) as ReactElement;
return (
<StyledChip
label={labelContent}
onDelete={onDelete}
deleteIcon={deleteIcon}
/>
);
};

View File

@ -1,8 +1,9 @@
import type { IFeatureToggle } from 'interfaces/featureToggle';
import { useContext, useState } from 'react';
import { Chip, styled, Tooltip } from '@mui/material';
import { styled, Tooltip, Chip } from '@mui/material';
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
import DeleteTagIcon from '@mui/icons-material/Cancel';
import ClearIcon from '@mui/icons-material/Clear';
import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import AccessContext from 'contexts/AccessContext';
@ -13,6 +14,8 @@ import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { StyledMetaDataItem } from './FeatureOverviewMetaData';
import { AddTagButton } from './AddTagButton';
import { Tag } from 'component/common/Tag/Tag';
import { useUiFlag } from 'hooks/useUiFlag';
const StyledLabel = styled('span')(({ theme }) => ({
marginTop: theme.spacing(1),
@ -56,6 +59,7 @@ interface IFeatureOverviewSidePanelTagsProps {
export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
const { tags, refetch } = useFeatureTags(feature.name);
const { deleteTagFromFeature } = useFeatureApi();
const isTagTypeColorEnabled = useUiFlag('tagTypeColor');
const [manageTagsOpen, setManageTagsOpen] = useState(false);
const [removeTagOpen, setRemoveTagOpen] = useState(false);
@ -87,6 +91,19 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
}
};
const renderTag = (tag: ITag) => (
<TagItem
key={`${tag.type}:${tag.value}`}
tag={tag}
isTagTypeColorEnabled={isTagTypeColorEnabled}
canUpdateTags={canUpdateTags}
onTagRemove={(tag) => {
setRemoveTagOpen(true);
setSelectedTag(tag);
}}
/>
);
return (
<>
{!tags.length ? (
@ -103,49 +120,7 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
<StyledTagRow>
<StyledLabel>Tags:</StyledLabel>
<StyledTagContainer>
{tags.map((tag) => {
const tagLabel = `${tag.type}:${tag.value}`;
const isOverflowing = tagLabel.length > 25;
return (
<StyledTag
key={tagLabel}
label={
<Tooltip
key={tagLabel}
title={
isOverflowing ? tagLabel : ''
}
arrow
>
<span>
{tagLabel.substring(0, 25)}
{isOverflowing ? (
<StyledEllipsis>
</StyledEllipsis>
) : (
''
)}
</span>
</Tooltip>
}
size='small'
deleteIcon={
<Tooltip title='Remove tag' arrow>
<DeleteTagIcon />
</Tooltip>
}
onDelete={
canUpdateTags
? () => {
setRemoveTagOpen(true);
setSelectedTag(tag);
}
: undefined
}
/>
);
})}
{tags.map(renderTag)}
{canUpdateTags ? (
<AddTagButton
project={feature.project}
@ -183,3 +158,71 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
</>
);
};
interface ITagItemProps {
tag: ITag;
isTagTypeColorEnabled: boolean;
canUpdateTags: boolean;
onTagRemove: (tag: ITag) => void;
}
const TagItem = ({
tag,
isTagTypeColorEnabled,
canUpdateTags,
onTagRemove,
}: ITagItemProps) => {
const tagLabel = `${tag.type}:${tag.value}`;
const isOverflowing = tagLabel.length > 25;
const deleteIcon = (
<Tooltip title='Remove tag' arrow>
<DeleteTagIcon />
</Tooltip>
);
const onDelete = canUpdateTags ? () => onTagRemove(tag) : undefined;
if (isTagTypeColorEnabled) {
const deleteIcon = (
<Tooltip title='Remove tag' arrow>
<ClearIcon sx={{ height: '20px', width: '20px' }} />
</Tooltip>
);
return (
<Tooltip key={tagLabel} title={isOverflowing ? tagLabel : ''} arrow>
<span>
<Tag
tag={tag}
onDelete={onDelete}
deleteIcon={deleteIcon}
/>
</span>
</Tooltip>
);
}
return (
<StyledTag
key={tagLabel}
label={
<Tooltip
key={tagLabel}
title={isOverflowing ? tagLabel : ''}
arrow
>
<span>
{tagLabel.substring(0, 25)}
{isOverflowing ? (
<StyledEllipsis></StyledEllipsis>
) : (
''
)}
</span>
</Tooltip>
}
size='small'
deleteIcon={deleteIcon}
onDelete={onDelete}
/>
);
};

View File

@ -1,6 +1,6 @@
import type { IFeatureToggle } from 'interfaces/featureToggle';
import { useContext, useState } from 'react';
import { Button, Chip, Divider, styled } from '@mui/material';
import { Button, Divider, styled } from '@mui/material';
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
import Add from '@mui/icons-material/Add';
import Cancel from '@mui/icons-material/Cancel';
@ -13,6 +13,7 @@ import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Tag } from 'component/common/Tag/Tag';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
@ -26,10 +27,6 @@ const StyledTagContainer = styled('div')(({ theme }) => ({
flexWrap: 'wrap',
}));
const StyledChip = styled(Chip)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: theme.spacing(3),
borderStyle: 'dashed',
@ -82,24 +79,21 @@ export const FeatureOverviewSidePanelTags = ({
<StyledContainer>
{header}
<StyledTagContainer>
{tags.map((tag) => {
const tagLabel = `${tag.type}:${tag.value}`;
return (
<StyledChip
key={tagLabel}
label={tagLabel}
deleteIcon={<Cancel titleAccess='Remove' />}
onDelete={
canUpdateTags
? () => {
setShowDelDialog(true);
setSelectedTag(tag);
}
: undefined
}
/>
);
})}
{tags.map((tag) => (
<Tag
key={`${tag.type}:${tag.value}`}
tag={tag}
onDelete={
canUpdateTags
? () => {
setShowDelDialog(true);
setSelectedTag(tag);
}
: undefined
}
deleteIcon={<Cancel titleAccess='Remove' />}
/>
))}
</StyledTagContainer>
<ConditionallyRender
condition={canUpdateTags}
@ -123,18 +117,18 @@ export const FeatureOverviewSidePanelTags = ({
<ManageTagsDialog open={openTagDialog} setOpen={setOpenTagDialog} />
<Dialogue
open={showDelDialog}
primaryButtonText='Delete tag'
primaryButtonText='Delete'
secondaryButtonText='Cancel'
onClose={() => {
setShowDelDialog(false);
setSelectedTag(undefined);
}}
onClick={() => {
setShowDelDialog(false);
handleDelete();
setSelectedTag(undefined);
}}
title='Delete tag?'
onClose={() => {
setShowDelDialog(false);
setSelectedTag(undefined);
}}
title='Delete tag'
>
You are about to delete tag:{' '}
<strong>

View File

@ -8,7 +8,7 @@ import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter';
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge';
import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
import { FavoriteAction } from './FavoriteAction/FavoriteAction';
import { Box, styled } from '@mui/material';
import { Box, styled, Chip, type ChipProps } from '@mui/material';
import { flexColumn } from 'themes/themeStyles';
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen';
@ -18,6 +18,7 @@ import { ProjectMembers } from './ProjectCardFooter/ProjectMembers/ProjectMember
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import type { ProjectSchema } from 'openapi';
import type { ITag } from 'interfaces/tags';
const StyledUpdated = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
@ -45,7 +46,30 @@ const StyledHeader = styled('div')(({ theme }) => ({
alignItems: 'center',
}));
type ProjectCardProps = ProjectSchema & { onHover?: () => void };
const StyledTags = styled('div')(({ theme }) => ({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(0.5),
marginTop: theme.spacing(1),
}));
interface StyledTagProps extends ChipProps {
tagColor?: string;
}
const StyledTag = styled(Chip)<StyledTagProps>(({ theme, tagColor }) => ({
backgroundColor: tagColor || theme.palette.primary.main,
color: theme.palette.primary.contrastText,
'& .MuiChip-label': {
padding: theme.spacing(0.5, 1),
fontSize: theme.fontSizes.smallerBody,
},
}));
interface ProjectCardProps extends ProjectSchema {
onHover?: () => void;
tags?: ITag[];
}
export const ProjectCard = ({
name,
@ -60,6 +84,7 @@ export const ProjectCard = ({
createdAt,
lastUpdatedAt,
lastReportedFlagUsage,
tags = [],
}: ProjectCardProps) => {
const { searchQuery } = useSearchHighlightContext();
@ -103,6 +128,21 @@ export const ProjectCard = ({
<ProjectLastSeen date={lastReportedFlagUsage} />
</div>
</StyledInfo>
<ConditionallyRender
condition={tags.length > 0}
show={
<StyledTags>
{tags.map((tag: ITag) => (
<StyledTag
key={`${tag.type}-${tag.value}`}
label={tag.value}
tagColor={tag.color}
size='small'
/>
))}
</StyledTags>
}
/>
</StyledProjectCardBody>
<ProjectCardFooter id={id} owners={owners}>
<ConditionallyRender

View File

@ -1,6 +1,7 @@
export interface ITag {
value: string;
type: string;
color?: string;
}
export interface ITagType {

View File

@ -102,11 +102,16 @@ class FeatureTagStore implements IFeatureTagStore {
const stopTimer = this.timer('getAllForFeature');
if (await this.featureExists(featureName)) {
const rows = await this.db
.select(COLUMNS)
.select([...COLUMNS, 'tag_types.color as color'])
.from<FeatureTagTable>(TABLE)
.leftJoin('tag_types', 'tag_types.name', 'feature_tag.tag_type')
.where({ feature_name: featureName });
stopTimer();
return rows.map(this.featureTagRowToTag);
return rows.map((row) => ({
type: row.tag_type,
value: row.tag_value,
color: row.color,
}));
} else {
throw new NotFoundError(
`Could not find feature with name ${featureName}`,

View File

@ -134,6 +134,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
'environments.sort_order as environment_sort_order',
'ft.tag_value as tag_value',
'ft.tag_type as tag_type',
'tag_types.color as tag_type_color',
'segments.name as segment_name',
'users.id as user_id',
'users.name as user_name',
@ -207,6 +208,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
'ft.feature_name',
'features.name',
)
.leftJoin('tag_types', 'tag_types.name', 'ft.tag_type')
.leftJoin(
'feature_strategies',
'feature_strategies.feature_name',
@ -548,6 +550,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
return {
value: r.tag_value,
type: r.tag_type,
color: r.tag_type_color,
};
}
@ -711,7 +714,7 @@ const applyMultiQueryParams = (
(Array.isArray(fields)
? val!.split(/:(.+)/).filter(Boolean)
: [val]
).map((s) => s.trim()),
).map((s) => s?.trim() || ''),
);
const baseSubQuery = createBaseQuery(values);

View File

@ -24,6 +24,13 @@ export const tagSchema = {
'The [type](https://docs.getunleash.io/reference/feature-toggles#tags) of the tag',
example: 'simple',
},
color: {
type: 'string',
description: 'The hexadecimal color code for the tag type.',
example: '#FFFFFF',
pattern: '^#[0-9A-Fa-f]{6}$',
nullable: true,
},
},
components: {},
} as const;

View File

@ -253,6 +253,7 @@ export interface IFeatureOverview {
lastSeenAt: Date;
environments: IEnvironmentOverview[];
lifecycle?: IFeatureLifecycleStage;
tags?: ITag[];
}
export type IFeatureSearchOverview = Exclude<
@ -353,6 +354,7 @@ export interface IFeatureToggleDeltaQuery extends IFeatureToggleQuery {
export interface ITag {
value: string;
type: string;
color?: string | null;
}
export interface IAddonParameterDefinition {