mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-21 13:47:39 +02:00
feat: project environments include visible property (#9427)
This commit is contained in:
parent
ae0fc86e99
commit
e7ac42080d
@ -19,8 +19,6 @@ const getStatusCode = (errorName: string): number => {
|
|||||||
return 400;
|
return 400;
|
||||||
case 'PasswordUndefinedError':
|
case 'PasswordUndefinedError':
|
||||||
return 400;
|
return 400;
|
||||||
case 'MinimumOneEnvironmentError':
|
|
||||||
return 400;
|
|
||||||
case 'InvalidTokenError':
|
case 'InvalidTokenError':
|
||||||
return 401;
|
return 401;
|
||||||
case 'UsedTokenError':
|
case 'UsedTokenError':
|
||||||
|
@ -5,7 +5,6 @@ import FeatureHasTagError from './feature-has-tag-error';
|
|||||||
import IncompatibleProjectError from './incompatible-project-error';
|
import IncompatibleProjectError from './incompatible-project-error';
|
||||||
import InvalidOperationError from './invalid-operation-error';
|
import InvalidOperationError from './invalid-operation-error';
|
||||||
import InvalidTokenError from './invalid-token-error';
|
import InvalidTokenError from './invalid-token-error';
|
||||||
import MinimumOneEnvironmentError from './minimum-one-environment-error';
|
|
||||||
import NameExistsError from './name-exists-error';
|
import NameExistsError from './name-exists-error';
|
||||||
import PermissionError from './permission-error';
|
import PermissionError from './permission-error';
|
||||||
import { OperationDeniedError } from './operation-denied-error';
|
import { OperationDeniedError } from './operation-denied-error';
|
||||||
@ -27,7 +26,6 @@ export {
|
|||||||
IncompatibleProjectError,
|
IncompatibleProjectError,
|
||||||
InvalidOperationError,
|
InvalidOperationError,
|
||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
MinimumOneEnvironmentError,
|
|
||||||
NameExistsError,
|
NameExistsError,
|
||||||
PermissionError,
|
PermissionError,
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
|
@ -8,7 +8,6 @@ export const UnleashApiErrorTypes = [
|
|||||||
'IncompatibleProjectError',
|
'IncompatibleProjectError',
|
||||||
'InvalidOperationError',
|
'InvalidOperationError',
|
||||||
'InvalidTokenError',
|
'InvalidTokenError',
|
||||||
'MinimumOneEnvironmentError',
|
|
||||||
'NameExistsError',
|
'NameExistsError',
|
||||||
'NoAccessError',
|
'NoAccessError',
|
||||||
'NotFoundError',
|
'NotFoundError',
|
||||||
|
@ -338,3 +338,54 @@ test('Override works correctly when enabling default and disabling prod and dev'
|
|||||||
expect(targetedEnvironment?.enabled).toBe(true);
|
expect(targetedEnvironment?.enabled).toBe(true);
|
||||||
expect(allOtherEnvironments.every((x) => !x)).toBe(true);
|
expect(allOtherEnvironments.every((x) => !x)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getProjectEnvironments also includes whether or not a given project is visible on a given environment', async () => {
|
||||||
|
const assertContains = (environments, envName, visible) => {
|
||||||
|
const env = environments.find((e) => e.name === envName);
|
||||||
|
expect(env).toBeDefined();
|
||||||
|
expect(env.visible).toBe(visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertContainsVisible = (environments, envName) => {
|
||||||
|
assertContains(environments, envName, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertContainsNotVisible = (environments, envName) => {
|
||||||
|
assertContains(environments, envName, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectId = 'default';
|
||||||
|
const firstEnvTest = 'some-connected-environment';
|
||||||
|
const secondEnvTest = 'some-also-connected-environment';
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: firstEnvTest,
|
||||||
|
type: 'production',
|
||||||
|
});
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: secondEnvTest,
|
||||||
|
type: 'production',
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.addEnvironmentToProject(
|
||||||
|
firstEnvTest,
|
||||||
|
projectId,
|
||||||
|
SYSTEM_USER_AUDIT,
|
||||||
|
);
|
||||||
|
await service.addEnvironmentToProject(
|
||||||
|
secondEnvTest,
|
||||||
|
projectId,
|
||||||
|
SYSTEM_USER_AUDIT,
|
||||||
|
);
|
||||||
|
let environments = await service.getProjectEnvironments(projectId);
|
||||||
|
assertContainsVisible(environments, firstEnvTest);
|
||||||
|
assertContainsVisible(environments, secondEnvTest);
|
||||||
|
|
||||||
|
await service.removeEnvironmentFromProject(
|
||||||
|
firstEnvTest,
|
||||||
|
projectId,
|
||||||
|
SYSTEM_USER_AUDIT,
|
||||||
|
);
|
||||||
|
environments = await service.getProjectEnvironments(projectId);
|
||||||
|
assertContainsNotVisible(environments, firstEnvTest);
|
||||||
|
assertContainsVisible(environments, secondEnvTest);
|
||||||
|
});
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
type IEnvironmentStore,
|
type IEnvironmentStore,
|
||||||
type IFeatureEnvironmentStore,
|
type IFeatureEnvironmentStore,
|
||||||
type IFeatureStrategiesStore,
|
type IFeatureStrategiesStore,
|
||||||
type IProjectEnvironment,
|
type IProjectsAvailableOnEnvironment,
|
||||||
type ISortOrder,
|
type ISortOrder,
|
||||||
type IUnleashConfig,
|
type IUnleashConfig,
|
||||||
type IUnleashStores,
|
type IUnleashStores,
|
||||||
@ -19,7 +19,6 @@ import NameExistsError from '../../error/name-exists-error';
|
|||||||
import { sortOrderSchema } from '../../services/sort-order-schema';
|
import { sortOrderSchema } from '../../services/sort-order-schema';
|
||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import type { IProjectStore } from '../../features/project/project-store-type';
|
import type { IProjectStore } from '../../features/project/project-store-type';
|
||||||
import MinimumOneEnvironmentError from '../../error/minimum-one-environment-error';
|
|
||||||
import type { IFlagResolver } from '../../types/experimental';
|
import type { IFlagResolver } from '../../types/experimental';
|
||||||
import type { CreateFeatureStrategySchema } from '../../openapi';
|
import type { CreateFeatureStrategySchema } from '../../openapi';
|
||||||
import type EventService from '../events/event-service';
|
import type EventService from '../events/event-service';
|
||||||
@ -77,8 +76,24 @@ export default class EnvironmentService {
|
|||||||
|
|
||||||
async getProjectEnvironments(
|
async getProjectEnvironments(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<IProjectEnvironment[]> {
|
): Promise<IProjectsAvailableOnEnvironment[]> {
|
||||||
return this.environmentStore.getProjectEnvironments(projectId);
|
// This function produces an object for every environment, in that object is a boolean
|
||||||
|
// describing whether or not that environment is enabled - aka not deprecated
|
||||||
|
const environments =
|
||||||
|
await this.projectStore.getEnvironmentsForProject(projectId);
|
||||||
|
const environmentsOnProject = new Set(
|
||||||
|
environments.map((env) => env.environment),
|
||||||
|
);
|
||||||
|
|
||||||
|
const allEnvironments =
|
||||||
|
await this.environmentStore.getProjectEnvironments(projectId);
|
||||||
|
|
||||||
|
return allEnvironments.map((env) => {
|
||||||
|
return {
|
||||||
|
...env,
|
||||||
|
visible: environmentsOnProject.has(env.name),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSortOrder(sortOrder: ISortOrder): Promise<void> {
|
async updateSortOrder(sortOrder: ISortOrder): Promise<void> {
|
||||||
@ -254,22 +269,13 @@ export default class EnvironmentService {
|
|||||||
const projectEnvs =
|
const projectEnvs =
|
||||||
await this.projectStore.getEnvironmentsForProject(projectId);
|
await this.projectStore.getEnvironmentsForProject(projectId);
|
||||||
|
|
||||||
if (projectEnvs.length > 1) {
|
await this.forceRemoveEnvironmentFromProject(environment, projectId);
|
||||||
await this.forceRemoveEnvironmentFromProject(
|
await this.eventService.storeEvent(
|
||||||
|
new ProjectEnvironmentRemoved({
|
||||||
|
project: projectId,
|
||||||
environment,
|
environment,
|
||||||
projectId,
|
auditUser,
|
||||||
);
|
}),
|
||||||
await this.eventService.storeEvent(
|
|
||||||
new ProjectEnvironmentRemoved({
|
|
||||||
project: projectId,
|
|
||||||
environment,
|
|
||||||
auditUser,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new MinimumOneEnvironmentError(
|
|
||||||
'You must always have one active environment',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,22 +98,6 @@ test('Should remove environment from project', async () => {
|
|||||||
expect(envs).toHaveLength(1);
|
expect(envs).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should not remove environment from project if project only has one environment enabled', async () => {
|
|
||||||
await app.request
|
|
||||||
.delete(`/api/admin/projects/default/environments/default`)
|
|
||||||
.expect(400)
|
|
||||||
.expect((r) => {
|
|
||||||
expect(r.body.details[0].message).toBe(
|
|
||||||
'You must always have one active environment',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const envs =
|
|
||||||
await db.stores.projectStore.getEnvironmentsForProject('default');
|
|
||||||
|
|
||||||
expect(envs).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should add default strategy to environment', async () => {
|
test('Should add default strategy to environment', async () => {
|
||||||
const defaultStrategy = {
|
const defaultStrategy = {
|
||||||
name: 'flexibleRollout',
|
name: 'flexibleRollout',
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import type { IEnvironment, IProjectEnvironment } from '../../types/model';
|
import type {
|
||||||
|
IEnvironment,
|
||||||
|
IProjectsAvailableOnEnvironment,
|
||||||
|
} from '../../types/model';
|
||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import type { IEnvironmentStore } from './environment-store-type';
|
import type { IEnvironmentStore } from './environment-store-type';
|
||||||
|
|
||||||
@ -140,7 +143,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
|
|||||||
async getProjectEnvironments(
|
async getProjectEnvironments(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<IProjectEnvironment[]> {
|
): Promise<IProjectsAvailableOnEnvironment[]> {
|
||||||
return Promise.reject(new Error('Not implemented'));
|
return Promise.reject(new Error('Not implemented'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,12 @@ export const environmentProjectSchema = {
|
|||||||
'The strategy configuration to add when enabling a feature environment by default',
|
'The strategy configuration to add when enabling a feature environment by default',
|
||||||
$ref: '#/components/schemas/createFeatureStrategySchema',
|
$ref: '#/components/schemas/createFeatureStrategySchema',
|
||||||
},
|
},
|
||||||
|
visible: {
|
||||||
|
type: 'boolean',
|
||||||
|
example: true,
|
||||||
|
description:
|
||||||
|
'Indicates whether the environment can be enabled for feature flags in the project',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
@ -212,6 +212,10 @@ export interface IProjectEnvironment extends IEnvironment {
|
|||||||
defaultStrategy?: CreateFeatureStrategySchema;
|
defaultStrategy?: CreateFeatureStrategySchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProjectsAvailableOnEnvironment extends IProjectEnvironment {
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IEnvironmentCreate {
|
export interface IEnvironmentCreate {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user