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:
parent
b15502ec5e
commit
2ede2a6578
@ -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={
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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 () => {
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -119,7 +119,7 @@ export const OldDependencyRow: FC<{ feature: IFeatureToggle }> = ({
|
||||
marginBottom: theme.spacing(0.4),
|
||||
})}
|
||||
>
|
||||
Add parent feature
|
||||
Add parent flag
|
||||
</PermissionButton>
|
||||
</StyledDetail>
|
||||
</FlexRow>
|
||||
|
@ -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);
|
||||
|
@ -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**.
|
||||
|
||||

|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user