1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

feat: welcome to project onboarding status rendering (#8076)

![image](https://github.com/user-attachments/assets/8a828f95-10bd-4294-b2f4-1d7f4e7f1a3d)
This commit is contained in:
Jaanus Sellin 2024-09-04 12:17:33 +03:00 committed by GitHub
parent 896707f7d5
commit f41a688edb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 280 additions and 17 deletions

View File

@ -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');
});

View File

@ -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

View File

@ -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.',
);
});

View File

@ -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>
</>
);
};

View File

@ -21,6 +21,9 @@ test('Show outdated SDKs and apps using them', async () => {
count: 57,
},
],
onboardingStatus: {
status: 'onboarded',
},
});
render(
<Routes>

View File

@ -24,6 +24,9 @@ const fallbackProject: IProjectOverview = {
projectActivityPastWindow: 0,
projectMembersAddedCurrentWindow: 0,
},
onboardingStatus: {
status: 'onboarded',
},
};
const useProjectOverview = (id: string, options: SWRConfiguration = {}) => {

View File

@ -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 {

View File

@ -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';

View File

@ -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;
/**

View File

@ -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;

View File

@ -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;
};

View File

@ -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;

View File

@ -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;
};

View File

@ -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;