mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
feat: feature creation limit crud together with frontend (#4221)
This commit is contained in:
parent
c387a19831
commit
3da1cbba47
@ -216,7 +216,6 @@ const getDeleteButtons = async () => {
|
|||||||
deleteButtons.push(...removeButton);
|
deleteButtons.push(...removeButton);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
console.log(deleteButtons);
|
|
||||||
return deleteButtons;
|
return deleteButtons;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,6 +12,24 @@ import UIContext from 'contexts/UIContext';
|
|||||||
import { CF_CREATE_BTN_ID } from 'utils/testIds';
|
import { CF_CREATE_BTN_ID } from 'utils/testIds';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { GO_BACK } from 'constants/navigate';
|
import { GO_BACK } from 'constants/navigate';
|
||||||
|
import { Alert, styled } from '@mui/material';
|
||||||
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const isFeatureLimitReached = (
|
||||||
|
featureLimit: number | null | undefined,
|
||||||
|
currentFeatureCount: number
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
featureLimit !== null &&
|
||||||
|
featureLimit !== undefined &&
|
||||||
|
featureLimit <= currentFeatureCount
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CreateFeature = () => {
|
const CreateFeature = () => {
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
@ -36,6 +54,8 @@ const CreateFeature = () => {
|
|||||||
errors,
|
errors,
|
||||||
} = useFeatureForm();
|
} = useFeatureForm();
|
||||||
|
|
||||||
|
const { project: projectInfo } = useProject(project);
|
||||||
|
|
||||||
const { createFeatureToggle, loading } = useFeatureApi();
|
const { createFeatureToggle, loading } = useFeatureApi();
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
const handleSubmit = async (e: Event) => {
|
||||||
@ -74,6 +94,11 @@ const CreateFeature = () => {
|
|||||||
navigate(GO_BACK);
|
navigate(GO_BACK);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const featureLimitReached =
|
||||||
|
isFeatureLimitReached(
|
||||||
|
projectInfo.featureLimit,
|
||||||
|
projectInfo.features.length
|
||||||
|
) && Boolean(uiConfig.flags.newProjectLayout);
|
||||||
return (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -84,6 +109,18 @@ const CreateFeature = () => {
|
|||||||
documentationLinkLabel="Feature toggle types documentation"
|
documentationLinkLabel="Feature toggle types documentation"
|
||||||
formatApiCode={formatApiCode}
|
formatApiCode={formatApiCode}
|
||||||
>
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={featureLimitReached}
|
||||||
|
show={
|
||||||
|
<StyledAlert severity="error">
|
||||||
|
<strong>Feature toggle project limit reached. </strong>{' '}
|
||||||
|
To be able to create more feature toggles in this
|
||||||
|
project please increase the feature toggle upper limit
|
||||||
|
in the project settings.
|
||||||
|
</StyledAlert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<FeatureForm
|
<FeatureForm
|
||||||
type={type}
|
type={type}
|
||||||
name={name}
|
name={name}
|
||||||
@ -104,6 +141,7 @@ const CreateFeature = () => {
|
|||||||
>
|
>
|
||||||
<CreateButton
|
<CreateButton
|
||||||
name="feature toggle"
|
name="feature toggle"
|
||||||
|
disabled={featureLimitReached}
|
||||||
permission={CREATE_FEATURE}
|
permission={CREATE_FEATURE}
|
||||||
projectId={project}
|
projectId={project}
|
||||||
data-testid={CF_CREATE_BTN_ID}
|
data-testid={CF_CREATE_BTN_ID}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { isFeatureLimitReached } from './CreateFeature';
|
||||||
|
|
||||||
|
test('isFeatureLimitReached should return false when featureLimit is null', async () => {
|
||||||
|
expect(isFeatureLimitReached(null, 5)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFeatureLimitReached should return false when featureLimit is undefined', async () => {
|
||||||
|
expect(isFeatureLimitReached(undefined, 5)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFeatureLimitReached should return false when featureLimit is smaller current feature count', async () => {
|
||||||
|
expect(isFeatureLimitReached(6, 5)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFeatureLimitReached should return true when featureLimit is smaller current feature count', async () => {
|
||||||
|
expect(isFeatureLimitReached(4, 5)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isFeatureLimitReached should return true when featureLimit is equal to current feature count', async () => {
|
||||||
|
expect(isFeatureLimitReached(5, 5)).toBe(true);
|
||||||
|
});
|
@ -236,7 +236,6 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
|||||||
</StyledRow>
|
</StyledRow>
|
||||||
</StyledFormControl>
|
</StyledFormControl>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
{children}
|
{children}
|
||||||
<StyledCancelButton onClick={handleCancel}>
|
<StyledCancelButton onClick={handleCancel}>
|
||||||
|
@ -230,8 +230,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
featureCount !== undefined &&
|
featureCount !== undefined &&
|
||||||
featureLimit !== undefined &&
|
Boolean(featureLimit)
|
||||||
featureLimit.length > 0
|
|
||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
<Box>
|
<Box>
|
||||||
|
@ -54,7 +54,8 @@ const EditProject = () => {
|
|||||||
project.name,
|
project.name,
|
||||||
project.description,
|
project.description,
|
||||||
defaultStickiness,
|
defaultStickiness,
|
||||||
project.mode
|
project.mode,
|
||||||
|
project.featureLimit ? String(project.featureLimit) : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatApiCode = () => {
|
const formatApiCode = () => {
|
||||||
|
@ -57,11 +57,18 @@ const useProjectForm = (
|
|||||||
name: projectName,
|
name: projectName,
|
||||||
description: projectDesc,
|
description: projectDesc,
|
||||||
defaultStickiness: projectStickiness,
|
defaultStickiness: projectStickiness,
|
||||||
featureLimit: featureLimit,
|
featureLimit: getFeatureLimitAsNumber(),
|
||||||
mode: projectMode,
|
mode: projectMode,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFeatureLimitAsNumber = () => {
|
||||||
|
if (featureLimit === '') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Number(featureLimit);
|
||||||
|
};
|
||||||
|
|
||||||
const validateProjectId = async () => {
|
const validateProjectId = async () => {
|
||||||
if (projectId.length === 0) {
|
if (projectId.length === 0) {
|
||||||
setErrors(prev => ({ ...prev, id: 'Id can not be empty.' }));
|
setErrors(prev => ({ ...prev, id: 'Id can not be empty.' }));
|
||||||
|
@ -26,6 +26,7 @@ export interface IProject {
|
|||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
mode: 'open' | 'protected';
|
mode: 'open' | 'protected';
|
||||||
defaultStickiness: string;
|
defaultStickiness: string;
|
||||||
|
featureLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectHealthReport extends IProject {
|
export interface IProjectHealthReport extends IProject {
|
||||||
|
@ -7,14 +7,12 @@ import {
|
|||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
IProject,
|
IProject,
|
||||||
IProjectWithCount,
|
IProjectWithCount,
|
||||||
ProjectMode,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
IProjectHealthUpdate,
|
IProjectHealthUpdate,
|
||||||
IProjectInsert,
|
IProjectInsert,
|
||||||
IProjectQuery,
|
IProjectQuery,
|
||||||
IProjectSettings,
|
IProjectSettings,
|
||||||
IProjectSettingsRow,
|
|
||||||
IProjectStore,
|
IProjectStore,
|
||||||
ProjectEnvironment,
|
ProjectEnvironment,
|
||||||
} from '../types/stores/project-store';
|
} from '../types/stores/project-store';
|
||||||
@ -35,7 +33,11 @@ const COLUMNS = [
|
|||||||
'updated_at',
|
'updated_at',
|
||||||
];
|
];
|
||||||
const TABLE = 'projects';
|
const TABLE = 'projects';
|
||||||
const SETTINGS_COLUMNS = ['project_mode', 'default_stickiness'];
|
const SETTINGS_COLUMNS = [
|
||||||
|
'project_mode',
|
||||||
|
'default_stickiness',
|
||||||
|
'feature_limit',
|
||||||
|
];
|
||||||
const SETTINGS_TABLE = 'project_settings';
|
const SETTINGS_TABLE = 'project_settings';
|
||||||
const PROJECT_ENVIRONMENTS = 'project_environments';
|
const PROJECT_ENVIRONMENTS = 'project_environments';
|
||||||
|
|
||||||
@ -94,6 +96,20 @@ class ProjectStore implements IProjectStore {
|
|||||||
return present;
|
return present;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isFeatureLimitReached(id: string): Promise<boolean> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT EXISTS(SELECT 1
|
||||||
|
FROM project_settings
|
||||||
|
LEFT JOIN features ON project_settings.project = features.project
|
||||||
|
WHERE project_settings.project = ?
|
||||||
|
GROUP BY project_settings.project
|
||||||
|
HAVING project_settings.feature_limit <= COUNT(features.project)) AS present`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
const { present } = result.rows[0];
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
|
||||||
async getProjectsWithCounts(
|
async getProjectsWithCounts(
|
||||||
query?: IProjectQuery,
|
query?: IProjectQuery,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
@ -219,6 +235,7 @@ class ProjectStore implements IProjectStore {
|
|||||||
project: project.id,
|
project: project.id,
|
||||||
project_mode: project.mode,
|
project_mode: project.mode,
|
||||||
default_stickiness: project.defaultStickiness,
|
default_stickiness: project.defaultStickiness,
|
||||||
|
feature_limit: project.featureLimit,
|
||||||
})
|
})
|
||||||
.returning('*');
|
.returning('*');
|
||||||
return this.mapRow({ ...row[0], ...settingsRow[0] });
|
return this.mapRow({ ...row[0], ...settingsRow[0] });
|
||||||
@ -245,12 +262,14 @@ class ProjectStore implements IProjectStore {
|
|||||||
.update({
|
.update({
|
||||||
project_mode: data.mode,
|
project_mode: data.mode,
|
||||||
default_stickiness: data.defaultStickiness,
|
default_stickiness: data.defaultStickiness,
|
||||||
|
feature_limit: data.featureLimit,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.db(SETTINGS_TABLE).insert({
|
await this.db(SETTINGS_TABLE).insert({
|
||||||
project: data.id,
|
project: data.id,
|
||||||
project_mode: data.mode,
|
project_mode: data.mode,
|
||||||
default_stickiness: data.defaultStickiness,
|
default_stickiness: data.defaultStickiness,
|
||||||
|
feature_limit: data.featureLimit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -486,24 +505,6 @@ class ProjectStore implements IProjectStore {
|
|||||||
return Number(members.count);
|
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: string,
|
|
||||||
mode: ProjectMode,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.db(SETTINGS_TABLE)
|
|
||||||
.update({
|
|
||||||
default_stickiness: defaultStickiness,
|
|
||||||
project_mode: mode,
|
|
||||||
})
|
|
||||||
.where({ project: projectId });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDefaultStrategy(
|
async getDefaultStrategy(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
@ -537,13 +538,6 @@ class ProjectStore implements IProjectStore {
|
|||||||
.then((res) => Number(res[0].count));
|
.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
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
mapLinkRow(row): IEnvironmentProjectLink {
|
mapLinkRow(row): IEnvironmentProjectLink {
|
||||||
return {
|
return {
|
||||||
@ -567,6 +561,7 @@ class ProjectStore implements IProjectStore {
|
|||||||
updatedAt: row.updated_at || new Date(),
|
updatedAt: row.updated_at || new Date(),
|
||||||
mode: row.project_mode || 'open',
|
mode: row.project_mode || 'open',
|
||||||
defaultStickiness: row.default_stickiness || 'default',
|
defaultStickiness: row.default_stickiness || 'default',
|
||||||
|
featureLimit: row.feature_limit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
|
UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
|
||||||
UPDATE_TAG_TYPE,
|
UPDATE_TAG_TYPE,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { InvalidOperationError } from '../../error';
|
import { PermissionError } from '../../error';
|
||||||
|
|
||||||
type Mode = 'regular' | 'change_request';
|
type Mode = 'regular' | 'change_request';
|
||||||
|
|
||||||
@ -149,9 +149,7 @@ export class ImportPermissionsService {
|
|||||||
mode,
|
mode,
|
||||||
);
|
);
|
||||||
if (missingPermissions.length > 0) {
|
if (missingPermissions.length > 0) {
|
||||||
throw new InvalidOperationError(
|
throw new PermissionError(missingPermissions);
|
||||||
'You are missing permissions to import',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,13 @@ export const healthOverviewSchema = {
|
|||||||
description:
|
description:
|
||||||
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
|
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
|
||||||
},
|
},
|
||||||
|
featureLimit: {
|
||||||
|
type: 'number',
|
||||||
|
nullable: true,
|
||||||
|
example: 100,
|
||||||
|
description:
|
||||||
|
'A limit on the number of features allowed in the project. Null if no limit.',
|
||||||
|
},
|
||||||
members: {
|
members: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
description: 'The number of users/members in the project.',
|
description: 'The number of users/members in the project.',
|
||||||
|
@ -51,6 +51,13 @@ export const projectOverviewSchema = {
|
|||||||
description:
|
description:
|
||||||
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
|
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
|
||||||
},
|
},
|
||||||
|
featureLimit: {
|
||||||
|
type: 'number',
|
||||||
|
nullable: true,
|
||||||
|
example: 100,
|
||||||
|
description:
|
||||||
|
'A limit on the number of features allowed in the project. Null if no limit.',
|
||||||
|
},
|
||||||
members: {
|
members: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
example: 4,
|
example: 4,
|
||||||
|
@ -921,6 +921,15 @@ class FeatureToggleService {
|
|||||||
this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
|
this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
|
||||||
await this.validateName(value.name);
|
await this.validateName(value.name);
|
||||||
const exists = await this.projectStore.hasProject(projectId);
|
const exists = await this.projectStore.hasProject(projectId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.flagResolver.isEnabled('newProjectLayout') &&
|
||||||
|
(await this.projectStore.isFeatureLimitReached(projectId))
|
||||||
|
) {
|
||||||
|
throw new InvalidOperationError(
|
||||||
|
'You have reached the maximum number of feature toggles for this project.',
|
||||||
|
);
|
||||||
|
}
|
||||||
if (exists) {
|
if (exists) {
|
||||||
let featureData;
|
let featureData;
|
||||||
if (isValidated) {
|
if (isValidated) {
|
||||||
|
@ -9,5 +9,6 @@ export const projectSchema = joi
|
|||||||
description: joi.string().allow(null).allow('').optional(),
|
description: joi.string().allow(null).allow('').optional(),
|
||||||
mode: joi.string().valid('open', 'protected').default('open'),
|
mode: joi.string().valid('open', 'protected').default('open'),
|
||||||
defaultStickiness: joi.string().default('default'),
|
defaultStickiness: joi.string().default('default'),
|
||||||
|
featureLimit: joi.number().allow(null).optional(),
|
||||||
})
|
})
|
||||||
.options({ allowUnknown: false, stripUnknown: true });
|
.options({ allowUnknown: false, stripUnknown: true });
|
||||||
|
@ -29,7 +29,6 @@ import {
|
|||||||
ProjectGroupAddedEvent,
|
ProjectGroupAddedEvent,
|
||||||
ProjectGroupRemovedEvent,
|
ProjectGroupRemovedEvent,
|
||||||
ProjectGroupUpdateRoleEvent,
|
ProjectGroupUpdateRoleEvent,
|
||||||
ProjectMode,
|
|
||||||
ProjectUserAddedEvent,
|
ProjectUserAddedEvent,
|
||||||
ProjectUserRemovedEvent,
|
ProjectUserRemovedEvent,
|
||||||
ProjectUserUpdateRoleEvent,
|
ProjectUserUpdateRoleEvent,
|
||||||
@ -37,11 +36,7 @@ import {
|
|||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
ProjectAccessAddedEvent,
|
ProjectAccessAddedEvent,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||||
IProjectQuery,
|
|
||||||
IProjectSettings,
|
|
||||||
IProjectStore,
|
|
||||||
} from '../types/stores/project-store';
|
|
||||||
import {
|
import {
|
||||||
IProjectAccessModel,
|
IProjectAccessModel,
|
||||||
IRoleDescriptor,
|
IRoleDescriptor,
|
||||||
@ -836,6 +831,7 @@ export default class ProjectService {
|
|||||||
name: project.name,
|
name: project.name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
mode: project.mode,
|
mode: project.mode,
|
||||||
|
featureLimit: project.featureLimit,
|
||||||
defaultStickiness: project.defaultStickiness,
|
defaultStickiness: project.defaultStickiness,
|
||||||
health: project.health || 0,
|
health: project.health || 0,
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
@ -847,20 +843,4 @@ export default class ProjectService {
|
|||||||
version: 1,
|
version: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectSettings(projectId: string): Promise<IProjectSettings> {
|
|
||||||
return this.store.getProjectSettings(projectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setProjectSettings(
|
|
||||||
projectId: string,
|
|
||||||
defaultStickiness: string,
|
|
||||||
mode: ProjectMode,
|
|
||||||
): Promise<void> {
|
|
||||||
return this.store.setProjectSettings(
|
|
||||||
projectId,
|
|
||||||
defaultStickiness,
|
|
||||||
mode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ export interface IProjectOverview {
|
|||||||
createdAt: Date | undefined;
|
createdAt: Date | undefined;
|
||||||
stats?: IProjectStats;
|
stats?: IProjectStats;
|
||||||
mode: ProjectMode;
|
mode: ProjectMode;
|
||||||
|
featureLimit?: number;
|
||||||
defaultStickiness: string;
|
defaultStickiness: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,6 +384,7 @@ export interface IProject {
|
|||||||
changeRequestsEnabled?: boolean;
|
changeRequestsEnabled?: boolean;
|
||||||
mode: ProjectMode;
|
mode: ProjectMode;
|
||||||
defaultStickiness: string;
|
defaultStickiness: string;
|
||||||
|
featureLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,11 +18,13 @@ export interface IProjectInsert {
|
|||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
changeRequestsEnabled?: boolean;
|
changeRequestsEnabled?: boolean;
|
||||||
mode: ProjectMode;
|
mode: ProjectMode;
|
||||||
|
featureLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectSettings {
|
export interface IProjectSettings {
|
||||||
mode: ProjectMode;
|
mode: ProjectMode;
|
||||||
defaultStickiness: string;
|
defaultStickiness: string;
|
||||||
|
featureLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectSettingsRow {
|
export interface IProjectSettingsRow {
|
||||||
@ -55,11 +57,6 @@ export type ProjectEnvironment = {
|
|||||||
defaultStrategy?: CreateFeatureStrategySchema;
|
defaultStrategy?: CreateFeatureStrategySchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IProjectEnvironmentWithChangeRequests {
|
|
||||||
environment: string;
|
|
||||||
changeRequestsEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IProjectStore extends Store<IProject, string> {
|
export interface IProjectStore extends Store<IProject, string> {
|
||||||
hasProject(id: string): Promise<boolean>;
|
hasProject(id: string): Promise<boolean>;
|
||||||
|
|
||||||
@ -109,13 +106,6 @@ export interface IProjectStore extends Store<IProject, string> {
|
|||||||
projects: string[],
|
projects: string[],
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
getProjectSettings(projectId: string): Promise<IProjectSettings>;
|
|
||||||
setProjectSettings(
|
|
||||||
projectId: string,
|
|
||||||
defaultStickiness: string,
|
|
||||||
mode: ProjectMode,
|
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
getDefaultStrategy(
|
getDefaultStrategy(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
@ -125,4 +115,6 @@ export interface IProjectStore extends Store<IProject, string> {
|
|||||||
environment: string,
|
environment: string,
|
||||||
strategy: CreateFeatureStrategySchema,
|
strategy: CreateFeatureStrategySchema,
|
||||||
): Promise<CreateFeatureStrategySchema>;
|
): Promise<CreateFeatureStrategySchema>;
|
||||||
|
|
||||||
|
isFeatureLimitReached(id: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
20
src/migrations/20230711163311-project-feature-limit.js
Normal file
20
src/migrations/20230711163311-project-feature-limit.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE project_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS "feature_limit" integer;
|
||||||
|
`,
|
||||||
|
cb(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE project_settings DROP COLUMN IF EXISTS "feature_limit";
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
29
src/test/fixtures/fake-project-store.ts
vendored
29
src/test/fixtures/fake-project-store.ts
vendored
@ -1,16 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
IProjectHealthUpdate,
|
IProjectHealthUpdate,
|
||||||
IProjectInsert,
|
IProjectInsert,
|
||||||
IProjectSettings,
|
|
||||||
IProjectStore,
|
IProjectStore,
|
||||||
ProjectEnvironment,
|
ProjectEnvironment,
|
||||||
} from '../../lib/types/stores/project-store';
|
} from '../../lib/types/stores/project-store';
|
||||||
import {
|
import { IEnvironment, IProject, IProjectWithCount } from '../../lib/types';
|
||||||
IEnvironment,
|
|
||||||
IProject,
|
|
||||||
IProjectWithCount,
|
|
||||||
ProjectMode,
|
|
||||||
} from '../../lib/types';
|
|
||||||
import NotFoundError from '../../lib/error/notfound-error';
|
import NotFoundError from '../../lib/error/notfound-error';
|
||||||
import {
|
import {
|
||||||
IEnvironmentProjectLink,
|
IEnvironmentProjectLink,
|
||||||
@ -167,22 +161,6 @@ export default class FakeProjectStore implements IProjectStore {
|
|||||||
throw new Error('Method not implemented');
|
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: string,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
mode: ProjectMode,
|
|
||||||
): Promise<void> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDefaultStrategy(
|
updateDefaultStrategy(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -202,4 +180,9 @@ export default class FakeProjectStore implements IProjectStore {
|
|||||||
): Promise<CreateFeatureStrategySchema | undefined> {
|
): Promise<CreateFeatureStrategySchema | undefined> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
isFeatureLimitReached(id: string): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user