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:
parent
9f3648dc81
commit
63f6af06da
@ -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.');
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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()],
|
||||
);
|
||||
|
||||
|
@ -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[]>;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
}
|
||||
`;
|
@ -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",
|
||||
}
|
||||
`;
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`updateProjectEnterpriseSettings schema 1`] = `
|
||||
exports[`projectOverviewSchema 1`] = `
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
|
@ -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();
|
||||
});
|
153
src/lib/openapi/spec/deprecated-project-overview-schema.ts
Normal file
153
src/lib/openapi/spec/deprecated-project-overview-schema.ts
Normal 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
|
||||
>;
|
18
src/lib/openapi/spec/feature-type-count-schema.test.ts
Normal file
18
src/lib/openapi/spec/feature-type-count-schema.test.ts
Normal 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();
|
||||
});
|
35
src/lib/openapi/spec/feature-type-count-schema.ts
Normal file
35
src/lib/openapi/spec/feature-type-count-schema.ts
Normal 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>;
|
@ -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';
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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>,
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user