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:
parent
27c977dcf7
commit
4f1c00122d
@ -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 [];
|
||||||
|
}
|
||||||
|
}
|
@ -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' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
@ -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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export interface IPersonalDashboardReadModel {
|
||||||
|
getPersonalFeatures(userId: number): Promise<{ name: string }[]>;
|
||||||
|
}
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -133,6 +133,7 @@ export * from './patch-schema';
|
|||||||
export * from './patches-schema';
|
export * from './patches-schema';
|
||||||
export * from './pats-schema';
|
export * from './pats-schema';
|
||||||
export * from './permission-schema';
|
export * from './permission-schema';
|
||||||
|
export * from './personal-dashboard-schema';
|
||||||
export * from './playground-constraint-schema';
|
export * from './playground-constraint-schema';
|
||||||
export * from './playground-feature-schema';
|
export * from './playground-feature-schema';
|
||||||
export * from './playground-request-schema';
|
export * from './playground-request-schema';
|
||||||
|
51
src/lib/openapi/spec/personal-dashboard-schema.ts
Normal file
51
src/lib/openapi/spec/personal-dashboard-schema.ts
Normal 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
|
||||||
|
>;
|
@ -35,6 +35,7 @@ import { SegmentsController } from '../../features/segment/segment-controller';
|
|||||||
import { InactiveUsersController } from '../../users/inactive/inactive-users-controller';
|
import { InactiveUsersController } from '../../users/inactive/inactive-users-controller';
|
||||||
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller';
|
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller';
|
||||||
import { SearchApi } from './search';
|
import { SearchApi } from './search';
|
||||||
|
import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller';
|
||||||
|
|
||||||
export class AdminApi extends Controller {
|
export class AdminApi extends Controller {
|
||||||
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
||||||
@ -120,6 +121,10 @@ export class AdminApi extends Controller {
|
|||||||
'/projects',
|
'/projects',
|
||||||
new ProjectController(config, services, db).router,
|
new ProjectController(config, services, db).router,
|
||||||
);
|
);
|
||||||
|
this.app.use(
|
||||||
|
'/personal-dashboard',
|
||||||
|
new PersonalDashboardController(config, services).router,
|
||||||
|
);
|
||||||
this.app.use(
|
this.app.use(
|
||||||
'/environments',
|
'/environments',
|
||||||
new EnvironmentsController(config, services).router,
|
new EnvironmentsController(config, services).router,
|
||||||
|
@ -146,6 +146,9 @@ import {
|
|||||||
createOnboardingService,
|
createOnboardingService,
|
||||||
} from '../features/onboarding/createOnboardingService';
|
} from '../features/onboarding/createOnboardingService';
|
||||||
import { OnboardingService } from '../features/onboarding/onboarding-service';
|
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 = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -401,6 +404,12 @@ export const createServices = (
|
|||||||
: createFakeOnboardingService(config).onboardingService;
|
: createFakeOnboardingService(config).onboardingService;
|
||||||
onboardingService.listen();
|
onboardingService.listen();
|
||||||
|
|
||||||
|
const personalDashboardService = new PersonalDashboardService(
|
||||||
|
db
|
||||||
|
? new PersonalDashboardReadModel(db)
|
||||||
|
: new FakePersonalDashboardReadModel(),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
accountService,
|
accountService,
|
||||||
@ -464,6 +473,7 @@ export const createServices = (
|
|||||||
transactionalFeatureLifecycleService,
|
transactionalFeatureLifecycleService,
|
||||||
integrationEventsService,
|
integrationEventsService,
|
||||||
onboardingService,
|
onboardingService,
|
||||||
|
personalDashboardService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -514,4 +524,5 @@ export {
|
|||||||
FeatureLifecycleService,
|
FeatureLifecycleService,
|
||||||
IntegrationEventsService,
|
IntegrationEventsService,
|
||||||
OnboardingService,
|
OnboardingService,
|
||||||
|
PersonalDashboardService,
|
||||||
};
|
};
|
||||||
|
@ -56,6 +56,7 @@ import type { JobService } from '../features/scheduler/job-service';
|
|||||||
import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service';
|
import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service';
|
||||||
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
||||||
import type { OnboardingService } from '../features/onboarding/onboarding-service';
|
import type { OnboardingService } from '../features/onboarding/onboarding-service';
|
||||||
|
import type { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -123,4 +124,5 @@ export interface IUnleashServices {
|
|||||||
transactionalFeatureLifecycleService: WithTransactional<FeatureLifecycleService>;
|
transactionalFeatureLifecycleService: WithTransactional<FeatureLifecycleService>;
|
||||||
integrationEventsService: IntegrationEventsService;
|
integrationEventsService: IntegrationEventsService;
|
||||||
onboardingService: OnboardingService;
|
onboardingService: OnboardingService;
|
||||||
|
personalDashboardService: PersonalDashboardService;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user