mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-08 01:15:49 +02:00
refactor: Project insights subdomain (#6634)
This commit is contained in:
parent
efb2df78c2
commit
87b9f4f713
@ -0,0 +1,67 @@
|
|||||||
|
import type { Db, IUnleashConfig } from '../../server-impl';
|
||||||
|
import FeatureToggleStore from '../feature-toggle/feature-toggle-store';
|
||||||
|
import ProjectStatsStore from '../../db/project-stats-store';
|
||||||
|
import {
|
||||||
|
createFakeFeatureToggleService,
|
||||||
|
createFeatureToggleService,
|
||||||
|
} from '../feature-toggle/createFeatureToggleService';
|
||||||
|
import FakeProjectStore from '../../../test/fixtures/fake-project-store';
|
||||||
|
import FakeFeatureToggleStore from '../feature-toggle/fakes/fake-feature-toggle-store';
|
||||||
|
import FakeProjectStatsStore from '../../../test/fixtures/fake-project-stats-store';
|
||||||
|
import FeatureTypeStore from '../../db/feature-type-store';
|
||||||
|
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
|
||||||
|
import { ProjectInsightsService } from './project-insights-service';
|
||||||
|
import ProjectStore from '../project/project-store';
|
||||||
|
|
||||||
|
export const createProjectInsightsService = (
|
||||||
|
db: Db,
|
||||||
|
config: IUnleashConfig,
|
||||||
|
): ProjectInsightsService => {
|
||||||
|
const { eventBus, getLogger, flagResolver } = config;
|
||||||
|
const projectStore = new ProjectStore(
|
||||||
|
db,
|
||||||
|
eventBus,
|
||||||
|
getLogger,
|
||||||
|
flagResolver,
|
||||||
|
);
|
||||||
|
const featureToggleStore = new FeatureToggleStore(
|
||||||
|
db,
|
||||||
|
eventBus,
|
||||||
|
getLogger,
|
||||||
|
flagResolver,
|
||||||
|
);
|
||||||
|
|
||||||
|
const featureTypeStore = new FeatureTypeStore(db, getLogger);
|
||||||
|
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
|
||||||
|
const featureToggleService = createFeatureToggleService(db, config);
|
||||||
|
|
||||||
|
return new ProjectInsightsService(
|
||||||
|
{
|
||||||
|
projectStore,
|
||||||
|
featureToggleStore,
|
||||||
|
featureTypeStore,
|
||||||
|
projectStatsStore,
|
||||||
|
},
|
||||||
|
featureToggleService,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFakeProjectInsightsService = (
|
||||||
|
config: IUnleashConfig,
|
||||||
|
): ProjectInsightsService => {
|
||||||
|
const projectStore = new FakeProjectStore();
|
||||||
|
const featureToggleStore = new FakeFeatureToggleStore();
|
||||||
|
const featureTypeStore = new FakeFeatureTypeStore();
|
||||||
|
const projectStatsStore = new FakeProjectStatsStore();
|
||||||
|
const featureToggleService = createFakeFeatureToggleService(config);
|
||||||
|
|
||||||
|
return new ProjectInsightsService(
|
||||||
|
{
|
||||||
|
projectStore,
|
||||||
|
featureToggleStore,
|
||||||
|
featureTypeStore,
|
||||||
|
projectStatsStore,
|
||||||
|
},
|
||||||
|
featureToggleService,
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,70 @@
|
|||||||
|
import type { Response } from 'express';
|
||||||
|
import Controller from '../../routes/controller';
|
||||||
|
import {
|
||||||
|
type IFlagResolver,
|
||||||
|
type IProjectParam,
|
||||||
|
type IUnleashConfig,
|
||||||
|
type IUnleashServices,
|
||||||
|
NONE,
|
||||||
|
serializeDates,
|
||||||
|
} from '../../types';
|
||||||
|
import type { ProjectInsightsService } from './project-insights-service';
|
||||||
|
import {
|
||||||
|
createResponseSchema,
|
||||||
|
projectInsightsSchema,
|
||||||
|
type ProjectInsightsSchema,
|
||||||
|
} from '../../openapi';
|
||||||
|
import { getStandardResponses } from '../../openapi/util/standard-responses';
|
||||||
|
import type { OpenApiService } from '../../services';
|
||||||
|
import type { IAuthRequest } from '../../routes/unleash-types';
|
||||||
|
|
||||||
|
export default class ProjectInsightsController extends Controller {
|
||||||
|
private projectInsightsService: ProjectInsightsService;
|
||||||
|
|
||||||
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
|
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||||
|
super(config);
|
||||||
|
this.projectInsightsService = services.projectInsightsService;
|
||||||
|
this.openApiService = services.openApiService;
|
||||||
|
this.flagResolver = config.flagResolver;
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: '/:projectId/insights',
|
||||||
|
handler: this.getProjectInsights,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
this.openApiService.validPath({
|
||||||
|
tags: ['Unstable'],
|
||||||
|
operationId: 'getProjectInsights',
|
||||||
|
summary: 'Get an overview of a project insights.',
|
||||||
|
description:
|
||||||
|
'This endpoint returns insights into the specified projects stats, health, lead time for changes, feature types used, members and change requests.',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('projectInsightsSchema'),
|
||||||
|
...getStandardResponses(401, 403, 404),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectInsights(
|
||||||
|
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
|
||||||
|
res: Response<ProjectInsightsSchema>,
|
||||||
|
): Promise<void> {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
const insights =
|
||||||
|
await this.projectInsightsService.getProjectInsights(projectId);
|
||||||
|
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
projectInsightsSchema.$id,
|
||||||
|
serializeDates(insights),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
164
src/lib/features/project-insights/project-insights-service.ts
Normal file
164
src/lib/features/project-insights/project-insights-service.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import type {
|
||||||
|
IFeatureToggleStore,
|
||||||
|
IFeatureTypeStore,
|
||||||
|
IProjectHealth,
|
||||||
|
IProjectStore,
|
||||||
|
IUnleashStores,
|
||||||
|
} from '../../types';
|
||||||
|
import type FeatureToggleService from '../feature-toggle/feature-toggle-service';
|
||||||
|
import { calculateAverageTimeToProd } from '../feature-toggle/time-to-production/time-to-production';
|
||||||
|
import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type';
|
||||||
|
import type { ProjectDoraMetricsSchema } from '../../openapi';
|
||||||
|
import { calculateProjectHealth } from '../../domain/project-health/project-health';
|
||||||
|
|
||||||
|
export class ProjectInsightsService {
|
||||||
|
private projectStore: IProjectStore;
|
||||||
|
|
||||||
|
private featureToggleStore: IFeatureToggleStore;
|
||||||
|
|
||||||
|
private featureTypeStore: IFeatureTypeStore;
|
||||||
|
|
||||||
|
private featureToggleService: FeatureToggleService;
|
||||||
|
|
||||||
|
private projectStatsStore: IProjectStatsStore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
projectStore,
|
||||||
|
featureToggleStore,
|
||||||
|
featureTypeStore,
|
||||||
|
projectStatsStore,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashStores,
|
||||||
|
| 'projectStore'
|
||||||
|
| 'featureToggleStore'
|
||||||
|
| 'projectStatsStore'
|
||||||
|
| 'featureTypeStore'
|
||||||
|
>,
|
||||||
|
featureToggleService: FeatureToggleService,
|
||||||
|
) {
|
||||||
|
this.projectStore = projectStore;
|
||||||
|
this.featureToggleStore = featureToggleStore;
|
||||||
|
this.featureTypeStore = featureTypeStore;
|
||||||
|
this.featureToggleService = featureToggleService;
|
||||||
|
this.projectStatsStore = projectStatsStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getDoraMetrics(
|
||||||
|
projectId: string,
|
||||||
|
): Promise<ProjectDoraMetricsSchema> {
|
||||||
|
const activeFeatureToggles = (
|
||||||
|
await this.featureToggleStore.getAll({ project: projectId })
|
||||||
|
).map((feature) => feature.name);
|
||||||
|
|
||||||
|
const archivedFeatureToggles = (
|
||||||
|
await this.featureToggleStore.getAll({
|
||||||
|
project: projectId,
|
||||||
|
archived: true,
|
||||||
|
})
|
||||||
|
).map((feature) => feature.name);
|
||||||
|
|
||||||
|
const featureToggleNames = [
|
||||||
|
...activeFeatureToggles,
|
||||||
|
...archivedFeatureToggles,
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectAverage = calculateAverageTimeToProd(
|
||||||
|
await this.projectStatsStore.getTimeToProdDates(projectId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleAverage =
|
||||||
|
await this.projectStatsStore.getTimeToProdDatesForFeatureToggles(
|
||||||
|
projectId,
|
||||||
|
featureToggleNames,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
features: toggleAverage,
|
||||||
|
projectAverage: projectAverage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getHealthInsights(projectId: string) {
|
||||||
|
const [overview, featureTypes] = await Promise.all([
|
||||||
|
this.getProjectHealth(projectId, false, undefined),
|
||||||
|
this.featureTypeStore.getAll(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { activeCount, potentiallyStaleCount, staleCount } =
|
||||||
|
calculateProjectHealth(overview.features, featureTypes);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeCount,
|
||||||
|
potentiallyStaleCount,
|
||||||
|
staleCount,
|
||||||
|
rating: overview.health,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectInsights(projectId: string) {
|
||||||
|
const result = {
|
||||||
|
members: {
|
||||||
|
active: 20,
|
||||||
|
inactive: 3,
|
||||||
|
totalPreviousMonth: 15,
|
||||||
|
},
|
||||||
|
changeRequests: {
|
||||||
|
total: 24,
|
||||||
|
approved: 5,
|
||||||
|
applied: 2,
|
||||||
|
rejected: 4,
|
||||||
|
reviewRequired: 10,
|
||||||
|
scheduled: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [stats, featureTypeCounts, health, leadTime] = await Promise.all([
|
||||||
|
this.projectStatsStore.getProjectStats(projectId),
|
||||||
|
this.featureToggleService.getFeatureTypeCounts({
|
||||||
|
projectId,
|
||||||
|
archived: false,
|
||||||
|
}),
|
||||||
|
this.getHealthInsights(projectId),
|
||||||
|
this.getDoraMetrics(projectId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { ...result, stats, featureTypeCounts, health, leadTime };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getProjectHealth(
|
||||||
|
projectId: string,
|
||||||
|
archived: boolean = false,
|
||||||
|
userId?: number,
|
||||||
|
): Promise<IProjectHealth> {
|
||||||
|
const [project, environments, features, members, projectStats] =
|
||||||
|
await Promise.all([
|
||||||
|
this.projectStore.get(projectId),
|
||||||
|
this.projectStore.getEnvironmentsForProject(projectId),
|
||||||
|
this.featureToggleService.getFeatureOverview({
|
||||||
|
projectId,
|
||||||
|
archived,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
this.projectStore.getMembersCountByProject(projectId),
|
||||||
|
this.projectStatsStore.getProjectStats(projectId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stats: projectStats,
|
||||||
|
name: project.name,
|
||||||
|
description: project.description!,
|
||||||
|
mode: project.mode,
|
||||||
|
featureLimit: project.featureLimit,
|
||||||
|
featureNaming: project.featureNaming,
|
||||||
|
defaultStickiness: project.defaultStickiness,
|
||||||
|
health: project.health || 0,
|
||||||
|
updatedAt: project.updatedAt,
|
||||||
|
createdAt: project.createdAt,
|
||||||
|
environments,
|
||||||
|
features: features,
|
||||||
|
members,
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
|
import {
|
||||||
|
type IUnleashTest,
|
||||||
|
setupAppWithCustomConfig,
|
||||||
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
|
|
||||||
|
let app: IUnleashTest;
|
||||||
|
let db: ITestDb;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('projects_insights', getLogger);
|
||||||
|
app = await setupAppWithCustomConfig(
|
||||||
|
db.stores,
|
||||||
|
{
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
strictSchemaValidation: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
db.rawDatabase,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('project insights happy path', async () => {
|
||||||
|
const { body } = await app.request
|
||||||
|
.get('/api/admin/projects/default/insights')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
stats: {
|
||||||
|
avgTimeToProdCurrentWindow: 0,
|
||||||
|
createdCurrentWindow: 0,
|
||||||
|
createdPastWindow: 0,
|
||||||
|
archivedCurrentWindow: 0,
|
||||||
|
archivedPastWindow: 0,
|
||||||
|
projectActivityCurrentWindow: 0,
|
||||||
|
projectActivityPastWindow: 0,
|
||||||
|
projectMembersAddedCurrentWindow: 0,
|
||||||
|
},
|
||||||
|
leadTime: { features: [], projectAverage: 0 },
|
||||||
|
featureTypeCounts: [],
|
||||||
|
health: {
|
||||||
|
activeCount: 0,
|
||||||
|
potentiallyStaleCount: 0,
|
||||||
|
staleCount: 0,
|
||||||
|
rating: 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
@ -20,8 +20,6 @@ import {
|
|||||||
deprecatedProjectOverviewSchema,
|
deprecatedProjectOverviewSchema,
|
||||||
type ProjectDoraMetricsSchema,
|
type ProjectDoraMetricsSchema,
|
||||||
projectDoraMetricsSchema,
|
projectDoraMetricsSchema,
|
||||||
projectInsightsSchema,
|
|
||||||
type ProjectInsightsSchema,
|
|
||||||
projectOverviewSchema,
|
projectOverviewSchema,
|
||||||
type ProjectsSchema,
|
type ProjectsSchema,
|
||||||
projectsSchema,
|
projectsSchema,
|
||||||
@ -42,6 +40,7 @@ import {
|
|||||||
import { NotFoundError } from '../../error';
|
import { NotFoundError } from '../../error';
|
||||||
import { projectApplicationsQueryParameters } from '../../openapi/spec/project-applications-query-parameters';
|
import { projectApplicationsQueryParameters } from '../../openapi/spec/project-applications-query-parameters';
|
||||||
import { normalizeQueryParams } from '../feature-search/search-utils';
|
import { normalizeQueryParams } from '../feature-search/search-utils';
|
||||||
|
import ProjectInsightsController from '../project-insights/project-insights-controller';
|
||||||
|
|
||||||
export default class ProjectController extends Controller {
|
export default class ProjectController extends Controller {
|
||||||
private projectService: ProjectService;
|
private projectService: ProjectService;
|
||||||
@ -119,26 +118,6 @@ export default class ProjectController extends Controller {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route({
|
|
||||||
method: 'get',
|
|
||||||
path: '/:projectId/insights',
|
|
||||||
handler: this.getProjectInsights,
|
|
||||||
permission: NONE,
|
|
||||||
middleware: [
|
|
||||||
this.openApiService.validPath({
|
|
||||||
tags: ['Unstable'],
|
|
||||||
operationId: 'getProjectInsights',
|
|
||||||
summary: 'Get an overview of a project insights.',
|
|
||||||
description:
|
|
||||||
'This endpoint returns insights into the specified projects stats, health, lead time for changes, feature types used, members and change requests.',
|
|
||||||
responses: {
|
|
||||||
200: createResponseSchema('projectInsightsSchema'),
|
|
||||||
...getStandardResponses(401, 403, 404),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/:projectId/dora',
|
path: '/:projectId/dora',
|
||||||
@ -201,6 +180,7 @@ export default class ProjectController extends Controller {
|
|||||||
createKnexTransactionStarter(db),
|
createKnexTransactionStarter(db),
|
||||||
).router,
|
).router,
|
||||||
);
|
);
|
||||||
|
this.use('/', new ProjectInsightsController(config, services).router);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(
|
async getProjects(
|
||||||
@ -244,22 +224,6 @@ export default class ProjectController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectInsights(
|
|
||||||
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
|
|
||||||
res: Response<ProjectInsightsSchema>,
|
|
||||||
): Promise<void> {
|
|
||||||
const { projectId } = req.params;
|
|
||||||
const insights =
|
|
||||||
await this.projectService.getProjectInsights(projectId);
|
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
|
||||||
200,
|
|
||||||
res,
|
|
||||||
projectInsightsSchema.$id,
|
|
||||||
serializeDates(insights),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectOverview(
|
async getProjectOverview(
|
||||||
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
|
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
|
||||||
res: Response<ProjectOverviewSchema>,
|
res: Response<ProjectOverviewSchema>,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
import {
|
import {
|
||||||
type IUnleashTest,
|
|
||||||
insertFeatureEnvironmentsLastSeen,
|
insertFeatureEnvironmentsLastSeen,
|
||||||
insertLastSeenAt,
|
insertLastSeenAt,
|
||||||
|
type IUnleashTest,
|
||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../../test/e2e/helpers/test-helper';
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
@ -287,30 +287,3 @@ test('response should include last seen at per environment for multiple environm
|
|||||||
|
|
||||||
expect(body.features[1].lastSeenAt).toBe('2023-10-01T12:34:56.000Z');
|
expect(body.features[1].lastSeenAt).toBe('2023-10-01T12:34:56.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('project insights happy path', async () => {
|
|
||||||
const { body } = await app.request
|
|
||||||
.get('/api/admin/projects/default/insights')
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
stats: {
|
|
||||||
avgTimeToProdCurrentWindow: 0,
|
|
||||||
createdCurrentWindow: 0,
|
|
||||||
createdPastWindow: 0,
|
|
||||||
archivedCurrentWindow: 0,
|
|
||||||
archivedPastWindow: 0,
|
|
||||||
projectActivityCurrentWindow: 0,
|
|
||||||
projectActivityPastWindow: 0,
|
|
||||||
projectMembersAddedCurrentWindow: 0,
|
|
||||||
},
|
|
||||||
featureTypeCounts: [],
|
|
||||||
health: {
|
|
||||||
activeCount: 0,
|
|
||||||
potentiallyStaleCount: 0,
|
|
||||||
staleCount: 0,
|
|
||||||
rating: 100,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -45,6 +45,7 @@ import { FavoritesService } from './favorites-service';
|
|||||||
import MaintenanceService from '../features/maintenance/maintenance-service';
|
import MaintenanceService from '../features/maintenance/maintenance-service';
|
||||||
import { AccountService } from './account-service';
|
import { AccountService } from './account-service';
|
||||||
import { SchedulerService } from '../features/scheduler/scheduler-service';
|
import { SchedulerService } from '../features/scheduler/scheduler-service';
|
||||||
|
import { ProjectInsightsService } from '../features/project-insights/project-insights-service';
|
||||||
import type { Knex } from 'knex';
|
import type { Knex } from 'knex';
|
||||||
import {
|
import {
|
||||||
createExportImportTogglesService,
|
createExportImportTogglesService,
|
||||||
@ -119,6 +120,10 @@ import {
|
|||||||
createFakeFrontendApiService,
|
createFakeFrontendApiService,
|
||||||
createFrontendApiService,
|
createFrontendApiService,
|
||||||
} from '../features/frontend-api/createFrontendApiService';
|
} from '../features/frontend-api/createFrontendApiService';
|
||||||
|
import {
|
||||||
|
createFakeProjectInsightsService,
|
||||||
|
createProjectInsightsService,
|
||||||
|
} from '../features/project-insights/createProjectInsightsService';
|
||||||
|
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -263,6 +268,9 @@ export const createServices = (
|
|||||||
const projectService = db
|
const projectService = db
|
||||||
? createProjectService(db, config)
|
? createProjectService(db, config)
|
||||||
: createFakeProjectService(config);
|
: createFakeProjectService(config);
|
||||||
|
const projectInsightsService = db
|
||||||
|
? createProjectInsightsService(db, config)
|
||||||
|
: createFakeProjectInsightsService(config);
|
||||||
|
|
||||||
const projectHealthService = new ProjectHealthService(
|
const projectHealthService = new ProjectHealthService(
|
||||||
stores,
|
stores,
|
||||||
@ -397,6 +405,7 @@ export const createServices = (
|
|||||||
clientFeatureToggleService,
|
clientFeatureToggleService,
|
||||||
featureSearchService,
|
featureSearchService,
|
||||||
inactiveUsersService,
|
inactiveUsersService,
|
||||||
|
projectInsightsService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -443,4 +452,5 @@ export {
|
|||||||
DependentFeaturesService,
|
DependentFeaturesService,
|
||||||
ClientFeatureToggleService,
|
ClientFeatureToggleService,
|
||||||
FeatureSearchService,
|
FeatureSearchService,
|
||||||
|
ProjectInsightsService,
|
||||||
};
|
};
|
||||||
|
@ -52,6 +52,7 @@ import type { WithTransactional } from '../db/transaction';
|
|||||||
import type { ClientFeatureToggleService } from '../features/client-feature-toggles/client-feature-toggle-service';
|
import type { ClientFeatureToggleService } from '../features/client-feature-toggles/client-feature-toggle-service';
|
||||||
import type { FeatureSearchService } from '../features/feature-search/feature-search-service';
|
import type { FeatureSearchService } from '../features/feature-search/feature-search-service';
|
||||||
import type { InactiveUsersService } from '../users/inactive/inactive-users-service';
|
import type { InactiveUsersService } from '../users/inactive/inactive-users-service';
|
||||||
|
import type { ProjectInsightsService } from '../features/project-insights/project-insights-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -113,4 +114,5 @@ export interface IUnleashServices {
|
|||||||
clientFeatureToggleService: ClientFeatureToggleService;
|
clientFeatureToggleService: ClientFeatureToggleService;
|
||||||
featureSearchService: FeatureSearchService;
|
featureSearchService: FeatureSearchService;
|
||||||
inactiveUsersService: InactiveUsersService;
|
inactiveUsersService: InactiveUsersService;
|
||||||
|
projectInsightsService: ProjectInsightsService;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user