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

feat: personal dashboard api (#8218)

This commit is contained in:
Mateusz Kwasniewski 2024-09-23 15:47:19 +02:00 committed by GitHub
parent 27c977dcf7
commit 4f1c00122d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 253 additions and 0 deletions

View File

@ -0,0 +1,9 @@
import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type';
export class FakePersonalDashboardReadModel
implements IPersonalDashboardReadModel
{
async getPersonalFeatures(userId: number): Promise<{ name: string }[]> {
return [];
}
}

View File

@ -0,0 +1,63 @@
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import {
type IUnleashTest,
setupAppWithAuth,
} from '../../../test/e2e/helpers/test-helper';
import getLogger from '../../../test/fixtures/no-logger';
let app: IUnleashTest;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('personal_dashboard', getLogger);
app = await setupAppWithAuth(
db.stores,
{
experimental: {
flags: {},
},
},
db.rawDatabase,
);
});
const loginUser = (email: string) => {
return app.request
.post(`/auth/demo/login`)
.send({
email,
})
.expect(200);
};
afterAll(async () => {
await app.destroy();
await db.destroy();
});
beforeEach(async () => {
await db.stores.featureToggleStore.deleteAll();
});
test('should return personal dashboard with own flags and favorited flags', async () => {
await loginUser('other_user@getunleash.io');
await app.createFeature('other_feature_a');
await app.createFeature('other_feature_b');
await loginUser('my_user@getunleash.io');
await app.createFeature('my_feature_c');
await app.createFeature('my_feature_d');
await app.favoriteFeature('other_feature_b');
await app.favoriteFeature('my_feature_d');
const { body } = await app.request.get(`/api/admin/personal-dashboard`);
expect(body).toMatchObject({
projects: [],
flags: [
{ name: 'my_feature_c' },
{ name: 'my_feature_d' },
{ name: 'other_feature_b' },
],
});
});

View File

@ -0,0 +1,73 @@
import { type IUnleashConfig, type IUnleashServices, NONE } from '../../types';
import type { OpenApiService } from '../../services';
import {
createResponseSchema,
getStandardResponses,
personalDashboardSchema,
type PersonalDashboardSchema,
} from '../../openapi';
import Controller from '../../routes/controller';
import type { Response } from 'express';
import type { IAuthRequest } from '../../routes/unleash-types';
import type { PersonalDashboardService } from './personal-dashboard-service';
const PATH = '';
export default class PersonalDashboardController extends Controller {
private openApiService: OpenApiService;
private personalDashboardService: PersonalDashboardService;
constructor(
config: IUnleashConfig,
{
openApiService,
personalDashboardService,
}: Pick<
IUnleashServices,
'openApiService' | 'personalDashboardService'
>,
) {
super(config);
this.openApiService = openApiService;
this.personalDashboardService = personalDashboardService;
this.route({
method: 'get',
path: PATH,
handler: this.getPersonalDashboard,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Unstable'],
summary: 'Get personal dashboard',
description:
'Return all projects and flags that are relevant to the user.',
operationId: 'getPersonalDashboard',
responses: {
200: createResponseSchema('personalDashboardSchema'),
...getStandardResponses(401, 403, 404),
},
}),
],
});
}
async getPersonalDashboard(
req: IAuthRequest,
res: Response<PersonalDashboardSchema>,
): Promise<void> {
const user = req.user;
const flags = await this.personalDashboardService.getPersonalFeatures(
user.id,
);
this.openApiService.respondWithValidation(
200,
res,
personalDashboardSchema.$id,
{ projects: [], flags },
);
}
}

View File

@ -0,0 +1,3 @@
export interface IPersonalDashboardReadModel {
getPersonalFeatures(userId: number): Promise<{ name: string }[]>;
}

View File

@ -0,0 +1,22 @@
import type { Db } from '../../db/db';
import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type';
export class PersonalDashboardReadModel implements IPersonalDashboardReadModel {
private db: Db;
constructor(db: Db) {
this.db = db;
}
getPersonalFeatures(userId: number): Promise<{ name: string }[]> {
return this.db<{ name: string }>('favorite_features')
.where('favorite_features.user_id', userId)
.select('feature as name')
.union(function () {
this.select('name')
.from('features')
.where('features.created_by_user_id', userId);
})
.orderBy('name', 'asc');
}
}

View File

@ -0,0 +1,13 @@
import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type';
export class PersonalDashboardService {
private personalDashboardReadModel: IPersonalDashboardReadModel;
constructor(personalDashboardReadModel: IPersonalDashboardReadModel) {
this.personalDashboardReadModel = personalDashboardReadModel;
}
getPersonalFeatures(userId: number): Promise<{ name: string }[]> {
return this.personalDashboardReadModel.getPersonalFeatures(userId);
}
}

View File

@ -133,6 +133,7 @@ export * from './patch-schema';
export * from './patches-schema';
export * from './pats-schema';
export * from './permission-schema';
export * from './personal-dashboard-schema';
export * from './playground-constraint-schema';
export * from './playground-feature-schema';
export * from './playground-request-schema';

View File

@ -0,0 +1,51 @@
import type { FromSchema } from 'json-schema-to-ts';
export const personalDashboardSchema = {
$id: '#/components/schemas/personalDashboardSchema',
type: 'object',
description: 'Project and flags relevant to the user',
additionalProperties: false,
required: ['projects', 'flags'],
properties: {
projects: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['id'],
properties: {
id: {
type: 'string',
example: 'my-project-id',
description: 'The id of the project',
},
},
},
description:
'A list of projects that a user participates in with any role e.g. member or owner or any custom role',
},
flags: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['name'],
properties: {
name: {
type: 'string',
example: 'my-flag',
description: 'The name of the flag',
},
},
},
description: 'A list of flags a user created or favorited',
},
},
components: {
schemas: {},
},
} as const;
export type PersonalDashboardSchema = FromSchema<
typeof personalDashboardSchema
>;

View File

@ -35,6 +35,7 @@ import { SegmentsController } from '../../features/segment/segment-controller';
import { InactiveUsersController } from '../../users/inactive/inactive-users-controller';
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller';
import { SearchApi } from './search';
import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller';
export class AdminApi extends Controller {
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
@ -120,6 +121,10 @@ export class AdminApi extends Controller {
'/projects',
new ProjectController(config, services, db).router,
);
this.app.use(
'/personal-dashboard',
new PersonalDashboardController(config, services).router,
);
this.app.use(
'/environments',
new EnvironmentsController(config, services).router,

View File

@ -146,6 +146,9 @@ import {
createOnboardingService,
} from '../features/onboarding/createOnboardingService';
import { OnboardingService } from '../features/onboarding/onboarding-service';
import { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service';
import { PersonalDashboardReadModel } from '../features/personal-dashboard/personal-dashboard-read-model';
import { FakePersonalDashboardReadModel } from '../features/personal-dashboard/fake-personal-dashboard-read-model';
export const createServices = (
stores: IUnleashStores,
@ -401,6 +404,12 @@ export const createServices = (
: createFakeOnboardingService(config).onboardingService;
onboardingService.listen();
const personalDashboardService = new PersonalDashboardService(
db
? new PersonalDashboardReadModel(db)
: new FakePersonalDashboardReadModel(),
);
return {
accessService,
accountService,
@ -464,6 +473,7 @@ export const createServices = (
transactionalFeatureLifecycleService,
integrationEventsService,
onboardingService,
personalDashboardService,
};
};
@ -514,4 +524,5 @@ export {
FeatureLifecycleService,
IntegrationEventsService,
OnboardingService,
PersonalDashboardService,
};

View File

@ -56,6 +56,7 @@ import type { JobService } from '../features/scheduler/job-service';
import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service';
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
import type { OnboardingService } from '../features/onboarding/onboarding-service';
import type { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service';
export interface IUnleashServices {
accessService: AccessService;
@ -123,4 +124,5 @@ export interface IUnleashServices {
transactionalFeatureLifecycleService: WithTransactional<FeatureLifecycleService>;
integrationEventsService: IntegrationEventsService;
onboardingService: OnboardingService;
personalDashboardService: PersonalDashboardService;
}