mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
feat: health trend insight (#8335)
This commit is contained in:
parent
5abc3b4732
commit
b975919395
@ -137,8 +137,12 @@ export const MyProjects: FC<{
|
|||||||
</List>
|
</List>
|
||||||
</SpacedGridItem>
|
</SpacedGridItem>
|
||||||
<SpacedGridItem item lg={4} md={1}>
|
<SpacedGridItem item lg={4} md={1}>
|
||||||
{activeProjectStage === 'onboarded' ? (
|
{activeProjectStage === 'onboarded' &&
|
||||||
<ProjectSetupComplete project={activeProject} />
|
personalDashboardProjectDetails ? (
|
||||||
|
<ProjectSetupComplete
|
||||||
|
project={activeProject}
|
||||||
|
insights={personalDashboardProjectDetails.insights}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{activeProjectStage === 'onboarding-started' ||
|
{activeProjectStage === 'onboarding-started' ||
|
||||||
activeProjectStage === 'loading' ? (
|
activeProjectStage === 'loading' ? (
|
||||||
|
@ -2,6 +2,7 @@ import { styled, Typography } from '@mui/material';
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
||||||
|
import type { PersonalDashboardProjectDetailsSchemaInsights } from '../../openapi';
|
||||||
|
|
||||||
const TitleContainer = styled('div')(({ theme }) => ({
|
const TitleContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -18,18 +19,17 @@ const ActionBox = styled('article')(({ theme }) => ({
|
|||||||
flexDirection: 'column',
|
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 (
|
return (
|
||||||
<ActionBox>
|
<>
|
||||||
<TitleContainer>
|
|
||||||
<Lightbulb color='primary' />
|
|
||||||
<h3>Project Insight</h3>
|
|
||||||
</TitleContainer>
|
|
||||||
<Typography>
|
<Typography>
|
||||||
This project already has connected SDKs and existing feature
|
This project already has connected SDKs and existing feature
|
||||||
flags.
|
flags.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography>
|
<Typography>
|
||||||
<Link to={`/projects/${project}?create=true`}>
|
<Link to={`/projects/${project}?create=true`}>
|
||||||
Create a new feature flag
|
Create a new feature flag
|
||||||
@ -40,6 +40,105 @@ export const ProjectSetupComplete: FC<{ project: string }> = ({ project }) => {
|
|||||||
</Link>{' '}
|
</Link>{' '}
|
||||||
to work with existing flags
|
to work with existing flags
|
||||||
</Typography>
|
</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>
|
</ActionBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -904,6 +904,7 @@ export * from './patchesSchema';
|
|||||||
export * from './patsSchema';
|
export * from './patsSchema';
|
||||||
export * from './permissionSchema';
|
export * from './permissionSchema';
|
||||||
export * from './personalDashboardProjectDetailsSchema';
|
export * from './personalDashboardProjectDetailsSchema';
|
||||||
|
export * from './personalDashboardProjectDetailsSchemaInsights';
|
||||||
export * from './personalDashboardProjectDetailsSchemaLatestEventsItem';
|
export * from './personalDashboardProjectDetailsSchemaLatestEventsItem';
|
||||||
export * from './personalDashboardProjectDetailsSchemaOnboardingStatus';
|
export * from './personalDashboardProjectDetailsSchemaOnboardingStatus';
|
||||||
export * from './personalDashboardProjectDetailsSchemaOnboardingStatusOneOf';
|
export * from './personalDashboardProjectDetailsSchemaOnboardingStatusOneOf';
|
||||||
@ -1144,6 +1145,10 @@ export * from './signalEndpointSignalsSchema';
|
|||||||
export * from './signalEndpointTokenSchema';
|
export * from './signalEndpointTokenSchema';
|
||||||
export * from './signalEndpointTokensSchema';
|
export * from './signalEndpointTokensSchema';
|
||||||
export * from './signalEndpointsSchema';
|
export * from './signalEndpointsSchema';
|
||||||
|
export * from './signalQueryResponseSchema';
|
||||||
|
export * from './signalQuerySignalSchema';
|
||||||
|
export * from './signalQuerySignalSchemaPayload';
|
||||||
|
export * from './signalQuerySignalSchemaSource';
|
||||||
export * from './signalSchema';
|
export * from './signalSchema';
|
||||||
export * from './signalSchemaPayload';
|
export * from './signalSchemaPayload';
|
||||||
export * from './signalSchemaSource';
|
export * from './signalSchemaSource';
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
* Do not edit manually.
|
* Do not edit manually.
|
||||||
* See `gen:api` script in package.json
|
* See `gen:api` script in package.json
|
||||||
*/
|
*/
|
||||||
|
import type { PersonalDashboardProjectDetailsSchemaInsights } from './personalDashboardProjectDetailsSchemaInsights';
|
||||||
import type { PersonalDashboardProjectDetailsSchemaLatestEventsItem } from './personalDashboardProjectDetailsSchemaLatestEventsItem';
|
import type { PersonalDashboardProjectDetailsSchemaLatestEventsItem } from './personalDashboardProjectDetailsSchemaLatestEventsItem';
|
||||||
import type { PersonalDashboardProjectDetailsSchemaOnboardingStatus } from './personalDashboardProjectDetailsSchemaOnboardingStatus';
|
import type { PersonalDashboardProjectDetailsSchemaOnboardingStatus } from './personalDashboardProjectDetailsSchemaOnboardingStatus';
|
||||||
import type { PersonalDashboardProjectDetailsSchemaOwners } from './personalDashboardProjectDetailsSchemaOwners';
|
import type { PersonalDashboardProjectDetailsSchemaOwners } from './personalDashboardProjectDetailsSchemaOwners';
|
||||||
@ -12,6 +13,8 @@ import type { PersonalDashboardProjectDetailsSchemaRolesItem } from './personalD
|
|||||||
* Project details in personal dashboard
|
* Project details in personal dashboard
|
||||||
*/
|
*/
|
||||||
export interface PersonalDashboardProjectDetailsSchema {
|
export interface PersonalDashboardProjectDetailsSchema {
|
||||||
|
/** Insights for the project */
|
||||||
|
insights: PersonalDashboardProjectDetailsSchemaInsights;
|
||||||
/** The latest events for the project. */
|
/** The latest events for the project. */
|
||||||
latestEvents: PersonalDashboardProjectDetailsSchemaLatestEventsItem[];
|
latestEvents: PersonalDashboardProjectDetailsSchemaLatestEventsItem[];
|
||||||
/** The current onboarding status of the project. */
|
/** The current onboarding status of the project. */
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -5,11 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export type PersonalDashboardSchemaAdminsItem = {
|
export type PersonalDashboardSchemaAdminsItem = {
|
||||||
/** @nullable */
|
|
||||||
email?: string;
|
email?: string;
|
||||||
/** The user ID. */
|
/** The user ID. */
|
||||||
id: number;
|
id: number;
|
||||||
/** @nullable */
|
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
/** The user's name. */
|
/** The user's name. */
|
||||||
name?: string;
|
name?: string;
|
||||||
|
19
frontend/src/openapi/models/signalQueryResponseSchema.ts
Normal file
19
frontend/src/openapi/models/signalQueryResponseSchema.ts
Normal 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;
|
||||||
|
}
|
41
frontend/src/openapi/models/signalQuerySignalSchema.ts
Normal file
41
frontend/src/openapi/models/signalQuerySignalSchema.ts
Normal 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;
|
||||||
|
}
|
@ -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 };
|
16
frontend/src/openapi/models/signalQuerySignalSchemaSource.ts
Normal file
16
frontend/src/openapi/models/signalQuerySignalSchemaSource.ts
Normal 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;
|
Loading…
Reference in New Issue
Block a user