mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: welcome to project onboarding status rendering (#8076)

This commit is contained in:
		
							parent
							
								
									896707f7d5
								
							
						
					
					
						commit
						f41a688edb
					
				| @ -28,6 +28,7 @@ const setupApi = () => { | ||||
|     testServerRoute(server, '/api/admin/ui-config', { | ||||
|         flags: { | ||||
|             flagCreator: true, | ||||
|             onboardingUI: true, | ||||
|         }, | ||||
|     }); | ||||
|     testServerRoute(server, '/api/admin/tags', { | ||||
| @ -162,3 +163,49 @@ test.skip('filters by flag author', async () => { | ||||
| 
 | ||||
|     expect(window.location.href).toContain('createdBy=IS%3A1'); | ||||
| }); | ||||
| 
 | ||||
| test('Project is onboarded', async () => { | ||||
|     const projectId = 'default'; | ||||
|     setupApi(); | ||||
|     testServerRoute(server, '/api/admin/projects/default/overview', { | ||||
|         onboardingStatus: { | ||||
|             status: 'onboarded', | ||||
|         }, | ||||
|     }); | ||||
|     render( | ||||
|         <Routes> | ||||
|             <Route | ||||
|                 path={'/projects/:projectId'} | ||||
|                 element={<ProjectFeatureToggles environments={[]} />} | ||||
|             /> | ||||
|         </Routes>, | ||||
|         { | ||||
|             route: `/projects/${projectId}`, | ||||
|         }, | ||||
|     ); | ||||
|     expect( | ||||
|         screen.queryByText('Welcome to your project'), | ||||
|     ).not.toBeInTheDocument(); | ||||
| }); | ||||
| 
 | ||||
| test('Project is not onboarded', async () => { | ||||
|     const projectId = 'default'; | ||||
|     setupApi(); | ||||
|     testServerRoute(server, '/api/admin/projects/default/overview', { | ||||
|         onboardingStatus: { | ||||
|             status: 'onboarding-started', | ||||
|         }, | ||||
|     }); | ||||
|     render( | ||||
|         <Routes> | ||||
|             <Route | ||||
|                 path={'/projects/:projectId'} | ||||
|                 element={<ProjectFeatureToggles environments={[]} />} | ||||
|             /> | ||||
|         </Routes>, | ||||
|         { | ||||
|             route: `/projects/${projectId}`, | ||||
|         }, | ||||
|     ); | ||||
|     await screen.findByText('Welcome to your project'); | ||||
| }); | ||||
|  | ||||
| @ -42,6 +42,7 @@ import { AvatarCell } from './AvatarCell'; | ||||
| import { ProjectOnboarding } from './ProjectOnboarding/ProjectOnboarding'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { styled } from '@mui/material'; | ||||
| import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; | ||||
| import { ConnectSdkDialog } from '../../../onboarding/ConnectSdkDialog'; | ||||
| 
 | ||||
| interface IPaginatedProjectFeatureTogglesProps { | ||||
| @ -65,6 +66,7 @@ export const ProjectFeatureToggles = ({ | ||||
| }: IPaginatedProjectFeatureTogglesProps) => { | ||||
|     const onboardingUIEnabled = useUiFlag('onboardingUI'); | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { project } = useProjectOverview(projectId); | ||||
| 
 | ||||
|     const { | ||||
|         features, | ||||
| @ -396,7 +398,10 @@ export const ProjectFeatureToggles = ({ | ||||
|     return ( | ||||
|         <Container> | ||||
|             <ConditionallyRender | ||||
|                 condition={onboardingUIEnabled} | ||||
|                 condition={ | ||||
|                     onboardingUIEnabled && | ||||
|                     project.onboardingStatus.status !== 'onboarded' | ||||
|                 } | ||||
|                 show={<ProjectOnboarding projectId={projectId} />} | ||||
|             /> | ||||
|             <PageContent | ||||
|  | ||||
| @ -0,0 +1,52 @@ | ||||
| import { render } from 'utils/testRenderer'; | ||||
| import { Route, Routes } from 'react-router-dom'; | ||||
| import { testServerRoute, testServerSetup } from 'utils/testServer'; | ||||
| import { WelcomeToProject } from './WelcomeToProject'; | ||||
| import { screen } from '@testing-library/react'; | ||||
| 
 | ||||
| const server = testServerSetup(); | ||||
| 
 | ||||
| test('Project can start onboarding', async () => { | ||||
|     const projectId = 'default'; | ||||
|     testServerRoute(server, '/api/admin/projects/default/overview', { | ||||
|         onboarding: { | ||||
|             onboardingStatus: 'onboarding-started', | ||||
|         }, | ||||
|     }); | ||||
|     render( | ||||
|         <Routes> | ||||
|             <Route | ||||
|                 path={'/projects/:projectId'} | ||||
|                 element={<WelcomeToProject projectId={projectId} />} | ||||
|             /> | ||||
|         </Routes>, | ||||
|         { | ||||
|             route: `/projects/${projectId}`, | ||||
|         }, | ||||
|     ); | ||||
|     await screen.findByText('The project currently holds no feature toggles.'); | ||||
| }); | ||||
| 
 | ||||
| test('Project can connect SDK', async () => { | ||||
|     const projectId = 'default'; | ||||
|     testServerRoute(server, '/api/admin/projects/default/overview', { | ||||
|         onboardingStatus: { | ||||
|             status: 'first-flag-created', | ||||
|             feature: 'default-feature', | ||||
|         }, | ||||
|     }); | ||||
|     render( | ||||
|         <Routes> | ||||
|             <Route | ||||
|                 path={'/projects/:projectId'} | ||||
|                 element={<WelcomeToProject projectId={projectId} />} | ||||
|             /> | ||||
|         </Routes>, | ||||
|         { | ||||
|             route: `/projects/${projectId}`, | ||||
|         }, | ||||
|     ); | ||||
|     await screen.findByText( | ||||
|         'Your project is not yet connected to any SDK. In order to start using your feature flag connect an SDK to the project.', | ||||
|     ); | ||||
| }); | ||||
| @ -1,13 +1,24 @@ | ||||
| import { styled, Typography } from '@mui/material'; | ||||
| import { styled, Typography, useTheme } from '@mui/material'; | ||||
| import Add from '@mui/icons-material/Add'; | ||||
| import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions'; | ||||
| import { FlagCreationButton } from '../ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; | ||||
| import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; | ||||
| import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; | ||||
| import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; | ||||
| import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; | ||||
| import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes'; | ||||
| 
 | ||||
| interface IWelcomeToProjectProps { | ||||
|     projectId: string; | ||||
| } | ||||
| 
 | ||||
| interface IExistingFlagsProps { | ||||
|     featureId: string; | ||||
|     projectId: string; | ||||
| } | ||||
| 
 | ||||
| const Container = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
| @ -44,7 +55,7 @@ const TitleContainer = styled('div')(({ theme }) => ({ | ||||
|     fontWeight: 'bold', | ||||
| })); | ||||
| 
 | ||||
| const CircleContainer = styled('span')(({ theme }) => ({ | ||||
| const NeutralCircleContainer = styled('span')(({ theme }) => ({ | ||||
|     width: '28px', | ||||
|     height: '28px', | ||||
|     display: 'flex', | ||||
| @ -54,7 +65,27 @@ const CircleContainer = styled('span')(({ theme }) => ({ | ||||
|     borderRadius: '50%', | ||||
| })); | ||||
| 
 | ||||
| const MainCircleContainer = styled(NeutralCircleContainer)(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.primary.main, | ||||
|     color: theme.palette.background.paper, | ||||
| })); | ||||
| 
 | ||||
| const TypeCircleContainer = styled(MainCircleContainer)(({ theme }) => ({ | ||||
|     borderRadius: '20%', | ||||
| })); | ||||
| 
 | ||||
| const StyledLink = styled(Link)({ | ||||
|     fontWeight: 'bold', | ||||
|     textDecoration: 'none', | ||||
|     display: 'flex', | ||||
|     justifyContent: 'center', | ||||
| }); | ||||
| 
 | ||||
| export const WelcomeToProject = ({ projectId }: IWelcomeToProjectProps) => { | ||||
|     const { project } = useProjectOverview(projectId); | ||||
|     const isFirstFlagCreated = | ||||
|         project.onboardingStatus.status === 'first-flag-created'; | ||||
| 
 | ||||
|     return ( | ||||
|         <Container> | ||||
|             <TitleBox> | ||||
| @ -67,21 +98,19 @@ export const WelcomeToProject = ({ projectId }: IWelcomeToProjectProps) => { | ||||
|             </TitleBox> | ||||
|             <Actions> | ||||
|                 <ActionBox> | ||||
|                     <TitleContainer> | ||||
|                         <CircleContainer>1</CircleContainer> | ||||
|                         Create a feature flag | ||||
|                     </TitleContainer> | ||||
|                     <Typography> | ||||
|                         <div> | ||||
|                             The project currently holds no feature toggles. | ||||
|                         </div> | ||||
|                         <div>Create a feature flag to get started.</div> | ||||
|                     </Typography> | ||||
|                     <FlagCreationButton /> | ||||
|                     {project.onboardingStatus.status === | ||||
|                     'first-flag-created' ? ( | ||||
|                         <ExistingFlag | ||||
|                             projectId={projectId} | ||||
|                             featureId={project.onboardingStatus.feature} | ||||
|                         /> | ||||
|                     ) : ( | ||||
|                         <CreateFlag /> | ||||
|                     )} | ||||
|                 </ActionBox> | ||||
|                 <ActionBox> | ||||
|                     <TitleContainer> | ||||
|                         <CircleContainer>2</CircleContainer> | ||||
|                         <NeutralCircleContainer>2</NeutralCircleContainer> | ||||
|                         Connect an SDK | ||||
|                     </TitleContainer> | ||||
|                     <Typography> | ||||
| @ -92,7 +121,7 @@ export const WelcomeToProject = ({ projectId }: IWelcomeToProjectProps) => { | ||||
|                         maxWidth='200px' | ||||
|                         projectId={projectId} | ||||
|                         Icon={Add} | ||||
|                         disabled={true} | ||||
|                         disabled={!isFirstFlagCreated} | ||||
|                         permission={CREATE_FEATURE} | ||||
|                     > | ||||
|                         Connect SDK | ||||
| @ -102,3 +131,55 @@ export const WelcomeToProject = ({ projectId }: IWelcomeToProjectProps) => { | ||||
|         </Container> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const CreateFlag = () => { | ||||
|     return ( | ||||
|         <> | ||||
|             <TitleContainer> | ||||
|                 <NeutralCircleContainer>1</NeutralCircleContainer> | ||||
|                 Create a feature flag | ||||
|             </TitleContainer> | ||||
|             <Typography> | ||||
|                 <div>The project currently holds no feature toggles.</div> | ||||
|                 <div>Create a feature flag to get started.</div> | ||||
|             </Typography> | ||||
|             <FlagCreationButton /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => { | ||||
|     const theme = useTheme(); | ||||
|     const { feature } = useFeature(projectId, featureId); | ||||
|     const { featureTypes } = useFeatureTypes(); | ||||
|     const IconComponent = getFeatureTypeIcons(feature.type); | ||||
|     const typeName = featureTypes.find( | ||||
|         (featureType) => featureType.id === feature.type, | ||||
|     )?.name; | ||||
|     const typeTitle = `${typeName || feature.type} flag`; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <TitleContainer> | ||||
|                 <MainCircleContainer>✓</MainCircleContainer> | ||||
|                 Create a feature flag | ||||
|             </TitleContainer> | ||||
|             <TitleContainer> | ||||
|                 <HtmlTooltip arrow title={typeTitle} describeChild> | ||||
|                     <TypeCircleContainer> | ||||
|                         <IconComponent /> | ||||
|                     </TypeCircleContainer> | ||||
|                 </HtmlTooltip> | ||||
|                 <StyledLink | ||||
|                     to={`/projects/${projectId}/features/${feature.name}`} | ||||
|                 > | ||||
|                     {feature.name} | ||||
|                 </StyledLink> | ||||
|             </TitleContainer> | ||||
|             <Typography> | ||||
|                 Your project is not yet connected to any SDK. In order to start | ||||
|                 using your feature flag connect an SDK to the project. | ||||
|             </Typography> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -21,6 +21,9 @@ test('Show outdated SDKs and apps using them', async () => { | ||||
|                 count: 57, | ||||
|             }, | ||||
|         ], | ||||
|         onboardingStatus: { | ||||
|             status: 'onboarded', | ||||
|         }, | ||||
|     }); | ||||
|     render( | ||||
|         <Routes> | ||||
|  | ||||
| @ -24,6 +24,9 @@ const fallbackProject: IProjectOverview = { | ||||
|         projectActivityPastWindow: 0, | ||||
|         projectMembersAddedCurrentWindow: 0, | ||||
|     }, | ||||
|     onboardingStatus: { | ||||
|         status: 'onboarded', | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| const useProjectOverview = (id: string, options: SWRConfiguration = {}) => { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import type { ProjectStatsSchema } from 'openapi'; | ||||
| import type { ProjectOverviewSchema, ProjectStatsSchema } from 'openapi'; | ||||
| import type { IFeatureFlagListItem } from './featureToggle'; | ||||
| import type { ProjectEnvironmentType } from 'component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef'; | ||||
| import type { ProjectMode } from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm'; | ||||
| @ -47,6 +47,7 @@ export interface IProjectOverview { | ||||
|     featureLimit?: number; | ||||
|     featureNaming?: FeatureNamingType; | ||||
|     archivedAt?: Date; | ||||
|     onboardingStatus: ProjectOverviewSchema['onboardingStatus']; | ||||
| } | ||||
| 
 | ||||
| export interface IProjectHealthReport extends IProject { | ||||
|  | ||||
| @ -942,6 +942,11 @@ export * from './projectInsightsSchemaHealth'; | ||||
| export * from './projectInsightsSchemaMembers'; | ||||
| export * from './projectOverviewSchema'; | ||||
| export * from './projectOverviewSchemaMode'; | ||||
| export * from './projectOverviewSchemaOnboardingStatus'; | ||||
| export * from './projectOverviewSchemaOnboardingStatusOneOf'; | ||||
| export * from './projectOverviewSchemaOnboardingStatusOneOfStatus'; | ||||
| export * from './projectOverviewSchemaOnboardingStatusOneOfThree'; | ||||
| export * from './projectOverviewSchemaOnboardingStatusOneOfThreeStatus'; | ||||
| export * from './projectRoleSchema'; | ||||
| export * from './projectRoleUsageSchema'; | ||||
| export * from './projectSchema'; | ||||
|  | ||||
| @ -7,6 +7,7 @@ import type { ProjectEnvironmentSchema } from './projectEnvironmentSchema'; | ||||
| import type { CreateFeatureNamingPatternSchema } from './createFeatureNamingPatternSchema'; | ||||
| import type { FeatureTypeCountSchema } from './featureTypeCountSchema'; | ||||
| import type { ProjectOverviewSchemaMode } from './projectOverviewSchemaMode'; | ||||
| import type { ProjectOverviewSchemaOnboardingStatus } from './projectOverviewSchemaOnboardingStatus'; | ||||
| import type { ProjectStatsSchema } from './projectStatsSchema'; | ||||
| 
 | ||||
| /** | ||||
| @ -50,6 +51,8 @@ export interface ProjectOverviewSchema { | ||||
|     mode?: ProjectOverviewSchemaMode; | ||||
|     /** The name of this project */ | ||||
|     name: string; | ||||
|     /** The current onboarding status of the project. */ | ||||
|     onboardingStatus: ProjectOverviewSchemaOnboardingStatus; | ||||
|     /** Project statistics */ | ||||
|     stats?: ProjectStatsSchema; | ||||
|     /** | ||||
|  | ||||
| @ -0,0 +1,14 @@ | ||||
| /** | ||||
|  * Generated by Orval | ||||
|  * Do not edit manually. | ||||
|  * See `gen:api` script in package.json | ||||
|  */ | ||||
| import type { ProjectOverviewSchemaOnboardingStatusOneOf } from './projectOverviewSchemaOnboardingStatusOneOf'; | ||||
| import type { ProjectOverviewSchemaOnboardingStatusOneOfThree } from './projectOverviewSchemaOnboardingStatusOneOfThree'; | ||||
| 
 | ||||
| /** | ||||
|  * The current onboarding status of the project. | ||||
|  */ | ||||
| export type ProjectOverviewSchemaOnboardingStatus = | ||||
|     | ProjectOverviewSchemaOnboardingStatusOneOf | ||||
|     | ProjectOverviewSchemaOnboardingStatusOneOfThree; | ||||
| @ -0,0 +1,10 @@ | ||||
| /** | ||||
|  * Generated by Orval | ||||
|  * Do not edit manually. | ||||
|  * See `gen:api` script in package.json | ||||
|  */ | ||||
| import type { ProjectOverviewSchemaOnboardingStatusOneOfStatus } from './projectOverviewSchemaOnboardingStatusOneOfStatus'; | ||||
| 
 | ||||
| export type ProjectOverviewSchemaOnboardingStatusOneOf = { | ||||
|     status: ProjectOverviewSchemaOnboardingStatusOneOfStatus; | ||||
| }; | ||||
| @ -0,0 +1,14 @@ | ||||
| /** | ||||
|  * Generated by Orval | ||||
|  * Do not edit manually. | ||||
|  * See `gen:api` script in package.json | ||||
|  */ | ||||
| 
 | ||||
| export type ProjectOverviewSchemaOnboardingStatusOneOfStatus = | ||||
|     (typeof ProjectOverviewSchemaOnboardingStatusOneOfStatus)[keyof typeof ProjectOverviewSchemaOnboardingStatusOneOfStatus]; | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-redeclare
 | ||||
| export const ProjectOverviewSchemaOnboardingStatusOneOfStatus = { | ||||
|     'onboarding-started': 'onboarding-started', | ||||
|     onboarded: 'onboarded', | ||||
| } as const; | ||||
| @ -0,0 +1,12 @@ | ||||
| /** | ||||
|  * Generated by Orval | ||||
|  * Do not edit manually. | ||||
|  * See `gen:api` script in package.json | ||||
|  */ | ||||
| import type { ProjectOverviewSchemaOnboardingStatusOneOfThreeStatus } from './projectOverviewSchemaOnboardingStatusOneOfThreeStatus'; | ||||
| 
 | ||||
| export type ProjectOverviewSchemaOnboardingStatusOneOfThree = { | ||||
|     /** The name of the feature flag */ | ||||
|     feature: string; | ||||
|     status: ProjectOverviewSchemaOnboardingStatusOneOfThreeStatus; | ||||
| }; | ||||
| @ -0,0 +1,13 @@ | ||||
| /** | ||||
|  * Generated by Orval | ||||
|  * Do not edit manually. | ||||
|  * See `gen:api` script in package.json | ||||
|  */ | ||||
| 
 | ||||
| export type ProjectOverviewSchemaOnboardingStatusOneOfThreeStatus = | ||||
|     (typeof ProjectOverviewSchemaOnboardingStatusOneOfThreeStatus)[keyof typeof ProjectOverviewSchemaOnboardingStatusOneOfThreeStatus]; | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-redeclare
 | ||||
| export const ProjectOverviewSchemaOnboardingStatusOneOfThreeStatus = { | ||||
|     'first-flag-created': 'first-flag-created', | ||||
| } as const; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user