diff --git a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx index 2f726d6c2772..2cbe7ffc29d2 100644 --- a/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx +++ b/frontend/src/component/feature/Dependencies/AddDependencyDialogue.tsx @@ -90,7 +90,7 @@ export const AddDependencyDialogue = ({ return ( ({ @@ -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 ? ( - - ) : ( - - ); return (
- - {FeatureOverviewSidePanel} -
- - + + - } - elseShow={} - /> + + ) : ( + <> + + + + )} + + + {flagOverviewRedesign ? ( + + ) : ( + + )} ({ + 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 = ({ project, onClick }) => ( + } + > + Add tag + +); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/Collaborators.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/Collaborators.tsx new file mode 100644 index 000000000000..70090b93d2a1 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/Collaborators.tsx @@ -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 = ({ collaborators }) => { + if (!collaborators || collaborators.length === 0) { + return null; + } + + return ( + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyRow.tsx index cb872ba5059f..29bc41e8d0db 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyRow.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyRow.tsx @@ -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 ( <> - - - Dependency: - + {canAddParentDependency ? ( + + + Dependency: + +
{ setShowDependencyDialogue(true); }} > - Add parent feature + Add parent flag - - } - /> - - - Dependency: - - - - {feature.dependencies[0]?.feature} - - - setShowDependencyDialogue(true) - } - onDelete={deleteDependency} - /> - } +
+
+ ) : null} + {hasParentDependency ? ( + + + Dependency: + + + + {feature.dependencies[0]?.feature} + + {checkAccess(UPDATE_FEATURE_DEPENDENCY, environment) ? ( + setShowDependencyDialogue(true)} + onDelete={deleteDependency} /> - - - } - /> - - - Dependency value: - - disabled - - } - /> - - - Dependency value: - - - - } - /> - - - Children: - - - - } - /> - - + + ) : null} + {hasParentDependency && !feature.dependencies[0]?.enabled ? ( + + + Dependency value: + + disabled + + ) : null} + {hasParentDependency && + Boolean(feature.dependencies[0]?.variants?.length) ? ( + + + Dependency value: + + + + ) : null} + {hasChildren ? ( + + Children: + setShowDependencyDialogue(false)} - showDependencyDialogue={showDependencyDialogue} /> - } - /> + + ) : null} + {feature.project ? ( + setShowDependencyDialogue(false)} + showDependencyDialogue={showDependencyDialogue} + /> + ) : null} ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.test.tsx index de8bf01633de..3acbd2d4df64 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.test.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.test.tsx @@ -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 () => { diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx index ba8c1656391b..f2e407595124 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx @@ -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 ( <> - - -

{capitalize(type || '')} flag

-
- + Flag details + {description ? ( {description} - } - /> + ) : null} + - Project: + Flag type: - {project} + {capitalizeFirst(type || ' ')} flag - - - Lifecycle: - - setArchiveDialogOpen(true)} - onComplete={() => - setMarkCompletedDialogueOpen(true) - } - onUncomplete={refetchFeature} - /> - - } - /> + {feature.lifecycle ? ( + + + Lifecycle: + + setArchiveDialogOpen(true)} + onComplete={() => + setMarkCompletedDialogueOpen(true) + } + onUncomplete={refetchFeature} + /> + + ) : null} - Created at: + Created: {formatDateYMD( @@ -154,65 +127,64 @@ const FeatureOverviewMetaData = () => { )} - ( - - - Created by: - - - - {feature.createdBy?.name} - - - - - )} - /> - } - /> + {feature.createdBy ? ( + + + Created by: + + + + {feature.createdBy?.name} + + + + ) : null} + {feature.collaborators?.users && + feature.collaborators?.users.length > 0 ? ( + + + Collaborators: + + + + + + ) : null} + {showDependentFeatures ? ( + + ) : null}
- 0} - show={ - setArchiveDialogOpen(false)} - /> - } - elseShow={ - { - navigate(`/projects/${projectId}`); - }} - onClose={() => setArchiveDialogOpen(false)} - projectId={projectId} - featureIds={[featureId]} - /> - } - /> - - } - /> + {feature.children.length > 0 ? ( + setArchiveDialogOpen(false)} + /> + ) : ( + { + navigate(`/projects/${projectId}`); + }} + onClose={() => setArchiveDialogOpen(false)} + projectId={projectId} + featureIds={[featureId]} + /> + )} + {feature.project ? ( + + ) : null} ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyRow.tsx index 556f6b555c22..6953e163cede 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyRow.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyRow.tsx @@ -119,7 +119,7 @@ export const OldDependencyRow: FC<{ feature: IFeatureToggle }> = ({ marginBottom: theme.spacing(0.4), })} > - Add parent feature + Add parent flag diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx index cd5d942c36fe..0f77b047edce 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx @@ -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 ( <> - - Tags: - { - setManageTagsOpen(true); - }} - > - Add tag - - - } - elseShow={ - - Tags: - - {tags.map((tag) => { - const tagLabel = `${tag.type}:${tag.value}`; - return ( - 35 ? tagLabel : '' - } - arrow - > - - - + {!tags.length ? ( + + Tags: + + + + + ) : ( + + Tags: + + {tags.map((tag) => { + const tagLabel = `${tag.type}:${tag.value}`; + const isOverflowing = tagLabel.length > 25; + return ( + { - setRemoveTagOpen( - true, - ); - setSelectedTag(tag); - } - : undefined - } - /> - - ); - })} - } - label='Add tag' - size='small' - onClick={() => setManageTagsOpen(true)} - /> - } + arrow + > + + {tagLabel.substring(0, 25)} + {isOverflowing ? ( + + … + + ) : ( + '' + )} + + + } + size='small' + deleteIcon={ + + + + } + onDelete={ + canUpdateTags + ? () => { + setRemoveTagOpen(true); + setSelectedTag(tag); + } + : undefined + } + /> + ); + })} + {canUpdateTags ? ( + - - - } - /> + ) : null} + + + )} { onClose={() => { setRemoveTagOpen(false); setSelectedTag(undefined); + refetch(); }} onClick={() => { setRemoveTagOpen(false); diff --git a/website/docs/reference/feature-toggles.mdx b/website/docs/reference/feature-toggles.mdx index 9936718a4b99..9268ee68bc6b 100644 --- a/website/docs/reference/feature-toggles.mdx +++ b/website/docs/reference/feature-toggles.mdx @@ -140,7 +140,7 @@ A child feature flag is evaluated only when both the child and its parent featur - Parent feature is disabled: Useful when the parent acts as a kill switch with inverted enabled/disabled logic. - Parent feature is enabled with variants: Useful for A/B testing scenarios where you need specific variant dependencies. -To add a dependency, you need the `update-feature-dependency` project permission. In the Admin UI, go to the feature flag you want to add a parent to and select **Add parent feature**. +To add a dependency, you need the `update-feature-dependency` project permission. In the Admin UI, go to the feature flag you want to add a parent to and select **Add parent flag**. ![Feature parent flag](/img/add-parent-flag.png)