mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: enabled environments override now also moves projects and toggles to new environments
This commit is contained in:
		
							parent
							
								
									c3b064adfc
								
							
						
					
					
						commit
						8410a8e3ac
					
				@ -163,20 +163,26 @@ export default class EnvironmentStore implements IEnvironmentStore {
 | 
				
			|||||||
        return mapRow(row[0]);
 | 
					        return mapRow(row[0]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async disableAllExcept(environments: string[]): Promise<void> {
 | 
					    async disable(environments: IEnvironment[]): Promise<void> {
 | 
				
			||||||
        await this.db(TABLE)
 | 
					        await this.db(TABLE)
 | 
				
			||||||
            .update({
 | 
					            .update({
 | 
				
			||||||
                enabled: false,
 | 
					                enabled: false,
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .whereNotIn('name', environments);
 | 
					            .whereIn(
 | 
				
			||||||
 | 
					                'name',
 | 
				
			||||||
 | 
					                environments.map((env) => env.name),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async enable(environments: string[]): Promise<void> {
 | 
					    async enable(environments: IEnvironment[]): Promise<void> {
 | 
				
			||||||
        await this.db(TABLE)
 | 
					        await this.db(TABLE)
 | 
				
			||||||
            .update({
 | 
					            .update({
 | 
				
			||||||
                enabled: true,
 | 
					                enabled: true,
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
            .whereIn('name', environments);
 | 
					            .whereIn(
 | 
				
			||||||
 | 
					                'name',
 | 
				
			||||||
 | 
					                environments.map((env) => env.name),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async delete(name: string): Promise<void> {
 | 
					    async delete(name: string): Promise<void> {
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,11 @@ const COLUMNS = [
 | 
				
			|||||||
];
 | 
					];
 | 
				
			||||||
const TABLE = 'projects';
 | 
					const TABLE = 'projects';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IEnvironmentProjectLink {
 | 
				
			||||||
 | 
					    environmentName: string;
 | 
				
			||||||
 | 
					    projectId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProjectStore implements IProjectStore {
 | 
					class ProjectStore implements IProjectStore {
 | 
				
			||||||
    private db: Knex;
 | 
					    private db: Knex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -197,6 +202,15 @@ class ProjectStore implements IProjectStore {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getProjectLinksForEnvironments(
 | 
				
			||||||
 | 
					        environments: string[],
 | 
				
			||||||
 | 
					    ): Promise<IEnvironmentProjectLink[]> {
 | 
				
			||||||
 | 
					        let rows = await this.db('project_environments')
 | 
				
			||||||
 | 
					            .select(['project_id', 'environment_name'])
 | 
				
			||||||
 | 
					            .whereIn('environment_name', environments);
 | 
				
			||||||
 | 
					        return rows.map(this.mapLinkRow);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async deleteEnvironmentForProject(
 | 
					    async deleteEnvironmentForProject(
 | 
				
			||||||
        id: string,
 | 
					        id: string,
 | 
				
			||||||
        environment: string,
 | 
					        environment: string,
 | 
				
			||||||
@ -251,6 +265,14 @@ class ProjectStore implements IProjectStore {
 | 
				
			|||||||
            .then((res) => Number(res[0].count));
 | 
					            .then((res) => Number(res[0].count));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
				
			||||||
 | 
					    mapLinkRow(row): IEnvironmentProjectLink {
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            environmentName: row.environment_name,
 | 
				
			||||||
 | 
					            projectId: row.project_id,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
					    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
				
			||||||
    mapRow(row): IProject {
 | 
					    mapRow(row): IProject {
 | 
				
			||||||
        if (!row) {
 | 
					        if (!row) {
 | 
				
			||||||
 | 
				
			|||||||
@ -95,25 +95,87 @@ export default class EnvironmentService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async overrideEnabledProjects(
 | 
					    async overrideEnabledProjects(
 | 
				
			||||||
        environmentsToEnable: string[],
 | 
					        environmentNamesToEnable: string[],
 | 
				
			||||||
    ): Promise<void> {
 | 
					    ): Promise<void> {
 | 
				
			||||||
        if (environmentsToEnable.length === 0) {
 | 
					        if (environmentNamesToEnable.length === 0) {
 | 
				
			||||||
            return Promise.resolve();
 | 
					            return Promise.resolve();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const environmentsExist = await Promise.all(
 | 
					        const allEnvironments = await this.environmentStore.getAll();
 | 
				
			||||||
            environmentsToEnable.map((env) =>
 | 
					        const existingEnvironmentsToEnable = allEnvironments.filter((env) =>
 | 
				
			||||||
                this.environmentStore.exists(env),
 | 
					            environmentNamesToEnable.includes(env.name),
 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if (!environmentsExist.every((exists) => exists)) {
 | 
					
 | 
				
			||||||
            this.logger.error(
 | 
					        if (
 | 
				
			||||||
 | 
					            existingEnvironmentsToEnable.length !==
 | 
				
			||||||
 | 
					            environmentNamesToEnable.length
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					            this.logger.warn(
 | 
				
			||||||
                "Found environment enabled overrides but some of the specified environments don't exist, no overrides will be executed",
 | 
					                "Found environment enabled overrides but some of the specified environments don't exist, no overrides will be executed",
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            return;
 | 
					            return Promise.resolve();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        await this.environmentStore.disableAllExcept(environmentsToEnable);
 | 
					
 | 
				
			||||||
        await this.environmentStore.enable(environmentsToEnable);
 | 
					        const environmentsNotAlreadyEnabled =
 | 
				
			||||||
 | 
					            existingEnvironmentsToEnable.filter((env) => env.enabled == false);
 | 
				
			||||||
 | 
					        const environmentsToDisable = allEnvironments.filter((env) => {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                !environmentNamesToEnable.includes(env.name) &&
 | 
				
			||||||
 | 
					                env.enabled == true
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.environmentStore.disable(environmentsToDisable);
 | 
				
			||||||
 | 
					        await this.environmentStore.enable(environmentsNotAlreadyEnabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.remapProjectsLinks(
 | 
				
			||||||
 | 
					            environmentsToDisable,
 | 
				
			||||||
 | 
					            environmentsNotAlreadyEnabled,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async remapProjectsLinks(
 | 
				
			||||||
 | 
					        toDisable: IEnvironment[],
 | 
				
			||||||
 | 
					        toEnable: IEnvironment[],
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        const projectLinks =
 | 
				
			||||||
 | 
					            await this.projectStore.getProjectLinksForEnvironments(
 | 
				
			||||||
 | 
					                toDisable.map((env) => env.name),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const unlinkTasks = projectLinks.map((link) => {
 | 
				
			||||||
 | 
					            return this.forceRemoveEnvironmentFromProject(
 | 
				
			||||||
 | 
					                link.environmentName,
 | 
				
			||||||
 | 
					                link.projectId,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await Promise.all(unlinkTasks.flat());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const uniqueProjects = [
 | 
				
			||||||
 | 
					            ...new Set(projectLinks.map((link) => link.projectId)),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let linkTasks = uniqueProjects.map((project) => {
 | 
				
			||||||
 | 
					            return toEnable.map((enabledEnv) => {
 | 
				
			||||||
 | 
					                return this.addEnvironmentToProject(enabledEnv.name, project);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await Promise.all(linkTasks.flat());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async forceRemoveEnvironmentFromProject(
 | 
				
			||||||
 | 
					        environment: string,
 | 
				
			||||||
 | 
					        projectId: string,
 | 
				
			||||||
 | 
					    ): Promise<void> {
 | 
				
			||||||
 | 
					        await this.featureEnvironmentStore.disconnectFeatures(
 | 
				
			||||||
 | 
					            environment,
 | 
				
			||||||
 | 
					            projectId,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        await this.featureEnvironmentStore.disconnectProject(
 | 
				
			||||||
 | 
					            environment,
 | 
				
			||||||
 | 
					            projectId,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async removeEnvironmentFromProject(
 | 
					    async removeEnvironmentFromProject(
 | 
				
			||||||
@ -125,11 +187,7 @@ export default class EnvironmentService {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (projectEnvs.length > 1) {
 | 
					        if (projectEnvs.length > 1) {
 | 
				
			||||||
            await this.featureEnvironmentStore.disconnectFeatures(
 | 
					            await this.forceRemoveEnvironmentFromProject(
 | 
				
			||||||
                environment,
 | 
					 | 
				
			||||||
                projectId,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            await this.featureEnvironmentStore.disconnectProject(
 | 
					 | 
				
			||||||
                environment,
 | 
					                environment,
 | 
				
			||||||
                projectId,
 | 
					                projectId,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,6 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
 | 
				
			|||||||
    updateSortOrder(id: string, value: number): Promise<void>;
 | 
					    updateSortOrder(id: string, value: number): Promise<void>;
 | 
				
			||||||
    importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>;
 | 
					    importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>;
 | 
				
			||||||
    delete(name: string): Promise<void>;
 | 
					    delete(name: string): Promise<void>;
 | 
				
			||||||
    disableAllExcept(environments: string[]): Promise<void>;
 | 
					    disable(environments: IEnvironment[]): Promise<void>;
 | 
				
			||||||
    enable(environments: string[]): Promise<void>;
 | 
					    enable(environments: IEnvironment[]): Promise<void>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import { IEnvironmentProjectLink } from 'lib/db/project-store';
 | 
				
			||||||
import { IProject, IProjectWithCount } from '../model';
 | 
					import { IProject, IProjectWithCount } from '../model';
 | 
				
			||||||
import { Store } from './store';
 | 
					import { Store } from './store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,4 +36,7 @@ export interface IProjectStore extends Store<IProject, string> {
 | 
				
			|||||||
    getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
 | 
					    getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
 | 
				
			||||||
    count(): Promise<number>;
 | 
					    count(): Promise<number>;
 | 
				
			||||||
    getAll(query?: IProjectQuery): Promise<IProject[]>;
 | 
					    getAll(query?: IProjectQuery): Promise<IProject[]>;
 | 
				
			||||||
 | 
					    getProjectLinksForEnvironments(
 | 
				
			||||||
 | 
					        environments: string[],
 | 
				
			||||||
 | 
					    ): Promise<IEnvironmentProjectLink[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -150,7 +150,7 @@ test('Setting an override disables all other envs', async () => {
 | 
				
			|||||||
        type: 'production',
 | 
					        type: 'production',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //Set these to the wrong state so we can assert that overriding them flips
 | 
					    //Set these to the wrong state so we can assert that overriding them flips their state
 | 
				
			||||||
    await service.toggleEnvironment(disabledEnvName, true);
 | 
					    await service.toggleEnvironment(disabledEnvName, true);
 | 
				
			||||||
    await service.toggleEnvironment(enabledEnvName, false);
 | 
					    await service.toggleEnvironment(enabledEnvName, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -165,7 +165,6 @@ test('Setting an override disables all other envs', async () => {
 | 
				
			|||||||
        .filter((x) => x.name != enabledEnvName)
 | 
					        .filter((x) => x.name != enabledEnvName)
 | 
				
			||||||
        .map((env) => env.enabled);
 | 
					        .map((env) => env.enabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log(allOtherEnvironments);
 | 
					 | 
				
			||||||
    expect(targetedEnvironment.enabled).toBe(true);
 | 
					    expect(targetedEnvironment.enabled).toBe(true);
 | 
				
			||||||
    expect(allOtherEnvironments.every((x) => x === false)).toBe(true);
 | 
					    expect(allOtherEnvironments.every((x) => x === false)).toBe(true);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@ -189,3 +188,47 @@ test('Passing an empty override does nothing', async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    expect(targetedEnvironment.enabled).toBe(true);
 | 
					    expect(targetedEnvironment.enabled).toBe(true);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('When given overrides should remap projects to override environments', async () => {
 | 
				
			||||||
 | 
					    const enabledEnvName = 'enabled';
 | 
				
			||||||
 | 
					    const ignoredEnvName = 'ignored';
 | 
				
			||||||
 | 
					    const disabledEnvName = 'disabled';
 | 
				
			||||||
 | 
					    const toggleName = 'test-toggle';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.stores.environmentStore.create({
 | 
				
			||||||
 | 
					        name: enabledEnvName,
 | 
				
			||||||
 | 
					        type: 'production',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.stores.environmentStore.create({
 | 
				
			||||||
 | 
					        name: ignoredEnvName,
 | 
				
			||||||
 | 
					        type: 'production',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.stores.environmentStore.create({
 | 
				
			||||||
 | 
					        name: disabledEnvName,
 | 
				
			||||||
 | 
					        type: 'production',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await service.toggleEnvironment(disabledEnvName, true);
 | 
				
			||||||
 | 
					    await service.toggleEnvironment(ignoredEnvName, true);
 | 
				
			||||||
 | 
					    await service.toggleEnvironment(enabledEnvName, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await stores.featureToggleStore.create('default', {
 | 
				
			||||||
 | 
					        name: toggleName,
 | 
				
			||||||
 | 
					        type: 'release',
 | 
				
			||||||
 | 
					        description: '',
 | 
				
			||||||
 | 
					        stale: false,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await service.addEnvironmentToProject(disabledEnvName, 'default');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await service.overrideEnabledProjects([enabledEnvName]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const projects = await stores.projectStore.getEnvironmentsForProject(
 | 
				
			||||||
 | 
					        'default',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(projects).toContain('enabled');
 | 
				
			||||||
 | 
					    expect(projects).not.toContain('default');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								src/test/fixtures/fake-environment-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								src/test/fixtures/fake-environment-store.ts
									
									
									
									
										vendored
									
									
								
							@ -10,16 +10,18 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    environments: IEnvironment[] = [];
 | 
					    environments: IEnvironment[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    disableAllExcept(environments: string[]): Promise<void> {
 | 
					    disable(environments: IEnvironment[]): Promise<void> {
 | 
				
			||||||
        for (let env of this.environments) {
 | 
					        for (let env of this.environments) {
 | 
				
			||||||
            if (!environments.includes(env.name)) env.enabled = false;
 | 
					            if (environments.map((e) => e.name).includes(env.name))
 | 
				
			||||||
 | 
					                env.enabled = false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return Promise.resolve();
 | 
					        return Promise.resolve();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    enable(environments: string[]): Promise<void> {
 | 
					    enable(environments: IEnvironment[]): Promise<void> {
 | 
				
			||||||
        for (let env of this.environments) {
 | 
					        for (let env of this.environments) {
 | 
				
			||||||
            if (environments.includes(env.name)) env.enabled = true;
 | 
					            if (environments.map((e) => e.name).includes(env.name))
 | 
				
			||||||
 | 
					                env.enabled = true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return Promise.resolve();
 | 
					        return Promise.resolve();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							@ -5,15 +5,23 @@ import {
 | 
				
			|||||||
} from '../../lib/types/stores/project-store';
 | 
					} from '../../lib/types/stores/project-store';
 | 
				
			||||||
import { IProject, IProjectWithCount } from '../../lib/types/model';
 | 
					import { IProject, IProjectWithCount } from '../../lib/types/model';
 | 
				
			||||||
import NotFoundError from '../../lib/error/notfound-error';
 | 
					import NotFoundError from '../../lib/error/notfound-error';
 | 
				
			||||||
 | 
					import { IEnvironmentProjectLink } from 'lib/db/project-store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class FakeProjectStore implements IProjectStore {
 | 
					export default class FakeProjectStore implements IProjectStore {
 | 
				
			||||||
 | 
					    projects: IProject[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    projectEnvironment: Map<string, Set<string>> = new Map();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getEnvironmentsForProject(): Promise<string[]> {
 | 
					    getEnvironmentsForProject(): Promise<string[]> {
 | 
				
			||||||
        throw new Error('Method not implemented.');
 | 
					        throw new Error('Method not implemented.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    projects: IProject[] = [];
 | 
					    getProjectLinksForEnvironments(
 | 
				
			||||||
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
    projectEnvironment: Map<string, Set<string>> = new Map();
 | 
					        environments: string[],
 | 
				
			||||||
 | 
					    ): Promise<IEnvironmentProjectLink[]> {
 | 
				
			||||||
 | 
					        throw new Error('Method not implemented.');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async addEnvironmentToProject(
 | 
					    async addEnvironmentToProject(
 | 
				
			||||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user