From f41a688edb8ecc340f6fe05a2c2cff56a3be8fb7 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Wed, 4 Sep 2024 12:17:33 +0300 Subject: [PATCH] feat: welcome to project onboarding status rendering (#8076) ![image](https://github.com/user-attachments/assets/8a828f95-10bd-4294-b2f4-1d7f4e7f1a3d) --- .../ProjectFeatureToggles.test.tsx | 47 ++++++++ .../ProjectFeatureToggles.tsx | 7 +- .../WelcomeToProject.test.tsx | 52 ++++++++ .../ProjectOnboarding/WelcomeToProject.tsx | 111 +++++++++++++++--- .../FlagTypesUsed/FlagTypesUsed.test.tsx | 3 + .../useProjectOverview/useProjectOverview.ts | 3 + frontend/src/interfaces/project.ts | 3 +- frontend/src/openapi/models/index.ts | 5 + .../openapi/models/projectOverviewSchema.ts | 3 + .../projectOverviewSchemaOnboardingStatus.ts | 14 +++ ...jectOverviewSchemaOnboardingStatusOneOf.ts | 10 ++ ...erviewSchemaOnboardingStatusOneOfStatus.ts | 14 +++ ...verviewSchemaOnboardingStatusOneOfThree.ts | 12 ++ ...wSchemaOnboardingStatusOneOfThreeStatus.ts | 13 ++ 14 files changed, 280 insertions(+), 17 deletions(-) create mode 100644 frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOnboarding/WelcomeToProject.test.tsx create mode 100644 frontend/src/openapi/models/projectOverviewSchemaOnboardingStatus.ts create mode 100644 frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOf.ts create mode 100644 frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfStatus.ts create mode 100644 frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThree.ts create mode 100644 frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThreeStatus.ts diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx index b6fad466b9..9a9867e70d 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.test.tsx @@ -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( + + } + /> + , + { + 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( + + } + /> + , + { + route: `/projects/${projectId}`, + }, + ); + await screen.findByText('Welcome to your project'); +}); diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx index c073b91202..1827870a41 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -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 ( } /> { + const projectId = 'default'; + testServerRoute(server, '/api/admin/projects/default/overview', { + onboarding: { + onboardingStatus: 'onboarding-started', + }, + }); + render( + + } + /> + , + { + 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( + + } + /> + , + { + 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.', + ); +}); diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOnboarding/WelcomeToProject.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOnboarding/WelcomeToProject.tsx index 38d0db02aa..55014d43e5 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOnboarding/WelcomeToProject.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectOnboarding/WelcomeToProject.tsx @@ -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 ( @@ -67,21 +98,19 @@ export const WelcomeToProject = ({ projectId }: IWelcomeToProjectProps) => { - - 1 - Create a feature flag - - -
- The project currently holds no feature toggles. -
-
Create a feature flag to get started.
-
- + {project.onboardingStatus.status === + 'first-flag-created' ? ( + + ) : ( + + )}
- 2 + 2 Connect an SDK @@ -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) => {
); }; + +const CreateFlag = () => { + return ( + <> + + 1 + Create a feature flag + + +
The project currently holds no feature toggles.
+
Create a feature flag to get started.
+
+ + + ); +}; + +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 ( + <> + + + Create a feature flag + + + + + + + + + {feature.name} + + + + Your project is not yet connected to any SDK. In order to start + using your feature flag connect an SDK to the project. + + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx b/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx index 2062b64d6b..e199b67040 100644 --- a/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx +++ b/frontend/src/component/project/Project/ProjectInsights/FlagTypesUsed/FlagTypesUsed.test.tsx @@ -21,6 +21,9 @@ test('Show outdated SDKs and apps using them', async () => { count: 57, }, ], + onboardingStatus: { + status: 'onboarded', + }, }); render( diff --git a/frontend/src/hooks/api/getters/useProjectOverview/useProjectOverview.ts b/frontend/src/hooks/api/getters/useProjectOverview/useProjectOverview.ts index 8ab01a0b9b..c3d37b21b7 100644 --- a/frontend/src/hooks/api/getters/useProjectOverview/useProjectOverview.ts +++ b/frontend/src/hooks/api/getters/useProjectOverview/useProjectOverview.ts @@ -24,6 +24,9 @@ const fallbackProject: IProjectOverview = { projectActivityPastWindow: 0, projectMembersAddedCurrentWindow: 0, }, + onboardingStatus: { + status: 'onboarded', + }, }; const useProjectOverview = (id: string, options: SWRConfiguration = {}) => { diff --git a/frontend/src/interfaces/project.ts b/frontend/src/interfaces/project.ts index 21f4fdb296..b77a996c7b 100644 --- a/frontend/src/interfaces/project.ts +++ b/frontend/src/interfaces/project.ts @@ -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 { diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts index 16ad454e46..6ec220645b 100644 --- a/frontend/src/openapi/models/index.ts +++ b/frontend/src/openapi/models/index.ts @@ -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'; diff --git a/frontend/src/openapi/models/projectOverviewSchema.ts b/frontend/src/openapi/models/projectOverviewSchema.ts index 4114792998..67a4988d8c 100644 --- a/frontend/src/openapi/models/projectOverviewSchema.ts +++ b/frontend/src/openapi/models/projectOverviewSchema.ts @@ -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; /** diff --git a/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatus.ts b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatus.ts new file mode 100644 index 0000000000..6ef958acbd --- /dev/null +++ b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatus.ts @@ -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; diff --git a/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOf.ts b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOf.ts new file mode 100644 index 0000000000..847c9c11c2 --- /dev/null +++ b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOf.ts @@ -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; +}; diff --git a/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfStatus.ts b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfStatus.ts new file mode 100644 index 0000000000..e15bf60639 --- /dev/null +++ b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfStatus.ts @@ -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; diff --git a/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThree.ts b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThree.ts new file mode 100644 index 0000000000..a8b81cc008 --- /dev/null +++ b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThree.ts @@ -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; +}; diff --git a/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThreeStatus.ts b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThreeStatus.ts new file mode 100644 index 0000000000..c492386263 --- /dev/null +++ b/frontend/src/openapi/models/projectOverviewSchemaOnboardingStatusOneOfThreeStatus.ts @@ -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;