1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-12 01:17:04 +02: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}
show={
<NewFeatureOverviewEnvironment <NewFeatureOverviewEnvironment
environmentId={environmentId} environmentId={environmentId}
/> />
} ) : (
elseShow={<FeatureOverviewEnvironments />} <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: 1, lineHeight: theme.typography.body1.lineHeight,
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}
show={
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Dependency: Dependency:
</StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
<div>
<StyledPermissionButton <StyledPermissionButton
size='small' size='small'
permission={UPDATE_FEATURE_DEPENDENCY} permission={UPDATE_FEATURE_DEPENDENCY}
@ -128,14 +123,12 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
setShowDependencyDialogue(true); setShowDependencyDialogue(true);
}} }}
> >
Add parent feature Add parent flag
</StyledPermissionButton> </StyledPermissionButton>
</div>
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : null}
/> {hasParentDependency ? (
<ConditionallyRender
condition={hasParentDependency}
show={
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Dependency: Dependency:
@ -146,44 +139,26 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
> >
{feature.dependencies[0]?.feature} {feature.dependencies[0]?.feature}
</StyledLink> </StyledLink>
<ConditionallyRender {checkAccess(UPDATE_FEATURE_DEPENDENCY, environment) ? (
condition={checkAccess(
UPDATE_FEATURE_DEPENDENCY,
environment,
)}
show={
<DependencyActions <DependencyActions
feature={feature.name} feature={feature.name}
onEdit={() => onEdit={() => setShowDependencyDialogue(true)}
setShowDependencyDialogue(true)
}
onDelete={deleteDependency} onDelete={deleteDependency}
/> />
} ) : null}
/>
</StyledMetaDataItemValue> </StyledMetaDataItemValue>
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : null}
/> {hasParentDependency && !feature.dependencies[0]?.enabled ? (
<ConditionallyRender
condition={
hasParentDependency && !feature.dependencies[0]?.enabled
}
show={
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Dependency value: Dependency value:
</StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
<span>disabled</span> <span>disabled</span>
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : null}
/> {hasParentDependency &&
<ConditionallyRender Boolean(feature.dependencies[0]?.variants?.length) ? (
condition={
hasParentDependency &&
Boolean(feature.dependencies[0]?.variants?.length)
}
show={
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Dependency value: Dependency value:
@ -192,26 +167,17 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
variants={feature.dependencies[0]?.variants || []} variants={feature.dependencies[0]?.variants || []}
/> />
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : null}
/> {hasChildren ? (
<ConditionallyRender
condition={hasChildren}
show={
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>Children:</StyledMetaDataItemLabel>
Children:
</StyledMetaDataItemLabel>
<ChildrenTooltip <ChildrenTooltip
childFeatures={feature.children} childFeatures={feature.children}
project={feature.project} project={feature.project}
/> />
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : null}
/> {feature.project ? (
<ConditionallyRender
condition={Boolean(feature.project)}
show={
<AddDependencyDialogue <AddDependencyDialogue
project={feature.project} project={feature.project}
featureId={feature.name} featureId={feature.name}
@ -219,8 +185,7 @@ export const DependencyRow = ({ feature }: IDependencyRowProps) => {
onClose={() => setShowDependencyDialogue(false)} onClose={() => setShowDependencyDialogue(false)}
showDependencyDialogue={showDependencyDialogue} 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,37 +79,29 @@ 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)}
show={
<StyledMetaDataItem data-loading> <StyledMetaDataItem data-loading>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Lifecycle: Lifecycle:
@ -141,11 +115,10 @@ const FeatureOverviewMetaData = () => {
onUncomplete={refetchFeature} onUncomplete={refetchFeature}
/> />
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : null}
/>
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Created at: Created:
</StyledMetaDataItemLabel> </StyledMetaDataItemLabel>
<StyledMetaDataItemText data-loading> <StyledMetaDataItemText data-loading>
{formatDateYMD( {formatDateYMD(
@ -154,9 +127,7 @@ const FeatureOverviewMetaData = () => {
)} )}
</StyledMetaDataItemText> </StyledMetaDataItemText>
</StyledMetaDataItem> </StyledMetaDataItem>
<ConditionallyRender {feature.createdBy ? (
condition={Boolean(feature.createdBy)}
show={() => (
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel> <StyledMetaDataItemLabel>
Created by: Created by:
@ -165,31 +136,36 @@ const FeatureOverviewMetaData = () => {
<StyledMetaDataItemText data-loading> <StyledMetaDataItemText data-loading>
{feature.createdBy?.name} {feature.createdBy?.name}
</StyledMetaDataItemText> </StyledMetaDataItemText>
<StyledUserAvatar </StyledMetaDataItemValue>
user={feature.createdBy} </StyledMetaDataItem>
) : null}
{feature.collaborators?.users &&
feature.collaborators?.users.length > 0 ? (
<StyledMetaDataItem>
<StyledMetaDataItemLabel>
Collaborators:
</StyledMetaDataItemLabel>
<StyledMetaDataItemValue>
<Collaborators
collaborators={feature.collaborators?.users}
/> />
</StyledMetaDataItemValue> </StyledMetaDataItemValue>
</StyledMetaDataItem> </StyledMetaDataItem>
)} ) : null}
/> {showDependentFeatures ? (
<ConditionallyRender <DependencyRow feature={feature} />
condition={showDependentFeatures} ) : null}
show={<DependencyRow feature={feature} />}
/>
<TagRow feature={feature} /> <TagRow feature={feature} />
</StyledBody> </StyledBody>
</StyledMetaDataContainer> </StyledMetaDataContainer>
<ConditionallyRender {feature.children.length > 0 ? (
condition={feature.children.length > 0}
show={
<FeatureArchiveNotAllowedDialog <FeatureArchiveNotAllowedDialog
features={feature.children} features={feature.children}
project={projectId} project={projectId}
isOpen={archiveDialogOpen} isOpen={archiveDialogOpen}
onClose={() => setArchiveDialogOpen(false)} onClose={() => setArchiveDialogOpen(false)}
/> />
} ) : (
elseShow={
<FeatureArchiveDialog <FeatureArchiveDialog
isOpen={archiveDialogOpen} isOpen={archiveDialogOpen}
onConfirm={() => { onConfirm={() => {
@ -199,11 +175,8 @@ const FeatureOverviewMetaData = () => {
projectId={projectId} projectId={projectId}
featureIds={[featureId]} featureIds={[featureId]}
/> />
} )}
/> {feature.project ? (
<ConditionallyRender
condition={Boolean(feature.project)}
show={
<MarkCompletedDialogue <MarkCompletedDialogue
isOpen={markCompletedDialogueOpen} isOpen={markCompletedDialogueOpen}
setIsOpen={setMarkCompletedDialogueOpen} setIsOpen={setMarkCompletedDialogueOpen}
@ -211,8 +184,7 @@ const FeatureOverviewMetaData = () => {
featureId={feature.name} featureId={feature.name}
onComplete={refetchFeature} onComplete={refetchFeature}
/> />
} ) : null}
/>
</> </>
); );
}; };

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}
show={
<StyledMetaDataItem> <StyledMetaDataItem>
<StyledMetaDataItemLabel>Tags:</StyledMetaDataItemLabel> <StyledLabel>Tags:</StyledLabel>
<StyledPermissionButton <StyledTagContainer>
size='small' <AddTagButton
permission={UPDATE_FEATURE} project={feature.project}
projectId={feature.project} onClick={handleAdd}
variant='text' />
onClick={() => { </StyledTagContainer>
setManageTagsOpen(true);
}}
>
Add tag
</StyledPermissionButton>
</StyledMetaDataItem> </StyledMetaDataItem>
} ) : (
elseShow={
<StyledTagRow> <StyledTagRow>
<StyledMetaDataItemLabel>Tags:</StyledMetaDataItemLabel> <StyledLabel>Tags:</StyledLabel>
<StyledTagContainer> <StyledTagContainer>
{tags.map((tag) => { {tags.map((tag) => {
const tagLabel = `${tag.type}:${tag.value}`; const tagLabel = `${tag.type}:${tag.value}`;
const isOverflowing = tagLabel.length > 25;
return ( return (
<StyledTag
label={
<Tooltip <Tooltip
key={tagLabel} key={tagLabel}
title={ title={
tagLabel.length > 35 ? tagLabel : '' isOverflowing ? tagLabel : ''
} }
arrow arrow
> >
<StyledAddedTag <span>
label={tagLabel} {tagLabel.substring(0, 25)}
{isOverflowing ? (
<StyledEllipsis>
</StyledEllipsis>
) : (
''
)}
</span>
</Tooltip>
}
size='small' size='small'
deleteIcon={ deleteIcon={
<Tooltip <Tooltip title='Remove tag' arrow>
title='Remove tag' <DeleteTagIcon />
arrow
>
<ClearIcon />
</Tooltip> </Tooltip>
} }
onDelete={ onDelete={
canUpdateTags canUpdateTags
? () => { ? () => {
setRemoveTagOpen( setRemoveTagOpen(true);
true,
);
setSelectedTag(tag); setSelectedTag(tag);
} }
: undefined : undefined
} }
/> />
</Tooltip>
); );
})} })}
<ConditionallyRender {canUpdateTags ? (
condition={canUpdateTags} <AddTagButton
show={ project={feature.project}
<StyledChip onClick={handleAdd}
icon={<Add />}
label='Add tag'
size='small'
onClick={() => setManageTagsOpen(true)}
/>
}
/> />
) : null}
</StyledTagContainer> </StyledTagContainer>
</StyledTagRow> </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)