mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
refactor: remove change requests from project insights api (#6685)
This commit is contained in:
parent
501da974d6
commit
d4f52cdb54
@ -12,6 +12,18 @@ const setupEnterpriseApi = () => {
|
||||
current: { enterprise: 'present' },
|
||||
},
|
||||
});
|
||||
testServerRoute(
|
||||
server,
|
||||
'/api/admin/projects/default/change-requests/count',
|
||||
{
|
||||
total: 14,
|
||||
approved: 2,
|
||||
applied: 0,
|
||||
rejected: 0,
|
||||
reviewRequired: 10,
|
||||
scheduled: 2,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setupOssApi = () => {
|
||||
@ -22,23 +34,11 @@ const setupOssApi = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const changeRequests = {
|
||||
applied: 0,
|
||||
total: 0,
|
||||
approved: 0,
|
||||
scheduled: 0,
|
||||
reviewRequired: 0,
|
||||
rejected: 0,
|
||||
};
|
||||
|
||||
test('Show enterprise hints', async () => {
|
||||
setupOssApi();
|
||||
render(
|
||||
<Routes>
|
||||
<Route
|
||||
path={'/projects/:projectId'}
|
||||
element={<ChangeRequests changeRequests={changeRequests} />}
|
||||
/>
|
||||
<Route path={'/projects/:projectId'} element={<ChangeRequests />} />
|
||||
</Routes>,
|
||||
{
|
||||
route: '/projects/default',
|
||||
@ -52,10 +52,7 @@ test('Show change requests info', async () => {
|
||||
setupEnterpriseApi();
|
||||
render(
|
||||
<Routes>
|
||||
<Route
|
||||
path={'/projects/:projectId'}
|
||||
element={<ChangeRequests changeRequests={changeRequests} />}
|
||||
/>
|
||||
<Route path={'/projects/:projectId'} element={<ChangeRequests />} />
|
||||
</Routes>,
|
||||
{
|
||||
route: '/projects/default',
|
||||
@ -63,4 +60,7 @@ test('Show change requests info', async () => {
|
||||
);
|
||||
|
||||
await screen.findByText('To be applied');
|
||||
await screen.findByText('10');
|
||||
await screen.findByText('4');
|
||||
await screen.findByText('14');
|
||||
});
|
||||
|
@ -4,8 +4,7 @@ import { Link } from 'react-router-dom';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||
import type { ProjectInsightsSchemaChangeRequests } from '../../../../../openapi';
|
||||
import type { FC } from 'react';
|
||||
import { useChangeRequestsCount } from 'hooks/api/getters/useChangeRequestsCount/useChangeRequestsCount';
|
||||
|
||||
const Container = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -83,14 +82,13 @@ const BigNumber = styled(Typography)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const ChangeRequests: FC<{
|
||||
changeRequests: ProjectInsightsSchemaChangeRequests;
|
||||
}> = ({ changeRequests }) => {
|
||||
export const ChangeRequests = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { isOss, isPro } = useUiConfig();
|
||||
const { data } = useChangeRequestsCount(projectId);
|
||||
|
||||
const { total, applied, rejected, reviewRequired, scheduled, approved } =
|
||||
changeRequests;
|
||||
data;
|
||||
const toBeApplied = scheduled + approved;
|
||||
|
||||
if (isOss() || isPro()) {
|
||||
|
@ -61,9 +61,7 @@ export const ProjectInsights = () => {
|
||||
<ProjectMembers projectId={projectId} members={data.members} />
|
||||
</NarrowContainer>
|
||||
<WideContainer>
|
||||
{data.changeRequests && (
|
||||
<ChangeRequests changeRequests={data.changeRequests} />
|
||||
)}
|
||||
<ChangeRequests />
|
||||
</WideContainer>
|
||||
</Grid>
|
||||
);
|
||||
|
@ -48,7 +48,7 @@ export const ProjectOverviewChangeRequests: FC<{ project: string }> = ({
|
||||
useChangeRequestsEnabled(project);
|
||||
const { data } = useChangeRequestsCount(project);
|
||||
|
||||
if (!isChangeRequestConfiguredInAnyEnv) {
|
||||
if (!isChangeRequestConfiguredInAnyEnv()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -49,14 +49,6 @@ const placeholderData: ProjectInsightsSchema = {
|
||||
currentMembers: 0,
|
||||
change: 0,
|
||||
},
|
||||
changeRequests: {
|
||||
total: 0,
|
||||
applied: 0,
|
||||
approved: 0,
|
||||
rejected: 0,
|
||||
reviewRequired: 0,
|
||||
scheduled: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const useProjectInsights = (projectId: string) => {
|
||||
|
@ -879,7 +879,6 @@ export * from './projectCreatedSchemaMode';
|
||||
export * from './projectDoraMetricsSchema';
|
||||
export * from './projectEnvironmentSchema';
|
||||
export * from './projectInsightsSchema';
|
||||
export * from './projectInsightsSchemaChangeRequests';
|
||||
export * from './projectInsightsSchemaHealth';
|
||||
export * from './projectInsightsSchemaMembers';
|
||||
export * from './projectOverviewSchema';
|
||||
|
@ -3,7 +3,6 @@
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
import type { ProjectInsightsSchemaChangeRequests } from './projectInsightsSchemaChangeRequests';
|
||||
import type { FeatureTypeCountSchema } from './featureTypeCountSchema';
|
||||
import type { ProjectInsightsSchemaHealth } from './projectInsightsSchemaHealth';
|
||||
import type { ProjectDoraMetricsSchema } from './projectDoraMetricsSchema';
|
||||
@ -14,8 +13,6 @@ import type { ProjectStatsSchema } from './projectStatsSchema';
|
||||
* A high-level overview of a project insights. It contains information such as project statistics, overall health, types of flags, members overview, change requests overview.
|
||||
*/
|
||||
export interface ProjectInsightsSchema {
|
||||
/** Count of change requests in different stages of the [process](https://docs.getunleash.io/reference/change-requests#change-request-flow). Only for enterprise users. */
|
||||
changeRequests?: ProjectInsightsSchemaChangeRequests;
|
||||
/** The number of features of each type */
|
||||
featureTypeCounts: FeatureTypeCountSchema[];
|
||||
/** Health summary of the project */
|
||||
|
@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* Count of change requests in different stages of the [process](https://docs.getunleash.io/reference/change-requests#change-request-flow). Only for enterprise users.
|
||||
*/
|
||||
export type ProjectInsightsSchemaChangeRequests = {
|
||||
/** The number of applied change requests */
|
||||
applied: number;
|
||||
/** The number of approved change requests */
|
||||
approved: number;
|
||||
/** The number of rejected change requests */
|
||||
rejected: number;
|
||||
/** The number of change requests awaiting the review */
|
||||
reviewRequired: number;
|
||||
/** The number of scheduled change requests */
|
||||
scheduled: number;
|
||||
/** The number of total change requests in this project */
|
||||
total: number;
|
||||
};
|
@ -8,8 +8,6 @@ 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';
|
||||
import { ProjectInsightsReadModel } from './project-insights-read-model';
|
||||
import { FakeProjectInsightsReadModel } from './fake-project-insights-read-model';
|
||||
import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store';
|
||||
import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store';
|
||||
|
||||
@ -39,18 +37,14 @@ export const createProjectInsightsService = (
|
||||
getLogger,
|
||||
flagResolver,
|
||||
);
|
||||
const projectInsightsReadModel = new ProjectInsightsReadModel(db);
|
||||
|
||||
return new ProjectInsightsService(
|
||||
{
|
||||
projectStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
projectStatsStore,
|
||||
featureStrategiesStore,
|
||||
},
|
||||
projectInsightsReadModel,
|
||||
);
|
||||
return new ProjectInsightsService({
|
||||
projectStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
projectStatsStore,
|
||||
featureStrategiesStore,
|
||||
});
|
||||
};
|
||||
|
||||
export const createFakeProjectInsightsService = () => {
|
||||
@ -59,23 +53,18 @@ export const createFakeProjectInsightsService = () => {
|
||||
const featureTypeStore = new FakeFeatureTypeStore();
|
||||
const projectStatsStore = new FakeProjectStatsStore();
|
||||
const featureStrategiesStore = new FakeFeatureStrategiesStore();
|
||||
const projectInsightsReadModel = new FakeProjectInsightsReadModel();
|
||||
const projectInsightsService = new ProjectInsightsService(
|
||||
{
|
||||
projectStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
projectStatsStore,
|
||||
featureStrategiesStore,
|
||||
},
|
||||
projectInsightsReadModel,
|
||||
);
|
||||
const projectInsightsService = new ProjectInsightsService({
|
||||
projectStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
projectStatsStore,
|
||||
featureStrategiesStore,
|
||||
});
|
||||
|
||||
return {
|
||||
projectInsightsService,
|
||||
projectStatsStore,
|
||||
featureToggleStore,
|
||||
projectStore,
|
||||
projectInsightsReadModel,
|
||||
};
|
||||
};
|
||||
|
@ -1,25 +0,0 @@
|
||||
import type {
|
||||
ChangeRequestCounts,
|
||||
IProjectInsightsReadModel,
|
||||
} from './project-insights-read-model-type';
|
||||
|
||||
const changeRequestCounts: ChangeRequestCounts = {
|
||||
total: 0,
|
||||
approved: 0,
|
||||
applied: 0,
|
||||
rejected: 0,
|
||||
reviewRequired: 0,
|
||||
scheduled: 0,
|
||||
};
|
||||
|
||||
export class FakeProjectInsightsReadModel implements IProjectInsightsReadModel {
|
||||
private counts: Record<string, ChangeRequestCounts> = {};
|
||||
|
||||
async getChangeRequests(projectId: string): Promise<ChangeRequestCounts> {
|
||||
return this.counts[projectId] ?? changeRequestCounts;
|
||||
}
|
||||
|
||||
async setChangeRequests(projectId: string, counts: ChangeRequestCounts) {
|
||||
this.counts[projectId] = counts;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
export type ChangeRequestCounts = {
|
||||
total: number;
|
||||
approved: number;
|
||||
applied: number;
|
||||
rejected: number;
|
||||
reviewRequired: number;
|
||||
scheduled: number;
|
||||
};
|
||||
|
||||
export interface IProjectInsightsReadModel {
|
||||
getChangeRequests(projectId: string): Promise<ChangeRequestCounts>;
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import type { IUser } from '../../types';
|
||||
import type { IProjectInsightsReadModel } from './project-insights-read-model-type';
|
||||
import {
|
||||
type ChangeRequestDBState,
|
||||
ProjectInsightsReadModel,
|
||||
} from './project-insights-read-model';
|
||||
|
||||
let projectInsightsReadModel: IProjectInsightsReadModel;
|
||||
let user: IUser;
|
||||
let db: ITestDb;
|
||||
const projectId = 'default';
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('project_insights_read_model', getLogger);
|
||||
projectInsightsReadModel = new ProjectInsightsReadModel(db.rawDatabase);
|
||||
user = await db.stores.userStore.insert({
|
||||
username: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.rawDatabase.table('change_requests').delete();
|
||||
});
|
||||
|
||||
const createChangeRequest = (id: number, state: string) =>
|
||||
db.rawDatabase.table('change_requests').insert({
|
||||
id,
|
||||
state,
|
||||
environment: 'default',
|
||||
project: projectId,
|
||||
created_by: user.id,
|
||||
});
|
||||
|
||||
test('can read change request status counts', async () => {
|
||||
const states: ChangeRequestDBState[] = [
|
||||
'Approved',
|
||||
'Approved',
|
||||
'Applied',
|
||||
'Rejected',
|
||||
'Scheduled',
|
||||
'In review',
|
||||
'Draft',
|
||||
'Cancelled',
|
||||
];
|
||||
await Promise.all(
|
||||
states.map((state, id) => createChangeRequest(id, state)),
|
||||
);
|
||||
|
||||
const changeRequests =
|
||||
await projectInsightsReadModel.getChangeRequests(projectId);
|
||||
|
||||
expect(changeRequests).toEqual({
|
||||
total: 6,
|
||||
approved: 2,
|
||||
applied: 1,
|
||||
rejected: 1,
|
||||
reviewRequired: 1,
|
||||
scheduled: 1,
|
||||
});
|
||||
});
|
@ -1,61 +0,0 @@
|
||||
import type {
|
||||
ChangeRequestCounts,
|
||||
IProjectInsightsReadModel,
|
||||
} from './project-insights-read-model-type';
|
||||
import type { Db } from '../../db/db';
|
||||
|
||||
export type ChangeRequestDBState =
|
||||
| 'Draft'
|
||||
| 'Cancelled'
|
||||
| '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);
|
||||
}
|
||||
}
|
@ -6,21 +6,12 @@ test('Return basic insights', async () => {
|
||||
projectStatsStore,
|
||||
featureToggleStore,
|
||||
projectStore,
|
||||
projectInsightsReadModel,
|
||||
} = createFakeProjectInsightsService();
|
||||
await featureToggleStore.create('default', {
|
||||
name: 'irrelevant',
|
||||
createdByUserId: 1,
|
||||
type: 'release',
|
||||
});
|
||||
await projectInsightsReadModel.setChangeRequests('default', {
|
||||
total: 5,
|
||||
approved: 1,
|
||||
applied: 1,
|
||||
rejected: 1,
|
||||
reviewRequired: 1,
|
||||
scheduled: 1,
|
||||
});
|
||||
await projectStore.create({
|
||||
id: 'default',
|
||||
name: 'irrelevant',
|
||||
@ -57,14 +48,6 @@ test('Return basic insights', async () => {
|
||||
rating: 100,
|
||||
},
|
||||
leadTime: { features: [], projectAverage: 0 },
|
||||
changeRequests: {
|
||||
total: 5,
|
||||
approved: 1,
|
||||
applied: 1,
|
||||
rejected: 1,
|
||||
reviewRequired: 1,
|
||||
scheduled: 1,
|
||||
},
|
||||
members: { currentMembers: 0, change: 0 },
|
||||
});
|
||||
});
|
||||
|
@ -13,7 +13,6 @@ import type {
|
||||
ProjectInsightsSchema,
|
||||
} from '../../openapi';
|
||||
import { calculateProjectHealth } from '../../domain/project-health/project-health';
|
||||
import type { IProjectInsightsReadModel } from './project-insights-read-model-type';
|
||||
import { subDays } from 'date-fns';
|
||||
|
||||
export class ProjectInsightsService {
|
||||
@ -27,31 +26,25 @@ export class ProjectInsightsService {
|
||||
|
||||
private projectStatsStore: IProjectStatsStore;
|
||||
|
||||
private projectInsightsReadModel: IProjectInsightsReadModel;
|
||||
|
||||
constructor(
|
||||
{
|
||||
projectStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
projectStatsStore,
|
||||
featureStrategiesStore,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
| 'projectStore'
|
||||
| 'featureToggleStore'
|
||||
| 'projectStatsStore'
|
||||
| 'featureTypeStore'
|
||||
| 'featureStrategiesStore'
|
||||
>,
|
||||
projectInsightsReadModel: IProjectInsightsReadModel,
|
||||
) {
|
||||
constructor({
|
||||
projectStore,
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
projectStatsStore,
|
||||
featureStrategiesStore,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
| 'projectStore'
|
||||
| 'featureToggleStore'
|
||||
| 'projectStatsStore'
|
||||
| 'featureTypeStore'
|
||||
| 'featureStrategiesStore'
|
||||
>) {
|
||||
this.projectStore = projectStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.featureStrategiesStore = featureStrategiesStore;
|
||||
this.projectStatsStore = projectStatsStore;
|
||||
this.projectInsightsReadModel = projectInsightsReadModel;
|
||||
}
|
||||
|
||||
async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> {
|
||||
@ -143,31 +136,23 @@ export class ProjectInsightsService {
|
||||
}
|
||||
|
||||
async getProjectInsights(projectId: string) {
|
||||
const [
|
||||
stats,
|
||||
featureTypeCounts,
|
||||
health,
|
||||
leadTime,
|
||||
changeRequests,
|
||||
members,
|
||||
] = await Promise.all([
|
||||
this.projectStatsStore.getProjectStats(projectId),
|
||||
this.featureToggleStore.getFeatureTypeCounts({
|
||||
projectId,
|
||||
archived: false,
|
||||
}),
|
||||
this.getHealthInsights(projectId),
|
||||
this.getDoraMetrics(projectId),
|
||||
this.projectInsightsReadModel.getChangeRequests(projectId),
|
||||
this.getProjectMembers(projectId),
|
||||
]);
|
||||
const [stats, featureTypeCounts, health, leadTime, members] =
|
||||
await Promise.all([
|
||||
this.projectStatsStore.getProjectStats(projectId),
|
||||
this.featureToggleStore.getFeatureTypeCounts({
|
||||
projectId,
|
||||
archived: false,
|
||||
}),
|
||||
this.getHealthInsights(projectId),
|
||||
this.getDoraMetrics(projectId),
|
||||
this.getProjectMembers(projectId),
|
||||
]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
featureTypeCounts,
|
||||
health,
|
||||
leadTime,
|
||||
changeRequests,
|
||||
members,
|
||||
};
|
||||
}
|
||||
|
@ -53,13 +53,5 @@ test('project insights happy path', async () => {
|
||||
staleCount: 0,
|
||||
rating: 100,
|
||||
},
|
||||
changeRequests: {
|
||||
total: 0,
|
||||
approved: 0,
|
||||
applied: 0,
|
||||
rejected: 0,
|
||||
reviewRequired: 0,
|
||||
scheduled: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -80,53 +80,6 @@ export const projectInsightsSchema = {
|
||||
},
|
||||
description: 'Active/inactive users summary',
|
||||
},
|
||||
changeRequests: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'total',
|
||||
'applied',
|
||||
'rejected',
|
||||
'reviewRequired',
|
||||
'approved',
|
||||
'scheduled',
|
||||
],
|
||||
properties: {
|
||||
total: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The number of total change requests in this project',
|
||||
example: 10,
|
||||
},
|
||||
applied: {
|
||||
type: 'number',
|
||||
description: 'The number of applied change requests',
|
||||
example: 5,
|
||||
},
|
||||
rejected: {
|
||||
type: 'number',
|
||||
description: 'The number of rejected change requests',
|
||||
example: 2,
|
||||
},
|
||||
reviewRequired: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The number of change requests awaiting the review',
|
||||
example: 2,
|
||||
},
|
||||
approved: {
|
||||
type: 'number',
|
||||
description: 'The number of approved change requests',
|
||||
example: 1,
|
||||
},
|
||||
scheduled: {
|
||||
type: 'number',
|
||||
description: 'The number of scheduled change requests',
|
||||
example: 1,
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Count of change requests in different stages of the [process](https://docs.getunleash.io/reference/change-requests#change-request-flow). Only for enterprise users.',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
|
Loading…
Reference in New Issue
Block a user