1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

feat: project details for personal dashboard (#8274)

This commit is contained in:
Mateusz Kwasniewski 2024-09-26 13:43:51 +02:00 committed by GitHub
parent d6f5280a98
commit 137b8ee260
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 78 additions and 99 deletions

View File

@ -1,7 +1,7 @@
import type { import type {
BasePersonalProject,
IPersonalDashboardReadModel, IPersonalDashboardReadModel,
PersonalFeature, PersonalFeature,
PersonalProject,
} from './personal-dashboard-read-model-type'; } from './personal-dashboard-read-model-type';
export class FakePersonalDashboardReadModel export class FakePersonalDashboardReadModel
@ -11,7 +11,7 @@ export class FakePersonalDashboardReadModel
return []; return [];
} }
async getPersonalProjects(userId: number): Promise<PersonalProject[]> { async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> {
return []; return [];
} }
} }

View File

@ -103,58 +103,22 @@ test('should return personal dashboard with membered projects', async () => {
{ {
name: 'Default', name: 'Default',
id: 'default', id: 'default',
roles: [ health: 100,
{ memberCount: 0,
name: 'Editor', featureCount: 0,
id: 2,
type: 'root',
},
],
owners: [
{
ownerType: 'system',
},
],
}, },
{ {
name: projectA.name, name: projectA.name,
id: projectA.id, id: projectA.id,
roles: [ health: 100,
{ memberCount: 2,
name: 'Member', featureCount: 0,
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: projectC.name, name: projectC.name,
id: projectC.id, id: projectC.id,
roles: [ memberCount: 1,
{ featureCount: 0,
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',
},
],
}, },
], ],
}); });
@ -197,47 +161,16 @@ test('should return projects where users are part of a group', async () => {
{ {
name: 'Default', name: 'Default',
id: 'default', id: 'default',
roles: [ health: 100,
{ memberCount: 0,
name: 'Editor', featureCount: 0,
id: 2,
type: 'root',
},
],
owners: [
{
ownerType: 'system',
},
],
}, },
{ {
name: projectA.name, name: projectA.name,
id: projectA.id, id: projectA.id,
roles: [ health: 100,
{ memberCount: 2,
name: 'Owner', featureCount: 0,
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',
},
],
}, },
], ],
}); });

View File

@ -1,20 +1,24 @@
import type { ProjectOwners } from '../project/project-owners-read-model.type'; import type { ProjectOwners } from '../project/project-owners-read-model.type';
export type PersonalFeature = { name: string; type: string; project: string }; export type PersonalFeature = { name: string; type: string; project: string };
export type PersonalProject = { export type BasePersonalProject = {
name: string; name: string;
id: string; id: string;
roles: { roles?: {
name: string; name: string;
id: number; id: number;
type: 'custom' | 'project' | 'root' | 'custom-root'; type: 'custom' | 'project' | 'root' | 'custom-root';
}[]; }[];
}; };
export type PersonalProjectWithOwners = PersonalProject & { export type PersonalProject = BasePersonalProject & {
owners: ProjectOwners; owners?: ProjectOwners;
} & {
health: number;
memberCount: number;
featureCount: number;
}; };
export interface IPersonalDashboardReadModel { export interface IPersonalDashboardReadModel {
getPersonalFeatures(userId: number): Promise<PersonalFeature[]>; getPersonalFeatures(userId: number): Promise<PersonalFeature[]>;
getPersonalProjects(userId: number): Promise<PersonalProject[]>; getPersonalProjects(userId: number): Promise<BasePersonalProject[]>;
} }

View File

@ -1,5 +1,6 @@
import type { Db } from '../../db/db'; import type { Db } from '../../db/db';
import type { import type {
BasePersonalProject,
IPersonalDashboardReadModel, IPersonalDashboardReadModel,
PersonalFeature, PersonalFeature,
PersonalProject, PersonalProject,
@ -18,7 +19,7 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel {
this.db = db; this.db = db;
} }
async getPersonalProjects(userId: number): Promise<PersonalProject[]> { async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> {
const result = await this.db<{ const result = await this.db<{
name: string; name: string;
id: string; id: string;

View File

@ -2,35 +2,48 @@ import type { IProjectOwnersReadModel } from '../project/project-owners-read-mod
import type { import type {
IPersonalDashboardReadModel, IPersonalDashboardReadModel,
PersonalFeature, PersonalFeature,
PersonalProjectWithOwners, PersonalProject,
} from './personal-dashboard-read-model-type'; } from './personal-dashboard-read-model-type';
import type { IProjectReadModel } from '../project/project-read-model-type';
export class PersonalDashboardService { export class PersonalDashboardService {
private personalDashboardReadModel: IPersonalDashboardReadModel; private personalDashboardReadModel: IPersonalDashboardReadModel;
private projectOwnersReadModel: IProjectOwnersReadModel; private projectOwnersReadModel: IProjectOwnersReadModel;
private projectReadModel: IProjectReadModel;
constructor( constructor(
personalDashboardReadModel: IPersonalDashboardReadModel, personalDashboardReadModel: IPersonalDashboardReadModel,
projectOwnersReadModel: IProjectOwnersReadModel, projectOwnersReadModel: IProjectOwnersReadModel,
projectReadModel: IProjectReadModel,
) { ) {
this.personalDashboardReadModel = personalDashboardReadModel; this.personalDashboardReadModel = personalDashboardReadModel;
this.projectOwnersReadModel = projectOwnersReadModel; this.projectOwnersReadModel = projectOwnersReadModel;
this.projectReadModel = projectReadModel;
} }
getPersonalFeatures(userId: number): Promise<PersonalFeature[]> { getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
return this.personalDashboardReadModel.getPersonalFeatures(userId); return this.personalDashboardReadModel.getPersonalFeatures(userId);
} }
async getPersonalProjects( async getPersonalProjects(userId: number): Promise<PersonalProject[]> {
userId: number, // TODO: add favorite projects in addition to membership projects
): Promise<PersonalProjectWithOwners[]> { const userProjectIds =
const projects = await this.projectReadModel.getProjectsByUser(userId);
await this.personalDashboardReadModel.getPersonalProjects(userId);
const withOwners = const projects = await this.projectReadModel.getProjectsForAdminUi({
await this.projectOwnersReadModel.addOwners(projects); ids: userProjectIds,
});
return withOwners; const normalizedProjects = projects.map((project) => ({
id: project.id,
name: project.name,
health: project.health,
memberCount: project.memberCount,
featureCount: project.featureCount,
}));
return normalizedProjects;
} }
} }

View File

@ -13,7 +13,13 @@ export const personalDashboardSchema = {
items: { items: {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
required: ['id', 'name', 'roles'], required: [
'id',
'name',
'health',
'memberCount',
'featureCount',
],
properties: { properties: {
id: { id: {
type: 'string', type: 'string',
@ -25,6 +31,22 @@ export const personalDashboardSchema = {
example: 'My Project', example: 'My Project',
description: 'The name of the 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, owners: projectSchema.properties.owners,
roles: { roles: {
type: 'array', type: 'array',

View File

@ -151,6 +151,8 @@ import { PersonalDashboardReadModel } from '../features/personal-dashboard/perso
import { FakePersonalDashboardReadModel } from '../features/personal-dashboard/fake-personal-dashboard-read-model'; import { FakePersonalDashboardReadModel } from '../features/personal-dashboard/fake-personal-dashboard-read-model';
import { ProjectOwnersReadModel } from '../features/project/project-owners-read-model'; import { ProjectOwnersReadModel } from '../features/project/project-owners-read-model';
import { FakeProjectOwnersReadModel } from '../features/project/fake-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 = ( export const createServices = (
stores: IUnleashStores, stores: IUnleashStores,
@ -406,12 +408,16 @@ export const createServices = (
: createFakeOnboardingService(config).onboardingService; : createFakeOnboardingService(config).onboardingService;
onboardingService.listen(); onboardingService.listen();
// TODO: move to composition root
const personalDashboardService = new PersonalDashboardService( const personalDashboardService = new PersonalDashboardService(
db db
? new PersonalDashboardReadModel(db) ? new PersonalDashboardReadModel(db)
: new FakePersonalDashboardReadModel(), : new FakePersonalDashboardReadModel(),
db ? new ProjectOwnersReadModel(db) : new FakeProjectOwnersReadModel(), db ? new ProjectOwnersReadModel(db) : new FakeProjectOwnersReadModel(),
db
? new ProjectReadModel(db, config.eventBus, config.flagResolver)
: new FakeProjectReadModel(),
); );
return { return {