Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: health trend insight #8335

Merged
merged 7 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions frontend/src/component/personalDashboard/MyProjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,12 @@ export const MyProjects: FC<{
</List>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
{activeProjectStage === 'onboarded' ? (
<ProjectSetupComplete project={activeProject} />
{activeProjectStage === 'onboarded' &&
personalDashboardProjectDetails ? (
Comment on lines +140 to +141
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: can you have an active project stage without personaldashboardprojectdetails? is this redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, but the orval type doesn't allow me to make this state more explicit so I ended up adding two expressions in here

<ProjectSetupComplete
project={activeProject}
insights={personalDashboardProjectDetails.insights}
/>
) : null}
{activeProjectStage === 'onboarding-started' ||
activeProjectStage === 'loading' ? (
Expand Down
99 changes: 92 additions & 7 deletions frontend/src/component/personalDashboard/ProjectSetupComplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand All @@ -40,6 +40,91 @@ export const ProjectSetupComplete: FC<{ project: string }> = ({ project }) => {
</Link>{' '}
to work with existing flags
</Typography>
</>
);
};

type HeathTrend = 'consistent' | 'improved' | 'declined' | 'unknown';

const determineProjectHealthTrend = (
insights: PersonalDashboardProjectDetailsSchemaInsights,
) => {
let trend: HeathTrend = 'unknown';
if (
insights.avgHealthCurrentWindow !== null &&
insights.avgHealthPastWindow !== null
) {
if (insights.avgHealthCurrentWindow > insights.avgHealthPastWindow) {
trend = 'improved';
} else if (
insights.avgHealthCurrentWindow < insights.avgHealthPastWindow
) {
trend = 'declined';
} else if (
insights.avgHealthPastWindow === insights.avgHealthCurrentWindow
) {
trend = 'consistent';
}
}
return trend;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could do an early return here by checking if they're both null and just return the string directly. would decrease nesting by one level and remove the mutable variable. Your choice, though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100%. Fixing

};

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>

{projectHealthTrend === 'unknown' ? (
<ConnectedSdkProject project={project} />
) : null}
{projectHealthTrend === 'improved' ? (
<Typography>
On average, your project health went up from{' '}
<PercentageScore>
{insights.avgHealthPastWindow}%
</PercentageScore>{' '}
to{' '}
<PercentageScore>
{insights.avgHealthCurrentWindow}%
</PercentageScore>{' '}
during the last 4 weeks.
</Typography>
) : null}
{projectHealthTrend === 'declined' ? (
<Typography>
On average, your project health went down from{' '}
<PercentageScore>
{insights.avgHealthPastWindow}%
</PercentageScore>{' '}
to{' '}
<PercentageScore>
{insights.avgHealthCurrentWindow}%
</PercentageScore>{' '}
during the last 4 weeks.
</Typography>
) : null}
{projectHealthTrend === 'consistent' ? (
<Typography>
On average, your project health has remained at{' '}
<PercentageScore>
{insights.avgHealthCurrentWindow}%
</PercentageScore>{' '}
during the last 4 weeks.
</Typography>
) : null}
{projectHealthTrend !== 'unknown' ? (
<Link to={`/projects/${project}/insights`}>
View more insights
</Link>
) : null}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd maybe consider extracting this into a variable above instead of a series of ternaries. Just to avoid any future issues 🤷🏼

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to a separate component with early returns

</ActionBox>
);
};
5 changes: 5 additions & 0 deletions frontend/src/openapi/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
*/

export type PersonalDashboardSchemaAdminsItem = {
/** @nullable */
email?: string;
/** The user ID. */
id: number;
/** @nullable */
imageUrl?: string;
/** The user's name. */
name?: string;
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/openapi/models/signalQueryResponseSchema.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 41 additions & 0 deletions frontend/src/openapi/models/signalQuerySignalSchema.ts
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions frontend/src/openapi/models/signalQuerySignalSchemaPayload.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions frontend/src/openapi/models/signalQuerySignalSchemaSource.ts
Original file line number Diff line number Diff line change
@@ -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