1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-23 00:16:25 +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 ( return (
<Dialogue <Dialogue
open={showDependencyDialogue} open={showDependencyDialogue}
title='Add parent feature dependency' title='Add parent flag dependency'
onClose={onClose} onClose={onClose}
onClick={manageDependency} onClick={manageDependency}
primaryButtonText={ primaryButtonText={

View File

@ -17,7 +17,6 @@ import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData'; import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData';
import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel'; import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { NewFeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment'; import { NewFeatureOverviewEnvironment } from './NewFeatureOverviewEnvironment/NewFeatureOverviewEnvironment';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
@ -51,39 +50,37 @@ const FeatureOverview = () => {
setLastViewed({ featureId, projectId }); setLastViewed({ featureId, projectId });
}, [featureId]); }, [featureId]);
const [environmentId, setEnvironmentId] = useState(''); const [environmentId, setEnvironmentId] = useState('');
const flagOverviewRedesign = useUiFlag('flagOverviewRedesign'); const flagOverviewRedesign = useUiFlag('flagOverviewRedesign');
const FeatureOverviewMetaData = flagOverviewRedesign
? NewFeatureOverviewMetaData
: OldFeatureOverviewMetaData;
const FeatureOverviewSidePanel = flagOverviewRedesign ? (
<NewFeatureOverviewSidePanel
environmentId={environmentId}
setEnvironmentId={setEnvironmentId}
/>
) : (
<OldFeatureOverviewSidePanel
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
);
return ( return (
<StyledContainer> <StyledContainer>
<div> <div>
<FeatureOverviewMetaData /> {flagOverviewRedesign ? (
{FeatureOverviewSidePanel} <>
<NewFeatureOverviewMetaData />
<NewFeatureOverviewSidePanel
environmentId={environmentId}
setEnvironmentId={setEnvironmentId}
/>
</>
) : (
<>
<OldFeatureOverviewMetaData />
<OldFeatureOverviewSidePanel
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
</>
)}
</div> </div>
<StyledMainContent> <StyledMainContent>
<ConditionallyRender {flagOverviewRedesign ? (
condition={flagOverviewRedesign} <NewFeatureOverviewEnvironment
show={ environmentId={environmentId}
<NewFeatureOverviewEnvironment />
environmentId={environmentId} ) : (
/> <FeatureOverviewEnvironments />
} )}
elseShow={<FeatureOverviewEnvironments />}
/>
</StyledMainContent> </StyledMainContent>
<Routes> <Routes>
<Route <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 { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue';
import type { IFeatureToggle } from 'interfaces/featureToggle'; import type { IFeatureToggle } from 'interfaces/featureToggle';
import { useState } from 'react'; import { useState } from 'react';
@ -26,11 +25,8 @@ import {
} from './FeatureOverviewMetaData'; } from './FeatureOverviewMetaData';
const StyledPermissionButton = styled(PermissionButton)(({ theme }) => ({ const StyledPermissionButton = styled(PermissionButton)(({ theme }) => ({
'&&&': { fontSize: theme.fontSizes.smallBody,
fontSize: theme.fontSizes.smallBody, lineHeight: theme.typography.body1.lineHeight,
lineHeight: 1,
margin: 0,
},
})); }));
const useDeleteDependency = (project: string, featureId: string) => { const useDeleteDependency = (project: string, featureId: string) => {
@ -112,13 +108,12 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
return ( return (
<> <>
<ConditionallyRender {canAddParentDependency ? (
condition={canAddParentDependency} <StyledMetaDataItem>
show={ <StyledMetaDataItemLabel>
<StyledMetaDataItem> Dependency:
<StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
Dependency: <div>
</StyledMetaDataItemLabel>
<StyledPermissionButton <StyledPermissionButton
size='small' size='small'
permission={UPDATE_FEATURE_DEPENDENCY} permission={UPDATE_FEATURE_DEPENDENCY}
@ -128,99 +123,69 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
setShowDependencyDialogue(true); setShowDependencyDialogue(true);
}} }}
> >
Add parent feature Add parent flag
</StyledPermissionButton> </StyledPermissionButton>
</StyledMetaDataItem> </div>
} </StyledMetaDataItem>
/> ) : null}
<ConditionallyRender {hasParentDependency ? (
condition={hasParentDependency} <StyledMetaDataItem>
show={ <StyledMetaDataItemLabel>
<StyledMetaDataItem> Dependency:
<StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
Dependency: <StyledMetaDataItemValue>
</StyledMetaDataItemLabel> <StyledLink
<StyledMetaDataItemValue> to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`}
<StyledLink >
to={`/projects/${feature.project}/features/${feature.dependencies[0]?.feature}`} {feature.dependencies[0]?.feature}
> </StyledLink>
{feature.dependencies[0]?.feature} {checkAccess(UPDATE_FEATURE_DEPENDENCY, environment) ? (
</StyledLink> <DependencyActions
<ConditionallyRender feature={feature.name}
condition={checkAccess( onEdit={() => setShowDependencyDialogue(true)}
UPDATE_FEATURE_DEPENDENCY, onDelete={deleteDependency}
environment,
)}
show={
<DependencyActions
feature={feature.name}
onEdit={() =>
setShowDependencyDialogue(true)
}
onDelete={deleteDependency}
/>
}
/> />
</StyledMetaDataItemValue> ) : null}
</StyledMetaDataItem> </StyledMetaDataItemValue>
} </StyledMetaDataItem>
/> ) : null}
<ConditionallyRender {hasParentDependency && !feature.dependencies[0]?.enabled ? (
condition={ <StyledMetaDataItem>
hasParentDependency && !feature.dependencies[0]?.enabled <StyledMetaDataItemLabel>
} Dependency value:
show={ </StyledMetaDataItemLabel>
<StyledMetaDataItem> <span>disabled</span>
<StyledMetaDataItemLabel> </StyledMetaDataItem>
Dependency value: ) : null}
</StyledMetaDataItemLabel> {hasParentDependency &&
<span>disabled</span> Boolean(feature.dependencies[0]?.variants?.length) ? (
</StyledMetaDataItem> <StyledMetaDataItem>
} <StyledMetaDataItemLabel>
/> Dependency value:
<ConditionallyRender </StyledMetaDataItemLabel>
condition={ <VariantsTooltip
hasParentDependency && variants={feature.dependencies[0]?.variants || []}
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}
/> />
} </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(); 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 () => { 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(); addParentButton.click();
await screen.findByText('Add parent feature dependency'); await screen.findByText('Add parent flag dependency');
}); });
test('show child', async () => { test('show child', async () => {
@ -291,7 +291,7 @@ test('edit dependency', async () => {
const editButton = await screen.findByText('Edit'); const editButton = await screen.findByText('Edit');
fireEvent.click(editButton); fireEvent.click(editButton);
await screen.findByText('Add parent feature dependency'); await screen.findByText('Add parent flag dependency');
}); });
test('show variant dependencies', async () => { 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 { useNavigate } from 'react-router-dom';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; 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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { useState } from 'react'; import { useState } from 'react';
@ -14,8 +12,9 @@ import { useLocationSettings } from 'hooks/useLocationSettings';
import { useShowDependentFeatures } from './useShowDependentFeatures'; import { useShowDependentFeatures } from './useShowDependentFeatures';
import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle'; import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle';
import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue'; import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import { TagRow } from './TagRow'; import { TagRow } from './TagRow';
import { capitalizeFirst } from 'utils/capitalizeFirst';
import { Collaborators } from './Collaborators';
const StyledMetaDataContainer = styled('div')(({ theme }) => ({ const StyledMetaDataContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(3), padding: theme.spacing(3),
@ -30,22 +29,10 @@ const StyledMetaDataContainer = styled('div')(({ theme }) => ({
}, },
})); }));
const StyledMetaDataHeader = styled('div')(({ theme }) => ({ const StyledTitle = styled('h2')(({ theme }) => ({
display: 'flex', fontSize: theme.typography.body1.fontSize,
alignItems: 'center', fontWeight: theme.typography.fontWeightBold,
gap: theme.spacing(2), marginBottom: theme.spacing(0.5),
'& > 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 StyledBody = styled('div')({ const StyledBody = styled('div')({
@ -57,7 +44,7 @@ export const StyledMetaDataItem = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
minHeight: theme.spacing(4.25), minHeight: theme.spacing(4.5),
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
})); }));
@ -76,11 +63,6 @@ export const StyledMetaDataItemValue = styled('div')(({ theme }) => ({
gap: theme.spacing(1), gap: theme.spacing(1),
})); }));
const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({
height: theme.spacing(3.5),
width: theme.spacing(3.5),
}));
const FeatureOverviewMetaData = () => { const FeatureOverviewMetaData = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
@ -97,55 +79,46 @@ const FeatureOverviewMetaData = () => {
const showDependentFeatures = useShowDependentFeatures(project); const showDependentFeatures = useShowDependentFeatures(project);
const FlagTypeIcon = getFeatureTypeIcons(type);
return ( return (
<> <>
<StyledMetaDataContainer> <StyledMetaDataContainer>
<StyledMetaDataHeader data-loading> <div>
<FlagTypeIcon /> <StyledTitle>Flag details</StyledTitle>
<h2>{capitalize(type || '')} flag</h2> {description ? (
</StyledMetaDataHeader>
<ConditionallyRender
condition={Boolean(description)}
show={
<StyledMetaDataItem data-loading> <StyledMetaDataItem data-loading>
<StyledMetaDataItemText> <StyledMetaDataItemText>
{description} {description}
</StyledMetaDataItemText> </StyledMetaDataItemText>
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : null}
/> </div>
<StyledBody> <StyledBody>
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Project: Flag type:
</StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
<StyledMetaDataItemText data-loading> <StyledMetaDataItemText data-loading>
{project} {capitalizeFirst(type || ' ')} flag
</StyledMetaDataItemText> </StyledMetaDataItemText>
</StyledMetaDataItem> </StyledMetaDataItem>
<ConditionallyRender {feature.lifecycle ? (
condition={Boolean(feature.lifecycle)} <StyledMetaDataItem data-loading>
show={ <StyledMetaDataItemLabel>
<StyledMetaDataItem data-loading> Lifecycle:
<StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
Lifecycle: <FeatureLifecycle
</StyledMetaDataItemLabel> feature={feature}
<FeatureLifecycle onArchive={() => setArchiveDialogOpen(true)}
feature={feature} onComplete={() =>
onArchive={() => setArchiveDialogOpen(true)} setMarkCompletedDialogueOpen(true)
onComplete={() => }
setMarkCompletedDialogueOpen(true) onUncomplete={refetchFeature}
} />
onUncomplete={refetchFeature} </StyledMetaDataItem>
/> ) : null}
</StyledMetaDataItem>
}
/>
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Created at: Created:
</StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
<StyledMetaDataItemText data-loading> <StyledMetaDataItemText data-loading>
{formatDateYMD( {formatDateYMD(
@ -154,65 +127,64 @@ const FeatureOverviewMetaData = () => {
)} )}
</StyledMetaDataItemText> </StyledMetaDataItemText>
</StyledMetaDataItem> </StyledMetaDataItem>
<ConditionallyRender {feature.createdBy ? (
condition={Boolean(feature.createdBy)} <StyledMetaDataItem>
show={() => ( <StyledMetaDataItemLabel>
<StyledMetaDataItem> Created by:
<StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
Created by: <StyledMetaDataItemValue>
</StyledMetaDataItemLabel> <StyledMetaDataItemText data-loading>
<StyledMetaDataItemValue> {feature.createdBy?.name}
<StyledMetaDataItemText data-loading> </StyledMetaDataItemText>
{feature.createdBy?.name} </StyledMetaDataItemValue>
</StyledMetaDataItemText> </StyledMetaDataItem>
<StyledUserAvatar ) : null}
user={feature.createdBy} {feature.collaborators?.users &&
/> feature.collaborators?.users.length > 0 ? (
</StyledMetaDataItemValue> <StyledMetaDataItem>
</StyledMetaDataItem> <StyledMetaDataItemLabel>
)} Collaborators:
/> </StyledMetaDataItemLabel>
<ConditionallyRender <StyledMetaDataItemValue>
condition={showDependentFeatures} <Collaborators
show={<DependencyRow feature={feature} />} collaborators={feature.collaborators?.users}
/> />
</StyledMetaDataItemValue>
</StyledMetaDataItem>
) : null}
{showDependentFeatures ? (
<DependencyRow feature={feature} />
) : null}
<TagRow feature={feature} /> <TagRow feature={feature} />
</StyledBody> </StyledBody>
</StyledMetaDataContainer> </StyledMetaDataContainer>
<ConditionallyRender {feature.children.length > 0 ? (
condition={feature.children.length > 0} <FeatureArchiveNotAllowedDialog
show={ features={feature.children}
<FeatureArchiveNotAllowedDialog project={projectId}
features={feature.children} isOpen={archiveDialogOpen}
project={projectId} onClose={() => setArchiveDialogOpen(false)}
isOpen={archiveDialogOpen} />
onClose={() => setArchiveDialogOpen(false)} ) : (
/> <FeatureArchiveDialog
} isOpen={archiveDialogOpen}
elseShow={ onConfirm={() => {
<FeatureArchiveDialog navigate(`/projects/${projectId}`);
isOpen={archiveDialogOpen} }}
onConfirm={() => { onClose={() => setArchiveDialogOpen(false)}
navigate(`/projects/${projectId}`); projectId={projectId}
}} featureIds={[featureId]}
onClose={() => setArchiveDialogOpen(false)} />
projectId={projectId} )}
featureIds={[featureId]} {feature.project ? (
/> <MarkCompletedDialogue
} isOpen={markCompletedDialogueOpen}
/> setIsOpen={setMarkCompletedDialogueOpen}
<ConditionallyRender projectId={feature.project}
condition={Boolean(feature.project)} featureId={feature.name}
show={ onComplete={refetchFeature}
<MarkCompletedDialogue />
isOpen={markCompletedDialogueOpen} ) : null}
setIsOpen={setMarkCompletedDialogueOpen}
projectId={feature.project}
featureId={feature.name}
onComplete={refetchFeature}
/>
}
/>
</> </>
); );
}; };

View File

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

View File

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