mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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
 | 
			
		||||
    implements IPersonalDashboardReadModel
 | 
			
		||||
{
 | 
			
		||||
    async getLatestHealthScores(
 | 
			
		||||
        project: string,
 | 
			
		||||
        count: number,
 | 
			
		||||
    ): Promise<number[]> {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -227,6 +227,10 @@ test('should return personal dashboard project details', async () => {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(body).toMatchObject({
 | 
			
		||||
        insights: {
 | 
			
		||||
            avgHealthCurrentWindow: null,
 | 
			
		||||
            avgHealthPastWindow: null,
 | 
			
		||||
        },
 | 
			
		||||
        owners: [{}],
 | 
			
		||||
        roles: [{}],
 | 
			
		||||
        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 () => {
 | 
			
		||||
 | 
			
		||||
@ -21,4 +21,5 @@ export type PersonalProject = BasePersonalProject & {
 | 
			
		||||
export interface IPersonalDashboardReadModel {
 | 
			
		||||
    getPersonalFeatures(userId: number): Promise<PersonalFeature[]>;
 | 
			
		||||
    getPersonalProjects(userId: number): Promise<BasePersonalProject[]>;
 | 
			
		||||
    getLatestHealthScores(project: string, count: number): Promise<number[]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,19 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel {
 | 
			
		||||
        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[]> {
 | 
			
		||||
        const result = await this.db<{
 | 
			
		||||
            name: string;
 | 
			
		||||
 | 
			
		||||
@ -136,11 +136,39 @@ export class PersonalDashboardService {
 | 
			
		||||
                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 {
 | 
			
		||||
            latestEvents: formattedEvents,
 | 
			
		||||
            onboardingStatus,
 | 
			
		||||
            owners,
 | 
			
		||||
            roles: projectRoles,
 | 
			
		||||
            insights: {
 | 
			
		||||
                avgHealthCurrentWindow,
 | 
			
		||||
                avgHealthPastWindow,
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,8 +7,36 @@ export const personalDashboardProjectDetailsSchema = {
 | 
			
		||||
    type: 'object',
 | 
			
		||||
    description: 'Project details in personal dashboard',
 | 
			
		||||
    additionalProperties: false,
 | 
			
		||||
    required: ['owners', 'roles', 'latestEvents', 'onboardingStatus'],
 | 
			
		||||
    required: [
 | 
			
		||||
        'owners',
 | 
			
		||||
        'roles',
 | 
			
		||||
        'latestEvents',
 | 
			
		||||
        'onboardingStatus',
 | 
			
		||||
        'insights',
 | 
			
		||||
    ],
 | 
			
		||||
    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,
 | 
			
		||||
        latestEvents: {
 | 
			
		||||
            type: 'array',
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user