mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Personal flags UI component (#8221)
This commit is contained in:
		
							parent
							
								
									4e8d9a2319
								
							
						
					
					
						commit
						fee2143edf
					
				| @ -22,6 +22,8 @@ import { WelcomeDialog } from './WelcomeDialog'; | ||||
| import { useLocalStorageState } from 'hooks/useLocalStorageState'; | ||||
| import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; | ||||
| import { ProjectSetupComplete } from './ProjectSetupComplete'; | ||||
| import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard'; | ||||
| import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | ||||
| 
 | ||||
| const ScreenExplanation = styled(Typography)(({ theme }) => ({ | ||||
|     marginTop: theme.spacing(1), | ||||
| @ -136,6 +138,38 @@ const useProjects = () => { | ||||
|     return { projects, activeProject, setActiveProject }; | ||||
| }; | ||||
| 
 | ||||
| const FlagListItem: FC<{ | ||||
|     flag: { name: string; project: string; type: string }; | ||||
|     selected: boolean; | ||||
|     onClick: () => void; | ||||
| }> = ({ flag, selected, onClick }) => { | ||||
|     const IconComponent = getFeatureTypeIcons(flag.type); | ||||
|     return ( | ||||
|         <ListItem key={flag.name} disablePadding={true} sx={{ mb: 1 }}> | ||||
|             <ListItemButton | ||||
|                 sx={projectStyle} | ||||
|                 selected={selected} | ||||
|                 onClick={onClick} | ||||
|             > | ||||
|                 <ProjectBox> | ||||
|                     <IconComponent color='primary' /> | ||||
|                     <StyledCardTitle>{flag.name}</StyledCardTitle> | ||||
|                     <IconButton | ||||
|                         component={Link} | ||||
|                         href={`projects/${flag.project}/features/${flag.name}`} | ||||
|                         size='small' | ||||
|                         sx={{ ml: 'auto' }} | ||||
|                     > | ||||
|                         <LinkIcon | ||||
|                             titleAccess={`projects/${flag.project}/features/${flag.name}`} | ||||
|                         /> | ||||
|                     </IconButton> | ||||
|                 </ProjectBox> | ||||
|             </ListItemButton> | ||||
|         </ListItem> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| export const PersonalDashboard = () => { | ||||
|     const { user } = useAuthUser(); | ||||
| 
 | ||||
| @ -143,6 +177,14 @@ export const PersonalDashboard = () => { | ||||
| 
 | ||||
|     const { projects, activeProject, setActiveProject } = useProjects(); | ||||
| 
 | ||||
|     const { personalDashboard } = usePersonalDashboard(); | ||||
|     const [activeFlag, setActiveFlag] = useState<string | null>(null); | ||||
|     useEffect(() => { | ||||
|         if (personalDashboard?.flags.length) { | ||||
|             setActiveFlag(personalDashboard.flags[0].name); | ||||
|         } | ||||
|     }, [JSON.stringify(personalDashboard)]); | ||||
| 
 | ||||
|     const { project: activeProjectOverview, loading } = | ||||
|         useProjectOverview(activeProject); | ||||
| 
 | ||||
| @ -256,11 +298,28 @@ export const PersonalDashboard = () => { | ||||
|                 </SpacedGridItem> | ||||
|                 <SpacedGridItem item lg={8} md={1} /> | ||||
|                 <SpacedGridItem item lg={4} md={1}> | ||||
|                     <Typography> | ||||
|                         You have not created or favorited any feature flags. | ||||
|                         Once you do, the will show up here. | ||||
|                     </Typography> | ||||
|                     {personalDashboard && personalDashboard.flags.length > 0 ? ( | ||||
|                         <List | ||||
|                             disablePadding={true} | ||||
|                             sx={{ maxHeight: '400px', overflow: 'auto' }} | ||||
|                         > | ||||
|                             {personalDashboard.flags.map((flag) => ( | ||||
|                                 <FlagListItem | ||||
|                                     key={flag.name} | ||||
|                                     flag={flag} | ||||
|                                     selected={flag.name === activeFlag} | ||||
|                                     onClick={() => setActiveFlag(flag.name)} | ||||
|                                 /> | ||||
|                             ))} | ||||
|                         </List> | ||||
|                     ) : ( | ||||
|                         <Typography> | ||||
|                             You have not created or favorited any feature flags. | ||||
|                             Once you do, they will show up here. | ||||
|                         </Typography> | ||||
|                     )} | ||||
|                 </SpacedGridItem> | ||||
| 
 | ||||
|                 <SpacedGridItem item lg={8} md={1}> | ||||
|                     <Typography sx={{ mb: 4 }}>Feature flag metrics</Typography> | ||||
|                     <PlaceholderFlagMetricsChart /> | ||||
|  | ||||
| @ -0,0 +1,31 @@ | ||||
| import useSWR from 'swr'; | ||||
| import { formatApiPath } from 'utils/formatPath'; | ||||
| import handleErrorResponses from '../httpErrorResponseHandler'; | ||||
| import type { PersonalDashboardSchema } from 'openapi'; | ||||
| 
 | ||||
| export interface IPersonalDashboardOutput { | ||||
|     personalDashboard?: PersonalDashboardSchema; | ||||
|     refetch: () => void; | ||||
|     loading: boolean; | ||||
|     error?: Error; | ||||
| } | ||||
| 
 | ||||
| export const usePersonalDashboard = (): IPersonalDashboardOutput => { | ||||
|     const { data, error, mutate } = useSWR( | ||||
|         formatApiPath('api/admin/personal-dashboard'), | ||||
|         fetcher, | ||||
|     ); | ||||
| 
 | ||||
|     return { | ||||
|         personalDashboard: data, | ||||
|         loading: !error && !data, | ||||
|         refetch: () => mutate(), | ||||
|         error, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const fetcher = (path: string) => { | ||||
|     return fetch(path) | ||||
|         .then(handleErrorResponses('Personal Dashboard')) | ||||
|         .then((res) => res.json()); | ||||
| }; | ||||
| @ -7,4 +7,6 @@ | ||||
| export type PersonalDashboardSchemaFlagsItem = { | ||||
|     /** The name of the flag */ | ||||
|     name: string; | ||||
|     project: string; | ||||
|     type: string; | ||||
| }; | ||||
|  | ||||
| @ -1,9 +1,12 @@ | ||||
| import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type'; | ||||
| import type { | ||||
|     IPersonalDashboardReadModel, | ||||
|     PersonalFeature, | ||||
| } from './personal-dashboard-read-model-type'; | ||||
| 
 | ||||
| export class FakePersonalDashboardReadModel | ||||
|     implements IPersonalDashboardReadModel | ||||
| { | ||||
|     async getPersonalFeatures(userId: number): Promise<{ name: string }[]> { | ||||
|     async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> { | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -55,9 +55,9 @@ test('should return personal dashboard with own flags and favorited flags', asyn | ||||
|     expect(body).toMatchObject({ | ||||
|         projects: [], | ||||
|         flags: [ | ||||
|             { name: 'my_feature_c' }, | ||||
|             { name: 'my_feature_d' }, | ||||
|             { name: 'other_feature_b' }, | ||||
|             { name: 'my_feature_d', type: 'release', project: 'default' }, | ||||
|             { name: 'my_feature_c', type: 'release', project: 'default' }, | ||||
|             { name: 'other_feature_b', type: 'release', project: 'default' }, | ||||
|         ], | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @ -1,3 +1,5 @@ | ||||
| export type PersonalFeature = { name: string; type: string; project: string }; | ||||
| 
 | ||||
| export interface IPersonalDashboardReadModel { | ||||
|     getPersonalFeatures(userId: number): Promise<{ name: string }[]>; | ||||
|     getPersonalFeatures(userId: number): Promise<PersonalFeature[]>; | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,8 @@ | ||||
| import type { Db } from '../../db/db'; | ||||
| import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type'; | ||||
| import type { | ||||
|     IPersonalDashboardReadModel, | ||||
|     PersonalFeature, | ||||
| } from './personal-dashboard-read-model-type'; | ||||
| 
 | ||||
| export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { | ||||
|     private db: Db; | ||||
| @ -8,15 +11,34 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { | ||||
|         this.db = db; | ||||
|     } | ||||
| 
 | ||||
|     getPersonalFeatures(userId: number): Promise<{ name: string }[]> { | ||||
|         return this.db<{ name: string }>('favorite_features') | ||||
|     async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> { | ||||
|         const result = await this.db<{ | ||||
|             name: string; | ||||
|             type: string; | ||||
|             project: string; | ||||
|         }>('favorite_features') | ||||
|             .join('features', 'favorite_features.feature', 'features.name') | ||||
|             .where('favorite_features.user_id', userId) | ||||
|             .select('feature as name') | ||||
|             .whereNull('features.archived_at') | ||||
|             .select( | ||||
|                 'features.name as name', | ||||
|                 'features.type', | ||||
|                 'features.project', | ||||
|                 'features.created_at', | ||||
|             ) | ||||
|             .union(function () { | ||||
|                 this.select('name') | ||||
|                 this.select('name', 'type', 'project', 'created_at') | ||||
|                     .from('features') | ||||
|                     .where('features.created_by_user_id', userId); | ||||
|                     .where('features.created_by_user_id', userId) | ||||
|                     .whereNull('features.archived_at'); | ||||
|             }) | ||||
|             .orderBy('name', 'asc'); | ||||
|             .orderBy('created_at', 'desc') | ||||
|             .limit(100); | ||||
| 
 | ||||
|         return result.map((row) => ({ | ||||
|             name: row.name, | ||||
|             type: row.type, | ||||
|             project: row.project, | ||||
|         })); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,7 @@ | ||||
| import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type'; | ||||
| import type { | ||||
|     IPersonalDashboardReadModel, | ||||
|     PersonalFeature, | ||||
| } from './personal-dashboard-read-model-type'; | ||||
| 
 | ||||
| export class PersonalDashboardService { | ||||
|     private personalDashboardReadModel: IPersonalDashboardReadModel; | ||||
| @ -7,7 +10,7 @@ export class PersonalDashboardService { | ||||
|         this.personalDashboardReadModel = personalDashboardReadModel; | ||||
|     } | ||||
| 
 | ||||
|     getPersonalFeatures(userId: number): Promise<{ name: string }[]> { | ||||
|     getPersonalFeatures(userId: number): Promise<PersonalFeature[]> { | ||||
|         return this.personalDashboardReadModel.getPersonalFeatures(userId); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -36,6 +36,16 @@ export const personalDashboardSchema = { | ||||
|                         example: 'my-flag', | ||||
|                         description: 'The name of the flag', | ||||
|                     }, | ||||
|                     project: { | ||||
|                         type: 'string', | ||||
|                         example: 'my-project-id', | ||||
|                         description: 'The id of the feature project', | ||||
|                     }, | ||||
|                     type: { | ||||
|                         type: 'string', | ||||
|                         example: 'release', | ||||
|                         description: 'The type of the feature flag', | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|             description: 'A list of flags a user created or favorited', | ||||
|  | ||||
| @ -54,6 +54,7 @@ process.nextTick(async () => { | ||||
|                         addonUsageMetrics: true, | ||||
|                         onboardingMetrics: true, | ||||
|                         onboardingUI: true, | ||||
|                         personalDashboardUI: true, | ||||
|                     }, | ||||
|                 }, | ||||
|                 authentication: { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user