From 6dc6e36084f1c4e92d5bb1c1b4b3b2ad04bd013c Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 20 Mar 2024 13:34:48 +0100 Subject: [PATCH] feat: expose stats, health and flag types insights (#6630) --- .../ProjectHealth/ProjectHealth.tsx | 15 +++-- .../features/project/createProjectService.ts | 6 ++ .../features/project/project-controller.ts | 60 ++--------------- src/lib/features/project/project-service.ts | 65 +++++++++++++++++++ src/lib/features/project/projects.e2e.test.ts | 21 +++++- 5 files changed, 104 insertions(+), 63 deletions(-) diff --git a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx b/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx index 3336df163d..5eea1e63b4 100644 --- a/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx +++ b/frontend/src/component/project/Project/ProjectInsights/ProjectHealth/ProjectHealth.tsx @@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import type { ProjectInsightsSchemaHealth } from '../../../../../openapi'; import type { FC } from 'react'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; const Dot = styled('span', { shouldForwardProp: (prop) => prop !== 'color', @@ -48,10 +49,16 @@ export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({ return ( Project Health - - Health alert! Review your flags and delete the stale - flags - + 0} + show={ + + Health alert! Review your flags and delete the + stale flags + + } + /> + ({ diff --git a/src/lib/features/project/createProjectService.ts b/src/lib/features/project/createProjectService.ts index 1bf9a2b0c0..6aed88e2aa 100644 --- a/src/lib/features/project/createProjectService.ts +++ b/src/lib/features/project/createProjectService.ts @@ -39,6 +39,8 @@ import { createPrivateProjectChecker, } from '../private-project/createPrivateProjectChecker'; import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store'; +import FeatureTypeStore from '../../db/feature-type-store'; +import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store'; export const createProjectService = ( db: Db, @@ -66,6 +68,7 @@ export const createProjectService = ( eventBus, getLogger, ); + const featureTypeStore = new FeatureTypeStore(db, getLogger); const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger); const accessService: AccessService = createAccessService(db, config); const featureToggleService = createFeatureToggleService(db, config); @@ -109,6 +112,7 @@ export const createProjectService = ( featureToggleStore, environmentStore, featureEnvironmentStore, + featureTypeStore, accountStore, projectStatsStore, }, @@ -133,6 +137,7 @@ export const createFakeProjectService = ( const accountStore = new FakeAccountStore(); const environmentStore = new FakeEnvironmentStore(); const featureEnvironmentStore = new FakeFeatureEnvironmentStore(); + const featureTypeStore = new FakeFeatureTypeStore(); const projectStatsStore = new FakeProjectStatsStore(); const { accessService } = createFakeAccessService(config); const featureToggleService = createFakeFeatureToggleService(config); @@ -168,6 +173,7 @@ export const createFakeProjectService = ( featureToggleStore, environmentStore, featureEnvironmentStore, + featureTypeStore, accountStore, projectStatsStore, }, diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index 6057f0e06c..605f2e6054 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -248,67 +248,15 @@ export default class ProjectController extends Controller { req: IAuthRequest, res: Response, ): Promise { - const result = { - stats: { - avgTimeToProdCurrentWindow: 17.1, - createdCurrentWindow: 3, - createdPastWindow: 6, - archivedCurrentWindow: 0, - archivedPastWindow: 1, - projectActivityCurrentWindow: 458, - projectActivityPastWindow: 578, - projectMembersAddedCurrentWindow: 0, - }, - featureTypeCounts: [ - { - type: 'experiment', - count: 4, - }, - { - type: 'permission', - count: 1, - }, - { - type: 'release', - count: 24, - }, - ], - leadTime: { - projectAverage: 17.1, - features: [ - { name: 'feature1', timeToProduction: 120 }, - { name: 'feature2', timeToProduction: 0 }, - { name: 'feature3', timeToProduction: 33 }, - { name: 'feature4', timeToProduction: 131 }, - { name: 'feature5', timeToProduction: 2 }, - ], - }, - health: { - rating: 80, - activeCount: 23, - potentiallyStaleCount: 3, - staleCount: 5, - }, - members: { - active: 20, - inactive: 3, - totalPreviousMonth: 15, - }, - changeRequests: { - total: 24, - approved: 5, - applied: 2, - rejected: 4, - reviewRequired: 10, - scheduled: 3, - }, - }; + const { projectId } = req.params; + const insights = + await this.projectService.getProjectInsights(projectId); this.openApiService.respondWithValidation( 200, res, projectInsightsSchema.$id, - serializeDates(result), + serializeDates(insights), ); } diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index c5e7ecc916..085a84b8d9 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -21,6 +21,7 @@ import { type IFeatureEnvironmentStore, type IFeatureNaming, type IFeatureToggleStore, + type IFeatureTypeStore, type IFlagResolver, type IProject, type IProjectApplications, @@ -75,6 +76,7 @@ import type { IProjectEnterpriseSettingsUpdate, IProjectQuery, } from './project-store-type'; +import { calculateProjectHealth } from '../../domain/project-health/project-health'; const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown'; @@ -119,6 +121,8 @@ export default class ProjectService { private featureEnvironmentStore: IFeatureEnvironmentStore; + private featureTypeStore: IFeatureTypeStore; + private environmentStore: IEnvironmentStore; private groupService: GroupService; @@ -148,6 +152,7 @@ export default class ProjectService { featureToggleStore, environmentStore, featureEnvironmentStore, + featureTypeStore, accountStore, projectStatsStore, }: Pick< @@ -159,6 +164,7 @@ export default class ProjectService { | 'featureEnvironmentStore' | 'accountStore' | 'projectStatsStore' + | 'featureTypeStore' >, config: IUnleashConfig, accessService: AccessService, @@ -174,6 +180,7 @@ export default class ProjectService { this.accessService = accessService; this.eventStore = eventStore; this.featureToggleStore = featureToggleStore; + this.featureTypeStore = featureTypeStore; this.featureToggleService = featureToggleService; this.favoritesService = favoriteService; this.privateProjectChecker = privateProjectChecker; @@ -722,6 +729,7 @@ export default class ProjectService { (r) => r.project === project && r.name === RoleName.OWNER, ); } + private async isAllowedToAddAccess( userAddingAccess: number, projectId: string, @@ -741,6 +749,7 @@ export default class ProjectService { userRoles.some((userRole) => userRole.id === roleId), ); } + async addAccess( projectId: string, roles: number[], @@ -1231,6 +1240,62 @@ export default class ProjectService { }; } + private async getHealthInsights(projectId: string) { + const [overview, featureTypes] = await Promise.all([ + this.getProjectHealth(projectId, false, undefined), + this.featureTypeStore.getAll(), + ]); + + const { activeCount, potentiallyStaleCount, staleCount } = + calculateProjectHealth(overview.features, featureTypes); + + return { + activeCount, + potentiallyStaleCount, + staleCount, + rating: overview.health, + }; + } + + async getProjectInsights(projectId: string) { + const result = { + leadTime: { + projectAverage: 17.1, + features: [ + { name: 'feature1', timeToProduction: 120 }, + { name: 'feature2', timeToProduction: 0 }, + { name: 'feature3', timeToProduction: 33 }, + { name: 'feature4', timeToProduction: 131 }, + { name: 'feature5', timeToProduction: 2 }, + ], + }, + members: { + active: 20, + inactive: 3, + totalPreviousMonth: 15, + }, + changeRequests: { + total: 24, + approved: 5, + applied: 2, + rejected: 4, + reviewRequired: 10, + scheduled: 3, + }, + }; + + const [stats, featureTypeCounts, health] = await Promise.all([ + this.projectStatsStore.getProjectStats(projectId), + this.featureToggleService.getFeatureTypeCounts({ + projectId, + archived: false, + }), + this.getHealthInsights(projectId), + ]); + + return { ...result, stats, featureTypeCounts, health }; + } + async getProjectHealth( projectId: string, archived: boolean = false, diff --git a/src/lib/features/project/projects.e2e.test.ts b/src/lib/features/project/projects.e2e.test.ts index d8d1c6de51..f656b2388f 100644 --- a/src/lib/features/project/projects.e2e.test.ts +++ b/src/lib/features/project/projects.e2e.test.ts @@ -294,8 +294,23 @@ test('project insights happy path', async () => { .expect('Content-Type', /json/) .expect(200); - expect(body.leadTime.features[0]).toEqual({ - name: 'feature1', - timeToProduction: 120, + expect(body).toMatchObject({ + stats: { + avgTimeToProdCurrentWindow: 0, + createdCurrentWindow: 0, + createdPastWindow: 0, + archivedCurrentWindow: 0, + archivedPastWindow: 0, + projectActivityCurrentWindow: 0, + projectActivityPastWindow: 0, + projectMembersAddedCurrentWindow: 0, + }, + featureTypeCounts: [], + health: { + activeCount: 0, + potentiallyStaleCount: 0, + staleCount: 0, + rating: 100, + }, }); });