mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +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 type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
|
||||
import type { FC } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
const Dot = styled('span', {
|
||||
shouldForwardProp: (prop) => prop !== 'color',
|
||||
@ -48,10 +49,16 @@ export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant='h3'>Project Health</Typography>
|
||||
<Alert severity='warning'>
|
||||
<b>Health alert!</b> Review your flags and delete the stale
|
||||
flags
|
||||
</Alert>
|
||||
<ConditionallyRender
|
||||
condition={staleCount > 0}
|
||||
show={
|
||||
<Alert severity='warning'>
|
||||
<b>Health alert!</b> Review your flags and delete the
|
||||
stale flags
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
|
||||
<Box
|
||||
data-loading
|
||||
sx={(theme) => ({
|
||||
|
@ -39,6 +39,8 @@ import {
|
||||
createPrivateProjectChecker,
|
||||
} from '../private-project/createPrivateProjectChecker';
|
||||
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 = (
|
||||
db: Db,
|
||||
@ -66,6 +68,7 @@ export const createProjectService = (
|
||||
eventBus,
|
||||
getLogger,
|
||||
);
|
||||
const featureTypeStore = new FeatureTypeStore(db, getLogger);
|
||||
const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger);
|
||||
const accessService: AccessService = createAccessService(db, config);
|
||||
const featureToggleService = createFeatureToggleService(db, config);
|
||||
@ -109,6 +112,7 @@ export const createProjectService = (
|
||||
featureToggleStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
featureTypeStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
},
|
||||
@ -133,6 +137,7 @@ export const createFakeProjectService = (
|
||||
const accountStore = new FakeAccountStore();
|
||||
const environmentStore = new FakeEnvironmentStore();
|
||||
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
|
||||
const featureTypeStore = new FakeFeatureTypeStore();
|
||||
const projectStatsStore = new FakeProjectStatsStore();
|
||||
const { accessService } = createFakeAccessService(config);
|
||||
const featureToggleService = createFakeFeatureToggleService(config);
|
||||
@ -168,6 +173,7 @@ export const createFakeProjectService = (
|
||||
featureToggleStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
featureTypeStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
},
|
||||
|
@ -248,67 +248,15 @@ export default class ProjectController extends Controller {
|
||||
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
|
||||
res: Response<ProjectInsightsSchema>,
|
||||
): Promise<void> {
|
||||
const result = {
|
||||
stats: {
|
||||
avgTimeToProdCurrentWindow: 17.1,
|
||||
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,
|
||||
},
|
||||
};
|
||||
const { projectId } = req.params;
|
||||
const insights =
|
||||
await this.projectService.getProjectInsights(projectId);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
projectInsightsSchema.$id,
|
||||
serializeDates(result),
|
||||
serializeDates(insights),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
type IFeatureEnvironmentStore,
|
||||
type IFeatureNaming,
|
||||
type IFeatureToggleStore,
|
||||
type IFeatureTypeStore,
|
||||
type IFlagResolver,
|
||||
type IProject,
|
||||
type IProjectApplications,
|
||||
@ -75,6 +76,7 @@ import type {
|
||||
IProjectEnterpriseSettingsUpdate,
|
||||
IProjectQuery,
|
||||
} from './project-store-type';
|
||||
import { calculateProjectHealth } from '../../domain/project-health/project-health';
|
||||
|
||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||
|
||||
@ -119,6 +121,8 @@ export default class ProjectService {
|
||||
|
||||
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
||||
|
||||
private featureTypeStore: IFeatureTypeStore;
|
||||
|
||||
private environmentStore: IEnvironmentStore;
|
||||
|
||||
private groupService: GroupService;
|
||||
@ -148,6 +152,7 @@ export default class ProjectService {
|
||||
featureToggleStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
featureTypeStore,
|
||||
accountStore,
|
||||
projectStatsStore,
|
||||
}: Pick<
|
||||
@ -159,6 +164,7 @@ export default class ProjectService {
|
||||
| 'featureEnvironmentStore'
|
||||
| 'accountStore'
|
||||
| 'projectStatsStore'
|
||||
| 'featureTypeStore'
|
||||
>,
|
||||
config: IUnleashConfig,
|
||||
accessService: AccessService,
|
||||
@ -174,6 +180,7 @@ export default class ProjectService {
|
||||
this.accessService = accessService;
|
||||
this.eventStore = eventStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.featureToggleService = featureToggleService;
|
||||
this.favoritesService = favoriteService;
|
||||
this.privateProjectChecker = privateProjectChecker;
|
||||
@ -722,6 +729,7 @@ export default class ProjectService {
|
||||
(r) => r.project === project && r.name === RoleName.OWNER,
|
||||
);
|
||||
}
|
||||
|
||||
private async isAllowedToAddAccess(
|
||||
userAddingAccess: number,
|
||||
projectId: string,
|
||||
@ -741,6 +749,7 @@ export default class ProjectService {
|
||||
userRoles.some((userRole) => userRole.id === roleId),
|
||||
);
|
||||
}
|
||||
|
||||
async addAccess(
|
||||
projectId: string,
|
||||
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(
|
||||
projectId: string,
|
||||
archived: boolean = false,
|
||||
|
@ -294,8 +294,23 @@ test('project insights happy path', async () => {
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
expect(body.leadTime.features[0]).toEqual({
|
||||
name: 'feature1',
|
||||
timeToProduction: 120,
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user