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 682988dc4578..1df56844b030 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,6 +1,7 @@ import type { IPersonalDashboardReadModel, PersonalFeature, + PersonalProject, } from './personal-dashboard-read-model-type'; export class FakePersonalDashboardReadModel @@ -9,4 +10,8 @@ export class FakePersonalDashboardReadModel async getPersonalFeatures(userId: number): Promise { return []; } + + 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 830dc9629f72..2512d1db8e2a 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 @@ -4,6 +4,7 @@ import { setupAppWithAuth, } from '../../../test/e2e/helpers/test-helper'; import getLogger from '../../../test/fixtures/no-logger'; +import type { IUser } from '../../types'; let app: IUnleashTest; let db: ITestDb; @@ -53,7 +54,6 @@ test('should return personal dashboard with own flags and favorited flags', asyn const { body } = await app.request.get(`/api/admin/personal-dashboard`); expect(body).toMatchObject({ - projects: [], flags: [ { name: 'my_feature_d', type: 'release', project: 'default' }, { name: 'my_feature_c', type: 'release', project: 'default' }, @@ -61,3 +61,80 @@ test('should return personal dashboard with own flags and favorited flags', asyn ], }); }); + +const createProject = async (name: string, user: IUser) => { + const auditUser = { + id: 1, + username: 'audit user', + ip: '127.0.0.1', + }; + const project = await app.services.projectService.createProject( + { + name, + }, + user, + auditUser, + ); + return project; +}; + +test('should return personal dashboard with membered projects', async () => { + const { body: user1 } = await loginUser('user1@test.com'); + const projectA = await createProject('Project A', user1); + await createProject('Project B', user1); + + const { body: user2 } = await loginUser('user2@test.com'); + const projectC = await createProject('Project C', user2); + + await app.services.projectService.addAccess( + projectA.id, + [5], // member role + [], + [user2.id], + user1, + ); + + const { body } = await app.request.get(`/api/admin/personal-dashboard`); + + expect(body).toMatchObject({ + projects: [ + { + name: 'Default', + id: 'default', + roles: [ + { + name: 'Editor', + id: 2, + type: 'root', + }, + ], + }, + { + name: projectA.name, + id: projectA.id, + roles: [ + { + name: 'Member', + id: 5, + type: 'project', + }, + ], + }, + { + name: projectC.name, + id: projectC.id, + roles: [ + { + name: 'Owner', + id: 4, + type: 'project', + }, + ], + }, + ], + }); +}); + +test('should return projects where users are part of a group', () => { + // TODO +}); diff --git a/src/lib/features/personal-dashboard/personal-dashboard-controller.ts b/src/lib/features/personal-dashboard/personal-dashboard-controller.ts index 057002884c19..a7e1f3d21fd1 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-controller.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-controller.ts @@ -63,11 +63,14 @@ export default class PersonalDashboardController extends Controller { user.id, ); + const projects = + await this.personalDashboardService.getPersonalProjects(user.id); + this.openApiService.respondWithValidation( 200, res, personalDashboardSchema.$id, - { projects: [], flags }, + { projects, flags }, ); } } 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 f63e43ae080b..d928aabddd08 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,5 +1,15 @@ export type PersonalFeature = { name: string; type: string; project: string }; +export type PersonalProject = { + name: string; + id: string; + roles: { + name: string; + id: number; + type: 'custom' | 'project' | 'root' | 'custom-root'; + }[]; +}; export interface IPersonalDashboardReadModel { getPersonalFeatures(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 317113cd755c..dd9a856355e8 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts @@ -2,6 +2,7 @@ import type { Db } from '../../db/db'; import type { IPersonalDashboardReadModel, PersonalFeature, + PersonalProject, } from './personal-dashboard-read-model-type'; export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { @@ -11,6 +12,55 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { this.db = db; } + async getPersonalProjects(userId: number): Promise { + const result = await this.db<{ + name: string; + id: string; + roleId: number; + roleName: string; + roleType: string; + }>('projects') + .join('role_user', 'projects.id', 'role_user.project') + .join('roles', 'role_user.role_id', 'roles.id') + .where('role_user.user_id', userId) + .whereNull('projects.archived_at') + .select( + 'projects.name', + 'projects.id', + 'roles.id as roleId', + 'roles.name as roleName', + 'roles.type as roleType', + ) + .limit(100); + + const dict = result.reduce((acc, row) => { + if (acc[row.id]) { + acc[row.id].roles.push({ + id: row.roleId, + name: row.roleName, + type: row.roleType, + }); + } else { + acc[row.id] = { + id: row.id, + name: row.name, + roles: [ + { + id: row.roleId, + name: row.roleName, + type: row.roleType, + }, + ], + }; + } + return acc; + }, {}); + + const projectList: PersonalProject[] = Object.values(dict); + projectList.sort((a, b) => a.name.localeCompare(b.name)); + return projectList; + } + async getPersonalFeatures(userId: number): Promise { const result = await this.db<{ name: string; diff --git a/src/lib/features/personal-dashboard/personal-dashboard-service.ts b/src/lib/features/personal-dashboard/personal-dashboard-service.ts index 4837d93324b5..d25a7e86856c 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-service.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-service.ts @@ -1,6 +1,7 @@ import type { IPersonalDashboardReadModel, PersonalFeature, + PersonalProject, } from './personal-dashboard-read-model-type'; export class PersonalDashboardService { @@ -13,4 +14,8 @@ export class PersonalDashboardService { getPersonalFeatures(userId: number): Promise { return this.personalDashboardReadModel.getPersonalFeatures(userId); } + + getPersonalProjects(userId: number): Promise { + return this.personalDashboardReadModel.getPersonalProjects(userId); + } } diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index ec98f861e4c3..0fd85c82480c 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -12,13 +12,53 @@ export const personalDashboardSchema = { items: { type: 'object', additionalProperties: false, - required: ['id'], + required: ['id', 'name', 'roles'], properties: { id: { type: 'string', example: 'my-project-id', description: 'The id of the project', }, + name: { + type: 'string', + example: 'My Project', + description: 'The name of the project', + }, + roles: { + type: 'array', + description: + 'The list of roles that the user has in this project.', + minItems: 1, + items: { + type: 'object', + description: 'An Unleash role.', + additionalProperties: false, + required: ['name', 'id', 'type'], + properties: { + name: { + type: 'string', + example: 'Owner', + description: 'The name of the role', + }, + id: { + type: 'integer', + example: 4, + description: 'The id of the role', + }, + type: { + type: 'string', + enum: [ + 'custom', + 'project', + 'root', + 'custom-root', + ], + example: 'project', + description: 'The type of the role', + }, + }, + }, + }, }, }, description: