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:
parent
b15502ec5e
commit
2ede2a6578
@ -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={
|
||||||
|
@ -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
|
||||||
|
@ -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 { 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}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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**.
|
||||||
|
|
||||||

|

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