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

feat: health trend insight (#8335)

This commit is contained in:
Mateusz Kwasniewski 2024-10-02 13:50:31 +02:00 committed by GitHub
parent 5abc3b4732
commit b975919395
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 227 additions and 11 deletions

View File

@ -137,8 +137,12 @@ export const MyProjects: FC<{
</List>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
{activeProjectStage === 'onboarded' ? (
<ProjectSetupComplete project={activeProject} />
{activeProjectStage === 'onboarded' &&
personalDashboardProjectDetails ? (
<ProjectSetupComplete
project={activeProject}
insights={personalDashboardProjectDetails.insights}
/>
) : null}
{activeProjectStage === 'onboarding-started' ||
activeProjectStage === 'loading' ? (

View File

@ -2,6 +2,7 @@ import { styled, Typography } from '@mui/material';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
import type { PersonalDashboardProjectDetailsSchemaInsights } from '../../openapi';
const TitleContainer = styled('div')(({ theme }) => ({
display: 'flex',
@ -18,18 +19,17 @@ const ActionBox = styled('article')(({ theme }) => ({
flexDirection: 'column',
}));
export const ProjectSetupComplete: FC<{ project: string }> = ({ project }) => {
const PercentageScore = styled('span')(({ theme }) => ({
color: theme.palette.primary.main,
}));
const ConnectedSdkProject: FC<{ project: string }> = ({ project }) => {
return (
<ActionBox>
<TitleContainer>
<Lightbulb color='primary' />
<h3>Project Insight</h3>
</TitleContainer>
<>
<Typography>
This project already has connected SDKs and existing feature
flags.
</Typography>
<Typography>
<Link to={`/projects/${project}?create=true`}>
Create a new feature flag
@ -40,6 +40,105 @@ export const ProjectSetupComplete: FC<{ project: string }> = ({ project }) => {
</Link>{' '}
to work with existing flags
</Typography>
</>
);
};
type HeathTrend = 'consistent' | 'improved' | 'declined' | 'unknown';
const determineProjectHealthTrend = (
insights: PersonalDashboardProjectDetailsSchemaInsights,
): HeathTrend => {
const { avgHealthCurrentWindow, avgHealthPastWindow } = insights;
if (avgHealthCurrentWindow === null || avgHealthPastWindow === null) {
return 'unknown';
}
if (avgHealthCurrentWindow > avgHealthPastWindow) {
return 'improved';
}
if (avgHealthCurrentWindow < avgHealthPastWindow) {
return 'declined';
}
return 'consistent';
};
const ProjectHealthMessage: FC<{
trend: HeathTrend;
insights: PersonalDashboardProjectDetailsSchemaInsights;
project: string;
}> = ({ trend, insights, project }) => {
const { avgHealthCurrentWindow, avgHealthPastWindow } = insights;
const improveMessage =
'Remember to archive your stale feature flags to keep the project health growing.';
const keepDoingMessage =
'This indicates that you are doing a good job of arching your feature flags.';
if (trend === 'improved') {
return (
<Typography>
On average, your project health went up from{' '}
<PercentageScore>{avgHealthPastWindow}%</PercentageScore> to{' '}
<PercentageScore>{avgHealthCurrentWindow}%</PercentageScore>{' '}
during the last 4 weeks. <br /> {keepDoingMessage}
</Typography>
);
}
if (trend === 'declined') {
return (
<Typography>
On average, your project health went down from{' '}
<PercentageScore>{avgHealthPastWindow}%</PercentageScore> to{' '}
<PercentageScore>{avgHealthCurrentWindow}%</PercentageScore>{' '}
during the last 4 weeks. <br /> {improveMessage}
</Typography>
);
}
if (trend === 'consistent') {
return (
<Typography>
On average, your project health has remained at{' '}
<PercentageScore>{avgHealthCurrentWindow}%</PercentageScore>{' '}
during the last 4 weeks. <br />
{avgHealthCurrentWindow && avgHealthCurrentWindow >= 70
? keepDoingMessage
: improveMessage}
</Typography>
);
}
return <ConnectedSdkProject project={project} />;
};
export const ProjectSetupComplete: FC<{
project: string;
insights: PersonalDashboardProjectDetailsSchemaInsights;
}> = ({ project, insights }) => {
const projectHealthTrend = determineProjectHealthTrend(insights);
return (
<ActionBox>
<TitleContainer>
<Lightbulb color='primary' />
<h3>Project Insight</h3>
</TitleContainer>
<ProjectHealthMessage
trend={projectHealthTrend}
insights={insights}
project={project}
/>
{projectHealthTrend !== 'unknown' && (
<Link to={`/projects/${project}/insights`}>
View more insights
</Link>
)}
</ActionBox>
);
};

View File

@ -904,6 +904,7 @@ export * from './patchesSchema';
export * from './patsSchema';
export * from './permissionSchema';
export * from './personalDashboardProjectDetailsSchema';
export * from './personalDashboardProjectDetailsSchemaInsights';
export * from './personalDashboardProjectDetailsSchemaLatestEventsItem';
export * from './personalDashboardProjectDetailsSchemaOnboardingStatus';
export * from './personalDashboardProjectDetailsSchemaOnboardingStatusOneOf';
@ -1144,6 +1145,10 @@ export * from './signalEndpointSignalsSchema';
export * from './signalEndpointTokenSchema';
export * from './signalEndpointTokensSchema';
export * from './signalEndpointsSchema';
export * from './signalQueryResponseSchema';
export * from './signalQuerySignalSchema';
export * from './signalQuerySignalSchemaPayload';
export * from './signalQuerySignalSchemaSource';
export * from './signalSchema';
export * from './signalSchemaPayload';
export * from './signalSchemaSource';

View File

@ -3,6 +3,7 @@
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { PersonalDashboardProjectDetailsSchemaInsights } from './personalDashboardProjectDetailsSchemaInsights';
import type { PersonalDashboardProjectDetailsSchemaLatestEventsItem } from './personalDashboardProjectDetailsSchemaLatestEventsItem';
import type { PersonalDashboardProjectDetailsSchemaOnboardingStatus } from './personalDashboardProjectDetailsSchemaOnboardingStatus';
import type { PersonalDashboardProjectDetailsSchemaOwners } from './personalDashboardProjectDetailsSchemaOwners';
@ -12,6 +13,8 @@ import type { PersonalDashboardProjectDetailsSchemaRolesItem } from './personalD
* Project details in personal dashboard
*/
export interface PersonalDashboardProjectDetailsSchema {
/** Insights for the project */
insights: PersonalDashboardProjectDetailsSchemaInsights;
/** The latest events for the project. */
latestEvents: PersonalDashboardProjectDetailsSchemaLatestEventsItem[];
/** The current onboarding status of the project. */

View File

@ -0,0 +1,21 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* Insights for the project
*/
export type PersonalDashboardProjectDetailsSchemaInsights = {
/**
* The average health score in the current window of the last 4 weeks
* @nullable
*/
avgHealthCurrentWindow: number | null;
/**
* The average health score in the previous 4 weeks before the current window
* @nullable
*/
avgHealthPastWindow: number | null;
};

View File

@ -5,11 +5,9 @@
*/
export type PersonalDashboardSchemaAdminsItem = {
/** @nullable */
email?: string;
/** The user ID. */
id: number;
/** @nullable */
imageUrl?: string;
/** The user's name. */
name?: string;

View File

@ -0,0 +1,19 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { SignalQuerySignalSchema } from './signalQuerySignalSchema';
/**
* A list of signals that have been registered by the system
*/
export interface SignalQueryResponseSchema {
/** The list of signals */
signals: SignalQuerySignalSchema[];
/**
* The total count of signals
* @minimum 0
*/
total: number;
}

View File

@ -0,0 +1,41 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
import type { SignalQuerySignalSchemaPayload } from './signalQuerySignalSchemaPayload';
import type { SignalQuerySignalSchemaSource } from './signalQuerySignalSchemaSource';
/**
* An object describing a signal enriched with source data.
*/
export interface SignalQuerySignalSchema {
/** The date and time of when the signal was created. */
createdAt: string;
/**
* The signal's ID. Signal IDs are incrementing integers. In other words, a more recently created signal will always have a higher ID than an older one.
* @minimum 1
*/
id: number;
/** The payload of the signal. */
payload?: SignalQuerySignalSchemaPayload;
/** The signal source type. Should be used along with `sourceId` to uniquely identify the resource that created this signal. */
source: SignalQuerySignalSchemaSource;
/**
* A more detailed description of the source that registered this signal.
* @nullable
*/
sourceDescription?: string | null;
/** The ID of the source that created this signal. Should be used along with `source` to uniquely identify the resource that created this signal. */
sourceId: number;
/**
* The name of the source that registered this signal.
* @nullable
*/
sourceName?: string | null;
/**
* The name of the token used to register this signal.
* @nullable
*/
tokenName?: string | null;
}

View File

@ -0,0 +1,10 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The payload of the signal.
*/
export type SignalQuerySignalSchemaPayload = { [key: string]: unknown };

View File

@ -0,0 +1,16 @@
/**
* Generated by Orval
* Do not edit manually.
* See `gen:api` script in package.json
*/
/**
* The signal source type. Should be used along with `sourceId` to uniquely identify the resource that created this signal.
*/
export type SignalQuerySignalSchemaSource =
(typeof SignalQuerySignalSchemaSource)[keyof typeof SignalQuerySignalSchemaSource];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const SignalQuerySignalSchemaSource = {
'signal-endpoint': 'signal-endpoint',
} as const;