mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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