From 137b8ee2605adfcab29efe05127ccc67295e75f0 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 26 Sep 2024 13:43:51 +0200 Subject: [PATCH] feat: project details for personal dashboard (#8274) --- .../fake-personal-dashboard-read-model.ts | 4 +- .../personal-dashboard-controller.e2e.test.ts | 95 +++---------------- .../personal-dashboard-read-model-type.ts | 14 ++- .../personal-dashboard-read-model.ts | 3 +- .../personal-dashboard-service.ts | 31 ++++-- .../openapi/spec/personal-dashboard-schema.ts | 24 ++++- src/lib/services/index.ts | 6 ++ 7 files changed, 78 insertions(+), 99 deletions(-) diff --git a/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts b/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts index 1df56844b030..27f26c551abd 100644 --- a/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts @@ -1,7 +1,7 @@ import type { + BasePersonalProject, IPersonalDashboardReadModel, PersonalFeature, - PersonalProject, } from './personal-dashboard-read-model-type'; export class FakePersonalDashboardReadModel @@ -11,7 +11,7 @@ export class FakePersonalDashboardReadModel return []; } - async getPersonalProjects(userId: number): Promise { + async getPersonalProjects(userId: number): Promise { return []; } } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts index 179c793ea691..b8b81d728e94 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts @@ -103,58 +103,22 @@ test('should return personal dashboard with membered projects', async () => { { name: 'Default', id: 'default', - roles: [ - { - name: 'Editor', - id: 2, - type: 'root', - }, - ], - owners: [ - { - ownerType: 'system', - }, - ], + health: 100, + memberCount: 0, + featureCount: 0, }, { name: projectA.name, id: projectA.id, - roles: [ - { - name: 'Member', - id: 5, - type: 'project', - }, - ], - owners: [ - { - email: 'user1@test.com', - imageUrl: - 'https://gravatar.com/avatar/a8cc79d8407a64b0d8982df34e3525afd298a479fe68f300651380730dbf23e9?s=42&d=retro&r=g', - name: 'user1@test.com', - ownerType: 'user', - }, - ], + health: 100, + memberCount: 2, + featureCount: 0, }, { name: projectC.name, id: projectC.id, - roles: [ - { - name: 'Owner', - id: 4, - type: 'project', - }, - ], - owners: [ - { - email: 'user2@test.com', - imageUrl: - 'https://gravatar.com/avatar/706150f3ef810ea66acb30c6d55f1a7e545338747072609e47df71c7c7ccc6a4?s=42&d=retro&r=g', - name: 'user2@test.com', - ownerType: 'user', - }, - ], + memberCount: 1, + featureCount: 0, }, ], }); @@ -197,47 +161,16 @@ test('should return projects where users are part of a group', async () => { { name: 'Default', id: 'default', - roles: [ - { - name: 'Editor', - id: 2, - type: 'root', - }, - ], - owners: [ - { - ownerType: 'system', - }, - ], + health: 100, + memberCount: 0, + featureCount: 0, }, { name: projectA.name, id: projectA.id, - roles: [ - { - name: 'Owner', - id: 4, - type: 'project', - }, - { - name: 'Member', - id: 5, - type: 'project', - }, - ], - owners: [ - { - email: 'user1@test.com', - imageUrl: - 'https://gravatar.com/avatar/a8cc79d8407a64b0d8982df34e3525afd298a479fe68f300651380730dbf23e9?s=42&d=retro&r=g', - name: 'user1@test.com', - ownerType: 'user', - }, - { - name: 'groupA', - ownerType: 'group', - }, - ], + health: 100, + memberCount: 2, + featureCount: 0, }, ], }); diff --git a/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts b/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts index b3db032ff8a7..6926957bcb40 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts @@ -1,20 +1,24 @@ import type { ProjectOwners } from '../project/project-owners-read-model.type'; export type PersonalFeature = { name: string; type: string; project: string }; -export type PersonalProject = { +export type BasePersonalProject = { name: string; id: string; - roles: { + roles?: { name: string; id: number; type: 'custom' | 'project' | 'root' | 'custom-root'; }[]; }; -export type PersonalProjectWithOwners = PersonalProject & { - owners: ProjectOwners; +export type PersonalProject = BasePersonalProject & { + owners?: ProjectOwners; +} & { + health: number; + memberCount: number; + featureCount: number; }; export interface IPersonalDashboardReadModel { getPersonalFeatures(userId: number): Promise; - getPersonalProjects(userId: number): Promise; + getPersonalProjects(userId: number): Promise; } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts index 9225a96f5433..87e4df16d9a7 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts @@ -1,5 +1,6 @@ import type { Db } from '../../db/db'; import type { + BasePersonalProject, IPersonalDashboardReadModel, PersonalFeature, PersonalProject, @@ -18,7 +19,7 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { this.db = db; } - async getPersonalProjects(userId: number): Promise { + async getPersonalProjects(userId: number): Promise { const result = await this.db<{ name: string; id: string; diff --git a/src/lib/features/personal-dashboard/personal-dashboard-service.ts b/src/lib/features/personal-dashboard/personal-dashboard-service.ts index f4aa5fccba79..f62bc8b7c61d 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-service.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-service.ts @@ -2,35 +2,48 @@ import type { IProjectOwnersReadModel } from '../project/project-owners-read-mod import type { IPersonalDashboardReadModel, PersonalFeature, - PersonalProjectWithOwners, + PersonalProject, } from './personal-dashboard-read-model-type'; +import type { IProjectReadModel } from '../project/project-read-model-type'; export class PersonalDashboardService { private personalDashboardReadModel: IPersonalDashboardReadModel; private projectOwnersReadModel: IProjectOwnersReadModel; + private projectReadModel: IProjectReadModel; + constructor( personalDashboardReadModel: IPersonalDashboardReadModel, projectOwnersReadModel: IProjectOwnersReadModel, + projectReadModel: IProjectReadModel, ) { this.personalDashboardReadModel = personalDashboardReadModel; this.projectOwnersReadModel = projectOwnersReadModel; + this.projectReadModel = projectReadModel; } getPersonalFeatures(userId: number): Promise { return this.personalDashboardReadModel.getPersonalFeatures(userId); } - async getPersonalProjects( - userId: number, - ): Promise { - const projects = - await this.personalDashboardReadModel.getPersonalProjects(userId); + async getPersonalProjects(userId: number): Promise { + // TODO: add favorite projects in addition to membership projects + const userProjectIds = + await this.projectReadModel.getProjectsByUser(userId); + + const projects = await this.projectReadModel.getProjectsForAdminUi({ + ids: userProjectIds, + }); - const withOwners = - await this.projectOwnersReadModel.addOwners(projects); + const normalizedProjects = projects.map((project) => ({ + id: project.id, + name: project.name, + health: project.health, + memberCount: project.memberCount, + featureCount: project.featureCount, + })); - return withOwners; + return normalizedProjects; } } diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index 16efbbe0919c..627556190973 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -13,7 +13,13 @@ export const personalDashboardSchema = { items: { type: 'object', additionalProperties: false, - required: ['id', 'name', 'roles'], + required: [ + 'id', + 'name', + 'health', + 'memberCount', + 'featureCount', + ], properties: { id: { type: 'string', @@ -25,6 +31,22 @@ export const personalDashboardSchema = { example: 'My Project', description: 'The name of the project', }, + health: { + type: 'number', + example: 50, + description: + "An indicator of the [project's health](https://docs.getunleash.io/reference/technical-debt#health-rating) on a scale from 0 to 100", + }, + memberCount: { + type: 'number', + example: 4, + description: 'The number of members this project has', + }, + featureCount: { + type: 'number', + example: 10, + description: 'The number of features this project has', + }, owners: projectSchema.properties.owners, roles: { type: 'array', diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 40c517171b06..1f8a8be26668 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -151,6 +151,8 @@ import { PersonalDashboardReadModel } from '../features/personal-dashboard/perso import { FakePersonalDashboardReadModel } from '../features/personal-dashboard/fake-personal-dashboard-read-model'; import { ProjectOwnersReadModel } from '../features/project/project-owners-read-model'; import { FakeProjectOwnersReadModel } from '../features/project/fake-project-owners-read-model'; +import { FakeProjectReadModel } from '../features/project/fake-project-read-model'; +import { ProjectReadModel } from '../features/project/project-read-model'; export const createServices = ( stores: IUnleashStores, @@ -406,12 +408,16 @@ export const createServices = ( : createFakeOnboardingService(config).onboardingService; onboardingService.listen(); + // TODO: move to composition root const personalDashboardService = new PersonalDashboardService( db ? new PersonalDashboardReadModel(db) : new FakePersonalDashboardReadModel(), db ? new ProjectOwnersReadModel(db) : new FakeProjectOwnersReadModel(), + db + ? new ProjectReadModel(db, config.eventBus, config.flagResolver) + : new FakeProjectReadModel(), ); return {