mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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;
 | 
			
		||||
        case 'PasswordUndefinedError':
 | 
			
		||||
            return 400;
 | 
			
		||||
        case 'MinimumOneEnvironmentError':
 | 
			
		||||
            return 400;
 | 
			
		||||
        case 'InvalidTokenError':
 | 
			
		||||
            return 401;
 | 
			
		||||
        case 'UsedTokenError':
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import FeatureHasTagError from './feature-has-tag-error';
 | 
			
		||||
import IncompatibleProjectError from './incompatible-project-error';
 | 
			
		||||
import InvalidOperationError from './invalid-operation-error';
 | 
			
		||||
import InvalidTokenError from './invalid-token-error';
 | 
			
		||||
import MinimumOneEnvironmentError from './minimum-one-environment-error';
 | 
			
		||||
import NameExistsError from './name-exists-error';
 | 
			
		||||
import PermissionError from './permission-error';
 | 
			
		||||
import { OperationDeniedError } from './operation-denied-error';
 | 
			
		||||
@ -27,7 +26,6 @@ export {
 | 
			
		||||
    IncompatibleProjectError,
 | 
			
		||||
    InvalidOperationError,
 | 
			
		||||
    InvalidTokenError,
 | 
			
		||||
    MinimumOneEnvironmentError,
 | 
			
		||||
    NameExistsError,
 | 
			
		||||
    PermissionError,
 | 
			
		||||
    ForbiddenError,
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ export const UnleashApiErrorTypes = [
 | 
			
		||||
    'IncompatibleProjectError',
 | 
			
		||||
    'InvalidOperationError',
 | 
			
		||||
    'InvalidTokenError',
 | 
			
		||||
    'MinimumOneEnvironmentError',
 | 
			
		||||
    'NameExistsError',
 | 
			
		||||
    'NoAccessError',
 | 
			
		||||
    'NotFoundError',
 | 
			
		||||
 | 
			
		||||
@ -338,3 +338,54 @@ test('Override works correctly when enabling default and disabling prod and dev'
 | 
			
		||||
    expect(targetedEnvironment?.enabled).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 IFeatureEnvironmentStore,
 | 
			
		||||
    type IFeatureStrategiesStore,
 | 
			
		||||
    type IProjectEnvironment,
 | 
			
		||||
    type IProjectsAvailableOnEnvironment,
 | 
			
		||||
    type ISortOrder,
 | 
			
		||||
    type IUnleashConfig,
 | 
			
		||||
    type IUnleashStores,
 | 
			
		||||
@ -19,7 +19,6 @@ import NameExistsError from '../../error/name-exists-error';
 | 
			
		||||
import { sortOrderSchema } from '../../services/sort-order-schema';
 | 
			
		||||
import NotFoundError from '../../error/notfound-error';
 | 
			
		||||
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 { CreateFeatureStrategySchema } from '../../openapi';
 | 
			
		||||
import type EventService from '../events/event-service';
 | 
			
		||||
@ -77,8 +76,24 @@ export default class EnvironmentService {
 | 
			
		||||
 | 
			
		||||
    async getProjectEnvironments(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
    ): Promise<IProjectEnvironment[]> {
 | 
			
		||||
        return this.environmentStore.getProjectEnvironments(projectId);
 | 
			
		||||
    ): Promise<IProjectsAvailableOnEnvironment[]> {
 | 
			
		||||
        // 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> {
 | 
			
		||||
@ -254,22 +269,13 @@ export default class EnvironmentService {
 | 
			
		||||
        const projectEnvs =
 | 
			
		||||
            await this.projectStore.getEnvironmentsForProject(projectId);
 | 
			
		||||
 | 
			
		||||
        if (projectEnvs.length > 1) {
 | 
			
		||||
            await this.forceRemoveEnvironmentFromProject(
 | 
			
		||||
        await this.forceRemoveEnvironmentFromProject(environment, projectId);
 | 
			
		||||
        await this.eventService.storeEvent(
 | 
			
		||||
            new ProjectEnvironmentRemoved({
 | 
			
		||||
                project: projectId,
 | 
			
		||||
                environment,
 | 
			
		||||
                projectId,
 | 
			
		||||
            );
 | 
			
		||||
            await this.eventService.storeEvent(
 | 
			
		||||
                new ProjectEnvironmentRemoved({
 | 
			
		||||
                    project: projectId,
 | 
			
		||||
                    environment,
 | 
			
		||||
                    auditUser,
 | 
			
		||||
                }),
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        throw new MinimumOneEnvironmentError(
 | 
			
		||||
            'You must always have one active environment',
 | 
			
		||||
                auditUser,
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -98,22 +98,6 @@ test('Should remove environment from project', async () => {
 | 
			
		||||
    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 () => {
 | 
			
		||||
    const defaultStrategy = {
 | 
			
		||||
        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 type { IEnvironmentStore } from './environment-store-type';
 | 
			
		||||
 | 
			
		||||
@ -140,7 +143,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
 | 
			
		||||
    async getProjectEnvironments(
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        projectId: string,
 | 
			
		||||
    ): Promise<IProjectEnvironment[]> {
 | 
			
		||||
    ): Promise<IProjectsAvailableOnEnvironment[]> {
 | 
			
		||||
        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',
 | 
			
		||||
            $ref: '#/components/schemas/createFeatureStrategySchema',
 | 
			
		||||
        },
 | 
			
		||||
        visible: {
 | 
			
		||||
            type: 'boolean',
 | 
			
		||||
            example: true,
 | 
			
		||||
            description:
 | 
			
		||||
                'Indicates whether the environment can be enabled for feature flags in the project',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        schemas: {
 | 
			
		||||
 | 
			
		||||
@ -212,6 +212,10 @@ export interface IProjectEnvironment extends IEnvironment {
 | 
			
		||||
    defaultStrategy?: CreateFeatureStrategySchema;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectsAvailableOnEnvironment extends IProjectEnvironment {
 | 
			
		||||
    visible: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IEnvironmentCreate {
 | 
			
		||||
    name: string;
 | 
			
		||||
    type: string;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user