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