Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add your projects (with roles) to personal dashboard api #8236

Merged
merged 9 commits into from
Sep 25, 2024
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
IPersonalDashboardReadModel,
PersonalFeature,
PersonalProject,
} from './personal-dashboard-read-model-type';

export class FakePersonalDashboardReadModel
Expand All @@ -9,4 +10,8 @@ export class FakePersonalDashboardReadModel
async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
return [];
}

async getPersonalProjects(userId: number): Promise<PersonalProject[]> {
return [];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,3 +62,84 @@ 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 () => {
// create project A with user 1
thomasheartman marked this conversation as resolved.
Show resolved Hide resolved
// create project B with user 1
const { body: user1 } = await loginUser('[email protected]');
const projectA = await createProject('Project A', user1);
await createProject('Project B', user1);

// create project C with user 2
const { body: user2 } = await loginUser('[email protected]');
const projectC = await createProject('Project C', user2);

// Add user 2 as a member of project A
await app.services.projectService.addAccess(
projectA.id,
[5],
thomasheartman marked this conversation as resolved.
Show resolved Hide resolved
[],
[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
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
);
}
}
Original file line number Diff line number Diff line change
@@ -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<PersonalFeature[]>;
getPersonalProjects(userId: number): Promise<PersonalProject[]>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -11,6 +12,55 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel {
this.db = db;
}

async getPersonalProjects(userId: number): Promise<PersonalProject[]> {
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<PersonalFeature[]> {
const result = await this.db<{
name: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
IPersonalDashboardReadModel,
PersonalFeature,
PersonalProject,
} from './personal-dashboard-read-model-type';

export class PersonalDashboardService {
Expand All @@ -13,4 +14,8 @@ export class PersonalDashboardService {
getPersonalFeatures(userId: number): Promise<PersonalFeature[]> {
return this.personalDashboardReadModel.getPersonalFeatures(userId);
}

getPersonalProjects(userId: number): Promise<PersonalProject[]> {
return this.personalDashboardReadModel.getPersonalProjects(userId);
}
}
42 changes: 41 additions & 1 deletion src/lib/openapi/spec/personal-dashboard-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading