mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-27 00:19:39 +01:00
feat: expose stats, health and flag types insights (#6630)
This commit is contained in:
parent
1becfc0202
commit
6dc6e36084
@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
|
import type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const Dot = styled('span', {
|
const Dot = styled('span', {
|
||||||
shouldForwardProp: (prop) => prop !== 'color',
|
shouldForwardProp: (prop) => prop !== 'color',
|
||||||
@ -48,10 +49,16 @@ export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Typography variant='h3'>Project Health</Typography>
|
<Typography variant='h3'>Project Health</Typography>
|
||||||
<Alert severity='warning'>
|
<ConditionallyRender
|
||||||
<b>Health alert!</b> Review your flags and delete the stale
|
condition={staleCount > 0}
|
||||||
flags
|
show={
|
||||||
</Alert>
|
<Alert severity='warning'>
|
||||||
|
<b>Health alert!</b> Review your flags and delete the
|
||||||
|
stale flags
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
data-loading
|
data-loading
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
|
@ -39,6 +39,8 @@ import {
|
|||||||
createPrivateProjectChecker,
|
createPrivateProjectChecker,
|
||||||
} from '../private-project/createPrivateProjectChecker';
|
} from '../private-project/createPrivateProjectChecker';
|
||||||
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
|
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
|
||||||
|
import FeatureTypeStore from '../../db/feature-type-store';
|
||||||
|
import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store';
|
||||||
|
|
||||||
export const createProjectService = (
|
export const createProjectService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -66,6 +68,7 @@ export const createProjectService = (
|
|||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
);
|
);
|
||||||
|
const featureTypeStore = new FeatureTypeStore(db, getLogger);
|
||||||
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
|
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
|
||||||
const accessService: AccessService = createAccessService(db, config);
|
const accessService: AccessService = createAccessService(db, config);
|
||||||
const featureToggleService = createFeatureToggleService(db, config);
|
const featureToggleService = createFeatureToggleService(db, config);
|
||||||
@ -109,6 +112,7 @@ export const createProjectService = (
|
|||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
featureEnvironmentStore,
|
featureEnvironmentStore,
|
||||||
|
featureTypeStore,
|
||||||
accountStore,
|
accountStore,
|
||||||
projectStatsStore,
|
projectStatsStore,
|
||||||
},
|
},
|
||||||
@ -133,6 +137,7 @@ export const createFakeProjectService = (
|
|||||||
const accountStore = new FakeAccountStore();
|
const accountStore = new FakeAccountStore();
|
||||||
const environmentStore = new FakeEnvironmentStore();
|
const environmentStore = new FakeEnvironmentStore();
|
||||||
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
|
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
|
||||||
|
const featureTypeStore = new FakeFeatureTypeStore();
|
||||||
const projectStatsStore = new FakeProjectStatsStore();
|
const projectStatsStore = new FakeProjectStatsStore();
|
||||||
const { accessService } = createFakeAccessService(config);
|
const { accessService } = createFakeAccessService(config);
|
||||||
const featureToggleService = createFakeFeatureToggleService(config);
|
const featureToggleService = createFakeFeatureToggleService(config);
|
||||||
@ -168,6 +173,7 @@ export const createFakeProjectService = (
|
|||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
featureEnvironmentStore,
|
featureEnvironmentStore,
|
||||||
|
featureTypeStore,
|
||||||
accountStore,
|
accountStore,
|
||||||
projectStatsStore,
|
projectStatsStore,
|
||||||
},
|
},
|
||||||
|
@ -248,67 +248,15 @@ export default class ProjectController extends Controller {
|
|||||||
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
|
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
|
||||||
res: Response<ProjectInsightsSchema>,
|
res: Response<ProjectInsightsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const result = {
|
const { projectId } = req.params;
|
||||||
stats: {
|
const insights =
|
||||||
avgTimeToProdCurrentWindow: 17.1,
|
await this.projectService.getProjectInsights(projectId);
|
||||||
createdCurrentWindow: 3,
|
|
||||||
createdPastWindow: 6,
|
|
||||||
archivedCurrentWindow: 0,
|
|
||||||
archivedPastWindow: 1,
|
|
||||||
projectActivityCurrentWindow: 458,
|
|
||||||
projectActivityPastWindow: 578,
|
|
||||||
projectMembersAddedCurrentWindow: 0,
|
|
||||||
},
|
|
||||||
featureTypeCounts: [
|
|
||||||
{
|
|
||||||
type: 'experiment',
|
|
||||||
count: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'permission',
|
|
||||||
count: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'release',
|
|
||||||
count: 24,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
health: {
|
|
||||||
rating: 80,
|
|
||||||
activeCount: 23,
|
|
||||||
potentiallyStaleCount: 3,
|
|
||||||
staleCount: 5,
|
|
||||||
},
|
|
||||||
members: {
|
|
||||||
active: 20,
|
|
||||||
inactive: 3,
|
|
||||||
totalPreviousMonth: 15,
|
|
||||||
},
|
|
||||||
changeRequests: {
|
|
||||||
total: 24,
|
|
||||||
approved: 5,
|
|
||||||
applied: 2,
|
|
||||||
rejected: 4,
|
|
||||||
reviewRequired: 10,
|
|
||||||
scheduled: 3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
res,
|
res,
|
||||||
projectInsightsSchema.$id,
|
projectInsightsSchema.$id,
|
||||||
serializeDates(result),
|
serializeDates(insights),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
type IFeatureEnvironmentStore,
|
type IFeatureEnvironmentStore,
|
||||||
type IFeatureNaming,
|
type IFeatureNaming,
|
||||||
type IFeatureToggleStore,
|
type IFeatureToggleStore,
|
||||||
|
type IFeatureTypeStore,
|
||||||
type IFlagResolver,
|
type IFlagResolver,
|
||||||
type IProject,
|
type IProject,
|
||||||
type IProjectApplications,
|
type IProjectApplications,
|
||||||
@ -75,6 +76,7 @@ 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';
|
||||||
|
|
||||||
@ -119,6 +121,8 @@ export default class ProjectService {
|
|||||||
|
|
||||||
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
||||||
|
|
||||||
|
private featureTypeStore: IFeatureTypeStore;
|
||||||
|
|
||||||
private environmentStore: IEnvironmentStore;
|
private environmentStore: IEnvironmentStore;
|
||||||
|
|
||||||
private groupService: GroupService;
|
private groupService: GroupService;
|
||||||
@ -148,6 +152,7 @@ export default class ProjectService {
|
|||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
environmentStore,
|
environmentStore,
|
||||||
featureEnvironmentStore,
|
featureEnvironmentStore,
|
||||||
|
featureTypeStore,
|
||||||
accountStore,
|
accountStore,
|
||||||
projectStatsStore,
|
projectStatsStore,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
@ -159,6 +164,7 @@ export default class ProjectService {
|
|||||||
| 'featureEnvironmentStore'
|
| 'featureEnvironmentStore'
|
||||||
| 'accountStore'
|
| 'accountStore'
|
||||||
| 'projectStatsStore'
|
| 'projectStatsStore'
|
||||||
|
| 'featureTypeStore'
|
||||||
>,
|
>,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
accessService: AccessService,
|
accessService: AccessService,
|
||||||
@ -174,6 +180,7 @@ export default class ProjectService {
|
|||||||
this.accessService = accessService;
|
this.accessService = accessService;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
|
this.featureTypeStore = featureTypeStore;
|
||||||
this.featureToggleService = featureToggleService;
|
this.featureToggleService = featureToggleService;
|
||||||
this.favoritesService = favoriteService;
|
this.favoritesService = favoriteService;
|
||||||
this.privateProjectChecker = privateProjectChecker;
|
this.privateProjectChecker = privateProjectChecker;
|
||||||
@ -722,6 +729,7 @@ export default class ProjectService {
|
|||||||
(r) => r.project === project && r.name === RoleName.OWNER,
|
(r) => r.project === project && r.name === RoleName.OWNER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async isAllowedToAddAccess(
|
private async isAllowedToAddAccess(
|
||||||
userAddingAccess: number,
|
userAddingAccess: number,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -741,6 +749,7 @@ export default class ProjectService {
|
|||||||
userRoles.some((userRole) => userRole.id === roleId),
|
userRoles.some((userRole) => userRole.id === roleId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccess(
|
async addAccess(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roles: number[],
|
roles: number[],
|
||||||
@ -1231,6 +1240,62 @@ 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,
|
||||||
|
@ -294,8 +294,23 @@ test('project insights happy path', async () => {
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(body.leadTime.features[0]).toEqual({
|
expect(body).toMatchObject({
|
||||||
name: 'feature1',
|
stats: {
|
||||||
timeToProduction: 120,
|
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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user