mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
feat: read change requests for insights (#6651)
This commit is contained in:
parent
fb88048acf
commit
8080a1d907
@ -12,6 +12,8 @@ import FeatureTypeStore from '../../db/feature-type-store';
|
|||||||
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
|
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
|
||||||
import { ProjectInsightsService } from './project-insights-service';
|
import { ProjectInsightsService } from './project-insights-service';
|
||||||
import ProjectStore from '../project/project-store';
|
import ProjectStore from '../project/project-store';
|
||||||
|
import { ProjectInsightsReadModel } from './project-insights-read-model';
|
||||||
|
import { FakeProjectInsightsReadModel } from './fake-project-insights-read-model';
|
||||||
|
|
||||||
export const createProjectInsightsService = (
|
export const createProjectInsightsService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -34,6 +36,7 @@ export const createProjectInsightsService = (
|
|||||||
const featureTypeStore = new FeatureTypeStore(db, getLogger);
|
const featureTypeStore = new FeatureTypeStore(db, getLogger);
|
||||||
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
|
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
|
||||||
const featureToggleService = createFeatureToggleService(db, config);
|
const featureToggleService = createFeatureToggleService(db, config);
|
||||||
|
const projectInsightsReadModel = new ProjectInsightsReadModel(db);
|
||||||
|
|
||||||
return new ProjectInsightsService(
|
return new ProjectInsightsService(
|
||||||
{
|
{
|
||||||
@ -43,6 +46,7 @@ export const createProjectInsightsService = (
|
|||||||
projectStatsStore,
|
projectStatsStore,
|
||||||
},
|
},
|
||||||
featureToggleService,
|
featureToggleService,
|
||||||
|
projectInsightsReadModel,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,6 +58,7 @@ export const createFakeProjectInsightsService = (
|
|||||||
const featureTypeStore = new FakeFeatureTypeStore();
|
const featureTypeStore = new FakeFeatureTypeStore();
|
||||||
const projectStatsStore = new FakeProjectStatsStore();
|
const projectStatsStore = new FakeProjectStatsStore();
|
||||||
const featureToggleService = createFakeFeatureToggleService(config);
|
const featureToggleService = createFakeFeatureToggleService(config);
|
||||||
|
const projectInsightsReadModel = new FakeProjectInsightsReadModel();
|
||||||
|
|
||||||
return new ProjectInsightsService(
|
return new ProjectInsightsService(
|
||||||
{
|
{
|
||||||
@ -63,5 +68,6 @@ export const createFakeProjectInsightsService = (
|
|||||||
projectStatsStore,
|
projectStatsStore,
|
||||||
},
|
},
|
||||||
featureToggleService,
|
featureToggleService,
|
||||||
|
projectInsightsReadModel,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import type {
|
||||||
|
ChangeRequestCounts,
|
||||||
|
IProjectInsightsReadModel,
|
||||||
|
} from './project-insights-read-model-type';
|
||||||
|
|
||||||
|
const changeRequestCounts: ChangeRequestCounts = {
|
||||||
|
total: 0,
|
||||||
|
approved: 0,
|
||||||
|
applied: 0,
|
||||||
|
rejected: 0,
|
||||||
|
reviewRequired: 10,
|
||||||
|
scheduled: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FakeProjectInsightsReadModel implements IProjectInsightsReadModel {
|
||||||
|
async getChangeRequests(projectId: string): Promise<ChangeRequestCounts> {
|
||||||
|
return changeRequestCounts;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
export type ChangeRequestCounts = {
|
||||||
|
total: number;
|
||||||
|
approved: number;
|
||||||
|
applied: number;
|
||||||
|
rejected: number;
|
||||||
|
reviewRequired: number;
|
||||||
|
scheduled: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IProjectInsightsReadModel {
|
||||||
|
getChangeRequests(projectId: string): Promise<ChangeRequestCounts>;
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import type {
|
||||||
|
ChangeRequestCounts,
|
||||||
|
IProjectInsightsReadModel,
|
||||||
|
} from './project-insights-read-model-type';
|
||||||
|
import type { Db } from '../../db/db';
|
||||||
|
|
||||||
|
export type ChangeRequestDBState =
|
||||||
|
| 'Approved'
|
||||||
|
| 'In review'
|
||||||
|
| 'Applied'
|
||||||
|
| 'Scheduled'
|
||||||
|
| 'Rejected';
|
||||||
|
|
||||||
|
export class ProjectInsightsReadModel implements IProjectInsightsReadModel {
|
||||||
|
private db: Db;
|
||||||
|
|
||||||
|
constructor(db: Db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChangeRequests(projectId: string): Promise<ChangeRequestCounts> {
|
||||||
|
const changeRequestCounts: ChangeRequestCounts = {
|
||||||
|
total: 0,
|
||||||
|
approved: 0,
|
||||||
|
applied: 0,
|
||||||
|
rejected: 0,
|
||||||
|
reviewRequired: 0,
|
||||||
|
scheduled: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows: Array<{ state: ChangeRequestDBState; count: string }> =
|
||||||
|
await this.db('change_requests')
|
||||||
|
.select('state')
|
||||||
|
.count('* as count')
|
||||||
|
.where('project', '=', projectId)
|
||||||
|
.groupBy('state');
|
||||||
|
|
||||||
|
return rows.reduce((acc, current) => {
|
||||||
|
if (current.state === 'Applied') {
|
||||||
|
acc.applied = Number(current.count);
|
||||||
|
acc.total += Number(current.count);
|
||||||
|
} else if (current.state === 'Approved') {
|
||||||
|
acc.approved = Number(current.count);
|
||||||
|
acc.total += Number(current.count);
|
||||||
|
} else if (current.state === 'Rejected') {
|
||||||
|
acc.rejected = Number(current.count);
|
||||||
|
acc.total += Number(current.count);
|
||||||
|
} else if (current.state === 'In review') {
|
||||||
|
acc.reviewRequired = Number(current.count);
|
||||||
|
acc.total += Number(current.count);
|
||||||
|
} else if (current.state === 'Scheduled') {
|
||||||
|
acc.scheduled = Number(current.count);
|
||||||
|
acc.total += Number(current.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, changeRequestCounts);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import { calculateAverageTimeToProd } from '../feature-toggle/time-to-production
|
|||||||
import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type';
|
import type { IProjectStatsStore } from '../../types/stores/project-stats-store-type';
|
||||||
import type { ProjectDoraMetricsSchema } from '../../openapi';
|
import type { ProjectDoraMetricsSchema } from '../../openapi';
|
||||||
import { calculateProjectHealth } from '../../domain/project-health/project-health';
|
import { calculateProjectHealth } from '../../domain/project-health/project-health';
|
||||||
|
import type { IProjectInsightsReadModel } from './project-insights-read-model-type';
|
||||||
|
|
||||||
export class ProjectInsightsService {
|
export class ProjectInsightsService {
|
||||||
private projectStore: IProjectStore;
|
private projectStore: IProjectStore;
|
||||||
@ -22,6 +23,8 @@ export class ProjectInsightsService {
|
|||||||
|
|
||||||
private projectStatsStore: IProjectStatsStore;
|
private projectStatsStore: IProjectStatsStore;
|
||||||
|
|
||||||
|
private projectInsightsReadModel: IProjectInsightsReadModel;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
projectStore,
|
projectStore,
|
||||||
@ -36,12 +39,14 @@ export class ProjectInsightsService {
|
|||||||
| 'featureTypeStore'
|
| 'featureTypeStore'
|
||||||
>,
|
>,
|
||||||
featureToggleService: FeatureToggleService,
|
featureToggleService: FeatureToggleService,
|
||||||
|
projectInsightsReadModel: IProjectInsightsReadModel,
|
||||||
) {
|
) {
|
||||||
this.projectStore = projectStore;
|
this.projectStore = projectStore;
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.featureTypeStore = featureTypeStore;
|
this.featureTypeStore = featureTypeStore;
|
||||||
this.featureToggleService = featureToggleService;
|
this.featureToggleService = featureToggleService;
|
||||||
this.projectStatsStore = projectStatsStore;
|
this.projectStatsStore = projectStatsStore;
|
||||||
|
this.projectInsightsReadModel = projectInsightsReadModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDoraMetrics(
|
private async getDoraMetrics(
|
||||||
@ -103,27 +108,28 @@ export class ProjectInsightsService {
|
|||||||
inactive: 3,
|
inactive: 3,
|
||||||
totalPreviousMonth: 15,
|
totalPreviousMonth: 15,
|
||||||
},
|
},
|
||||||
changeRequests: {
|
|
||||||
total: 24,
|
|
||||||
approved: 5,
|
|
||||||
applied: 2,
|
|
||||||
rejected: 4,
|
|
||||||
reviewRequired: 10,
|
|
||||||
scheduled: 3,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [stats, featureTypeCounts, health, leadTime] = await Promise.all([
|
const [stats, featureTypeCounts, health, leadTime, changeRequests] =
|
||||||
this.projectStatsStore.getProjectStats(projectId),
|
await Promise.all([
|
||||||
this.featureToggleService.getFeatureTypeCounts({
|
this.projectStatsStore.getProjectStats(projectId),
|
||||||
projectId,
|
this.featureToggleService.getFeatureTypeCounts({
|
||||||
archived: false,
|
projectId,
|
||||||
}),
|
archived: false,
|
||||||
this.getHealthInsights(projectId),
|
}),
|
||||||
this.getDoraMetrics(projectId),
|
this.getHealthInsights(projectId),
|
||||||
]);
|
this.getDoraMetrics(projectId),
|
||||||
|
this.projectInsightsReadModel.getChangeRequests(projectId),
|
||||||
|
]);
|
||||||
|
|
||||||
return { ...result, stats, featureTypeCounts, health, leadTime };
|
return {
|
||||||
|
...result,
|
||||||
|
stats,
|
||||||
|
featureTypeCounts,
|
||||||
|
health,
|
||||||
|
leadTime,
|
||||||
|
changeRequests,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getProjectHealth(
|
private async getProjectHealth(
|
||||||
|
@ -34,6 +34,8 @@ test('project insights happy path', async () => {
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
console.log(body);
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
stats: {
|
stats: {
|
||||||
avgTimeToProdCurrentWindow: 0,
|
avgTimeToProdCurrentWindow: 0,
|
||||||
@ -53,5 +55,13 @@ test('project insights happy path', async () => {
|
|||||||
staleCount: 0,
|
staleCount: 0,
|
||||||
rating: 100,
|
rating: 100,
|
||||||
},
|
},
|
||||||
|
changeRequests: {
|
||||||
|
total: 0,
|
||||||
|
approved: 0,
|
||||||
|
applied: 0,
|
||||||
|
rejected: 0,
|
||||||
|
reviewRequired: 0,
|
||||||
|
scheduled: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -76,7 +76,6 @@ import type {
|
|||||||
IProjectEnterpriseSettingsUpdate,
|
IProjectEnterpriseSettingsUpdate,
|
||||||
IProjectQuery,
|
IProjectQuery,
|
||||||
} from './project-store-type';
|
} from './project-store-type';
|
||||||
import { calculateProjectHealth } from '../../domain/project-health/project-health';
|
|
||||||
|
|
||||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||||
|
|
||||||
@ -1240,62 +1239,6 @@ export default class ProjectService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {
|
|
||||||
leadTime: {
|
|
||||||
projectAverage: 17.1,
|
|
||||||
features: [
|
|
||||||
{ name: 'feature1', timeToProduction: 120 },
|
|
||||||
{ name: 'feature2', timeToProduction: 0 },
|
|
||||||
{ name: 'feature3', timeToProduction: 33 },
|
|
||||||
{ name: 'feature4', timeToProduction: 131 },
|
|
||||||
{ name: 'feature5', timeToProduction: 2 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
members: {
|
|
||||||
active: 20,
|
|
||||||
inactive: 3,
|
|
||||||
totalPreviousMonth: 15,
|
|
||||||
},
|
|
||||||
changeRequests: {
|
|
||||||
total: 24,
|
|
||||||
approved: 5,
|
|
||||||
applied: 2,
|
|
||||||
rejected: 4,
|
|
||||||
reviewRequired: 10,
|
|
||||||
scheduled: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const [stats, featureTypeCounts, health] = await Promise.all([
|
|
||||||
this.projectStatsStore.getProjectStats(projectId),
|
|
||||||
this.featureToggleService.getFeatureTypeCounts({
|
|
||||||
projectId,
|
|
||||||
archived: false,
|
|
||||||
}),
|
|
||||||
this.getHealthInsights(projectId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { ...result, stats, featureTypeCounts, health };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProjectHealth(
|
async getProjectHealth(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
|
Loading…
Reference in New Issue
Block a user