mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
feat: project mode (#3334)
This commit is contained in:
parent
a983cf15b9
commit
1064dfa40c
@ -19,6 +19,7 @@ const CreateProject = () => {
|
||||
const {
|
||||
projectId,
|
||||
projectName,
|
||||
projectMode,
|
||||
projectDesc,
|
||||
setProjectId,
|
||||
setProjectName,
|
||||
@ -28,6 +29,7 @@ const CreateProject = () => {
|
||||
validateProjectId,
|
||||
validateName,
|
||||
setProjectStickiness,
|
||||
setProjectMode,
|
||||
projectStickiness,
|
||||
errors,
|
||||
} = useProjectForm();
|
||||
@ -92,8 +94,10 @@ const CreateProject = () => {
|
||||
projectId={projectId}
|
||||
setProjectId={setProjectId}
|
||||
projectName={projectName}
|
||||
projectMode={projectMode}
|
||||
projectStickiness={projectStickiness}
|
||||
setProjectStickiness={setProjectStickiness}
|
||||
setProjectMode={setProjectMode}
|
||||
setProjectName={setProjectName}
|
||||
projectDesc={projectDesc}
|
||||
setProjectDesc={setProjectDesc}
|
||||
|
@ -31,10 +31,12 @@ const EditProject = () => {
|
||||
projectName,
|
||||
projectDesc,
|
||||
projectStickiness,
|
||||
projectMode,
|
||||
setProjectId,
|
||||
setProjectName,
|
||||
setProjectDesc,
|
||||
setProjectStickiness,
|
||||
setProjectMode,
|
||||
getProjectPayload,
|
||||
clearErrors,
|
||||
validateProjectId,
|
||||
@ -44,7 +46,8 @@ const EditProject = () => {
|
||||
id,
|
||||
project.name,
|
||||
project.description,
|
||||
defaultStickiness
|
||||
defaultStickiness,
|
||||
project.mode
|
||||
);
|
||||
|
||||
const formatApiCode = () => {
|
||||
@ -111,9 +114,11 @@ const EditProject = () => {
|
||||
projectId={projectId}
|
||||
setProjectId={setProjectId}
|
||||
projectName={projectName}
|
||||
projectMode={projectMode}
|
||||
setProjectName={setProjectName}
|
||||
projectStickiness={projectStickiness}
|
||||
setProjectStickiness={setProjectStickiness}
|
||||
setProjectMode={setProjectMode}
|
||||
projectDesc={projectDesc}
|
||||
setProjectDesc={setProjectDesc}
|
||||
mode="Edit"
|
||||
|
@ -1,23 +1,27 @@
|
||||
import React from 'react';
|
||||
import { trim } from 'component/common/util';
|
||||
import {
|
||||
StyledForm,
|
||||
StyledButton,
|
||||
StyledButtonContainer,
|
||||
StyledContainer,
|
||||
StyledDescription,
|
||||
StyledForm,
|
||||
StyledInput,
|
||||
StyledTextField,
|
||||
StyledButtonContainer,
|
||||
StyledButton,
|
||||
} from './ProjectForm.styles';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import Select from 'component/common/select';
|
||||
|
||||
interface IProjectForm {
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectDesc: string;
|
||||
projectStickiness?: string;
|
||||
projectMode?: string;
|
||||
setProjectStickiness?: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectMode?: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectId: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||
@ -37,17 +41,20 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
projectName,
|
||||
projectDesc,
|
||||
projectStickiness,
|
||||
projectMode,
|
||||
setProjectId,
|
||||
setProjectName,
|
||||
setProjectDesc,
|
||||
setProjectStickiness,
|
||||
setProjectMode,
|
||||
errors,
|
||||
mode,
|
||||
validateProjectId,
|
||||
clearErrors,
|
||||
}) => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { projectScopedStickiness } = uiConfig.flags;
|
||||
const { projectScopedStickiness, projectMode: projectModeFlag } =
|
||||
uiConfig.flags;
|
||||
|
||||
return (
|
||||
<StyledForm onSubmit={handleSubmit}>
|
||||
@ -113,6 +120,30 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(projectModeFlag)}
|
||||
show={
|
||||
<>
|
||||
<StyledDescription>
|
||||
What is your project mode?
|
||||
</StyledDescription>
|
||||
<Select
|
||||
id="project-mode"
|
||||
value={projectMode}
|
||||
label="Project mode"
|
||||
name="Project mode"
|
||||
onChange={e => {
|
||||
setProjectMode?.(e.target.value);
|
||||
}}
|
||||
options={[
|
||||
{ key: 'open', label: 'open' },
|
||||
{ key: 'protected', label: 'protected' },
|
||||
]}
|
||||
style={{ minWidth: '150px' }}
|
||||
></Select>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
|
||||
<StyledButtonContainer>
|
||||
|
@ -7,7 +7,8 @@ const useProjectForm = (
|
||||
initialProjectId = '',
|
||||
initialProjectName = '',
|
||||
initialProjectDesc = '',
|
||||
initialProjectStickiness = 'default'
|
||||
initialProjectStickiness = 'default',
|
||||
initialProjectMode = 'open'
|
||||
) => {
|
||||
const [projectId, setProjectId] = useState(initialProjectId);
|
||||
const { defaultStickiness } = useDefaultProjectSettings(projectId);
|
||||
@ -17,6 +18,7 @@ const useProjectForm = (
|
||||
const [projectStickiness, setProjectStickiness] = useState(
|
||||
defaultStickiness || initialProjectStickiness
|
||||
);
|
||||
const [projectMode, setProjectMode] = useState(initialProjectMode);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const { validateId } = useProjectApi();
|
||||
@ -33,12 +35,17 @@ const useProjectForm = (
|
||||
setProjectDesc(initialProjectDesc);
|
||||
}, [initialProjectDesc]);
|
||||
|
||||
useEffect(() => {
|
||||
setProjectMode(initialProjectMode);
|
||||
}, [initialProjectMode]);
|
||||
|
||||
const getProjectPayload = () => {
|
||||
return {
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
description: projectDesc,
|
||||
projectStickiness,
|
||||
mode: projectMode,
|
||||
};
|
||||
};
|
||||
|
||||
@ -74,10 +81,12 @@ const useProjectForm = (
|
||||
projectName,
|
||||
projectDesc,
|
||||
projectStickiness,
|
||||
projectMode,
|
||||
setProjectId,
|
||||
setProjectName,
|
||||
setProjectDesc,
|
||||
setProjectStickiness,
|
||||
setProjectMode,
|
||||
getProjectPayload,
|
||||
validateName,
|
||||
validateProjectId,
|
||||
|
@ -12,6 +12,7 @@ const fallbackProject: IProject = {
|
||||
version: '1',
|
||||
description: 'Default',
|
||||
favorite: false,
|
||||
mode: 'open',
|
||||
stats: {
|
||||
archivedCurrentWindow: 0,
|
||||
archivedPastWindow: 0,
|
||||
|
@ -23,6 +23,7 @@ export interface IProject {
|
||||
stats: ProjectStatsSchema;
|
||||
favorite: boolean;
|
||||
features: IFeatureToggleListItem[];
|
||||
mode: 'open' | 'protected';
|
||||
}
|
||||
|
||||
export interface IProjectHealthReport extends IProject {
|
||||
|
@ -51,6 +51,7 @@ export interface IFlags {
|
||||
bulkOperations?: boolean;
|
||||
projectScopedSegments?: boolean;
|
||||
projectScopedStickiness?: boolean;
|
||||
projectMode?: boolean;
|
||||
}
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -2,7 +2,12 @@ import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import { IEnvironment, IProject, IProjectWithCount } from '../types/model';
|
||||
import {
|
||||
IEnvironment,
|
||||
IProject,
|
||||
IProjectWithCount,
|
||||
ProjectMode,
|
||||
} from '../types/model';
|
||||
import {
|
||||
IProjectHealthUpdate,
|
||||
IProjectInsert,
|
||||
@ -26,6 +31,8 @@ const COLUMNS = [
|
||||
'updated_at',
|
||||
];
|
||||
const TABLE = 'projects';
|
||||
const SETTINGS_COLUMNS = ['project_mode'];
|
||||
const SETTINGS_TABLE = 'project_settings';
|
||||
|
||||
export interface IEnvironmentProjectLink {
|
||||
environmentName: string;
|
||||
@ -63,7 +70,7 @@ class ProjectStore implements IProjectStore {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
fieldToRow(data): IProjectInsert {
|
||||
fieldToRow(data): Omit<IProjectInsert, 'mode'> {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
@ -168,8 +175,13 @@ class ProjectStore implements IProjectStore {
|
||||
|
||||
async get(id: string): Promise<IProject> {
|
||||
return this.db
|
||||
.first(COLUMNS)
|
||||
.first([...COLUMNS, ...SETTINGS_COLUMNS])
|
||||
.from(TABLE)
|
||||
.leftJoin(
|
||||
SETTINGS_TABLE,
|
||||
`${SETTINGS_TABLE}.project`,
|
||||
`${TABLE}.id`,
|
||||
)
|
||||
.where({ id })
|
||||
.then(this.mapRow);
|
||||
}
|
||||
@ -189,11 +201,19 @@ class ProjectStore implements IProjectStore {
|
||||
.update({ health: healthUpdate.health, updated_at: new Date() });
|
||||
}
|
||||
|
||||
async create(project: IProjectInsert): Promise<IProject> {
|
||||
async create(
|
||||
project: IProjectInsert & { mode: ProjectMode },
|
||||
): Promise<IProject> {
|
||||
const row = await this.db(TABLE)
|
||||
.insert(this.fieldToRow(project))
|
||||
.returning('*');
|
||||
return this.mapRow(row[0]);
|
||||
const settingsRow = await this.db(SETTINGS_TABLE)
|
||||
.insert({
|
||||
project: project.id,
|
||||
project_mode: project.mode,
|
||||
})
|
||||
.returning('*');
|
||||
return this.mapRow({ ...row[0], ...settingsRow[0] });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
@ -202,6 +222,12 @@ class ProjectStore implements IProjectStore {
|
||||
await this.db(TABLE)
|
||||
.where({ id: data.id })
|
||||
.update(this.fieldToRow(data));
|
||||
await this.db(SETTINGS_TABLE)
|
||||
.where({ project: data.id })
|
||||
.update({
|
||||
project_mode: data.mode,
|
||||
})
|
||||
.returning('*');
|
||||
} catch (err) {
|
||||
this.logger.error('Could not update project, error: ', err);
|
||||
}
|
||||
@ -460,7 +486,7 @@ class ProjectStore implements IProjectStore {
|
||||
createdAt: row.created_at,
|
||||
health: row.health ?? 100,
|
||||
updatedAt: row.updated_at || new Date(),
|
||||
mode: 'open',
|
||||
mode: row.project_mode || 'open',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ const createProject = async () => {
|
||||
name: DEFAULT_PROJECT,
|
||||
description: '',
|
||||
id: DEFAULT_PROJECT,
|
||||
mode: 'open' as const,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -109,6 +109,7 @@ const createProject = async () => {
|
||||
name: DEFAULT_PROJECT,
|
||||
description: '',
|
||||
id: DEFAULT_PROJECT,
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${DEFAULT_PROJECT}/environments`)
|
||||
|
@ -25,6 +25,14 @@ export const healthOverviewSchema = {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['open', 'protected'],
|
||||
example: 'open',
|
||||
nullable: true,
|
||||
description:
|
||||
'A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or `null`',
|
||||
},
|
||||
members: {
|
||||
type: 'number',
|
||||
},
|
||||
|
@ -36,6 +36,13 @@ export const projectOverviewSchema = {
|
||||
example: 'DX squad feature release',
|
||||
description: 'Additional information about the project',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['open', 'protected'],
|
||||
example: 'open',
|
||||
description:
|
||||
'A mode of the project affecting what actions are possible in this project',
|
||||
},
|
||||
members: {
|
||||
type: 'number',
|
||||
example: 4,
|
||||
|
@ -59,9 +59,8 @@ export const projectSchema = {
|
||||
type: 'string',
|
||||
enum: ['open', 'protected'],
|
||||
example: 'open',
|
||||
nullable: true,
|
||||
description:
|
||||
'A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or `null`',
|
||||
'A mode of the project affecting what actions are possible in this project',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
|
@ -7,5 +7,6 @@ export const projectSchema = joi
|
||||
id: nameType,
|
||||
name: joi.string().required(),
|
||||
description: joi.string().allow(null).allow('').optional(),
|
||||
mode: joi.string().valid('open', 'protected').default('open'),
|
||||
})
|
||||
.options({ allowUnknown: false, stripUnknown: true });
|
||||
|
@ -166,7 +166,7 @@ export default class ProjectService {
|
||||
}
|
||||
|
||||
async createProject(
|
||||
newProject: Pick<IProject, 'id' | 'name'>,
|
||||
newProject: Pick<IProject, 'id' | 'name' | 'mode'>,
|
||||
user: IUser,
|
||||
): Promise<IProject> {
|
||||
const data = await projectSchema.validateAsync(newProject);
|
||||
@ -853,6 +853,7 @@ export default class ProjectService {
|
||||
stats: projectStats,
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
mode: project.mode,
|
||||
health: project.health || 0,
|
||||
favorite: favorite,
|
||||
updatedAt: project.updatedAt,
|
||||
|
@ -518,6 +518,7 @@ test('Should not import an existing project', async () => {
|
||||
id: 'default',
|
||||
name: 'default',
|
||||
description: 'Some fancy description for project',
|
||||
mode: 'open' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -546,6 +547,7 @@ test('Should drop projects before import if specified', async () => {
|
||||
id: 'fancy',
|
||||
name: 'extra',
|
||||
description: 'Not expected to be seen after import',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await stateService.import({ data, dropBeforeImport: true });
|
||||
const hasProject = await stores.projectStore.hasProject('fancy');
|
||||
@ -558,6 +560,7 @@ test('Should export projects', async () => {
|
||||
id: 'fancy',
|
||||
name: 'extra',
|
||||
description: 'No surprises here',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
const exported = await stateService.export({
|
||||
includeFeatureToggles: false,
|
||||
@ -579,6 +582,7 @@ test('exporting to new format works', async () => {
|
||||
id: 'fancy',
|
||||
name: 'extra',
|
||||
description: 'No surprises here',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'dev',
|
||||
@ -622,7 +626,7 @@ test('exporting variants to v4 format should not include variants in features',
|
||||
|
||||
exported.featureEnvironments.forEach((fe) => {
|
||||
expect(fe.variants).toHaveLength(1);
|
||||
expect(fe.variants[0].name).toBe(`${fe.environment}-variant`);
|
||||
expect(fe.variants?.[0].name).toBe(`${fe.environment}-variant`);
|
||||
});
|
||||
expect(exported.environments).toHaveLength(3);
|
||||
});
|
||||
@ -633,6 +637,7 @@ test('featureStrategies can keep existing', async () => {
|
||||
id: 'fancy',
|
||||
name: 'extra',
|
||||
description: 'No surprises here',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'dev',
|
||||
@ -679,6 +684,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
|
||||
id: 'fancy',
|
||||
name: 'extra',
|
||||
description: 'No surprises here',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'dev',
|
||||
|
@ -173,6 +173,8 @@ export interface IFeatureOverview {
|
||||
environments: IEnvironmentOverview[];
|
||||
}
|
||||
|
||||
export type ProjectMode = 'open' | 'protected';
|
||||
|
||||
export interface IProjectOverview {
|
||||
name: string;
|
||||
description: string;
|
||||
@ -184,6 +186,7 @@ export interface IProjectOverview {
|
||||
favorite?: boolean;
|
||||
updatedAt?: Date;
|
||||
stats?: IProjectStats;
|
||||
mode: ProjectMode;
|
||||
}
|
||||
|
||||
export interface IProjectHealthReport extends IProjectOverview {
|
||||
@ -368,7 +371,6 @@ export interface IProject {
|
||||
changeRequestsEnabled?: boolean;
|
||||
mode: ProjectMode;
|
||||
}
|
||||
export type ProjectMode = 'open' | 'protected';
|
||||
|
||||
export interface ICustomRole {
|
||||
id: number;
|
||||
|
@ -2,7 +2,12 @@ import {
|
||||
IEnvironmentProjectLink,
|
||||
IProjectMembersCount,
|
||||
} from '../../db/project-store';
|
||||
import { IEnvironment, IProject, IProjectWithCount } from '../model';
|
||||
import {
|
||||
IEnvironment,
|
||||
IProject,
|
||||
IProjectWithCount,
|
||||
ProjectMode,
|
||||
} from '../model';
|
||||
import { Store } from './store';
|
||||
|
||||
export interface IProjectInsert {
|
||||
@ -11,6 +16,7 @@ export interface IProjectInsert {
|
||||
description: string;
|
||||
updatedAt?: Date;
|
||||
changeRequestsEnabled?: boolean;
|
||||
mode: ProjectMode;
|
||||
}
|
||||
|
||||
export interface IProjectArchived {
|
||||
|
@ -75,11 +75,13 @@ test('Should get archived toggles via project', async () => {
|
||||
id: 'proj-1',
|
||||
name: 'proj-1',
|
||||
description: '',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await db.stores.projectStore.create({
|
||||
id: 'proj-2',
|
||||
name: 'proj-2',
|
||||
description: '',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
|
||||
await db.stores.featureToggleStore.create('proj-1', {
|
||||
|
@ -36,6 +36,7 @@ test('should return instance statistics with correct number of projects', async
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
description: 'lorem',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
|
||||
return app.request
|
||||
|
@ -544,7 +544,7 @@ describe('Interacting with features using project IDs that belong to other proje
|
||||
rootRole: RoleName.ADMIN,
|
||||
});
|
||||
await app.services.projectService.createProject(
|
||||
{ name: otherProject, id: otherProject },
|
||||
{ name: otherProject, id: otherProject, mode: 'open' },
|
||||
dummyAdmin,
|
||||
);
|
||||
|
||||
@ -701,8 +701,8 @@ test('Should patch feature toggle', async () => {
|
||||
});
|
||||
const updateForOurToggle = events.find((e) => e.data.name === name);
|
||||
expect(updateForOurToggle).toBeTruthy();
|
||||
expect(updateForOurToggle.data.description).toBe('New desc');
|
||||
expect(updateForOurToggle.data.type).toBe('kill-switch');
|
||||
expect(updateForOurToggle?.data.description).toBe('New desc');
|
||||
expect(updateForOurToggle?.data.type).toBe('kill-switch');
|
||||
});
|
||||
|
||||
test('Should patch feature toggle and not remove variants', async () => {
|
||||
@ -1956,6 +1956,7 @@ test('Should not allow changing project to target project without the same enabl
|
||||
name: 'Project to be moved to',
|
||||
id: targetProject,
|
||||
description: '',
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
await db.stores.environmentStore.create({
|
||||
@ -2042,6 +2043,7 @@ test('Should allow changing project to target project with the same enabled envi
|
||||
name: 'Project to be moved to',
|
||||
id: targetProject,
|
||||
description: '',
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
await db.stores.environmentStore.create({
|
||||
@ -2284,6 +2286,7 @@ test('Can create toggle with impression data on different project', async () =>
|
||||
id: 'impression-data',
|
||||
name: 'ImpressionData',
|
||||
description: '',
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
const toggle = {
|
||||
@ -2313,6 +2316,7 @@ test('should reject invalid constraint values for multi-valued constraints', asy
|
||||
id: uuidv4(),
|
||||
name: uuidv4(),
|
||||
description: '',
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
const toggle = await db.stores.featureToggleStore.create(project.id, {
|
||||
@ -2359,6 +2363,7 @@ test('should add default constraint values for single-valued constraints', async
|
||||
id: uuidv4(),
|
||||
name: uuidv4(),
|
||||
description: '',
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
const toggle = await db.stores.featureToggleStore.create(project.id, {
|
||||
@ -2418,6 +2423,7 @@ test('should allow long parameter values', async () => {
|
||||
id: uuidv4(),
|
||||
name: uuidv4(),
|
||||
description: uuidv4(),
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
const toggle = await db.stores.featureToggleStore.create(project.id, {
|
||||
|
@ -26,7 +26,12 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
test('Should ONLY return default project', async () => {
|
||||
projectStore.create({ id: 'test2', name: 'test', description: '' });
|
||||
projectStore.create({
|
||||
id: 'test2',
|
||||
name: 'test',
|
||||
description: '',
|
||||
mode: 'open',
|
||||
});
|
||||
|
||||
const { body } = await app.request
|
||||
.get('/api/admin/projects')
|
||||
|
@ -154,6 +154,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
|
||||
name: projectId,
|
||||
id: projectId,
|
||||
description: 'Project for export',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await app.services.environmentService.addEnvironmentToProject(
|
||||
environment,
|
||||
@ -201,6 +202,7 @@ test('Roundtrip with tags works', async () => {
|
||||
name: projectId,
|
||||
id: projectId,
|
||||
description: 'Project for export',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await app.services.environmentService.addEnvironmentToProject(
|
||||
environment,
|
||||
@ -265,6 +267,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
|
||||
name: projectId,
|
||||
id: projectId,
|
||||
description: 'Project for export',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
@ -364,6 +367,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
|
||||
name: projectId,
|
||||
id: projectId,
|
||||
description: 'Project for export',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
await app.services.environmentService.addEnvironmentToProject(
|
||||
environment,
|
||||
|
@ -289,6 +289,7 @@ test('returns a feature toggles impression data for a different project', async
|
||||
id: 'impression-data-client',
|
||||
name: 'ImpressionData',
|
||||
description: '',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await db.stores.projectStore.create(project);
|
||||
|
@ -35,6 +35,7 @@ beforeAll(async () => {
|
||||
id: project2,
|
||||
name: 'Test Project 2',
|
||||
description: '',
|
||||
mode: 'open' as const,
|
||||
});
|
||||
|
||||
await environmentService.addEnvironmentToProject(environment, project);
|
||||
|
@ -1859,6 +1859,16 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"members": {
|
||||
"type": "number",
|
||||
},
|
||||
"mode": {
|
||||
"description": "A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or \`null\`",
|
||||
"enum": [
|
||||
"open",
|
||||
"protected",
|
||||
],
|
||||
"example": "open",
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
@ -1912,6 +1922,16 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"members": {
|
||||
"type": "number",
|
||||
},
|
||||
"mode": {
|
||||
"description": "A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or \`null\`",
|
||||
"enum": [
|
||||
"open",
|
||||
"protected",
|
||||
],
|
||||
"example": "open",
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
},
|
||||
@ -2771,6 +2791,15 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"example": 4,
|
||||
"type": "number",
|
||||
},
|
||||
"mode": {
|
||||
"description": "A mode of the project affecting what actions are possible in this project",
|
||||
"enum": [
|
||||
"open",
|
||||
"protected",
|
||||
],
|
||||
"example": "open",
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of this project",
|
||||
"example": "dx-squad",
|
||||
@ -2837,13 +2866,12 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"type": "number",
|
||||
},
|
||||
"mode": {
|
||||
"description": "A mode of the project affecting what actions are possible in this project. During a rollout of project modes this feature can be optional or \`null\`",
|
||||
"description": "A mode of the project affecting what actions are possible in this project",
|
||||
"enum": [
|
||||
"open",
|
||||
"protected",
|
||||
],
|
||||
"example": "open",
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
|
@ -100,7 +100,10 @@ const createProject = async (id: string, name: string): Promise<void> => {
|
||||
name: randomId(),
|
||||
email: `${randomId()}@example.com`,
|
||||
});
|
||||
await app.services.projectService.createProject({ id, name }, user);
|
||||
await app.services.projectService.createProject(
|
||||
{ id, name, mode: 'open' },
|
||||
user,
|
||||
);
|
||||
};
|
||||
|
||||
test('should require a frontend token or an admin token', async () => {
|
||||
@ -188,14 +191,14 @@ test('should allow requests with a token secret alias', async () => {
|
||||
.expect((res) => expect(res.body.toggles[0].name).toEqual(featureB));
|
||||
await app.request
|
||||
.get('/api/frontend')
|
||||
.set('Authorization', tokenA.alias)
|
||||
.set('Authorization', tokenA.alias!)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => expect(res.body.toggles).toHaveLength(1))
|
||||
.expect((res) => expect(res.body.toggles[0].name).toEqual(featureA));
|
||||
await app.request
|
||||
.get('/api/frontend')
|
||||
.set('Authorization', tokenB.alias)
|
||||
.set('Authorization', tokenB.alias!)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => expect(res.body.toggles).toHaveLength(1))
|
||||
@ -952,7 +955,7 @@ test('should terminate data polling when stop is called', async () => {
|
||||
frontendToken.secret,
|
||||
);
|
||||
|
||||
const logTrap = [];
|
||||
const logTrap: any[] = [];
|
||||
const getDebugLogger = (): Logger => {
|
||||
return {
|
||||
/* eslint-disable-next-line */
|
||||
@ -978,7 +981,7 @@ test('should terminate data polling when stop is called', async () => {
|
||||
},
|
||||
db.stores,
|
||||
app.services,
|
||||
user,
|
||||
user!,
|
||||
);
|
||||
|
||||
await proxyRepository.start();
|
||||
|
@ -36,6 +36,7 @@ beforeAll(async () => {
|
||||
id: 'test-project',
|
||||
name: 'Test Project',
|
||||
description: 'Fancy',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const user = await stores.userStore.insert({
|
||||
name: 'Some Name',
|
||||
@ -219,11 +220,11 @@ test('should return user with multiple projects', async () => {
|
||||
tokens[1].secret,
|
||||
);
|
||||
|
||||
expect(multiProjectUser.projects).toStrictEqual([
|
||||
expect(multiProjectUser!.projects).toStrictEqual([
|
||||
'test-project',
|
||||
'default',
|
||||
]);
|
||||
expect(singleProjectUser.projects).toStrictEqual(['test-project']);
|
||||
expect(singleProjectUser!.projects).toStrictEqual(['test-project']);
|
||||
});
|
||||
|
||||
test('should not partially create token if projects are invalid', async () => {
|
||||
|
@ -100,6 +100,7 @@ test('should list all projects', async () => {
|
||||
id: 'test-list',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -113,6 +114,7 @@ test('should create new project', async () => {
|
||||
id: 'test',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'protected' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -121,6 +123,7 @@ test('should create new project', async () => {
|
||||
expect(project.name).toEqual(ret.name);
|
||||
expect(project.description).toEqual(ret.description);
|
||||
expect(ret.createdAt).toBeTruthy();
|
||||
expect(ret.mode).toEqual('protected');
|
||||
});
|
||||
|
||||
test('should delete project', async () => {
|
||||
@ -128,6 +131,7 @@ test('should delete project', async () => {
|
||||
id: 'test-delete',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -145,6 +149,7 @@ test('should not be able to delete project with toggles', async () => {
|
||||
id: 'test-delete-with-toggles',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
await stores.featureToggleStore.create(project.id, {
|
||||
@ -180,6 +185,7 @@ test('should not be able to create existing project', async () => {
|
||||
id: 'test-delete',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
try {
|
||||
await projectService.createProject(project, user);
|
||||
@ -210,13 +216,14 @@ test('should update project', async () => {
|
||||
id: 'test-update',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
const updatedProject = {
|
||||
id: 'test-update',
|
||||
name: 'New name',
|
||||
description: 'Blah longer desc',
|
||||
mode: 'open' as const,
|
||||
mode: 'protected' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -226,6 +233,7 @@ test('should update project', async () => {
|
||||
|
||||
expect(updatedProject.name).toBe(readProject.name);
|
||||
expect(updatedProject.description).toBe(readProject.description);
|
||||
expect(updatedProject.mode).toBe('protected');
|
||||
});
|
||||
|
||||
test('should give error when getting unknown project', async () => {
|
||||
@ -241,6 +249,7 @@ test('should get list of users with access to project', async () => {
|
||||
id: 'test-roles-access',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
const { users } = await projectService.getAccessToProject(project.id);
|
||||
@ -262,6 +271,7 @@ test('should add a member user to the project', async () => {
|
||||
id: 'add-users',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -317,6 +327,7 @@ test('should add admin users to the project', async () => {
|
||||
id: 'add-admin-users',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -363,6 +374,7 @@ test('add user should fail if user already have access', async () => {
|
||||
id: 'add-users-twice',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -397,6 +409,7 @@ test('should remove user from the project', async () => {
|
||||
id: 'remove-users',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -431,6 +444,7 @@ test('should not remove user from the project', async () => {
|
||||
id: 'remove-users-not-allowed',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -454,6 +468,7 @@ test('should not change project if feature toggle project does not match current
|
||||
id: 'test-change-project',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
const toggle = { name: 'test-toggle' };
|
||||
@ -480,6 +495,7 @@ test('should return 404 if no project is found with the project id', async () =>
|
||||
id: 'test-change-project-2',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
const toggle = { name: 'test-toggle-2' };
|
||||
@ -504,12 +520,14 @@ test('should fail if user is not authorized', async () => {
|
||||
id: 'test-change-project-3',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
const projectDestination = {
|
||||
id: 'test-change-project-dest',
|
||||
name: 'New project 2',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
const toggle = { name: 'test-toggle-3' };
|
||||
@ -537,8 +555,16 @@ test('should fail if user is not authorized', async () => {
|
||||
});
|
||||
|
||||
test('should change project when checks pass', async () => {
|
||||
const projectA = { id: randomId(), name: randomId() };
|
||||
const projectB = { id: randomId(), name: randomId() };
|
||||
const projectA = {
|
||||
id: randomId(),
|
||||
name: randomId(),
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const projectB = {
|
||||
id: randomId(),
|
||||
name: randomId(),
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const toggle = { name: randomId() };
|
||||
|
||||
await projectService.createProject(projectA, user);
|
||||
@ -558,8 +584,16 @@ test('should change project when checks pass', async () => {
|
||||
});
|
||||
|
||||
test('changing project should emit event even if user does not have a username set', async () => {
|
||||
const projectA = { id: randomId(), name: randomId() };
|
||||
const projectB = { id: randomId(), name: randomId() };
|
||||
const projectA = {
|
||||
id: randomId(),
|
||||
name: randomId(),
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const projectB = {
|
||||
id: randomId(),
|
||||
name: randomId(),
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const toggle = { name: randomId() };
|
||||
await projectService.createProject(projectA, user);
|
||||
await projectService.createProject(projectB, user);
|
||||
@ -576,8 +610,16 @@ test('changing project should emit event even if user does not have a username s
|
||||
}, 10000);
|
||||
|
||||
test('should require equal project environments to move features', async () => {
|
||||
const projectA = { id: randomId(), name: randomId() };
|
||||
const projectB = { id: randomId(), name: randomId() };
|
||||
const projectA = {
|
||||
id: randomId(),
|
||||
name: randomId(),
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const projectB = {
|
||||
id: randomId(),
|
||||
name: randomId(),
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const environment = { name: randomId(), type: 'production' };
|
||||
const toggle = { name: randomId() };
|
||||
|
||||
@ -605,6 +647,7 @@ test('A newly created project only gets connected to enabled environments', asyn
|
||||
id: 'environment-test',
|
||||
name: 'New environment project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const enabledEnv = 'connection_test';
|
||||
await db.stores.environmentStore.create({
|
||||
@ -631,6 +674,7 @@ test('should have environments sorted in order', async () => {
|
||||
id: 'environment-order-test',
|
||||
name: 'Environment testing project',
|
||||
description: '',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const first = 'test';
|
||||
const second = 'abc';
|
||||
@ -669,6 +713,7 @@ test('should add a user to the project with a custom role', async () => {
|
||||
id: 'add-users-custom-role',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -719,6 +764,7 @@ test('should delete role entries when deleting project', async () => {
|
||||
id: 'test-delete-users-1',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -770,6 +816,7 @@ test('should change a users role in the project', async () => {
|
||||
id: 'test-change-user-role',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -836,6 +883,7 @@ test('should update role for user on project', async () => {
|
||||
id: 'update-users',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -873,6 +921,7 @@ test('should able to assign role without existing members', async () => {
|
||||
id: 'update-users-test',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -915,6 +964,7 @@ test('should not update role for user on project when she is the owner', async (
|
||||
id: 'update-users-not-allowed',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
|
||||
@ -948,6 +998,7 @@ test('Should allow bulk update of group permissions', async () => {
|
||||
const project = {
|
||||
id: 'bulk-update-project',
|
||||
name: 'bulk-update-project',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectService.createProject(project, user.id);
|
||||
const groupStore = stores.groupStore;
|
||||
@ -1024,6 +1075,7 @@ test('Should allow bulk update of only groups', async () => {
|
||||
const project = {
|
||||
id: 'bulk-update-project-only',
|
||||
name: 'bulk-update-project-only',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
const groupStore = stores.groupStore;
|
||||
|
||||
@ -1064,6 +1116,7 @@ test('should only count active feature toggles for project', async () => {
|
||||
id: 'only-active',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -1091,6 +1144,7 @@ test('should list projects with all features archived', async () => {
|
||||
id: 'only-archived',
|
||||
name: 'Listed project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
@ -1126,6 +1180,7 @@ test('should calculate average time to production', async () => {
|
||||
const project = {
|
||||
id: 'average-time-to-prod',
|
||||
name: 'average-time-to-prod',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user.id);
|
||||
@ -1184,6 +1239,7 @@ test('should get correct amount of features created in current and past window',
|
||||
const project = {
|
||||
id: 'features-created',
|
||||
name: 'features-created',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user.id);
|
||||
@ -1219,6 +1275,7 @@ test('should get correct amount of features archived in current and past window'
|
||||
const project = {
|
||||
id: 'features-archived',
|
||||
name: 'features-archived',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user.id);
|
||||
@ -1268,6 +1325,7 @@ test('should get correct amount of project members for current and past window',
|
||||
const project = {
|
||||
id: 'features-members',
|
||||
name: 'features-members',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user.id);
|
||||
|
@ -31,6 +31,7 @@ test('should create new project', async () => {
|
||||
id: 'test',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectStore.create(project);
|
||||
const ret = await projectStore.get('test');
|
||||
@ -48,6 +49,7 @@ test('should delete project', async () => {
|
||||
id: 'test-delete',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
await projectStore.create(project);
|
||||
await projectStore.delete(project.id);
|
||||
@ -64,12 +66,14 @@ test('should update project', async () => {
|
||||
id: 'test-update',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
const updatedProject = {
|
||||
id: 'test-update',
|
||||
name: 'New name',
|
||||
description: 'Blah longer desc',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await projectStore.create(project);
|
||||
@ -93,11 +97,17 @@ test('should import projects', async () => {
|
||||
const projectsCount = (await projectStore.getAll()).length;
|
||||
|
||||
const projectsToImport: IProjectInsert[] = [
|
||||
{ description: 'some project desc', name: 'some name', id: 'someId' },
|
||||
{
|
||||
description: 'some project desc',
|
||||
name: 'some name',
|
||||
id: 'someId',
|
||||
mode: 'open' as const,
|
||||
},
|
||||
{
|
||||
description: 'another project',
|
||||
name: 'another name',
|
||||
id: 'anotherId',
|
||||
mode: 'open' as const,
|
||||
},
|
||||
];
|
||||
|
||||
@ -110,8 +120,8 @@ test('should import projects', async () => {
|
||||
|
||||
expect(projects.length - projectsCount).toBe(2);
|
||||
expect(someId).toBeDefined();
|
||||
expect(someId.name).toBe('some name');
|
||||
expect(someId.description).toBe('some project desc');
|
||||
expect(someId?.name).toBe('some name');
|
||||
expect(someId?.description).toBe('some project desc');
|
||||
expect(anotherId).toBeDefined();
|
||||
});
|
||||
|
||||
@ -120,6 +130,7 @@ test('should add environment to project', async () => {
|
||||
id: 'test-env',
|
||||
name: 'New project with env',
|
||||
description: 'Blah',
|
||||
mode: 'open' as const,
|
||||
};
|
||||
|
||||
await environmentStore.create({
|
||||
|
Loading…
Reference in New Issue
Block a user