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