mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: personal dashboard project avg health scores (#8328)
This commit is contained in:
parent
a874ac085d
commit
f47ae12263
@ -8,6 +8,13 @@ import type {
|
|||||||
export class FakePersonalDashboardReadModel
|
export class FakePersonalDashboardReadModel
|
||||||
implements IPersonalDashboardReadModel
|
implements IPersonalDashboardReadModel
|
||||||
{
|
{
|
||||||
|
async getLatestHealthScores(
|
||||||
|
project: string,
|
||||||
|
count: number,
|
||||||
|
): Promise<number[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
|
async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -227,6 +227,10 @@ test('should return personal dashboard project details', async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
|
insights: {
|
||||||
|
avgHealthCurrentWindow: null,
|
||||||
|
avgHealthPastWindow: null,
|
||||||
|
},
|
||||||
owners: [{}],
|
owners: [{}],
|
||||||
roles: [{}],
|
roles: [{}],
|
||||||
onboardingStatus: {
|
onboardingStatus: {
|
||||||
@ -260,6 +264,41 @@ test('should return personal dashboard project details', async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const insertHealthScore = (id: string, health: number) => {
|
||||||
|
const irrelevantFlagTrendDetails = {
|
||||||
|
total_flags: 10,
|
||||||
|
stale_flags: 10,
|
||||||
|
potentially_stale_flags: 10,
|
||||||
|
};
|
||||||
|
return db.rawDatabase('flag_trends').insert({
|
||||||
|
...irrelevantFlagTrendDetails,
|
||||||
|
id,
|
||||||
|
project: project.id,
|
||||||
|
health,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await insertHealthScore('2024-01', 80);
|
||||||
|
await insertHealthScore('2024-02', 80);
|
||||||
|
await insertHealthScore('2024-03', 80);
|
||||||
|
await insertHealthScore('2024-04', 81);
|
||||||
|
|
||||||
|
await insertHealthScore('2024-05', 90);
|
||||||
|
await insertHealthScore('2024-06', 91);
|
||||||
|
await insertHealthScore('2024-07', 91);
|
||||||
|
await insertHealthScore('2024-08', 91);
|
||||||
|
|
||||||
|
const { body: bodyWithHealthScores } = await app.request.get(
|
||||||
|
`/api/admin/personal-dashboard/${project.id}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(bodyWithHealthScores).toMatchObject({
|
||||||
|
insights: {
|
||||||
|
avgHealthPastWindow: 80,
|
||||||
|
avgHealthCurrentWindow: 91,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return Unleash admins', async () => {
|
test('should return Unleash admins', async () => {
|
||||||
|
@ -21,4 +21,5 @@ export type PersonalProject = BasePersonalProject & {
|
|||||||
export interface IPersonalDashboardReadModel {
|
export interface IPersonalDashboardReadModel {
|
||||||
getPersonalFeatures(userId: number): Promise<PersonalFeature[]>;
|
getPersonalFeatures(userId: number): Promise<PersonalFeature[]>;
|
||||||
getPersonalProjects(userId: number): Promise<BasePersonalProject[]>;
|
getPersonalProjects(userId: number): Promise<BasePersonalProject[]>;
|
||||||
|
getLatestHealthScores(project: string, count: number): Promise<number[]>;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,19 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel {
|
|||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLatestHealthScores(
|
||||||
|
project: string,
|
||||||
|
count: number,
|
||||||
|
): Promise<number[]> {
|
||||||
|
const results = await this.db<{ health: number }>('flag_trends')
|
||||||
|
.select('health')
|
||||||
|
.orderBy('created_at', 'desc')
|
||||||
|
.where('project', project)
|
||||||
|
.limit(count);
|
||||||
|
|
||||||
|
return results.map((row) => Number(row.health));
|
||||||
|
}
|
||||||
|
|
||||||
async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> {
|
async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> {
|
||||||
const result = await this.db<{
|
const result = await this.db<{
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -136,11 +136,39 @@ export class PersonalDashboardService {
|
|||||||
type: role.type as PersonalDashboardProjectDetailsSchema['roles'][number]['type'],
|
type: role.type as PersonalDashboardProjectDetailsSchema['roles'][number]['type'],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const healthScores =
|
||||||
|
await this.personalDashboardReadModel.getLatestHealthScores(
|
||||||
|
projectId,
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
let avgHealthCurrentWindow: number | null = null;
|
||||||
|
let avgHealthPastWindow: number | null = null;
|
||||||
|
|
||||||
|
if (healthScores.length >= 4) {
|
||||||
|
avgHealthCurrentWindow = Math.round(
|
||||||
|
healthScores
|
||||||
|
.slice(0, 4)
|
||||||
|
.reduce((acc, score) => acc + score, 0) / 4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (healthScores.length >= 8) {
|
||||||
|
avgHealthPastWindow = Math.round(
|
||||||
|
healthScores
|
||||||
|
.slice(4, 8)
|
||||||
|
.reduce((acc, score) => acc + score, 0) / 4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
latestEvents: formattedEvents,
|
latestEvents: formattedEvents,
|
||||||
onboardingStatus,
|
onboardingStatus,
|
||||||
owners,
|
owners,
|
||||||
roles: projectRoles,
|
roles: projectRoles,
|
||||||
|
insights: {
|
||||||
|
avgHealthCurrentWindow,
|
||||||
|
avgHealthPastWindow,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,36 @@ export const personalDashboardProjectDetailsSchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Project details in personal dashboard',
|
description: 'Project details in personal dashboard',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['owners', 'roles', 'latestEvents', 'onboardingStatus'],
|
required: [
|
||||||
|
'owners',
|
||||||
|
'roles',
|
||||||
|
'latestEvents',
|
||||||
|
'onboardingStatus',
|
||||||
|
'insights',
|
||||||
|
],
|
||||||
properties: {
|
properties: {
|
||||||
|
insights: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Insights for the project',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['avgHealthCurrentWindow', 'avgHealthPastWindow'],
|
||||||
|
properties: {
|
||||||
|
avgHealthCurrentWindow: {
|
||||||
|
type: 'number',
|
||||||
|
description:
|
||||||
|
'The average health score in the current window of the last 4 weeks',
|
||||||
|
example: 80,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
avgHealthPastWindow: {
|
||||||
|
type: 'number',
|
||||||
|
description:
|
||||||
|
'The average health score in the previous 4 weeks before the current window',
|
||||||
|
example: 70,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
onboardingStatus: projectOverviewSchema.properties.onboardingStatus,
|
onboardingStatus: projectOverviewSchema.properties.onboardingStatus,
|
||||||
latestEvents: {
|
latestEvents: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
Loading…
Reference in New Issue
Block a user