mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: flag exposure in personal dashboard (#8247)
This commit is contained in:
		
							parent
							
								
									289324fd02
								
							
						
					
					
						commit
						a1a24ea0b1
					
				| @ -43,6 +43,7 @@ export const FeatureLifecycle: FC<{ | |||||||
|     return currentStage ? ( |     return currentStage ? ( | ||||||
|         <FeatureLifecycleTooltip |         <FeatureLifecycleTooltip | ||||||
|             stage={currentStage!} |             stage={currentStage!} | ||||||
|  |             project={feature.project} | ||||||
|             onArchive={onArchive} |             onArchive={onArchive} | ||||||
|             onComplete={onComplete} |             onComplete={onComplete} | ||||||
|             onUncomplete={onUncompleteHandler} |             onUncomplete={onUncompleteHandler} | ||||||
|  | |||||||
| @ -8,7 +8,6 @@ import { | |||||||
|     DELETE_FEATURE, |     DELETE_FEATURE, | ||||||
|     UPDATE_FEATURE, |     UPDATE_FEATURE, | ||||||
| } from 'component/providers/AccessProvider/permissions'; | } from 'component/providers/AccessProvider/permissions'; | ||||||
| import { Route, Routes } from 'react-router-dom'; |  | ||||||
| 
 | 
 | ||||||
| const currentTime = '2024-04-25T08:05:00.000Z'; | const currentTime = '2024-04-25T08:05:00.000Z'; | ||||||
| const twoMinutesAgo = '2024-04-25T08:03:00.000Z'; | const twoMinutesAgo = '2024-04-25T08:03:00.000Z'; | ||||||
| @ -23,24 +22,17 @@ const renderOpenTooltip = ( | |||||||
|     loading = false, |     loading = false, | ||||||
| ) => { | ) => { | ||||||
|     render( |     render( | ||||||
|         <Routes> |         <FeatureLifecycleTooltip | ||||||
|             <Route |             stage={stage} | ||||||
|                 path={'/projects/:projectId'} |             onArchive={onArchive} | ||||||
|                 element={ |             onComplete={onComplete} | ||||||
|                     <FeatureLifecycleTooltip |             onUncomplete={onUncomplete} | ||||||
|                         stage={stage} |             loading={loading} | ||||||
|                         onArchive={onArchive} |             project={'default'} | ||||||
|                         onComplete={onComplete} |         > | ||||||
|                         onUncomplete={onUncomplete} |             <span>child</span> | ||||||
|                         loading={loading} |         </FeatureLifecycleTooltip>, | ||||||
|                     > |  | ||||||
|                         <span>child</span> |  | ||||||
|                     </FeatureLifecycleTooltip> |  | ||||||
|                 } |  | ||||||
|             /> |  | ||||||
|         </Routes>, |  | ||||||
|         { |         { | ||||||
|             route: '/projects/default', |  | ||||||
|             permissions: [ |             permissions: [ | ||||||
|                 { permission: DELETE_FEATURE }, |                 { permission: DELETE_FEATURE }, | ||||||
|                 { permission: UPDATE_FEATURE }, |                 { permission: UPDATE_FEATURE }, | ||||||
|  | |||||||
| @ -25,7 +25,6 @@ import { isSafeToArchive } from './isSafeToArchive'; | |||||||
| import { useLocationSettings } from 'hooks/useLocationSettings'; | import { useLocationSettings } from 'hooks/useLocationSettings'; | ||||||
| import { formatDateYMDHMS } from 'utils/formatDate'; | import { formatDateYMDHMS } from 'utils/formatDate'; | ||||||
| import { formatDistanceToNow, parseISO } from 'date-fns'; | import { formatDistanceToNow, parseISO } from 'date-fns'; | ||||||
| import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; |  | ||||||
| 
 | 
 | ||||||
| const TimeLabel = styled('span')(({ theme }) => ({ | const TimeLabel = styled('span')(({ theme }) => ({ | ||||||
|     color: theme.palette.text.secondary, |     color: theme.palette.text.secondary, | ||||||
| @ -96,7 +95,7 @@ const StageBox = styled(Box, { | |||||||
|         ...(active && { |         ...(active && { | ||||||
|             backgroundColor: theme.palette.primary.light, |             backgroundColor: theme.palette.primary.light, | ||||||
|             color: theme.palette.primary.contrastText, |             color: theme.palette.primary.contrastText, | ||||||
|             fontWeight: theme.fontWeight.bold, |             fontWeight: theme.typography.fontWeightBold, | ||||||
|             borderRadius: theme.spacing(0.5), |             borderRadius: theme.spacing(0.5), | ||||||
|         }), |         }), | ||||||
|     }, |     }, | ||||||
| @ -247,17 +246,16 @@ const PreLiveStageDescription: FC<{ children?: React.ReactNode }> = ({ | |||||||
| const BoldTitle = styled(Typography)(({ theme }) => ({ | const BoldTitle = styled(Typography)(({ theme }) => ({ | ||||||
|     marginTop: theme.spacing(1), |     marginTop: theme.spacing(1), | ||||||
|     marginBottom: theme.spacing(1), |     marginBottom: theme.spacing(1), | ||||||
|     fontSize: theme.fontSizes.smallBody, |     fontSize: theme.typography.body2.fontSize, | ||||||
|     fontWeight: theme.fontWeight.bold, |     fontWeight: theme.typography.fontWeightBold, | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const LiveStageDescription: FC<{ | const LiveStageDescription: FC<{ | ||||||
|     onComplete: () => void; |     onComplete: () => void; | ||||||
|     loading: boolean; |     loading: boolean; | ||||||
|     children?: React.ReactNode; |     children?: React.ReactNode; | ||||||
| }> = ({ children, onComplete, loading }) => { |     project: string; | ||||||
|     const projectId = useRequiredPathParam('projectId'); | }> = ({ children, onComplete, loading, project }) => { | ||||||
| 
 |  | ||||||
|     return ( |     return ( | ||||||
|         <> |         <> | ||||||
|             <BoldTitle>Is this feature complete?</BoldTitle> |             <BoldTitle>Is this feature complete?</BoldTitle> | ||||||
| @ -276,7 +274,7 @@ const LiveStageDescription: FC<{ | |||||||
|                 size='small' |                 size='small' | ||||||
|                 onClick={onComplete} |                 onClick={onComplete} | ||||||
|                 disabled={loading} |                 disabled={loading} | ||||||
|                 projectId={projectId} |                 projectId={project} | ||||||
|             > |             > | ||||||
|                 Mark completed |                 Mark completed | ||||||
|             </PermissionButton> |             </PermissionButton> | ||||||
| @ -294,9 +292,8 @@ const SafeToArchive: FC<{ | |||||||
|     onArchive: () => void; |     onArchive: () => void; | ||||||
|     onUncomplete: () => void; |     onUncomplete: () => void; | ||||||
|     loading: boolean; |     loading: boolean; | ||||||
| }> = ({ onArchive, onUncomplete, loading }) => { |     project: string; | ||||||
|     const projectId = useRequiredPathParam('projectId'); | }> = ({ onArchive, onUncomplete, loading, project }) => { | ||||||
| 
 |  | ||||||
|     return ( |     return ( | ||||||
|         <> |         <> | ||||||
|             <BoldTitle>Safe to archive</BoldTitle> |             <BoldTitle>Safe to archive</BoldTitle> | ||||||
| @ -324,7 +321,7 @@ const SafeToArchive: FC<{ | |||||||
|                     size='small' |                     size='small' | ||||||
|                     onClick={onUncomplete} |                     onClick={onUncomplete} | ||||||
|                     disabled={loading} |                     disabled={loading} | ||||||
|                     projectId={projectId} |                     projectId={project} | ||||||
|                 > |                 > | ||||||
|                     Revert to live |                     Revert to live | ||||||
|                 </PermissionButton> |                 </PermissionButton> | ||||||
| @ -335,7 +332,7 @@ const SafeToArchive: FC<{ | |||||||
|                     size='small' |                     size='small' | ||||||
|                     sx={{ mb: 2 }} |                     sx={{ mb: 2 }} | ||||||
|                     onClick={onArchive} |                     onClick={onArchive} | ||||||
|                     projectId={projectId} |                     projectId={project} | ||||||
|                 > |                 > | ||||||
|                     Archive feature |                     Archive feature | ||||||
|                 </PermissionButton> |                 </PermissionButton> | ||||||
| @ -393,7 +390,15 @@ const CompletedStageDescription: FC<{ | |||||||
|         lastSeenAt: string; |         lastSeenAt: string; | ||||||
|     }>; |     }>; | ||||||
|     children?: React.ReactNode; |     children?: React.ReactNode; | ||||||
| }> = ({ children, environments, onArchive, onUncomplete, loading }) => { |     project: string; | ||||||
|  | }> = ({ | ||||||
|  |     children, | ||||||
|  |     environments, | ||||||
|  |     onArchive, | ||||||
|  |     onUncomplete, | ||||||
|  |     loading, | ||||||
|  |     project, | ||||||
|  | }) => { | ||||||
|     return ( |     return ( | ||||||
|         <ConditionallyRender |         <ConditionallyRender | ||||||
|             condition={isSafeToArchive(environments)} |             condition={isSafeToArchive(environments)} | ||||||
| @ -402,6 +407,7 @@ const CompletedStageDescription: FC<{ | |||||||
|                     onArchive={onArchive} |                     onArchive={onArchive} | ||||||
|                     onUncomplete={onUncomplete} |                     onUncomplete={onUncomplete} | ||||||
|                     loading={loading} |                     loading={loading} | ||||||
|  |                     project={project} | ||||||
|                 /> |                 /> | ||||||
|             } |             } | ||||||
|             elseShow={ |             elseShow={ | ||||||
| @ -432,11 +438,20 @@ const FormatElapsedTime: FC<{ | |||||||
| export const FeatureLifecycleTooltip: FC<{ | export const FeatureLifecycleTooltip: FC<{ | ||||||
|     children: React.ReactElement<any, any>; |     children: React.ReactElement<any, any>; | ||||||
|     stage: LifecycleStage; |     stage: LifecycleStage; | ||||||
|  |     project: string; | ||||||
|     onArchive: () => void; |     onArchive: () => void; | ||||||
|     onComplete: () => void; |     onComplete: () => void; | ||||||
|     onUncomplete: () => void; |     onUncomplete: () => void; | ||||||
|     loading: boolean; |     loading: boolean; | ||||||
| }> = ({ children, stage, onArchive, onComplete, onUncomplete, loading }) => ( | }> = ({ | ||||||
|  |     children, | ||||||
|  |     stage, | ||||||
|  |     project, | ||||||
|  |     onArchive, | ||||||
|  |     onComplete, | ||||||
|  |     onUncomplete, | ||||||
|  |     loading, | ||||||
|  | }) => ( | ||||||
|     <HtmlTooltip |     <HtmlTooltip | ||||||
|         maxHeight={800} |         maxHeight={800} | ||||||
|         maxWidth={350} |         maxWidth={350} | ||||||
| @ -482,6 +497,7 @@ export const FeatureLifecycleTooltip: FC<{ | |||||||
|                         <LiveStageDescription |                         <LiveStageDescription | ||||||
|                             onComplete={onComplete} |                             onComplete={onComplete} | ||||||
|                             loading={loading} |                             loading={loading} | ||||||
|  |                             project={project} | ||||||
|                         > |                         > | ||||||
|                             <Environments environments={stage.environments} /> |                             <Environments environments={stage.environments} /> | ||||||
|                         </LiveStageDescription> |                         </LiveStageDescription> | ||||||
| @ -492,6 +508,7 @@ export const FeatureLifecycleTooltip: FC<{ | |||||||
|                             onArchive={onArchive} |                             onArchive={onArchive} | ||||||
|                             onUncomplete={onUncomplete} |                             onUncomplete={onUncomplete} | ||||||
|                             loading={loading} |                             loading={loading} | ||||||
|  |                             project={project} | ||||||
|                         > |                         > | ||||||
|                             <Environments environments={stage.environments} /> |                             <Environments environments={stage.environments} /> | ||||||
|                         </CompletedStageDescription> |                         </CompletedStageDescription> | ||||||
|  | |||||||
| @ -0,0 +1,72 @@ | |||||||
|  | import { type FC, useState } from 'react'; | ||||||
|  | import { useNavigate } from 'react-router-dom'; | ||||||
|  | import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||||
|  | import type { ILastSeenEnvironments } from 'interfaces/featureToggle'; | ||||||
|  | import { Box } from '@mui/material'; | ||||||
|  | import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen'; | ||||||
|  | import { FeatureLifecycle } from './FeatureLifecycle'; | ||||||
|  | import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog'; | ||||||
|  | import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; | ||||||
|  | import { MarkCompletedDialogue } from './MarkCompletedDialogue'; | ||||||
|  | 
 | ||||||
|  | export const FlagExposure: FC<{ | ||||||
|  |     project: string; | ||||||
|  |     flagName: string; | ||||||
|  |     onArchive: () => void; | ||||||
|  | }> = ({ project, flagName, onArchive }) => { | ||||||
|  |     const navigate = useNavigate(); | ||||||
|  |     const { feature, refetchFeature } = useFeature(project, flagName); | ||||||
|  |     const lastSeenEnvironments: ILastSeenEnvironments[] = | ||||||
|  |         feature.environments?.map((env) => ({ | ||||||
|  |             name: env.name, | ||||||
|  |             lastSeenAt: env.lastSeenAt, | ||||||
|  |             enabled: env.enabled, | ||||||
|  |             yes: env.yes, | ||||||
|  |             no: env.no, | ||||||
|  |         })); | ||||||
|  |     const [showDelDialog, setShowDelDialog] = useState(false); | ||||||
|  |     const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] = | ||||||
|  |         useState(false); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <Box sx={{ display: 'flex' }}> | ||||||
|  |             <FeatureEnvironmentSeen | ||||||
|  |                 featureLastSeen={feature.lastSeenAt} | ||||||
|  |                 environments={lastSeenEnvironments} | ||||||
|  |             /> | ||||||
|  |             <FeatureLifecycle | ||||||
|  |                 feature={feature} | ||||||
|  |                 onArchive={() => setShowDelDialog(true)} | ||||||
|  |                 onComplete={() => setShowMarkCompletedDialogue(true)} | ||||||
|  |                 onUncomplete={refetchFeature} | ||||||
|  |             /> | ||||||
|  | 
 | ||||||
|  |             {feature.children.length > 0 ? ( | ||||||
|  |                 <FeatureArchiveNotAllowedDialog | ||||||
|  |                     features={feature.children} | ||||||
|  |                     project={project} | ||||||
|  |                     isOpen={showDelDialog} | ||||||
|  |                     onClose={() => setShowDelDialog(false)} | ||||||
|  |                 /> | ||||||
|  |             ) : ( | ||||||
|  |                 <FeatureArchiveDialog | ||||||
|  |                     isOpen={showDelDialog} | ||||||
|  |                     onConfirm={onArchive} | ||||||
|  |                     onClose={() => setShowDelDialog(false)} | ||||||
|  |                     projectId={project} | ||||||
|  |                     featureIds={[flagName]} | ||||||
|  |                 /> | ||||||
|  |             )} | ||||||
|  | 
 | ||||||
|  |             {feature.project ? ( | ||||||
|  |                 <MarkCompletedDialogue | ||||||
|  |                     isOpen={showMarkCompletedDialogue} | ||||||
|  |                     setIsOpen={setShowMarkCompletedDialogue} | ||||||
|  |                     projectId={feature.project} | ||||||
|  |                     featureId={feature.name} | ||||||
|  |                     onComplete={refetchFeature} | ||||||
|  |                 /> | ||||||
|  |             ) : null} | ||||||
|  |         </Box> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -24,6 +24,7 @@ import { ProjectSetupComplete } from './ProjectSetupComplete'; | |||||||
| import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard'; | import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard'; | ||||||
| import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | ||||||
| import type { PersonalDashboardSchema } from '../../openapi'; | import type { PersonalDashboardSchema } from '../../openapi'; | ||||||
|  | import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure'; | ||||||
| 
 | 
 | ||||||
| const ScreenExplanation = styled(Typography)(({ theme }) => ({ | const ScreenExplanation = styled(Typography)(({ theme }) => ({ | ||||||
|     marginTop: theme.spacing(1), |     marginTop: theme.spacing(1), | ||||||
| @ -177,7 +178,8 @@ export const PersonalDashboard = () => { | |||||||
| 
 | 
 | ||||||
|     const { projects, activeProject, setActiveProject } = useProjects(); |     const { projects, activeProject, setActiveProject } = useProjects(); | ||||||
| 
 | 
 | ||||||
|     const { personalDashboard } = usePersonalDashboard(); |     const { personalDashboard, refetch: refetchDashboard } = | ||||||
|  |         usePersonalDashboard(); | ||||||
|     const [activeFlag, setActiveFlag] = useState< |     const [activeFlag, setActiveFlag] = useState< | ||||||
|         PersonalDashboardSchema['flags'][0] | null |         PersonalDashboardSchema['flags'][0] | null | ||||||
|     >(null); |     >(null); | ||||||
| @ -298,7 +300,20 @@ export const PersonalDashboard = () => { | |||||||
|                 <SpacedGridItem item lg={4} md={1}> |                 <SpacedGridItem item lg={4} md={1}> | ||||||
|                     <Typography variant='h3'>My feature flags</Typography> |                     <Typography variant='h3'>My feature flags</Typography> | ||||||
|                 </SpacedGridItem> |                 </SpacedGridItem> | ||||||
|                 <SpacedGridItem item lg={8} md={1} /> |                 <SpacedGridItem | ||||||
|  |                     item | ||||||
|  |                     lg={8} | ||||||
|  |                     md={1} | ||||||
|  |                     sx={{ display: 'flex', justifyContent: 'flex-end' }} | ||||||
|  |                 > | ||||||
|  |                     {activeFlag ? ( | ||||||
|  |                         <FlagExposure | ||||||
|  |                             project={activeFlag.project} | ||||||
|  |                             flagName={activeFlag.name} | ||||||
|  |                             onArchive={refetchDashboard} | ||||||
|  |                         /> | ||||||
|  |                     ) : null} | ||||||
|  |                 </SpacedGridItem> | ||||||
|                 <SpacedGridItem item lg={4} md={1}> |                 <SpacedGridItem item lg={4} md={1}> | ||||||
|                     {personalDashboard && personalDashboard.flags.length > 0 ? ( |                     {personalDashboard && personalDashboard.flags.length > 0 ? ( | ||||||
|                         <List |                         <List | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user