1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-23 13:46:45 +02:00

chore(adminapi)!: Remove deprecated project overview (#10069)

BREAKING CHANGE: As part of the preparation for a new major (7.0) this
removes /api/admin/projects/{projectId} endpoint. It has been deprecated
since 5.8, and we don't use it anymore in our frontend.
This commit is contained in:
Christopher Kolstad 2025-06-04 09:59:36 +02:00 committed by GitHub
parent cf87f8cfe1
commit 8d7a0fdd7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 3 additions and 637 deletions

View File

@ -203,30 +203,6 @@ test('Trying to add a strategy configuration to environment not connected to fla
});
});
test('Can get project overview', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'project-overview',
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(201)
.expect((res) => {
expect(res.body.name).toBe('project-overview');
expect(res.body.createdAt).toBeTruthy();
});
await app.request
.get('/api/admin/projects/default')
.expect(200)
.expect((r) => {
expect(r.body.name).toBe('Default');
expect(r.body.features).toHaveLength(2);
expect(r.body.members).toBe(0);
});
});
test('should list dependencies and children', async () => {
const parent = uuidv4();
const child = uuidv4();
@ -390,76 +366,6 @@ test('Can get features for project', async () => {
});
});
test('Project overview includes environment connected to feature', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'com.test.environment',
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(201)
.expect((res) => {
expect(res.body.name).toBe('com.test.environment');
expect(res.body.createdAt).toBeTruthy();
});
await db.stores.environmentStore.create({
name: 'project-overview',
type: 'production',
});
await app.request
.post('/api/admin/projects/default/environments')
.send({ environment: 'project-overview' })
.expect(200);
return app.request
.get('/api/admin/projects/default')
.expect(200)
.expect((r) => {
expect(r.body.features[0].environments[0].name).toBe(DEFAULT_ENV);
expect(r.body.features[0].environments[1].name).toBe(
'project-overview',
);
});
});
test('Disconnecting environment from project, removes environment from features in project overview', async () => {
await app.request
.post('/api/admin/projects/default/features')
.send({
name: 'com.test.disconnect.environment',
enabled: false,
strategies: [{ name: 'default' }],
})
.set('Content-Type', 'application/json')
.expect(201)
.expect((res) => {
expect(res.body.name).toBe('com.test.disconnect.environment');
expect(res.body.createdAt).toBeTruthy();
});
await db.stores.environmentStore.create({
name: 'dis-project-overview',
type: 'production',
});
await app.request
.post('/api/admin/projects/default/environments')
.send({ environment: 'dis-project-overview' })
.expect(200);
await app.request
.delete('/api/admin/projects/default/environments/dis-project-overview')
.expect(200);
return app.request
.get('/api/admin/projects/default')
.expect(200)
.expect((r) => {
expect(
r.body.features.some(
(e) => e.environment === 'dis-project-overview',
),
).toBeFalsy();
});
});
test('Can enable/disable environment for feature with strategies', async () => {
const envName = 'enable-feature-environment';
const featureName = 'com.test.enable.environment';
@ -756,7 +662,7 @@ describe('Interacting with features using project IDs that belong to other proje
// ensure the new project has been created
await app.request
.get(`/api/admin/projects/${otherProject}`)
.get(`/api/admin/projects/${otherProject}/health-report`)
.expect(200);
// create flag in default project

View File

@ -15,8 +15,6 @@ import type ProjectService from './project-service.js';
import VariantsController from '../../routes/admin-api/project/variants.js';
import {
createResponseSchema,
type DeprecatedProjectOverviewSchema,
deprecatedProjectOverviewSchema,
outdatedSdksSchema,
type OutdatedSdksSchema,
type ProjectDoraMetricsSchema,
@ -95,29 +93,6 @@ export default class ProjectController extends Controller {
],
});
this.route({
method: 'get',
path: '/:projectId',
handler: this.getDeprecatedProjectOverview,
permission: NONE,
middleware: [
this.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',
@ -260,27 +235,6 @@ export default class ProjectController 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

@ -2,15 +2,12 @@ import dbInit, {
type ITestDb,
} from '../../../test/e2e/helpers/database-init.js';
import {
insertFeatureEnvironmentsLastSeen,
insertLastSeenAt,
type IUnleashTest,
setupAppWithCustomConfig,
} from '../../../test/e2e/helpers/test-helper.js';
import getLogger from '../../../test/fixtures/no-logger.js';
import type { IProjectStore } from '../../types/index.js';
import { DEFAULT_ENV } from '../../util/index.js';
let app: IUnleashTest;
let db: ITestDb;
@ -45,76 +42,6 @@ afterAll(async () => {
await db.destroy();
});
test('should report has strategies and enabled strategies', async () => {
const app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {},
},
},
db.rawDatabase,
);
await app.createFeature('featureWithStrategies');
await app.createFeature('featureWithoutStrategies');
await app.createFeature('featureWithDisabledStrategies');
await app.addStrategyToFeatureEnv(
{
name: 'default',
},
DEFAULT_ENV,
'featureWithStrategies',
);
await app.addStrategyToFeatureEnv(
{
name: 'default',
disabled: true,
},
DEFAULT_ENV,
'featureWithDisabledStrategies',
);
const { body } = await app.request
.get('/api/admin/projects/default')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject({
features: [
{
name: 'featureWithStrategies',
environments: [
{
name: 'default',
hasStrategies: true,
hasEnabledStrategies: true,
},
],
},
{
name: 'featureWithoutStrategies',
environments: [
{
name: 'default',
hasStrategies: false,
hasEnabledStrategies: false,
},
],
},
{
name: 'featureWithDisabledStrategies',
environments: [
{
name: 'default',
hasStrategies: true,
hasEnabledStrategies: false,
},
],
},
],
});
});
test('Should ONLY return default project', async () => {
projectStore.create({
id: 'test2',
@ -140,13 +67,6 @@ test('response should include created_at', async () => {
expect(body.projects[0].createdAt).toBeDefined();
});
test('response for default project should include created_at', async () => {
const { body } = await app.request
.get('/api/admin/projects/default')
.expect('Content-Type', /json/)
.expect(200);
expect(body.createdAt).toBeDefined();
});
test('response for project overview should include feature type counts', async () => {
await app.createFeature({
name: 'my-new-release-toggle',
@ -173,120 +93,3 @@ test('response for project overview should include feature type counts', async (
],
});
});
test('response should include last seen at per environment', async () => {
await app.createFeature('my-new-feature-toggle');
await insertLastSeenAt(
'my-new-feature-toggle',
db.rawDatabase,
'default',
testDate,
);
await insertFeatureEnvironmentsLastSeen(
'my-new-feature-toggle',
db.rawDatabase,
'default',
);
const { body } = await app.request
.get('/api/admin/projects/default')
.expect('Content-Type', /json/)
.expect(200);
expect(body.features[0].environments[0].lastSeenAt).toEqual(testDate);
expect(body.features[0].lastSeenAt).toEqual(testDate);
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
db.stores,
{},
db.rawDatabase,
);
const response = await appWithLastSeenRefactor.request
.get('/api/admin/projects/default')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.features[0].environments[0].lastSeenAt).toEqual(
'2023-10-01T12:34:56.000Z',
);
expect(response.body.features[0].lastSeenAt).toEqual(
'2023-10-01T12:34:56.000Z',
);
});
test('response should include last seen at per environment for multiple environments', async () => {
const appWithLastSeenRefactor = await setupAppWithCustomConfig(
db.stores,
{},
db.rawDatabase,
);
await app.createFeature('my-new-feature-toggle');
await db.stores.environmentStore.create({
name: 'development',
type: 'development',
sortOrder: 1,
enabled: true,
});
await db.stores.environmentStore.create({
name: 'production',
type: 'production',
sortOrder: 2,
enabled: true,
});
await appWithLastSeenRefactor.services.projectService.addEnvironmentToProject(
'default',
'development',
);
await appWithLastSeenRefactor.services.projectService.addEnvironmentToProject(
'default',
'production',
);
await appWithLastSeenRefactor.createFeature(
'multiple-environment-last-seen-at',
);
await insertLastSeenAt(
'multiple-environment-last-seen-at',
db.rawDatabase,
'default',
'2023-10-01T12:32:56.000Z',
);
await insertLastSeenAt(
'multiple-environment-last-seen-at',
db.rawDatabase,
'development',
'2023-10-01T12:34:56.000Z',
);
await insertLastSeenAt(
'multiple-environment-last-seen-at',
db.rawDatabase,
'production',
'2023-10-01T12:33:56.000Z',
);
const { body } = await appWithLastSeenRefactor.request
.get('/api/admin/projects/default')
.expect('Content-Type', /json/)
.expect(200);
const featureEnvironments = body.features[1].environments;
const [def, development, production] = featureEnvironments;
expect(def.name).toBe('default');
expect(def.lastSeenAt).toEqual('2023-10-01T12:32:56.000Z');
expect(development.name).toBe('development');
expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z');
expect(production.name).toBe('production');
expect(production.lastSeenAt).toEqual('2023-10-01T12:33:56.000Z');
expect(body.features[1].lastSeenAt).toBe('2023-10-01T12:34:56.000Z');
});

View File

@ -1,28 +0,0 @@
import type { DeprecatedProjectOverviewSchema } from './deprecated-project-overview-schema.js';
import { validateSchema } from '../validate.js';
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

@ -1,155 +0,0 @@
import type { FromSchema } from 'json-schema-to-ts';
import { parametersSchema } from './parameters-schema.js';
import { variantSchema } from './variant-schema.js';
import { overrideSchema } from './override-schema.js';
import { featureStrategySchema } from './feature-strategy-schema.js';
import { featureSchema } from './feature-schema.js';
import { constraintSchema } from './constraint-schema.js';
import { environmentSchema } from './environment-schema.js';
import { featureEnvironmentSchema } from './feature-environment-schema.js';
import { projectStatsSchema } from './project-stats-schema.js';
import { createFeatureStrategySchema } from './create-feature-strategy-schema.js';
import { projectEnvironmentSchema } from './project-environment-schema.js';
import { createStrategyVariantSchema } from './create-strategy-variant-schema.js';
import { strategyVariantSchema } from './strategy-variant-schema.js';
import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema.js';
import { tagSchema } from './tag-schema.js';
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#project-status) 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,
tagSchema,
},
},
} as const;
export type DeprecatedProjectOverviewSchema = FromSchema<
typeof deprecatedProjectOverviewSchema
>;

View File

@ -63,7 +63,6 @@ export * from './custom-metrics-schema.js';
export * from './date-schema.js';
export * from './dependencies-exist-schema.js';
export * from './dependent-feature-schema.js';
export * from './deprecated-project-overview-schema.js';
export * from './dora-features-schema.js';
export * from './edge-token-schema.js';
export * from './email-schema.js';

View File

@ -71,7 +71,7 @@ const unfavoriteProject = async (projectName = 'default') => {
const getProject = async (projectName = 'default') => {
return app.request
.get(`/api/admin/projects/${projectName}`)
.get(`/api/admin/projects/${projectName}/overview`)
.set('Content-Type', 'application/json')
.expect(200);
};
@ -122,28 +122,6 @@ beforeEach(async () => {
await loginRegularUser();
});
test('should be favorited in project endpoint', async () => {
const featureName = 'test-feature';
await createFeature(featureName);
await favoriteFeature(featureName);
await favoriteProject();
const { body } = await app.request
.get('/api/admin/projects/default')
.set('Content-Type', 'application/json')
.expect(200);
expect(body).toMatchObject({
favorite: true,
features: [
{
name: featureName,
favorite: true,
},
],
});
});
test('feature should not be favorited by default', async () => {
const featureName = 'test-feature';
await createFeature(featureName);

View File

@ -65,15 +65,11 @@ test('Project with no stale toggles should have 100% health rating', async () =>
})
.expect(201);
await app.request
.get('/api/admin/projects/fresh')
.get('/api/admin/projects/fresh/health-report')
.expect(200)
.expect('Content-Type', /json/)
.expect((res) => {
expect(res.body.health).toBe(100);
expect(res.body.environments).toHaveLength(1);
expect(res.body.environments).toStrictEqual([
{ environment: 'default' },
]);
});
});
@ -251,93 +247,6 @@ test('Health report for non-existing project yields 404', async () => {
.expect(404);
});
test('Sorts environments by sort order', async () => {
const envOne = 'my-sorted-env1';
const envTwo = 'my-sorted-env2';
const featureName = 'My-new-toggle';
const defaultEnvName = 'default';
await db.stores.environmentStore.create({
name: envOne,
type: 'production',
sortOrder: 0,
});
await db.stores.environmentStore.create({
name: envTwo,
type: 'production',
sortOrder: 500,
});
await app.request
.post('/api/admin/projects/default/environments')
.send({
environment: envOne,
})
.expect(200);
await app.request
.post('/api/admin/projects/default/environments')
.send({
environment: envTwo,
})
.expect(200);
await app.request
.post('/api/admin/projects/default/features')
.send({ name: featureName })
.expect(201);
await app.request.get('/api/admin/projects/default').expect((res) => {
const feature = res.body.features[0];
expect(feature.environments[0].name).toBe(envOne);
expect(feature.environments[1].name).toBe(defaultEnvName);
expect(feature.environments[2].name).toBe(envTwo);
});
});
test('Sorts environments correctly if sort order is equal', async () => {
const envOne = 'my-sorted-env3';
const envTwo = 'my-sorted-env4';
const featureName = 'My-new-toggle-2';
await db.stores.environmentStore.create({
name: envOne,
type: 'production',
sortOrder: -5,
});
await db.stores.environmentStore.create({
name: envTwo,
type: 'production',
sortOrder: -5,
});
await app.request
.post('/api/admin/projects/default/environments')
.send({
environment: envOne,
})
.expect(200);
await app.request
.post('/api/admin/projects/default/environments')
.send({
environment: envTwo,
})
.expect(200);
await app.request
.post('/api/admin/projects/default/features')
.send({ name: featureName })
.expect(201);
await app.request.get('/api/admin/projects/default').expect((res) => {
const feature = res.body.features[0];
expect(feature.environments[0].name).toBe(envOne);
expect(feature.environments[1].name).toBe(envTwo);
});
});
test('Update update_at when setHealth runs', async () => {
await app.services.projectHealthService.setProjectHealthRating('default');
await app.request