diff --git a/frontend/src/component/common/Tag/Tag.tsx b/frontend/src/component/common/Tag/Tag.tsx
new file mode 100644
index 0000000000..0632e1d52c
--- /dev/null
+++ b/frontend/src/component/common/Tag/Tag.tsx
@@ -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 = (
+
+ {showColorDot && }
+ {label}
+
+ ) as ReactElement;
+
+ return (
+
+ );
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx
index 788354d316..33fb673995 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx
@@ -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) => (
+ {
+ setRemoveTagOpen(true);
+ setSelectedTag(tag);
+ }}
+ />
+ );
+
return (
<>
{!tags.length ? (
@@ -103,49 +120,7 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
Tags:
- {tags.map((tag) => {
- const tagLabel = `${tag.type}:${tag.value}`;
- const isOverflowing = tagLabel.length > 25;
- return (
-
-
- {tagLabel.substring(0, 25)}
- {isOverflowing ? (
-
- …
-
- ) : (
- ''
- )}
-
-
- }
- size='small'
- deleteIcon={
-
-
-
- }
- onDelete={
- canUpdateTags
- ? () => {
- setRemoveTagOpen(true);
- setSelectedTag(tag);
- }
- : undefined
- }
- />
- );
- })}
+ {tags.map(renderTag)}
{canUpdateTags ? (
{
>
);
};
+
+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 = (
+
+
+
+ );
+ const onDelete = canUpdateTags ? () => onTagRemove(tag) : undefined;
+
+ if (isTagTypeColorEnabled) {
+ const deleteIcon = (
+
+
+
+ );
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {tagLabel.substring(0, 25)}
+ {isOverflowing ? (
+ …
+ ) : (
+ ''
+ )}
+
+
+ }
+ size='small'
+ deleteIcon={deleteIcon}
+ onDelete={onDelete}
+ />
+ );
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
index 6a91257fd2..00206d4137 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
@@ -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 = ({
{header}
- {tags.map((tag) => {
- const tagLabel = `${tag.type}:${tag.value}`;
- return (
- }
- onDelete={
- canUpdateTags
- ? () => {
- setShowDelDialog(true);
- setSelectedTag(tag);
- }
- : undefined
- }
- />
- );
- })}
+ {tags.map((tag) => (
+ {
+ setShowDelDialog(true);
+ setSelectedTag(tag);
+ }
+ : undefined
+ }
+ deleteIcon={}
+ />
+ ))}
{
- 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:{' '}
diff --git a/frontend/src/component/project/ProjectCard/ProjectCard.tsx b/frontend/src/component/project/ProjectCard/ProjectCard.tsx
index afa3caba06..9d9464d7c2 100644
--- a/frontend/src/component/project/ProjectCard/ProjectCard.tsx
+++ b/frontend/src/component/project/ProjectCard/ProjectCard.tsx
@@ -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)(({ 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 = ({
+ 0}
+ show={
+
+ {tags.map((tag: ITag) => (
+
+ ))}
+
+ }
+ />
(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}`,
diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts
index e7dbda8daa..c27baa7d78 100644
--- a/src/lib/features/feature-search/feature-search-store.ts
+++ b/src/lib/features/feature-search/feature-search-store.ts
@@ -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);
diff --git a/src/lib/openapi/spec/tag-schema.ts b/src/lib/openapi/spec/tag-schema.ts
index e08342f080..f402fb54ab 100644
--- a/src/lib/openapi/spec/tag-schema.ts
+++ b/src/lib/openapi/spec/tag-schema.ts
@@ -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;
diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts
index d15867d1a4..d894c7678c 100644
--- a/src/lib/types/model.ts
+++ b/src/lib/types/model.ts
@@ -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 {