1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-23 00:16:25 +01:00

refactor: remove change requests from project insights api (#6685)

This commit is contained in:
Mateusz Kwasniewski 2024-03-25 14:44:32 +01:00 committed by GitHub
parent 501da974d6
commit d4f52cdb54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 62 additions and 363 deletions

View File

@ -12,6 +12,18 @@ const setupEnterpriseApi = () => {
current: { enterprise: 'present' }, 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 = () => { 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 () => { test('Show enterprise hints', async () => {
setupOssApi(); setupOssApi();
render( render(
<Routes> <Routes>
<Route <Route path={'/projects/:projectId'} element={<ChangeRequests />} />
path={'/projects/:projectId'}
element={<ChangeRequests changeRequests={changeRequests} />}
/>
</Routes>, </Routes>,
{ {
route: '/projects/default', route: '/projects/default',
@ -52,10 +52,7 @@ test('Show change requests info', async () => {
setupEnterpriseApi(); setupEnterpriseApi();
render( render(
<Routes> <Routes>
<Route <Route path={'/projects/:projectId'} element={<ChangeRequests />} />
path={'/projects/:projectId'}
element={<ChangeRequests changeRequests={changeRequests} />}
/>
</Routes>, </Routes>,
{ {
route: '/projects/default', route: '/projects/default',
@ -63,4 +60,7 @@ test('Show change requests info', async () => {
); );
await screen.findByText('To be applied'); await screen.findByText('To be applied');
await screen.findByText('10');
await screen.findByText('4');
await screen.findByText('14');
}); });

View File

@ -4,8 +4,7 @@ import { Link } from 'react-router-dom';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import type { ProjectInsightsSchemaChangeRequests } from '../../../../../openapi'; import { useChangeRequestsCount } from 'hooks/api/getters/useChangeRequestsCount/useChangeRequestsCount';
import type { FC } from 'react';
const Container = styled(Box)(({ theme }) => ({ const Container = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
@ -83,14 +82,13 @@ const BigNumber = styled(Typography)(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
})); }));
export const ChangeRequests: FC<{ export const ChangeRequests = () => {
changeRequests: ProjectInsightsSchemaChangeRequests;
}> = ({ changeRequests }) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const { isOss, isPro } = useUiConfig(); const { isOss, isPro } = useUiConfig();
const { data } = useChangeRequestsCount(projectId);
const { total, applied, rejected, reviewRequired, scheduled, approved } = const { total, applied, rejected, reviewRequired, scheduled, approved } =
changeRequests; data;
const toBeApplied = scheduled + approved; const toBeApplied = scheduled + approved;
if (isOss() || isPro()) { if (isOss() || isPro()) {

View File

@ -61,9 +61,7 @@ export const ProjectInsights = () => {
<ProjectMembers projectId={projectId} members={data.members} /> <ProjectMembers projectId={projectId} members={data.members} />
</NarrowContainer> </NarrowContainer>
<WideContainer> <WideContainer>
{data.changeRequests && ( <ChangeRequests />
<ChangeRequests changeRequests={data.changeRequests} />
)}
</WideContainer> </WideContainer>
</Grid> </Grid>
); );

View File

@ -48,7 +48,7 @@ export const ProjectOverviewChangeRequests: FC<{ project: string }> = ({
useChangeRequestsEnabled(project); useChangeRequestsEnabled(project);
const { data } = useChangeRequestsCount(project); const { data } = useChangeRequestsCount(project);
if (!isChangeRequestConfiguredInAnyEnv) { if (!isChangeRequestConfiguredInAnyEnv()) {
return null; return null;
} }

View File

@ -49,14 +49,6 @@ const placeholderData: ProjectInsightsSchema = {
currentMembers: 0, currentMembers: 0,
change: 0, change: 0,
}, },
changeRequests: {
total: 0,
applied: 0,
approved: 0,
rejected: 0,
reviewRequired: 0,
scheduled: 0,
},
}; };
export const useProjectInsights = (projectId: string) => { export const useProjectInsights = (projectId: string) => {

View File

@ -879,7 +879,6 @@ export * from './projectCreatedSchemaMode';
export * from './projectDoraMetricsSchema'; export * from './projectDoraMetricsSchema';
export * from './projectEnvironmentSchema'; export * from './projectEnvironmentSchema';
export * from './projectInsightsSchema'; export * from './projectInsightsSchema';
export * from './projectInsightsSchemaChangeRequests';
export * from './projectInsightsSchemaHealth'; export * from './projectInsightsSchemaHealth';
export * from './projectInsightsSchemaMembers'; export * from './projectInsightsSchemaMembers';
export * from './projectOverviewSchema'; export * from './projectOverviewSchema';

View File

@ -3,7 +3,6 @@
* Do not edit manually. * Do not edit manually.
* See `gen:api` script in package.json * See `gen:api` script in package.json
*/ */
import type { ProjectInsightsSchemaChangeRequests } from './projectInsightsSchemaChangeRequests';
import type { FeatureTypeCountSchema } from './featureTypeCountSchema'; import type { FeatureTypeCountSchema } from './featureTypeCountSchema';
import type { ProjectInsightsSchemaHealth } from './projectInsightsSchemaHealth'; import type { ProjectInsightsSchemaHealth } from './projectInsightsSchemaHealth';
import type { ProjectDoraMetricsSchema } from './projectDoraMetricsSchema'; 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. * 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 { 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 */ /** The number of features of each type */
featureTypeCounts: FeatureTypeCountSchema[]; featureTypeCounts: FeatureTypeCountSchema[];
/** Health summary of the project */ /** Health summary of the project */

View File

@ -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;
};

View File

@ -8,8 +8,6 @@ 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';
import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store'; import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store';
import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store'; import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store';
@ -39,18 +37,14 @@ export const createProjectInsightsService = (
getLogger, getLogger,
flagResolver, flagResolver,
); );
const projectInsightsReadModel = new ProjectInsightsReadModel(db);
return new ProjectInsightsService( return new ProjectInsightsService({
{
projectStore, projectStore,
featureToggleStore, featureToggleStore,
featureTypeStore, featureTypeStore,
projectStatsStore, projectStatsStore,
featureStrategiesStore, featureStrategiesStore,
}, });
projectInsightsReadModel,
);
}; };
export const createFakeProjectInsightsService = () => { export const createFakeProjectInsightsService = () => {
@ -59,23 +53,18 @@ export const createFakeProjectInsightsService = () => {
const featureTypeStore = new FakeFeatureTypeStore(); const featureTypeStore = new FakeFeatureTypeStore();
const projectStatsStore = new FakeProjectStatsStore(); const projectStatsStore = new FakeProjectStatsStore();
const featureStrategiesStore = new FakeFeatureStrategiesStore(); const featureStrategiesStore = new FakeFeatureStrategiesStore();
const projectInsightsReadModel = new FakeProjectInsightsReadModel(); const projectInsightsService = new ProjectInsightsService({
const projectInsightsService = new ProjectInsightsService(
{
projectStore, projectStore,
featureToggleStore, featureToggleStore,
featureTypeStore, featureTypeStore,
projectStatsStore, projectStatsStore,
featureStrategiesStore, featureStrategiesStore,
}, });
projectInsightsReadModel,
);
return { return {
projectInsightsService, projectInsightsService,
projectStatsStore, projectStatsStore,
featureToggleStore, featureToggleStore,
projectStore, projectStore,
projectInsightsReadModel,
}; };
}; };

View File

@ -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;
}
}

View File

@ -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>;
}

View File

@ -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,
});
});

View File

@ -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);
}
}

View File

@ -6,21 +6,12 @@ test('Return basic insights', async () => {
projectStatsStore, projectStatsStore,
featureToggleStore, featureToggleStore,
projectStore, projectStore,
projectInsightsReadModel,
} = createFakeProjectInsightsService(); } = createFakeProjectInsightsService();
await featureToggleStore.create('default', { await featureToggleStore.create('default', {
name: 'irrelevant', name: 'irrelevant',
createdByUserId: 1, createdByUserId: 1,
type: 'release', type: 'release',
}); });
await projectInsightsReadModel.setChangeRequests('default', {
total: 5,
approved: 1,
applied: 1,
rejected: 1,
reviewRequired: 1,
scheduled: 1,
});
await projectStore.create({ await projectStore.create({
id: 'default', id: 'default',
name: 'irrelevant', name: 'irrelevant',
@ -57,14 +48,6 @@ test('Return basic insights', async () => {
rating: 100, rating: 100,
}, },
leadTime: { features: [], projectAverage: 0 }, leadTime: { features: [], projectAverage: 0 },
changeRequests: {
total: 5,
approved: 1,
applied: 1,
rejected: 1,
reviewRequired: 1,
scheduled: 1,
},
members: { currentMembers: 0, change: 0 }, members: { currentMembers: 0, change: 0 },
}); });
}); });

View File

@ -13,7 +13,6 @@ import type {
ProjectInsightsSchema, ProjectInsightsSchema,
} from '../../openapi'; } 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';
import { subDays } from 'date-fns'; import { subDays } from 'date-fns';
export class ProjectInsightsService { export class ProjectInsightsService {
@ -27,10 +26,7 @@ export class ProjectInsightsService {
private projectStatsStore: IProjectStatsStore; private projectStatsStore: IProjectStatsStore;
private projectInsightsReadModel: IProjectInsightsReadModel; constructor({
constructor(
{
projectStore, projectStore,
featureToggleStore, featureToggleStore,
featureTypeStore, featureTypeStore,
@ -43,15 +39,12 @@ export class ProjectInsightsService {
| 'projectStatsStore' | 'projectStatsStore'
| 'featureTypeStore' | 'featureTypeStore'
| 'featureStrategiesStore' | 'featureStrategiesStore'
>, >) {
projectInsightsReadModel: IProjectInsightsReadModel,
) {
this.projectStore = projectStore; this.projectStore = projectStore;
this.featureToggleStore = featureToggleStore; this.featureToggleStore = featureToggleStore;
this.featureTypeStore = featureTypeStore; this.featureTypeStore = featureTypeStore;
this.featureStrategiesStore = featureStrategiesStore; this.featureStrategiesStore = featureStrategiesStore;
this.projectStatsStore = projectStatsStore; this.projectStatsStore = projectStatsStore;
this.projectInsightsReadModel = projectInsightsReadModel;
} }
async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> { async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> {
@ -143,14 +136,8 @@ export class ProjectInsightsService {
} }
async getProjectInsights(projectId: string) { async getProjectInsights(projectId: string) {
const [ const [stats, featureTypeCounts, health, leadTime, members] =
stats, await Promise.all([
featureTypeCounts,
health,
leadTime,
changeRequests,
members,
] = await Promise.all([
this.projectStatsStore.getProjectStats(projectId), this.projectStatsStore.getProjectStats(projectId),
this.featureToggleStore.getFeatureTypeCounts({ this.featureToggleStore.getFeatureTypeCounts({
projectId, projectId,
@ -158,7 +145,6 @@ export class ProjectInsightsService {
}), }),
this.getHealthInsights(projectId), this.getHealthInsights(projectId),
this.getDoraMetrics(projectId), this.getDoraMetrics(projectId),
this.projectInsightsReadModel.getChangeRequests(projectId),
this.getProjectMembers(projectId), this.getProjectMembers(projectId),
]); ]);
@ -167,7 +153,6 @@ export class ProjectInsightsService {
featureTypeCounts, featureTypeCounts,
health, health,
leadTime, leadTime,
changeRequests,
members, members,
}; };
} }

View File

@ -53,13 +53,5 @@ 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,
},
}); });
}); });

View File

@ -80,53 +80,6 @@ export const projectInsightsSchema = {
}, },
description: 'Active/inactive users summary', 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: { components: {
schemas: { schemas: {