mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
Feat/add cypress tests for project scoped stickiness (#3340)
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
bc011e9876
commit
32e1ad44ed
117
frontend/cypress/integration/projects/settings.spec.ts
Normal file
117
frontend/cypress/integration/projects/settings.spec.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
type UserCredentials = { email: string; password: string };
|
||||
const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE'));
|
||||
const randomId = String(Math.random()).split('.')[1];
|
||||
const featureToggleName = `settings-${randomId}`;
|
||||
const baseUrl = Cypress.config().baseUrl;
|
||||
let strategyId = '';
|
||||
const userName = `settings-user-${randomId}`;
|
||||
const projectName = `stickiness-project-${randomId}`;
|
||||
|
||||
// Disable all active splash pages by visiting them.
|
||||
const disableActiveSplashScreens = () => {
|
||||
cy.visit(`/splash/operators`);
|
||||
};
|
||||
|
||||
const disableFeatureStrategiesProdGuard = () => {
|
||||
localStorage.setItem(
|
||||
'useFeatureStrategyProdGuardSettings:v2',
|
||||
JSON.stringify({ hide: true })
|
||||
);
|
||||
};
|
||||
|
||||
describe('notifications', () => {
|
||||
before(() => {
|
||||
disableFeatureStrategiesProdGuard();
|
||||
disableActiveSplashScreens();
|
||||
cy.login();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.request(
|
||||
'DELETE',
|
||||
`${baseUrl}/api/admin/features/${featureToggleName}`
|
||||
);
|
||||
|
||||
cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit(`/projects`);
|
||||
if (document.querySelector("[data-testid='CLOSE_SPLASH']")) {
|
||||
cy.get("[data-testid='CLOSE_SPLASH']").click();
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`);
|
||||
});
|
||||
|
||||
const createFeature = () => {
|
||||
cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click();
|
||||
|
||||
cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as(
|
||||
'createFeature'
|
||||
);
|
||||
|
||||
cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName);
|
||||
cy.get("[data-testid='CF_DESC_ID'").type('hello-world');
|
||||
cy.get("[data-testid='CF_CREATE_BTN_ID']").click();
|
||||
cy.wait('@createFeature');
|
||||
};
|
||||
|
||||
const createProject = () => {
|
||||
cy.get('[data-testid=NAVIGATE_TO_CREATE_PROJECT').click();
|
||||
|
||||
cy.get("[data-testid='PROJECT_ID_INPUT']").type(projectName);
|
||||
cy.get("[data-testid='PROJECT_NAME_INPUT']").type(projectName);
|
||||
cy.get("[id='stickiness-select']")
|
||||
.first()
|
||||
.click()
|
||||
.get('[data-testid=SELECT_ITEM_ID-userId')
|
||||
.first()
|
||||
.click();
|
||||
cy.get("[data-testid='CREATE_PROJECT_BTN']").click();
|
||||
};
|
||||
|
||||
it('should store default project stickiness when creating, retrieve it when editing a project', () => {
|
||||
createProject();
|
||||
|
||||
cy.visit(`/projects/${projectName}`);
|
||||
if (document.querySelector("[data-testid='CLOSE_SPLASH']")) {
|
||||
cy.get("[data-testid='CLOSE_SPLASH']").click();
|
||||
}
|
||||
cy.get("[data-testid='NAVIGATE_TO_EDIT_PROJECT']").click();
|
||||
|
||||
//then
|
||||
cy.get("[id='stickiness-select']")
|
||||
.first()
|
||||
.should('have.text', 'userId');
|
||||
});
|
||||
|
||||
it('should respect the default project stickiness when creating a Gradual Rollout Strategy', () => {
|
||||
createProject();
|
||||
createFeature();
|
||||
cy.visit(
|
||||
`/projects/default/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=flexibleRollout`
|
||||
);
|
||||
|
||||
//then
|
||||
cy.get("[id='stickiness-select']")
|
||||
.first()
|
||||
.should('have.text', 'userId');
|
||||
});
|
||||
|
||||
it('should respect the default project stickiness when creating a variant', () => {
|
||||
createProject();
|
||||
createFeature();
|
||||
|
||||
cy.visit(`/projects/default/features/${featureToggleName}/variants`);
|
||||
|
||||
cy.get("[data-testid='EDIT_VARIANTS_BUTTON']").click();
|
||||
//then
|
||||
cy.get('#menu-stickiness').first().should('have.text', 'userId');
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import Select from 'component/common/select';
|
||||
import { SelectChangeEvent } from '@mui/material';
|
||||
import { SelectChangeEvent, useTheme } from '@mui/material';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
const builtInStickinessOptions = [
|
||||
{ key: 'default', label: 'default' },
|
||||
@ -23,6 +23,7 @@ export const StickinessSelect = ({
|
||||
dataTestId,
|
||||
}: IStickinessSelectProps) => {
|
||||
const { context } = useUnleashContext();
|
||||
const theme = useTheme();
|
||||
|
||||
const resolveStickinessOptions = () =>
|
||||
builtInStickinessOptions.concat(
|
||||
@ -54,7 +55,7 @@ export const StickinessSelect = ({
|
||||
style={{
|
||||
width: 'inherit',
|
||||
minWidth: '100%',
|
||||
marginBottom: '16px',
|
||||
marginBottom: theme.spacing(2),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -11,6 +11,8 @@ import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
|
||||
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
|
||||
|
||||
const CreateProject = () => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { refetchUser } = useAuthUser();
|
||||
@ -34,8 +36,7 @@ const CreateProject = () => {
|
||||
errors,
|
||||
} = useProjectForm();
|
||||
|
||||
const { createProject, setDefaultProjectStickiness, loading } =
|
||||
useProjectApi();
|
||||
const { createProject, loading } = useProjectApi();
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
@ -47,10 +48,6 @@ const CreateProject = () => {
|
||||
const payload = getProjectPayload();
|
||||
try {
|
||||
await createProject(payload);
|
||||
setDefaultProjectStickiness(
|
||||
projectId,
|
||||
payload.projectStickiness
|
||||
);
|
||||
refetchUser();
|
||||
navigate(`/projects/${projectId}`);
|
||||
setToastData({
|
||||
@ -105,7 +102,11 @@ const CreateProject = () => {
|
||||
clearErrors={clearErrors}
|
||||
validateProjectId={validateProjectId}
|
||||
>
|
||||
<CreateButton name="project" permission={CREATE_PROJECT} />
|
||||
<CreateButton
|
||||
name="project"
|
||||
permission={CREATE_PROJECT}
|
||||
data-testid={CREATE_PROJECT_BTN}
|
||||
/>
|
||||
</ProjectForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
|
@ -16,6 +16,8 @@ import { Alert } from '@mui/material';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
|
||||
|
||||
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
|
||||
|
||||
const EditProject = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
@ -71,9 +73,9 @@ const EditProject = () => {
|
||||
if (validName) {
|
||||
try {
|
||||
await editProject(id, payload);
|
||||
setDefaultProjectStickiness(
|
||||
await setDefaultProjectStickiness(
|
||||
projectId,
|
||||
payload.projectStickiness
|
||||
payload.defaultStickiness
|
||||
);
|
||||
refetch();
|
||||
navigate(`/projects/${id}`);
|
||||
@ -128,6 +130,7 @@ const EditProject = () => {
|
||||
<UpdateButton
|
||||
permission={UPDATE_PROJECT}
|
||||
projectId={projectId}
|
||||
data-testid={EDIT_PROJECT_BTN}
|
||||
/>
|
||||
</ProjectForm>
|
||||
</FormTemplate>
|
||||
|
@ -44,6 +44,8 @@ import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi
|
||||
import { ImportModal } from './Import/ImportModal';
|
||||
import { IMPORT_BUTTON } from 'utils/testIds';
|
||||
|
||||
const NAVIGATE_TO_EDIT_PROJECT = 'NAVIGATE_TO_EDIT_PROJECT';
|
||||
|
||||
export const Project = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const params = useQueryParams();
|
||||
@ -167,6 +169,7 @@ export const Project = () => {
|
||||
}
|
||||
tooltipProps={{ title: 'Edit project' }}
|
||||
data-loading
|
||||
data-testid={NAVIGATE_TO_EDIT_PROJECT}
|
||||
>
|
||||
<Edit />
|
||||
</PermissionIconButton>
|
||||
@ -246,6 +249,7 @@ export const Project = () => {
|
||||
label={tab.title}
|
||||
value={tab.path}
|
||||
onClick={() => navigate(tab.path)}
|
||||
data-testid={`TAB_${tab.title}`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
|
@ -13,6 +13,7 @@ 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';
|
||||
import { DefaultStickiness, ProjectMode } from '../hooks/useProjectForm';
|
||||
|
||||
interface IProjectForm {
|
||||
projectId: string;
|
||||
@ -20,8 +21,10 @@ interface IProjectForm {
|
||||
projectDesc: string;
|
||||
projectStickiness?: string;
|
||||
projectMode?: string;
|
||||
setProjectStickiness?: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectMode?: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectStickiness?: React.Dispatch<
|
||||
React.SetStateAction<DefaultStickiness>
|
||||
>;
|
||||
setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>;
|
||||
setProjectId: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||
@ -33,6 +36,11 @@ interface IProjectForm {
|
||||
validateProjectId: () => void;
|
||||
}
|
||||
|
||||
const PROJECT_STICKINESS_SELECT = 'PROJECT_STICKINESS_SELECT';
|
||||
const PROJECT_ID_INPUT = 'PROJECT_ID_INPUT';
|
||||
const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT';
|
||||
const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT';
|
||||
|
||||
const ProjectForm: React.FC<IProjectForm> = ({
|
||||
children,
|
||||
handleSubmit,
|
||||
@ -69,6 +77,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
onFocus={() => clearErrors()}
|
||||
onBlur={validateProjectId}
|
||||
disabled={mode === 'Edit'}
|
||||
data-testid={PROJECT_ID_INPUT}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
@ -83,6 +92,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onFocus={() => clearErrors()}
|
||||
data-testid={PROJECT_NAME_INPUT}
|
||||
required
|
||||
/>
|
||||
|
||||
@ -96,6 +106,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
maxRows={4}
|
||||
value={projectDesc}
|
||||
onChange={e => setProjectDesc(e.target.value)}
|
||||
data-testid={PROJECT_DESCRIPTION_INPUT}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
@ -111,9 +122,12 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
<StickinessSelect
|
||||
label="Stickiness"
|
||||
value={projectStickiness}
|
||||
data-testid={PROJECT_STICKINESS_SELECT}
|
||||
onChange={e =>
|
||||
setProjectStickiness &&
|
||||
setProjectStickiness(e.target.value)
|
||||
setProjectStickiness(
|
||||
e.target.value as DefaultStickiness
|
||||
)
|
||||
}
|
||||
editable
|
||||
/>
|
||||
@ -133,7 +147,9 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
||||
label="Project mode"
|
||||
name="Project mode"
|
||||
onChange={e => {
|
||||
setProjectMode?.(e.target.value);
|
||||
setProjectMode?.(
|
||||
e.target.value as ProjectMode
|
||||
);
|
||||
}}
|
||||
options={[
|
||||
{ key: 'open', label: 'open' },
|
||||
|
@ -3,22 +3,27 @@ import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useDefaultProjectSettings } from 'hooks/useDefaultProjectSettings';
|
||||
|
||||
export type ProjectMode = 'open' | 'protected';
|
||||
export type DefaultStickiness = 'default' | 'userId' | 'sessionId' | 'random';
|
||||
|
||||
const useProjectForm = (
|
||||
initialProjectId = '',
|
||||
initialProjectName = '',
|
||||
initialProjectDesc = '',
|
||||
initialProjectStickiness = 'default',
|
||||
initialProjectMode = 'open'
|
||||
initialProjectStickiness: DefaultStickiness = 'default',
|
||||
initialProjectMode: ProjectMode = 'open'
|
||||
) => {
|
||||
const [projectId, setProjectId] = useState(initialProjectId);
|
||||
const { defaultStickiness } = useDefaultProjectSettings(projectId);
|
||||
|
||||
const [projectName, setProjectName] = useState(initialProjectName);
|
||||
const [projectDesc, setProjectDesc] = useState(initialProjectDesc);
|
||||
const [projectStickiness, setProjectStickiness] = useState(
|
||||
defaultStickiness || initialProjectStickiness
|
||||
);
|
||||
const [projectMode, setProjectMode] = useState(initialProjectMode);
|
||||
const [projectStickiness, setProjectStickiness] =
|
||||
useState<DefaultStickiness>(
|
||||
defaultStickiness || initialProjectStickiness
|
||||
);
|
||||
const [projectMode, setProjectMode] =
|
||||
useState<ProjectMode>(initialProjectMode);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const { validateId } = useProjectApi();
|
||||
@ -39,12 +44,16 @@ const useProjectForm = (
|
||||
setProjectMode(initialProjectMode);
|
||||
}, [initialProjectMode]);
|
||||
|
||||
useEffect(() => {
|
||||
setProjectStickiness(initialProjectStickiness);
|
||||
}, [initialProjectStickiness]);
|
||||
|
||||
const getProjectPayload = () => {
|
||||
return {
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
description: projectDesc,
|
||||
projectStickiness,
|
||||
defaultStickiness: projectStickiness,
|
||||
mode: projectMode,
|
||||
};
|
||||
};
|
||||
@ -55,7 +64,7 @@ const useProjectForm = (
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await validateId(getProjectPayload());
|
||||
await validateId(getProjectPayload().id);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
setErrors(prev => ({ ...prev, id: formatUnknownError(error) }));
|
||||
|
@ -59,6 +59,8 @@ interface ICreateButtonData {
|
||||
endIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const NAVIGATE_TO_CREATE_PROJECT = 'NAVIGATE_TO_CREATE_PROJECT';
|
||||
|
||||
function resolveCreateButtonData(
|
||||
isOss: boolean,
|
||||
hasAccess: boolean
|
||||
@ -185,6 +187,7 @@ export const ProjectListNew = () => {
|
||||
permission={CREATE_PROJECT}
|
||||
disabled={createButtonData.disabled}
|
||||
tooltipProps={createButtonData.tooltip}
|
||||
data-testid={NAVIGATE_TO_CREATE_PROJECT}
|
||||
>
|
||||
New project
|
||||
</ResponsiveButton>
|
||||
|
@ -5,6 +5,8 @@ interface ICreatePayload {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
mode: 'open' | 'protected';
|
||||
defaultStickiness: 'default' | 'userId' | 'sessionId' | 'random';
|
||||
}
|
||||
|
||||
interface IAccessesPayload {
|
||||
@ -33,13 +35,12 @@ const useProjectApi = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const validateId = async (payload: ICreatePayload) => {
|
||||
const validateId = async (id: ICreatePayload['id']) => {
|
||||
const path = `api/admin/projects/validate`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ id }),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
@ -207,10 +208,10 @@ const useProjectApi = () => {
|
||||
projectId: string,
|
||||
stickiness: string
|
||||
) => {
|
||||
const path = `api/admin/projects/${projectId}/stickiness`;
|
||||
const path = `api/admin/projects/${projectId}/settings`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ stickiness }),
|
||||
body: JSON.stringify({ defaultStickiness: stickiness }),
|
||||
});
|
||||
|
||||
return makeRequest(req.caller, req.id);
|
||||
|
@ -3,10 +3,14 @@ import { SWRConfiguration } from 'swr';
|
||||
import { useCallback } from 'react';
|
||||
import handleErrorResponses from './api/getters/httpErrorResponseHandler';
|
||||
import { useConditionalSWR } from './api/getters/useConditionalSWR/useConditionalSWR';
|
||||
import {
|
||||
DefaultStickiness,
|
||||
ProjectMode,
|
||||
} from 'component/project/Project/hooks/useProjectForm';
|
||||
|
||||
export interface IStickinessResponse {
|
||||
defaultStickiness?: string;
|
||||
mode?: string;
|
||||
export interface ISettingsResponse {
|
||||
defaultStickiness?: DefaultStickiness;
|
||||
mode?: ProjectMode;
|
||||
}
|
||||
const DEFAULT_STICKINESS = 'default';
|
||||
export const useDefaultProjectSettings = (
|
||||
@ -18,7 +22,7 @@ export const useDefaultProjectSettings = (
|
||||
const PATH = `/api/admin/projects/${projectId}/settings`;
|
||||
const { projectScopedStickiness } = uiConfig.flags;
|
||||
|
||||
const { data, error, mutate } = useConditionalSWR<IStickinessResponse>(
|
||||
const { data, error, mutate } = useConditionalSWR<ISettingsResponse>(
|
||||
Boolean(projectId) && Boolean(projectScopedStickiness),
|
||||
{},
|
||||
['useDefaultProjectSettings', PATH],
|
||||
@ -26,7 +30,8 @@ export const useDefaultProjectSettings = (
|
||||
options
|
||||
);
|
||||
|
||||
const defaultStickiness = data?.defaultStickiness ?? DEFAULT_STICKINESS;
|
||||
const defaultStickiness: DefaultStickiness =
|
||||
data?.defaultStickiness ?? DEFAULT_STICKINESS;
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
mutate().catch(console.warn);
|
||||
|
@ -3,24 +3,27 @@ import { Logger, LogProvider } from '../logger';
|
||||
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
DefaultStickiness,
|
||||
IEnvironment,
|
||||
IFlagResolver,
|
||||
IProject,
|
||||
IProjectWithCount,
|
||||
ProjectMode,
|
||||
} from '../types/model';
|
||||
} from '../types';
|
||||
import {
|
||||
IProjectHealthUpdate,
|
||||
IProjectInsert,
|
||||
IProjectQuery,
|
||||
IProjectSettings,
|
||||
IProjectSettingsRow,
|
||||
IProjectStore,
|
||||
} from '../types/stores/project-store';
|
||||
import { DEFAULT_ENV } from '../util/constants';
|
||||
import { DEFAULT_ENV } from '../util';
|
||||
import metricsHelper from '../util/metrics-helper';
|
||||
import { DB_TIME } from '../metric-events';
|
||||
import EventEmitter from 'events';
|
||||
import { IFlagResolver } from '../types';
|
||||
import Raw = Knex.Raw;
|
||||
import { Db } from './db';
|
||||
import Raw = Knex.Raw;
|
||||
|
||||
const COLUMNS = [
|
||||
'id',
|
||||
@ -160,6 +163,7 @@ class ProjectStore implements IProjectStore {
|
||||
memberCount: Number(row.number_of_users) || 0,
|
||||
updatedAt: row.updated_at,
|
||||
mode: 'open',
|
||||
defaultStickiness: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
@ -202,7 +206,7 @@ class ProjectStore implements IProjectStore {
|
||||
}
|
||||
|
||||
async create(
|
||||
project: IProjectInsert & { mode: ProjectMode },
|
||||
project: IProjectInsert & IProjectSettings,
|
||||
): Promise<IProject> {
|
||||
const row = await this.db(TABLE)
|
||||
.insert(this.fieldToRow(project))
|
||||
@ -211,6 +215,7 @@ class ProjectStore implements IProjectStore {
|
||||
.insert({
|
||||
project: project.id,
|
||||
project_mode: project.mode,
|
||||
default_stickiness: project.defaultStickiness,
|
||||
})
|
||||
.returning('*');
|
||||
return this.mapRow({ ...row[0], ...settingsRow[0] });
|
||||
@ -226,6 +231,7 @@ class ProjectStore implements IProjectStore {
|
||||
.where({ project: data.id })
|
||||
.update({
|
||||
project_mode: data.mode,
|
||||
default_stickiness: data.defaultStickiness,
|
||||
})
|
||||
.returning('*');
|
||||
} catch (err) {
|
||||
@ -458,6 +464,24 @@ class ProjectStore implements IProjectStore {
|
||||
return Number(members.count);
|
||||
}
|
||||
|
||||
async getProjectSettings(projectId: string): Promise<IProjectSettings> {
|
||||
const row = await this.db(SETTINGS_TABLE).where({ project: projectId });
|
||||
return this.mapSettingsRow(row[0]);
|
||||
}
|
||||
|
||||
async setProjectSettings(
|
||||
projectId: string,
|
||||
defaultStickiness: DefaultStickiness,
|
||||
mode: ProjectMode,
|
||||
): Promise<void> {
|
||||
await this.db(SETTINGS_TABLE)
|
||||
.update({
|
||||
default_stickiness: defaultStickiness,
|
||||
project_mode: mode,
|
||||
})
|
||||
.where({ project: projectId });
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
return this.db
|
||||
.from(TABLE)
|
||||
@ -465,6 +489,13 @@ class ProjectStore implements IProjectStore {
|
||||
.then((res) => Number(res[0].count));
|
||||
}
|
||||
|
||||
mapSettingsRow(row?: IProjectSettingsRow): IProjectSettings {
|
||||
return {
|
||||
defaultStickiness: row?.default_stickiness || 'default',
|
||||
mode: row?.project_mode || 'open',
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
mapLinkRow(row): IEnvironmentProjectLink {
|
||||
return {
|
||||
|
@ -133,7 +133,6 @@ import {
|
||||
importTogglesSchema,
|
||||
importTogglesValidateSchema,
|
||||
importTogglesValidateItemSchema,
|
||||
projectSettingsSchema,
|
||||
} from './spec';
|
||||
import { IServerOption } from '../types';
|
||||
import { mapValues, omitKeys } from '../util';
|
||||
@ -257,7 +256,6 @@ export const schemas = {
|
||||
stateSchema,
|
||||
strategiesSchema,
|
||||
strategySchema,
|
||||
projectSettingsSchema,
|
||||
tagsBulkAddSchema,
|
||||
tagSchema,
|
||||
tagsSchema,
|
||||
|
@ -25,6 +25,13 @@ export const healthOverviewSchema = {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
},
|
||||
defaultStickiness: {
|
||||
type: 'string',
|
||||
enum: ['default', 'userId', 'sessionId', 'random'],
|
||||
example: 'userId',
|
||||
description:
|
||||
'A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['open', 'protected'],
|
||||
|
@ -129,7 +129,6 @@ export * from './project-overview-schema';
|
||||
export * from './import-toggles-validate-item-schema';
|
||||
export * from './import-toggles-validate-schema';
|
||||
export * from './import-toggles-schema';
|
||||
export * from './project-settings-schema';
|
||||
export * from './tags-bulk-add-schema';
|
||||
export * from './upsert-segment-schema';
|
||||
export * from './batch-features-schema';
|
||||
|
@ -36,6 +36,13 @@ export const projectOverviewSchema = {
|
||||
example: 'DX squad feature release',
|
||||
description: 'Additional information about the project',
|
||||
},
|
||||
defaultStickiness: {
|
||||
type: 'string',
|
||||
enum: ['default', 'userId', 'sessionId', 'random'],
|
||||
example: 'userId',
|
||||
description:
|
||||
'A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy',
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['open', 'protected'],
|
||||
|
@ -62,6 +62,13 @@ export const projectSchema = {
|
||||
description:
|
||||
'A mode of the project affecting what actions are possible in this project',
|
||||
},
|
||||
defaultStickiness: {
|
||||
type: 'string',
|
||||
enum: ['default', 'userId', 'sessionId', 'random'],
|
||||
example: 'userId',
|
||||
description:
|
||||
'A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
@ -1,23 +0,0 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const projectSettingsSchema = {
|
||||
$id: '#/components/schemas/projectSettingsSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['defaultStickiness'],
|
||||
properties: {
|
||||
defaultStickiness: {
|
||||
type: 'string',
|
||||
example: 'userId',
|
||||
nullable: true,
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['open', 'protected', 'private'],
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type ProjectSettingsSchema = FromSchema<typeof projectSettingsSchema>;
|
@ -7,7 +7,6 @@ import {
|
||||
IUnleashServices,
|
||||
NONE,
|
||||
serializeDates,
|
||||
UPDATE_PROJECT,
|
||||
} from '../../../types';
|
||||
import ProjectFeaturesController from './project-features';
|
||||
import EnvironmentsController from './environments';
|
||||
@ -16,11 +15,8 @@ import ProjectService from '../../../services/project-service';
|
||||
import VariantsController from './variants';
|
||||
import {
|
||||
createResponseSchema,
|
||||
emptyResponse,
|
||||
ProjectOverviewSchema,
|
||||
projectOverviewSchema,
|
||||
ProjectSettingsSchema,
|
||||
projectSettingsSchema,
|
||||
projectsSchema,
|
||||
ProjectsSchema,
|
||||
} from '../../../openapi';
|
||||
@ -28,10 +24,6 @@ import { OpenApiService, SettingService } from '../../../services';
|
||||
import { IAuthRequest } from '../../unleash-types';
|
||||
import { ProjectApiTokenController } from './api-token';
|
||||
import ProjectArchiveController from './project-archive';
|
||||
import NotFoundError from '../../../error/notfound-error';
|
||||
|
||||
const STICKINESS_KEY = 'stickiness';
|
||||
const DEFAULT_STICKINESS = 'default';
|
||||
|
||||
export default class ProjectApi extends Controller {
|
||||
private projectService: ProjectService;
|
||||
@ -78,40 +70,6 @@ export default class ProjectApi extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:projectId/settings',
|
||||
handler: this.getProjectSettings,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
services.openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
operationId: 'getProjectSettings',
|
||||
responses: {
|
||||
200: createResponseSchema('projectSettingsSchema'),
|
||||
404: emptyResponse,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/:projectId/settings',
|
||||
handler: this.setProjectSettings,
|
||||
permission: UPDATE_PROJECT,
|
||||
middleware: [
|
||||
services.openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
operationId: 'setProjectSettings',
|
||||
responses: {
|
||||
200: createResponseSchema('projectSettingsSchema'),
|
||||
404: emptyResponse,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.use('/', new ProjectFeaturesController(config, services).router);
|
||||
this.use('/', new EnvironmentsController(config, services).router);
|
||||
this.use('/', new ProjectHealthReport(config, services).router);
|
||||
@ -159,61 +117,4 @@ export default class ProjectApi extends Controller {
|
||||
serializeDates(overview),
|
||||
);
|
||||
}
|
||||
|
||||
async getProjectSettings(
|
||||
req: IAuthRequest<IProjectParam, unknown, unknown, unknown>,
|
||||
res: Response<ProjectSettingsSchema>,
|
||||
): Promise<void> {
|
||||
if (!this.config.flagResolver.isEnabled('projectScopedStickiness')) {
|
||||
throw new NotFoundError('Project scoped stickiness is not enabled');
|
||||
}
|
||||
const { projectId } = req.params;
|
||||
const stickinessSettings = await this.settingService.get<object>(
|
||||
STICKINESS_KEY,
|
||||
{
|
||||
[projectId]: 'default',
|
||||
},
|
||||
);
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
projectSettingsSchema.$id,
|
||||
{ defaultStickiness: stickinessSettings[projectId] },
|
||||
);
|
||||
}
|
||||
|
||||
async setProjectSettings(
|
||||
req: IAuthRequest<
|
||||
IProjectParam,
|
||||
ProjectSettingsSchema,
|
||||
ProjectSettingsSchema,
|
||||
unknown
|
||||
>,
|
||||
res: Response<ProjectSettingsSchema>,
|
||||
): Promise<void> {
|
||||
if (!this.config.flagResolver.isEnabled('projectScopedStickiness')) {
|
||||
throw new NotFoundError('Project scoped stickiness is not enabled');
|
||||
}
|
||||
const { projectId } = req.params;
|
||||
const { defaultStickiness } = req.body;
|
||||
const stickinessSettings = await this.settingService.get<{}>(
|
||||
STICKINESS_KEY,
|
||||
{
|
||||
[projectId]: DEFAULT_STICKINESS,
|
||||
},
|
||||
);
|
||||
stickinessSettings[projectId] = defaultStickiness;
|
||||
await this.settingService.insert(
|
||||
STICKINESS_KEY,
|
||||
stickinessSettings,
|
||||
req.user.name,
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
projectSettingsSchema.$id,
|
||||
{ defaultStickiness },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,5 +8,9 @@ export const projectSchema = joi
|
||||
name: joi.string().required(),
|
||||
description: joi.string().allow(null).allow('').optional(),
|
||||
mode: joi.string().valid('open', 'protected').default('open'),
|
||||
defaultStickiness: joi
|
||||
.string()
|
||||
.valid('default', 'userId', 'sessionId', 'random')
|
||||
.default('default'),
|
||||
})
|
||||
.options({ allowUnknown: false, stripUnknown: true });
|
||||
|
@ -7,44 +7,50 @@ import { nameType } from '../routes/util';
|
||||
import { projectSchema } from './project-schema';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
DEFAULT_PROJECT,
|
||||
DefaultStickiness,
|
||||
FEATURE_ENVIRONMENT_ENABLED,
|
||||
FeatureToggle,
|
||||
IAccountStore,
|
||||
IEnvironmentStore,
|
||||
IEventStore,
|
||||
IFeatureEnvironmentStore,
|
||||
IFeatureToggleStore,
|
||||
IFeatureTypeStore,
|
||||
IProject,
|
||||
IProjectOverview,
|
||||
IProjectWithCount,
|
||||
IUnleashConfig,
|
||||
IUnleashStores,
|
||||
IUserWithRole,
|
||||
MOVE_FEATURE_TOGGLE,
|
||||
PROJECT_CREATED,
|
||||
PROJECT_DELETED,
|
||||
PROJECT_UPDATED,
|
||||
ProjectGroupAddedEvent,
|
||||
ProjectGroupRemovedEvent,
|
||||
ProjectGroupUpdateRoleEvent,
|
||||
ProjectMode,
|
||||
ProjectUserAddedEvent,
|
||||
ProjectUserRemovedEvent,
|
||||
ProjectUserUpdateRoleEvent,
|
||||
} from '../types/events';
|
||||
import { IAccountStore, IUnleashConfig, IUnleashStores } from '../types';
|
||||
import {
|
||||
FeatureToggle,
|
||||
IProject,
|
||||
IProjectOverview,
|
||||
IProjectWithCount,
|
||||
IUserWithRole,
|
||||
RoleName,
|
||||
} from '../types/model';
|
||||
import { IEnvironmentStore } from '../types/stores/environment-store';
|
||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||
} from '../types';
|
||||
import {
|
||||
IProjectQuery,
|
||||
IProjectSettings,
|
||||
IProjectStore,
|
||||
} from '../types/stores/project-store';
|
||||
import {
|
||||
IProjectAccessModel,
|
||||
IRoleDescriptor,
|
||||
} from '../types/stores/access-store';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import FeatureToggleService from './feature-toggle-service';
|
||||
import { MOVE_FEATURE_TOGGLE } from '../types/permissions';
|
||||
import NoAccessError from '../error/no-access-error';
|
||||
import IncompatibleProjectError from '../error/incompatible-project-error';
|
||||
import { DEFAULT_PROJECT } from '../types/project';
|
||||
import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
||||
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
||||
import { arraysHaveSameItems } from '../util/arraysHaveSameItems';
|
||||
import { arraysHaveSameItems } from '../util';
|
||||
import { GroupService } from './group-service';
|
||||
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
||||
import { FavoritesService } from './favorites-service';
|
||||
@ -165,7 +171,10 @@ export default class ProjectService {
|
||||
}
|
||||
|
||||
async createProject(
|
||||
newProject: Pick<IProject, 'id' | 'name' | 'mode'>,
|
||||
newProject: Pick<
|
||||
IProject,
|
||||
'id' | 'name' | 'mode' | 'defaultStickiness'
|
||||
>,
|
||||
user: IUser,
|
||||
): Promise<IProject> {
|
||||
const data = await projectSchema.validateAsync(newProject);
|
||||
@ -827,6 +836,7 @@ export default class ProjectService {
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
mode: project.mode,
|
||||
defaultStickiness: project.defaultStickiness || 'default',
|
||||
health: project.health || 0,
|
||||
favorite: favorite,
|
||||
updatedAt: project.updatedAt,
|
||||
@ -836,4 +846,20 @@ export default class ProjectService {
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
async getProjectSettings(projectId: string): Promise<IProjectSettings> {
|
||||
return this.store.getProjectSettings(projectId);
|
||||
}
|
||||
|
||||
async setProjectSettings(
|
||||
projectId: string,
|
||||
defaultStickiness: DefaultStickiness,
|
||||
mode: ProjectMode,
|
||||
): Promise<void> {
|
||||
return this.store.setProjectSettings(
|
||||
projectId,
|
||||
defaultStickiness,
|
||||
mode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -175,6 +175,8 @@ export interface IFeatureOverview {
|
||||
|
||||
export type ProjectMode = 'open' | 'protected';
|
||||
|
||||
export type DefaultStickiness = 'default' | 'sessionId' | 'userId' | 'random';
|
||||
|
||||
export interface IProjectOverview {
|
||||
name: string;
|
||||
description: string;
|
||||
@ -187,6 +189,8 @@ export interface IProjectOverview {
|
||||
updatedAt?: Date;
|
||||
stats?: IProjectStats;
|
||||
mode: ProjectMode;
|
||||
|
||||
defaultStickiness: DefaultStickiness;
|
||||
}
|
||||
|
||||
export interface IProjectHealthReport extends IProjectOverview {
|
||||
@ -370,6 +374,7 @@ export interface IProject {
|
||||
updatedAt?: Date;
|
||||
changeRequestsEnabled?: boolean;
|
||||
mode: ProjectMode;
|
||||
defaultStickiness?: DefaultStickiness;
|
||||
}
|
||||
|
||||
export interface ICustomRole {
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
IProjectMembersCount,
|
||||
} from '../../db/project-store';
|
||||
import {
|
||||
DefaultStickiness,
|
||||
IEnvironment,
|
||||
IProject,
|
||||
IProjectWithCount,
|
||||
@ -19,6 +20,16 @@ export interface IProjectInsert {
|
||||
mode: ProjectMode;
|
||||
}
|
||||
|
||||
export interface IProjectSettings {
|
||||
mode: ProjectMode;
|
||||
defaultStickiness: DefaultStickiness;
|
||||
}
|
||||
|
||||
export interface IProjectSettingsRow {
|
||||
project_mode: ProjectMode;
|
||||
default_stickiness: DefaultStickiness;
|
||||
}
|
||||
|
||||
export interface IProjectArchived {
|
||||
id: string;
|
||||
archived: boolean;
|
||||
@ -86,4 +97,11 @@ export interface IProjectStore extends Store<IProject, string> {
|
||||
environment: string,
|
||||
projects: string[],
|
||||
): Promise<void>;
|
||||
|
||||
getProjectSettings(projectId: string): Promise<IProjectSettings>;
|
||||
setProjectSettings(
|
||||
projectId: string,
|
||||
defaultStickiness: DefaultStickiness,
|
||||
mode: ProjectMode,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ test('Should ONLY return default project', async () => {
|
||||
name: 'test',
|
||||
description: '',
|
||||
mode: 'open',
|
||||
defaultStickiness: 'default',
|
||||
});
|
||||
|
||||
const { body } = await app.request
|
||||
@ -41,27 +42,3 @@ test('Should ONLY return default project', async () => {
|
||||
expect(body.projects).toHaveLength(1);
|
||||
expect(body.projects[0].id).toBe('default');
|
||||
});
|
||||
|
||||
test('Should store and retrieve default project stickiness', async () => {
|
||||
const appWithDefaultStickiness = await setupAppWithCustomConfig(db.stores, {
|
||||
experimental: {
|
||||
flags: {
|
||||
projectScopedStickiness: true,
|
||||
strictSchemaValidation: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const reqBody = { defaultStickiness: 'userId' };
|
||||
|
||||
await appWithDefaultStickiness.request
|
||||
.post('/api/admin/projects/default/settings')
|
||||
.send(reqBody)
|
||||
.expect(200);
|
||||
|
||||
const { body } = await appWithDefaultStickiness.request
|
||||
.get('/api/admin/projects/default/settings')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
|
||||
expect(body).toStrictEqual(reqBody);
|
||||
});
|
||||
|
@ -1841,6 +1841,17 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"healthOverviewSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"defaultStickiness": {
|
||||
"description": "A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy",
|
||||
"enum": [
|
||||
"default",
|
||||
"userId",
|
||||
"sessionId",
|
||||
"random",
|
||||
],
|
||||
"example": "userId",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
@ -1904,6 +1915,17 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"activeCount": {
|
||||
"type": "number",
|
||||
},
|
||||
"defaultStickiness": {
|
||||
"description": "A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy",
|
||||
"enum": [
|
||||
"default",
|
||||
"userId",
|
||||
"sessionId",
|
||||
"random",
|
||||
],
|
||||
"example": "userId",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
@ -2759,6 +2781,17 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"additionalProperties": false,
|
||||
"description": "A high-level overview of a project. It contains information such as project statistics, the name of the project, what members and what features it contains, etc.",
|
||||
"properties": {
|
||||
"defaultStickiness": {
|
||||
"description": "A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy",
|
||||
"enum": [
|
||||
"default",
|
||||
"userId",
|
||||
"sessionId",
|
||||
"random",
|
||||
],
|
||||
"example": "userId",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"description": "Additional information about the project",
|
||||
"example": "DX squad feature release",
|
||||
@ -2841,6 +2874,17 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
},
|
||||
"defaultStickiness": {
|
||||
"description": "A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy",
|
||||
"enum": [
|
||||
"default",
|
||||
"userId",
|
||||
"sessionId",
|
||||
"random",
|
||||
],
|
||||
"example": "userId",
|
||||
"type": "string",
|
||||
},
|
||||
"description": {
|
||||
"description": "Additional information about the project",
|
||||
"example": "DX squad feature release",
|
||||
@ -2898,29 +2942,6 @@ exports[`should serve the OpenAPI spec 1`] = `
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"projectSettingsSchema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"defaultStickiness": {
|
||||
"example": "userId",
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"open",
|
||||
"protected",
|
||||
"private",
|
||||
],
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"defaultStickiness",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"projectStatsSchema": {
|
||||
"additionalProperties": false,
|
||||
"description": "Statistics for a project, including the average time to production, number of features created, the project activity and more.
|
||||
@ -7561,70 +7582,6 @@ If the provided project does not exist, the list of events will be empty.",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/projects/{projectId}/settings": {
|
||||
"get": {
|
||||
"operationId": "getProjectSettings",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "projectId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/projectSettingsSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "projectSettingsSchema",
|
||||
},
|
||||
"404": {
|
||||
"description": "This response has no body.",
|
||||
},
|
||||
},
|
||||
"tags": [
|
||||
"Projects",
|
||||
],
|
||||
},
|
||||
"post": {
|
||||
"operationId": "setProjectSettings",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "projectId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/projectSettingsSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "projectSettingsSchema",
|
||||
},
|
||||
"404": {
|
||||
"description": "This response has no body.",
|
||||
},
|
||||
},
|
||||
"tags": [
|
||||
"Projects",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/projects/{projectId}/stale": {
|
||||
"post": {
|
||||
"description": "This endpoint stales the specified features.",
|
||||
|
19
src/test/fixtures/fake-project-store.ts
vendored
19
src/test/fixtures/fake-project-store.ts
vendored
@ -1,12 +1,15 @@
|
||||
import {
|
||||
IProjectHealthUpdate,
|
||||
IProjectInsert,
|
||||
IProjectSettings,
|
||||
IProjectStore,
|
||||
} from '../../lib/types/stores/project-store';
|
||||
import {
|
||||
DefaultStickiness,
|
||||
IEnvironment,
|
||||
IProject,
|
||||
IProjectWithCount,
|
||||
ProjectMode,
|
||||
} from '../../lib/types/model';
|
||||
import NotFoundError from '../../lib/error/notfound-error';
|
||||
import {
|
||||
@ -161,4 +164,20 @@ export default class FakeProjectStore implements IProjectStore {
|
||||
): Promise<number> {
|
||||
throw new Error('Method not implemented');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getProjectSettings(projectId: string): Promise<IProjectSettings> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
setProjectSettings(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
defaultStickiness: DefaultStickiness,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
mode: ProjectMode,
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user