1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

feat: new flag info box (#9308)

- updated spacing of elements
- modified header and "flag type" 
- added "collaborators"
- refactored tags

Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
Tymoteusz Czech 2025-02-18 10:30:52 +01:00 committed by GitHub
parent b15502ec5e
commit 2ede2a6578
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 340 additions and 355 deletions

View File

@ -90,7 +90,7 @@ export const AddDependencyDialogue = ({
return (
<Dialogue
open={showDependencyDialogue}
title='Add parent feature dependency'
title='Add parent flag dependency'
onClose={onClose}
onClick={manageDependency}
primaryButtonText={

View File

@ -17,7 +17,6 @@ import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
import { useUiFlag } from 'hooks/useUiFlag';
import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData';
import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { NewFeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
const StyledContainer = styled('div')(({ theme }) => ({
@ -51,39 +50,37 @@ const FeatureOverview = () => {
setLastViewed({ featureId, projectId });
}, [featureId]);
const [environmentId, setEnvironmentId] = useState('');
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
const FeatureOverviewMetaData = flagOverviewRedesign
? NewFeatureOverviewMetaData
: OldFeatureOverviewMetaData;
const FeatureOverviewSidePanel = flagOverviewRedesign ? (
<NewFeatureOverviewSidePanel
environmentId={environmentId}
setEnvironmentId={setEnvironmentId}
/>
) : (
<OldFeatureOverviewSidePanel
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
);
return (
<StyledContainer>
<div>
<FeatureOverviewMetaData />
{FeatureOverviewSidePanel}
{flagOverviewRedesign ? (
<>
<NewFeatureOverviewMetaData />
<NewFeatureOverviewSidePanel
environmentId={environmentId}
setEnvironmentId={setEnvironmentId}
/>
</>
) : (
<>
<OldFeatureOverviewMetaData />
<OldFeatureOverviewSidePanel
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
</>
)}
</div>
<StyledMainContent>
<ConditionallyRender
condition={flagOverviewRedesign}
show={
<NewFeatureOverviewEnvironment
environmentId={environmentId}
/>
}
elseShow={<FeatureOverviewEnvironments />}
/>
{flagOverviewRedesign ? (
<NewFeatureOverviewEnvironment
environmentId={environmentId}
/>
) : (
<FeatureOverviewEnvironments />
)}
</StyledMainContent>
<Routes>
<Route

View File

@ -0,0 +1,35 @@
import type { FC } from 'react';
import AddIcon from '@mui/icons-material/Add';
import { styled } from '@mui/material';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
const StyledAddTagButton = styled(PermissionButton)(({ theme }) => ({
lineHeight: theme.typography.body1.lineHeight,
borderRadius: theme.shape.borderRadiusExtraLarge,
background: theme.palette.secondary.light,
padding: theme.spacing(0.5, 1),
height: theme.spacing(3.5),
}));
const StyledAddIcon = styled(AddIcon)(({ theme }) => ({
fontSize: theme.typography.body2.fontSize,
}));
type AddTagButtonProps = {
project: string;
onClick: () => void;
};
export const AddTagButton: FC<AddTagButtonProps> = ({ project, onClick }) => (
<StyledAddTagButton
size='small'
permission={UPDATE_FEATURE}
projectId={project}
variant='text'
onClick={onClick}
startIcon={<StyledAddIcon />}
>
Add tag
</StyledAddTagButton>
);

View File

@ -0,0 +1,34 @@
import { styled } from '@mui/material';
import {
AvatarComponent,
AvatarGroup,
} from 'component/common/AvatarGroup/AvatarGroup';
import type { Collaborator } from 'interfaces/featureToggle';
import type { FC } from 'react';
const StyledAvatarComponent = styled(AvatarComponent)(({ theme }) => ({
width: theme.spacing(2.5),
height: theme.spacing(2.5),
}));
const StyledAvatarGroup = styled(AvatarGroup)({
flexWrap: 'nowrap',
});
type CollaboratorsProps = {
collaborators: Collaborator[] | undefined;
};
export const Collaborators: FC<CollaboratorsProps> = ({ collaborators }) => {
if (!collaborators || collaborators.length === 0) {
return null;
}
return (
<StyledAvatarGroup
users={collaborators}
avatarLimit={9}
AvatarComponent={StyledAvatarComponent}
/>
);
};

View File

@ -1,4 +1,3 @@
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import type { IFeatureToggle } from 'interfaces/featureToggle';
import { useState } from 'react';
@ -26,11 +25,8 @@ import {
} from './FeatureOverviewMetaData';
const StyledPermissionButton = styled(PermissionButton)(({ theme }) => ({
'&&&': {
fontSize: theme.fontSizes.smallBody,
lineHeight: 1,
margin: 0,
},
fontSize: theme.fontSizes.smallBody,
lineHeight: theme.typography.body1.lineHeight,
}));
const useDeleteDependency = (project: string, featureId: string) => {
@ -112,13 +108,12 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
return (
<>
<ConditionallyRender
condition={canAddParentDependency}
show={
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency:
</StyledMetaDataItemLabel>
{canAddParentDependency ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency:
</StyledMetaDataItemLabel>
<div>
<StyledPermissionButton
size='small'
permission={UPDATE_FEATURE_DEPENDENCY}
@ -128,99 +123,69 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
setShowDependencyDialogue(true);
}}
>
Add parent feature
Add parent flag
</StyledPermissionButton>
</StyledMetaDataItem>
}
/>
<ConditionallyRender
condition={hasParentDependency}
show={
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<StyledLink
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
>
{feature.dependencies[0]?.feature}
</StyledLink>
<ConditionallyRender
condition={checkAccess(
UPDATE_FEATURE_DEPENDENCY,
environment,
)}
show={
<DependencyActions
feature={feature.name}
onEdit={() =>
setShowDependencyDialogue(true)
}
onDelete={deleteDependency}
/>
}
</div>
</StyledMetaDataItem>
) : null}
{hasParentDependency ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<StyledLink
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
>
{feature.dependencies[0]?.feature}
</StyledLink>
{checkAccess(UPDATE_FEATURE_DEPENDENCY, environment) ? (
<DependencyActions
feature={feature.name}
onEdit={() => setShowDependencyDialogue(true)}
onDelete={deleteDependency}
/>
</StyledMetaDataItemValue>
</StyledMetaDataItem>
}
/>
<ConditionallyRender
condition={
hasParentDependency && !feature.dependencies[0]?.enabled
}
show={
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency value:
</StyledMetaDataItemLabel>
<span>disabled</span>
</StyledMetaDataItem>
}
/>
<ConditionallyRender
condition={
hasParentDependency &&
Boolean(feature.dependencies[0]?.variants?.length)
}
show={
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency value:
</StyledMetaDataItemLabel>
<VariantsTooltip
variants={feature.dependencies[0]?.variants || []}
/>
</StyledMetaDataItem>
}
/>
<ConditionallyRender
condition={hasChildren}
show={
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Children:
</StyledMetaDataItemLabel>
<ChildrenTooltip
childFeatures={feature.children}
project={feature.project}
/>
</StyledMetaDataItem>
}
/>
<ConditionallyRender
condition={Boolean(feature.project)}
show={
<AddDependencyDialogue
project={feature.project}
featureId={feature.name}
parentDependency={feature.dependencies[0]}
onClose={() => setShowDependencyDialogue(false)}
showDependencyDialogue={showDependencyDialogue}
) : null}
</StyledMetaDataItemValue>
</StyledMetaDataItem>
) : null}
{hasParentDependency && !feature.dependencies[0]?.enabled ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency value:
</StyledMetaDataItemLabel>
<span>disabled</span>
</StyledMetaDataItem>
) : null}
{hasParentDependency &&
Boolean(feature.dependencies[0]?.variants?.length) ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Dependency value:
</StyledMetaDataItemLabel>
<VariantsTooltip
variants={feature.dependencies[0]?.variants || []}
/>
}
/>
</StyledMetaDataItem>
) : null}
{hasChildren ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>Children:</StyledMetaDataItemLabel>
<ChildrenTooltip
childFeatures={feature.children}
project={feature.project}
/>
</StyledMetaDataItem>
) : null}
{feature.project ? (
<AddDependencyDialogue
project={feature.project}
featureId={feature.name}
parentDependency={feature.dependencies[0]}
onClose={() => setShowDependencyDialogue(false)}
showDependencyDialogue={showDependencyDialogue}
/>
) : null}
</>
);
};

View File

@ -97,11 +97,11 @@ test('show dependency dialogue', async () => {
},
);
const addParentButton = await screen.findByText('Add parent feature');
const addParentButton = await screen.findByText('Add parent flag');
addParentButton.click();
await screen.findByText('Add parent feature dependency');
await screen.findByText('Add parent flag dependency');
});
test('show dependency dialogue for OSS with dependencies', async () => {
@ -127,11 +127,11 @@ test('show dependency dialogue for OSS with dependencies', async () => {
},
);
const addParentButton = await screen.findByText('Add parent feature');
const addParentButton = await screen.findByText('Add parent flag');
addParentButton.click();
await screen.findByText('Add parent feature dependency');
await screen.findByText('Add parent flag dependency');
});
test('show child', async () => {
@ -291,7 +291,7 @@ test('edit dependency', async () => {
const editButton = await screen.findByText('Edit');
fireEvent.click(editButton);
await screen.findByText('Add parent feature dependency');
await screen.findByText('Add parent flag dependency');
});
test('show variant dependencies', async () => {

View File

@ -1,8 +1,6 @@
import { capitalize, styled } from '@mui/material';
import { styled } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { useState } from 'react';
@ -14,8 +12,9 @@ import { useLocationSettings } from 'hooks/useLocationSettings';
import { useShowDependentFeatures } from './useShowDependentFeatures';
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import { TagRow } from './TagRow';
import { capitalizeFirst } from 'utils/capitalizeFirst';
import { Collaborators } from './Collaborators';
const StyledMetaDataContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(3),
@ -30,22 +29,10 @@ const StyledMetaDataContainer = styled('div')(({ theme }) => ({
},
}));
const StyledMetaDataHeader = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(2),
'& > svg': {
height: theme.spacing(5),
width: theme.spacing(5),
padding: theme.spacing(0.5),
backgroundColor: theme.palette.background.alternative,
fill: theme.palette.primary.contrastText,
borderRadius: theme.shape.borderRadiusMedium,
},
'& > h2': {
fontSize: theme.fontSizes.mainHeader,
fontWeight: 'normal',
},
const StyledTitle = styled('h2')(({ theme }) => ({
fontSize: theme.typography.body1.fontSize,
fontWeight: theme.typography.fontWeightBold,
marginBottom: theme.spacing(0.5),
}));
const StyledBody = styled('div')({
@ -57,7 +44,7 @@ export const StyledMetaDataItem = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
minHeight: theme.spacing(4.25),
minHeight: theme.spacing(4.5),
fontSize: theme.fontSizes.smallBody,
}));
@ -76,11 +63,6 @@ export const StyledMetaDataItemValue = styled('div')(({ theme }) => ({
gap: theme.spacing(1),
}));
const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({
height: theme.spacing(3.5),
width: theme.spacing(3.5),
}));
const FeatureOverviewMetaData = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
@ -97,55 +79,46 @@ const FeatureOverviewMetaData = () => {
const showDependentFeatures = useShowDependentFeatures(project);
const FlagTypeIcon = getFeatureTypeIcons(type);
return (
<>
<StyledMetaDataContainer>
<StyledMetaDataHeader data-loading>
<FlagTypeIcon />
<h2>{capitalize(type || '')} flag</h2>
</StyledMetaDataHeader>
<ConditionallyRender
condition={Boolean(description)}
show={
<div>
<StyledTitle>Flag details</StyledTitle>
{description ? (
<StyledMetaDataItem data-loading>
<StyledMetaDataItemText>
{description}
</StyledMetaDataItemText>
</StyledMetaDataItem>
}
/>
) : null}
</div>
<StyledBody>
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Project:
Flag type:
</StyledMetaDataItemLabel>
<StyledMetaDataItemText data-loading>
{project}
{capitalizeFirst(type || ' ')} flag
</StyledMetaDataItemText>
</StyledMetaDataItem>
<ConditionallyRender
condition={Boolean(feature.lifecycle)}
show={
<StyledMetaDataItem data-loading>
<StyledMetaDataItemLabel>
Lifecycle:
</StyledMetaDataItemLabel>
<FeatureLifecycle
feature={feature}
onArchive={() => setArchiveDialogOpen(true)}
onComplete={() =>
setMarkCompletedDialogueOpen(true)
}
onUncomplete={refetchFeature}
/>
</StyledMetaDataItem>
}
/>
{feature.lifecycle ? (
<StyledMetaDataItem data-loading>
<StyledMetaDataItemLabel>
Lifecycle:
</StyledMetaDataItemLabel>
<FeatureLifecycle
feature={feature}
onArchive={() => setArchiveDialogOpen(true)}
onComplete={() =>
setMarkCompletedDialogueOpen(true)
}
onUncomplete={refetchFeature}
/>
</StyledMetaDataItem>
) : null}
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Created at:
Created:
</StyledMetaDataItemLabel>
<StyledMetaDataItemText data-loading>
{formatDateYMD(
@ -154,65 +127,64 @@ const FeatureOverviewMetaData = () => {
)}
</StyledMetaDataItemText>
</StyledMetaDataItem>
<ConditionallyRender
condition={Boolean(feature.createdBy)}
show={() => (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Created by:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<StyledMetaDataItemText data-loading>
{feature.createdBy?.name}
</StyledMetaDataItemText>
<StyledUserAvatar
user={feature.createdBy}
/>
</StyledMetaDataItemValue>
</StyledMetaDataItem>
)}
/>
<ConditionallyRender
condition={showDependentFeatures}
show={<DependencyRow feature={feature} />}
/>
{feature.createdBy ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Created by:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<StyledMetaDataItemText data-loading>
{feature.createdBy?.name}
</StyledMetaDataItemText>
</StyledMetaDataItemValue>
</StyledMetaDataItem>
) : null}
{feature.collaborators?.users &&
feature.collaborators?.users.length > 0 ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Collaborators:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<Collaborators
collaborators={feature.collaborators?.users}
/>
</StyledMetaDataItemValue>
</StyledMetaDataItem>
) : null}
{showDependentFeatures ? (
<DependencyRow feature={feature} />
) : null}
<TagRow feature={feature} />
</StyledBody>
</StyledMetaDataContainer>
<ConditionallyRender
condition={feature.children.length > 0}
show={
<FeatureArchiveNotAllowedDialog
features={feature.children}
project={projectId}
isOpen={archiveDialogOpen}
onClose={() => setArchiveDialogOpen(false)}
/>
}
elseShow={
<FeatureArchiveDialog
isOpen={archiveDialogOpen}
onConfirm={() => {
navigate(`/projects/${projectId}`);
}}
onClose={() => setArchiveDialogOpen(false)}
projectId={projectId}
featureIds={[featureId]}
/>
}
/>
<ConditionallyRender
condition={Boolean(feature.project)}
show={
<MarkCompletedDialogue
isOpen={markCompletedDialogueOpen}
setIsOpen={setMarkCompletedDialogueOpen}
projectId={feature.project}
featureId={feature.name}
onComplete={refetchFeature}
/>
}
/>
{feature.children.length > 0 ? (
<FeatureArchiveNotAllowedDialog
features={feature.children}
project={projectId}
isOpen={archiveDialogOpen}
onClose={() => setArchiveDialogOpen(false)}
/>
) : (
<FeatureArchiveDialog
isOpen={archiveDialogOpen}
onConfirm={() => {
navigate(`/projects/${projectId}`);
}}
onClose={() => setArchiveDialogOpen(false)}
projectId={projectId}
featureIds={[featureId]}
/>
)}
{feature.project ? (
<MarkCompletedDialogue
isOpen={markCompletedDialogueOpen}
setIsOpen={setMarkCompletedDialogueOpen}
projectId={feature.project}
featureId={feature.name}
onComplete={refetchFeature}
/>
) : null}
</>
);
};

View File

@ -119,7 +119,7 @@ export const OldDependencyRow: FC<{ feature: IFeatureToggle }> = ({
marginBottom: theme.spacing(0.4),
})}
>
Add parent feature
Add parent flag
</PermissionButton>
</StyledDetail>
</FlexRow>

View File

@ -2,8 +2,7 @@ import type { IFeatureToggle } from 'interfaces/featureToggle';
import { useContext, useState } from 'react';
import { Chip, styled, Tooltip } from '@mui/material';
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
import Add from '@mui/icons-material/Add';
import ClearIcon from '@mui/icons-material/Clear';
import DeleteTagIcon from '@mui/icons-material/Cancel';
import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import AccessContext from 'contexts/AccessContext';
@ -12,57 +11,42 @@ import type { ITag } from 'interfaces/tags';
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 {
StyledMetaDataItem,
StyledMetaDataItemLabel,
} from './FeatureOverviewMetaData';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { StyledMetaDataItem } from './FeatureOverviewMetaData';
import { AddTagButton } from './AddTagButton';
const StyledPermissionButton = styled(PermissionButton)(({ theme }) => ({
'&&&': {
fontSize: theme.fontSizes.smallBody,
lineHeight: 1,
margin: 0,
},
const StyledLabel = styled('span')(({ theme }) => ({
marginTop: theme.spacing(1),
color: theme.palette.text.secondary,
marginRight: theme.spacing(1),
}));
const StyledTagRow = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'start',
minHeight: theme.spacing(4.25),
lineHeight: theme.spacing(4.25),
justifyContent: 'space-between',
flexWrap: 'wrap',
minHeight: theme.spacing(4.5),
fontSize: theme.fontSizes.smallBody,
justifyContent: 'start',
}));
const StyledTagContainer = styled('div')(({ theme }) => ({
display: 'flex',
flex: 1,
overflow: 'hidden',
gap: theme.spacing(1),
flexWrap: 'wrap',
marginTop: theme.spacing(0.75),
}));
const StyledChip = styled(Chip)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
const StyledTag = styled(Chip)(({ theme }) => ({
overflowWrap: 'anywhere',
lineHeight: theme.typography.body1.lineHeight,
backgroundColor: theme.palette.neutral.light,
color: theme.palette.neutral.dark,
'&&& > svg': {
color: theme.palette.neutral.dark,
fontSize: theme.fontSizes.smallBody,
},
color: theme.palette.text.primary,
padding: theme.spacing(0.25),
height: theme.spacing(3.5),
}));
const StyledAddedTag = styled(StyledChip)(({ theme }) => ({
backgroundColor: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&&& > svg': {
color: theme.palette.secondary.dark,
fontSize: theme.fontSizes.smallBody,
},
const StyledEllipsis = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
}));
interface IFeatureOverviewSidePanelTagsProps {
@ -81,6 +65,10 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
const { hasAccess } = useContext(AccessContext);
const canUpdateTags = hasAccess(UPDATE_FEATURE, feature.project);
const handleAdd = () => {
setManageTagsOpen(true);
};
const handleRemove = async () => {
if (!selectedTag) return;
try {
@ -101,78 +89,71 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
return (
<>
<ConditionallyRender
condition={!tags.length}
show={
<StyledMetaDataItem>
<StyledMetaDataItemLabel>Tags:</StyledMetaDataItemLabel>
<StyledPermissionButton
size='small'
permission={UPDATE_FEATURE}
projectId={feature.project}
variant='text'
onClick={() => {
setManageTagsOpen(true);
}}
>
Add tag
</StyledPermissionButton>
</StyledMetaDataItem>
}
elseShow={
<StyledTagRow>
<StyledMetaDataItemLabel>Tags:</StyledMetaDataItemLabel>
<StyledTagContainer>
{tags.map((tag) => {
const tagLabel = `${tag.type}:${tag.value}`;
return (
<Tooltip
key={tagLabel}
title={
tagLabel.length > 35 ? tagLabel : ''
}
arrow
>
<StyledAddedTag
label={tagLabel}
size='small'
deleteIcon={
<Tooltip
title='Remove tag'
arrow
>
<ClearIcon />
</Tooltip>
{!tags.length ? (
<StyledMetaDataItem>
<StyledLabel>Tags:</StyledLabel>
<StyledTagContainer>
<AddTagButton
project={feature.project}
onClick={handleAdd}
/>
</StyledTagContainer>
</StyledMetaDataItem>
) : (
<StyledTagRow>
<StyledLabel>Tags:</StyledLabel>
<StyledTagContainer>
{tags.map((tag) => {
const tagLabel = `${tag.type}:${tag.value}`;
const isOverflowing = tagLabel.length > 25;
return (
<StyledTag
label={
<Tooltip
key={tagLabel}
title={
isOverflowing ? tagLabel : ''
}
onDelete={
canUpdateTags
? () => {
setRemoveTagOpen(
true,
);
setSelectedTag(tag);
}
: undefined
}
/>
</Tooltip>
);
})}
<ConditionallyRender
condition={canUpdateTags}
show={
<StyledChip
icon={<Add />}
label='Add tag'
size='small'
onClick={() => setManageTagsOpen(true)}
/>
}
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
}
/>
);
})}
{canUpdateTags ? (
<AddTagButton
project={feature.project}
onClick={handleAdd}
/>
</StyledTagContainer>
</StyledTagRow>
}
/>
) : null}
</StyledTagContainer>
</StyledTagRow>
)}
<ManageTagsDialog
open={manageTagsOpen}
setOpen={setManageTagsOpen}
@ -184,6 +165,7 @@ export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => {
onClose={() => {
setRemoveTagOpen(false);
setSelectedTag(undefined);
refetch();
}}
onClick={() => {
setRemoveTagOpen(false);

View File

@ -140,7 +140,7 @@ A child feature flag is evaluated only when both the child and its parent featur
- Parent feature is disabled: Useful when the parent acts as a kill switch with inverted enabled/disabled logic.
- Parent feature is enabled with variants: Useful for A/B testing scenarios where you need specific variant dependencies.
To add a dependency, you need the `update-feature-dependency` project permission. In the Admin UI, go to the feature flag you want to add a parent to and select **Add parent feature**.
To add a dependency, you need the `update-feature-dependency` project permission. In the Admin UI, go to the feature flag you want to add a parent to and select **Add parent flag**.
![Feature parent flag](/img/add-parent-flag.png)