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

feat: new project overview backend (#5344)

Adding new project overview endpoint and deprecating the old one.
The new one has extra info about feature types, but does not have
features anymore, because features are coming from search endpoint.
This commit is contained in:
Jaanus Sellin 2023-12-01 11:20:24 +02:00 committed by GitHub
parent 9f3648dc81
commit 63f6af06da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 532 additions and 35 deletions

View File

@ -8,11 +8,13 @@ import {
FeatureToggleDTO,
IFeatureEnvironment,
IFeatureToggleQuery,
IFeatureTypeCount,
IVariant,
} from 'lib/types/model';
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
import { EnvironmentFeatureNames } from '../feature-toggle-store';
import { FeatureConfigurationClient } from '../types/feature-toggle-strategies-store-type';
import { IFeatureProjectUserParams } from '../feature-toggle-controller';
export default class FakeFeatureToggleStore implements IFeatureToggleStore {
features: FeatureToggle[] = [];
@ -314,4 +316,10 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
isPotentiallyStale(): Promise<boolean> {
throw new Error('Method not implemented.');
}
getFeatureTypeCounts(
params: IFeatureProjectUserParams,
): Promise<IFeatureTypeCount[]> {
throw new Error('Method not implemented.');
}
}

View File

@ -30,6 +30,7 @@ import {
IFeatureToggleClientStore,
IFeatureToggleQuery,
IFeatureToggleStore,
IFeatureTypeCount,
IFlagResolver,
IProjectStore,
ISegment,
@ -1087,6 +1088,12 @@ class FeatureToggleService {
return this.featureStrategiesStore.getFeatureOverview(params);
}
async getFeatureTypeCounts(
params: IFeatureProjectUserParams,
): Promise<IFeatureTypeCount[]> {
return this.featureToggleStore.getFeatureTypeCounts(params);
}
async getFeatureToggle(
featureName: string,
): Promise<FeatureToggleWithEnvironment> {

View File

@ -18,10 +18,13 @@ import { DEFAULT_ENV } from '../../../lib/util';
import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder';
import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type';
import { IFlagResolver } from '../../../lib/types';
import { IFeatureTypeCount, IFlagResolver } from '../../../lib/types';
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
import { IFeatureProjectUserParams } from './feature-toggle-controller';
export type EnvironmentFeatureNames = { [key: string]: string[] };
export type EnvironmentFeatureNames = {
[key: string]: string[];
};
const FEATURE_COLUMNS = [
'name',
@ -278,6 +281,27 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
);
}
async getFeatureTypeCounts({
projectId,
archived,
}: IFeatureProjectUserParams): Promise<IFeatureTypeCount[]> {
const query = this.db<FeaturesTable>(TABLE)
.select('type')
.count('type')
.groupBy('type');
query.where({
project: projectId,
archived,
});
const result = await query;
return result.map((row) => ({
type: row.type,
count: Number(row.count),
}));
}
async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
const query = this.db<FeaturesTable>(TABLE).orderBy('name', 'asc');
query.whereIn('name', names);
@ -596,9 +620,10 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
.update('variants', variantsString)
.where('feature_name', featureName);
const row = await this.db(TABLE)
.select(FEATURE_COLUMNS)
.where({ project: project, name: featureName });
const row = await this.db(TABLE).select(FEATURE_COLUMNS).where({
project: project,
name: featureName,
});
const toggle = this.rowToFeature(row[0]);
toggle.variants = newVariants;
@ -606,17 +631,23 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
return toggle;
}
async updatePotentiallyStaleFeatures(
currentTime?: string,
): Promise<{ name: string; potentiallyStale: boolean; project: string }[]> {
async updatePotentiallyStaleFeatures(currentTime?: string): Promise<
{
name: string;
potentiallyStale: boolean;
project: string;
}[]
> {
const query = this.db.raw(
`SELECT name, project, potentially_stale, (? > (features.created_at + ((
SELECT feature_types.lifetime_days
FROM feature_types
WHERE feature_types.id = features.type
) * INTERVAL '1 day'))) as current_staleness
FROM features
WHERE NOT stale = true`,
`SELECT name,
project,
potentially_stale,
(? > (features.created_at + ((SELECT feature_types.lifetime_days
FROM feature_types
WHERE feature_types.id = features.type) *
INTERVAL '1 day'))) as current_staleness
FROM features
WHERE NOT stale = true`,
[currentTime || this.db.fn.now()],
);

View File

@ -2,11 +2,13 @@ import {
FeatureToggle,
FeatureToggleDTO,
IFeatureToggleQuery,
IFeatureTypeCount,
IVariant,
} from '../../../types/model';
import { Store } from '../../../types/stores/store';
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
import { FeatureConfigurationClient } from './feature-toggle-strategies-store-type';
import { IFeatureProjectUserParams } from '../feature-toggle-controller';
export interface IFeatureToggleStoreQuery {
archived: boolean;
@ -17,30 +19,46 @@ export interface IFeatureToggleStoreQuery {
export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
count(query?: Partial<IFeatureToggleStoreQuery>): Promise<number>;
setLastSeen(data: LastSeenInput[]): Promise<void>;
getProjectId(name: string): Promise<string | undefined>;
create(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
update(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
archive(featureName: string): Promise<FeatureToggle>;
batchArchive(featureNames: string[]): Promise<FeatureToggle[]>;
batchStale(
featureNames: string[],
stale: boolean,
): Promise<FeatureToggle[]>;
batchDelete(featureNames: string[]): Promise<void>;
batchRevive(featureNames: string[]): Promise<FeatureToggle[]>;
revive(featureName: string): Promise<FeatureToggle>;
getAll(query?: Partial<IFeatureToggleStoreQuery>): Promise<FeatureToggle[]>;
getAllByNames(names: string[]): Promise<FeatureToggle[]>;
getFeatureToggleList(
featureQuery?: IFeatureToggleQuery,
userId?: number,
archived?: boolean,
): Promise<FeatureToggle[]>;
getArchivedFeatures(project?: string): Promise<FeatureToggle[]>;
getPlaygroundFeatures(
featureQuery?: IFeatureToggleQuery,
): Promise<FeatureConfigurationClient[]>;
countByDate(queryModifiers: {
archived?: boolean;
project?: string;
@ -48,9 +66,15 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
range?: string[];
dateAccessor: string;
}): Promise<number>;
updatePotentiallyStaleFeatures(
currentTime?: string,
): Promise<{ name: string; potentiallyStale: boolean; project: string }[]>;
updatePotentiallyStaleFeatures(currentTime?: string): Promise<
{
name: string;
potentiallyStale: boolean;
project: string;
}[]
>;
isPotentiallyStale(featureName: string): Promise<boolean>;
/**
@ -59,6 +83,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
* TODO: Remove before release 5.0
*/
getVariants(featureName: string): Promise<IVariant[]>;
/**
* TODO: Remove before release 5.0
* @deprecated - Variants should be fetched from FeatureEnvironmentStore (since variants are now; since 4.18, connected to environments)
@ -73,4 +98,8 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
): Promise<FeatureToggle>;
disableAllEnvironmentsForFeatures(names: string[]): Promise<void>;
getFeatureTypeCounts(
params: IFeatureProjectUserParams,
): Promise<IFeatureTypeCount[]>;
}

View File

@ -91,6 +91,7 @@ import {
profileSchema,
projectEnvironmentSchema,
projectOverviewSchema,
deprecatedProjectOverviewSchema,
projectSchema,
projectsSchema,
projectStatsSchema,
@ -167,6 +168,7 @@ import {
dependenciesExistSchema,
validateArchiveFeaturesSchema,
searchFeaturesSchema,
featureTypeCountSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
@ -375,7 +377,7 @@ export const schemas: UnleashSchemas = {
variantFlagSchema,
variantsSchema,
versionSchema,
projectOverviewSchema,
deprecatedProjectOverviewSchema,
importTogglesSchema,
importTogglesValidateSchema,
importTogglesValidateItemSchema,
@ -397,6 +399,8 @@ export const schemas: UnleashSchemas = {
dependenciesExistSchema,
validateArchiveFeaturesSchema,
searchFeaturesSchema,
featureTypeCountSchema,
projectOverviewSchema,
};
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`deprecatedProjectOverviewSchema 1`] = `
{
"errors": [
{
"instancePath": "",
"keyword": "required",
"message": "must have required property 'version'",
"params": {
"missingProperty": "version",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/deprecatedProjectOverviewSchema",
}
`;

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`featureTypeCountSchema 1`] = `
{
"errors": [
{
"instancePath": "",
"keyword": "required",
"message": "must have required property 'type'",
"params": {
"missingProperty": "type",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/featureTypeCountSchema",
}
`;

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`updateProjectEnterpriseSettings schema 1`] = `
exports[`projectOverviewSchema 1`] = `
{
"errors": [
{

View File

@ -0,0 +1,28 @@
import { DeprecatedProjectOverviewSchema } from './deprecated-project-overview-schema';
import { validateSchema } from '../validate';
test('deprecatedProjectOverviewSchema', () => {
const data: DeprecatedProjectOverviewSchema = {
name: 'project',
version: 3,
featureNaming: {
description: 'naming description',
example: 'a',
pattern: '[aZ]',
},
};
expect(
validateSchema(
'#/components/schemas/deprecatedProjectOverviewSchema',
data,
),
).toBeUndefined();
expect(
validateSchema(
'#/components/schemas/deprecatedProjectOverviewSchema',
{},
),
).toMatchSnapshot();
});

View File

@ -0,0 +1,153 @@
import { FromSchema } from 'json-schema-to-ts';
import { parametersSchema } from './parameters-schema';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { featureSchema } from './feature-schema';
import { constraintSchema } from './constraint-schema';
import { environmentSchema } from './environment-schema';
import { featureEnvironmentSchema } from './feature-environment-schema';
import { projectStatsSchema } from './project-stats-schema';
import { createFeatureStrategySchema } from './create-feature-strategy-schema';
import { projectEnvironmentSchema } from './project-environment-schema';
import { createStrategyVariantSchema } from './create-strategy-variant-schema';
import { strategyVariantSchema } from './strategy-variant-schema';
import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema';
export const deprecatedProjectOverviewSchema = {
$id: '#/components/schemas/deprecatedProjectOverviewSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'name'],
description:
'A high-level overview of a project. It contains information such as project statistics, the name of the project, what members and what features it contains, etc.',
properties: {
stats: {
$ref: '#/components/schemas/projectStatsSchema',
description: 'Project statistics',
},
version: {
type: 'integer',
example: 1,
description:
'The schema version used to describe the project overview',
},
name: {
type: 'string',
example: 'dx-squad',
description: 'The name of this project',
},
description: {
type: 'string',
nullable: true,
example: 'DX squad feature release',
description: 'Additional information about the project',
},
defaultStickiness: {
type: 'string',
example: 'userId',
description:
'A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy',
},
mode: {
type: 'string',
enum: ['open', 'protected', 'private'],
example: 'open',
description:
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
},
featureLimit: {
type: 'number',
nullable: true,
example: 100,
description:
'A limit on the number of features allowed in the project. Null if no limit.',
},
featureNaming: {
$ref: '#/components/schemas/createFeatureNamingPatternSchema',
},
members: {
type: 'number',
example: 4,
description: 'The number of members this project has',
},
health: {
type: 'number',
example: 50,
description:
"An indicator of the [project's health](https://docs.getunleash.io/reference/technical-debt#health-rating) on a scale from 0 to 100",
},
environments: {
type: 'array',
items: {
$ref: '#/components/schemas/projectEnvironmentSchema',
},
example: [
{ environment: 'development' },
{
environment: 'production',
defaultStrategy: {
name: 'flexibleRollout',
constraints: [],
parameters: {
rollout: '50',
stickiness: 'customAppName',
groupId: 'stickytoggle',
},
},
},
],
description: 'The environments that are enabled for this project',
},
features: {
type: 'array',
items: {
$ref: '#/components/schemas/featureSchema',
},
description:
'The full list of features in this project (excluding archived features)',
},
updatedAt: {
type: 'string',
format: 'date-time',
nullable: true,
example: '2023-02-10T08:36:35.262Z',
description: 'When the project was last updated.',
},
createdAt: {
type: 'string',
format: 'date-time',
nullable: true,
example: '2023-02-10T08:36:35.262Z',
description: 'When the project was created.',
},
favorite: {
type: 'boolean',
example: true,
description:
'`true` if the project was favorited, otherwise `false`.',
},
},
components: {
schemas: {
environmentSchema,
projectEnvironmentSchema,
createFeatureStrategySchema,
createStrategyVariantSchema,
constraintSchema,
featureSchema,
featureEnvironmentSchema,
overrideSchema,
parametersSchema,
featureStrategySchema,
strategyVariantSchema,
variantSchema,
projectStatsSchema,
createFeatureNamingPatternSchema,
},
},
} as const;
export type DeprecatedProjectOverviewSchema = FromSchema<
typeof deprecatedProjectOverviewSchema
>;

View File

@ -0,0 +1,18 @@
import { DeprecatedProjectOverviewSchema } from './deprecated-project-overview-schema';
import { validateSchema } from '../validate';
import { FeatureTypeCountSchema } from './feature-type-count-schema';
test('featureTypeCountSchema', () => {
const data: FeatureTypeCountSchema = {
type: 'release',
count: 1,
};
expect(
validateSchema('#/components/schemas/featureTypeCountSchema', data),
).toBeUndefined();
expect(
validateSchema('#/components/schemas/featureTypeCountSchema', {}),
).toMatchSnapshot();
});

View File

@ -0,0 +1,35 @@
import { FromSchema } from 'json-schema-to-ts';
import { variantSchema } from './variant-schema';
import { constraintSchema } from './constraint-schema';
import { overrideSchema } from './override-schema';
import { parametersSchema } from './parameters-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { tagSchema } from './tag-schema';
import { featureEnvironmentSchema } from './feature-environment-schema';
import { strategyVariantSchema } from './strategy-variant-schema';
export const featureTypeCountSchema = {
$id: '#/components/schemas/featureTypeCountSchema',
type: 'object',
additionalProperties: false,
required: ['type', 'count'],
description: 'A count of feature flags of a specific type',
properties: {
type: {
type: 'string',
example: 'kill-switch',
description:
'Type of the flag e.g. experiment, kill-switch, release, operational, permission',
},
count: {
type: 'number',
example: 1,
description: 'Number of feature flags of this type',
},
},
components: {
schemas: {},
},
} as const;
export type FeatureTypeCountSchema = FromSchema<typeof featureTypeCountSchema>;

View File

@ -135,6 +135,7 @@ export * from './export-result-schema';
export * from './export-query-schema';
export * from './push-variants-schema';
export * from './project-stats-schema';
export * from './deprecated-project-overview-schema';
export * from './project-overview-schema';
export * from './import-toggles-validate-item-schema';
export * from './import-toggles-validate-schema';
@ -168,3 +169,4 @@ export * from './dependencies-exist-schema';
export * from './validate-archive-features-schema';
export * from './search-features-schema';
export * from './feature-search-query-parameters';
export * from './feature-type-count-schema';

View File

@ -1,7 +1,8 @@
import { ProjectOverviewSchema } from './project-overview-schema';
import { DeprecatedProjectOverviewSchema } from './deprecated-project-overview-schema';
import { validateSchema } from '../validate';
import { ProjectOverviewSchema } from './project-overview-schema';
test('updateProjectEnterpriseSettings schema', () => {
test('projectOverviewSchema', () => {
const data: ProjectOverviewSchema = {
name: 'project',
version: 3,
@ -10,6 +11,12 @@ test('updateProjectEnterpriseSettings schema', () => {
example: 'a',
pattern: '[aZ]',
},
featureTypeCounts: [
{
type: 'release',
count: 1,
},
],
};
expect(

View File

@ -13,6 +13,7 @@ import { projectEnvironmentSchema } from './project-environment-schema';
import { createStrategyVariantSchema } from './create-strategy-variant-schema';
import { strategyVariantSchema } from './strategy-variant-schema';
import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema';
import { featureTypeCountSchema } from './feature-type-count-schema';
export const projectOverviewSchema = {
$id: '#/components/schemas/projectOverviewSchema',
@ -92,20 +93,20 @@ export const projectOverviewSchema = {
parameters: {
rollout: '50',
stickiness: 'customAppName',
groupId: 'stickytoggle',
groupId: 'stickyFlag',
},
},
},
],
description: 'The environments that are enabled for this project',
},
features: {
featureTypeCounts: {
type: 'array',
items: {
$ref: '#/components/schemas/featureSchema',
$ref: '#/components/schemas/featureTypeCountSchema',
},
description:
'The full list of features in this project (excluding archived features)',
'The number of features of each type that are in this project',
},
updatedAt: {
type: 'string',
@ -144,6 +145,7 @@ export const projectOverviewSchema = {
variantSchema,
projectStatsSchema,
createFeatureNamingPatternSchema,
featureTypeCountSchema,
},
},
} as const;

View File

@ -20,7 +20,7 @@ import UserAdminController from './user-admin';
import EmailController from './email';
import UserFeedbackController from './user-feedback';
import UserSplashController from './user-splash';
import ProjectApi from './project';
import ProjectApi from './project/project-api';
import { EnvironmentsController } from './environments';
import ConstraintsController from './constraints';
import PatController from './user/pat';

View File

@ -17,10 +17,11 @@ import {
createResponseSchema,
ProjectDoraMetricsSchema,
projectDoraMetricsSchema,
ProjectOverviewSchema,
projectOverviewSchema,
DeprecatedProjectOverviewSchema,
deprecatedProjectOverviewSchema,
projectsSchema,
ProjectsSchema,
projectOverviewSchema,
} from '../../../openapi';
import { getStandardResponses } from '../../../openapi/util/standard-responses';
import { OpenApiService, SettingService } from '../../../services';
@ -31,6 +32,7 @@ import { createKnexTransactionStarter } from '../../../db/transaction';
import { Db } from '../../../db/db';
import { InvalidOperationError } from '../../../error';
import DependentFeaturesController from '../../../features/dependent-features/dependent-features-controller';
import { ProjectOverviewSchema } from 'lib/openapi/spec/project-overview-schema';
export default class ProjectApi extends Controller {
private projectService: ProjectService;
@ -68,6 +70,29 @@ export default class ProjectApi extends Controller {
this.route({
method: 'get',
path: '/:projectId',
handler: this.getDeprecatedProjectOverview,
permission: NONE,
middleware: [
services.openApiService.validPath({
tags: ['Projects'],
operationId: 'getDeprecatedProjectOverview',
summary: 'Get an overview of a project. (deprecated)',
deprecated: true,
description:
'This endpoint returns an overview of the specified projects stats, project health, number of members, which environments are configured, and the features in the project.',
responses: {
200: createResponseSchema(
'deprecatedProjectOverviewSchema',
),
...getStandardResponses(401, 403, 404),
},
}),
],
});
this.route({
method: 'get',
path: '/:projectId/overview',
handler: this.getProjectOverview,
permission: NONE,
middleware: [
@ -76,7 +101,7 @@ export default class ProjectApi extends Controller {
operationId: 'getProjectOverview',
summary: 'Get an overview of a project.',
description:
'This endpoint returns an overview of the specified projects stats, project health, number of members, which environments are configured, and the features in the project.',
'This endpoint returns an overview of the specified projects stats, project health, number of members, which environments are configured, and the features types in the project.',
responses: {
200: createResponseSchema('projectOverviewSchema'),
...getStandardResponses(401, 403, 404),
@ -148,6 +173,27 @@ export default class ProjectApi extends Controller {
);
}
async getDeprecatedProjectOverview(
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
res: Response<DeprecatedProjectOverviewSchema>,
): Promise<void> {
const { projectId } = req.params;
const { archived } = req.query;
const { user } = req;
const overview = await this.projectService.getProjectHealth(
projectId,
archived,
user.id,
);
this.openApiService.respondWithValidation(
200,
res,
deprecatedProjectOverviewSchema.$id,
serializeDates(overview),
);
}
async getProjectOverview(
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
res: Response<ProjectOverviewSchema>,

View File

@ -47,7 +47,7 @@ export default class ProjectHealthService {
): Promise<IProjectHealthReport> {
const featureTypes = await this.featureTypeStore.getAll();
const overview = await this.projectService.getProjectOverview(
const overview = await this.projectService.getProjectHealth(
projectId,
false,
undefined,

View File

@ -40,6 +40,7 @@ import {
IFeatureNaming,
CreateProject,
IProjectUpdate,
IProjectHealth,
} from '../types';
import {
IProjectQuery,
@ -1100,11 +1101,11 @@ export default class ProjectService {
};
}
async getProjectOverview(
async getProjectHealth(
projectId: string,
archived: boolean = false,
userId?: number,
): Promise<IProjectOverview> {
): Promise<IProjectHealth> {
const [
project,
environments,
@ -1149,6 +1150,55 @@ export default class ProjectService {
};
}
async getProjectOverview(
projectId: string,
archived: boolean = false,
userId?: number,
): Promise<IProjectOverview> {
const [
project,
environments,
featureTypeCounts,
members,
favorite,
projectStats,
] = await Promise.all([
this.projectStore.get(projectId),
this.projectStore.getEnvironmentsForProject(projectId),
this.featureToggleService.getFeatureTypeCounts({
projectId,
archived,
userId,
}),
this.projectStore.getMembersCountByProject(projectId),
userId
? this.favoritesService.isFavoriteProject({
project: projectId,
userId,
})
: Promise.resolve(false),
this.projectStatsStore.getProjectStats(projectId),
]);
return {
stats: projectStats,
name: project.name,
description: project.description!,
mode: project.mode,
featureLimit: project.featureLimit,
featureNaming: project.featureNaming,
defaultStickiness: project.defaultStickiness,
health: project.health || 0,
favorite: favorite,
updatedAt: project.updatedAt,
createdAt: project.createdAt,
environments,
featureTypeCounts,
members,
version: 1,
};
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
removeModeForNonEnterprise(data): any {
if (this.isEnterprise) {

View File

@ -212,6 +212,11 @@ export interface IFeatureOverview {
environments: IEnvironmentOverview[];
}
export interface IFeatureTypeCount {
type: string;
count: number;
}
export type ProjectMode = 'open' | 'protected' | 'private';
export interface IFeatureNaming {
@ -220,7 +225,7 @@ export interface IFeatureNaming {
description?: string | null;
}
export interface IProjectOverview {
export interface IProjectHealth {
name: string;
description: string;
environments: ProjectEnvironment[];
@ -238,7 +243,25 @@ export interface IProjectOverview {
defaultStickiness: string;
}
export interface IProjectHealthReport extends IProjectOverview {
export interface IProjectOverview {
name: string;
description: string;
environments: ProjectEnvironment[];
featureTypeCounts: IFeatureTypeCount[];
members: number;
version: number;
health: number;
favorite?: boolean;
updatedAt?: Date;
createdAt: Date | undefined;
stats?: IProjectStats;
mode: ProjectMode;
featureLimit?: number;
featureNaming?: IFeatureNaming;
defaultStickiness: string;
}
export interface IProjectHealthReport extends IProjectHealth {
staleCount: number;
potentiallyStaleCount: number;
activeCount: number;

View File

@ -144,6 +144,24 @@ test('response for default project should include created_at', async () => {
expect(body.createdAt).toBeDefined();
});
test('response for project overview should include feature type counts', async () => {
await app.createFeature({ name: 'my-new-release-toggle', type: 'release' });
await app.createFeature({
name: 'my-new-development-toggle',
type: 'development',
});
const { body } = await app.request
.get('/api/admin/projects/default/overview')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject({
featureTypeCounts: [
{ type: 'development', count: 1 },
{ type: 'release', count: 1 },
],
});
});
test('response should include last seen at per environment', async () => {
await app.createFeature('my-new-feature-toggle');