mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
feat: disallow clone toggle on change request enabled (#3383)
This commit is contained in:
parent
663f26b712
commit
2caab45801
@ -18,6 +18,7 @@ import { getTogglePath } from 'utils/routePathHelpers';
|
|||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { useChangeRequestsEnabled } from '../../../hooks/useChangeRequestsEnabled';
|
||||||
|
|
||||||
const StyledPage = styled(Paper)(({ theme }) => ({
|
const StyledPage = styled(Paper)(({ theme }) => ({
|
||||||
overflow: 'visible',
|
overflow: 'visible',
|
||||||
@ -65,6 +66,8 @@ export const CopyFeatureToggle = () => {
|
|||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { feature } = useFeature(projectId, featureId);
|
const { feature } = useFeature(projectId, featureId);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { isChangeRequestConfiguredInAnyEnv } =
|
||||||
|
useChangeRequestsEnabled(projectId);
|
||||||
|
|
||||||
const setValue: ChangeEventHandler<HTMLInputElement> = event => {
|
const setValue: ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
const value = trim(event.target.value);
|
const value = trim(event.target.value);
|
||||||
@ -152,7 +155,12 @@ export const CopyFeatureToggle = () => {
|
|||||||
label="Replace groupId"
|
label="Replace groupId"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" color="primary" variant="contained">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
disabled={isChangeRequestConfiguredInAnyEnv()}
|
||||||
|
>
|
||||||
<FileCopy />
|
<FileCopy />
|
||||||
Create from copy
|
Create from copy
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -10,4 +10,5 @@ export interface IChangeRequestAccessReadModel {
|
|||||||
project: string,
|
project: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
): Promise<boolean>;
|
): Promise<boolean>;
|
||||||
|
isChangeRequestsEnabledForProject(project: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -19,4 +19,8 @@ export class FakeChangeRequestAccessReadModel
|
|||||||
public async isChangeRequestsEnabled(): Promise<boolean> {
|
public async isChangeRequestsEnabled(): Promise<boolean> {
|
||||||
return this.isChangeRequestEnabled;
|
return this.isChangeRequestEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isChangeRequestsEnabledForProject(): Promise<boolean> {
|
||||||
|
return this.isChangeRequestEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,4 +49,18 @@ export class ChangeRequestAccessReadModel
|
|||||||
const { present } = result.rows[0];
|
const { present } = result.rows[0];
|
||||||
return present;
|
return present;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isChangeRequestsEnabledForProject(
|
||||||
|
project: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT EXISTS(SELECT 1
|
||||||
|
FROM change_request_settings
|
||||||
|
WHERE project = ?
|
||||||
|
) AS present`,
|
||||||
|
[project],
|
||||||
|
);
|
||||||
|
const { present } = result.rows[0];
|
||||||
|
return present;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -882,6 +882,15 @@ class FeatureToggleService {
|
|||||||
replaceGroupId: boolean = true, // eslint-disable-line
|
replaceGroupId: boolean = true, // eslint-disable-line
|
||||||
userName: string,
|
userName: string,
|
||||||
): Promise<FeatureToggle> {
|
): Promise<FeatureToggle> {
|
||||||
|
const changeRequestEnabled =
|
||||||
|
await this.changeRequestAccessReadModel.isChangeRequestsEnabledForProject(
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
if (changeRequestEnabled) {
|
||||||
|
throw new NoAccessError(
|
||||||
|
`Cloning not allowed. Project ${projectId} has change requests enabled.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`${userName} clones feature toggle ${featureName} to ${newFeatureName}`,
|
`${userName} clones feature toggle ${featureName} to ${newFeatureName}`,
|
||||||
);
|
);
|
||||||
@ -1754,7 +1763,7 @@ class FeatureToggleService {
|
|||||||
project: string,
|
project: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
featureName: string,
|
featureName: string,
|
||||||
user: User,
|
user?: User,
|
||||||
) {
|
) {
|
||||||
const hasEnvironment =
|
const hasEnvironment =
|
||||||
await this.featureEnvironmentStore.featureHasEnvironment(
|
await this.featureEnvironmentStore.featureHasEnvironment(
|
||||||
|
@ -83,14 +83,18 @@ const updateStrategy = async (
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('feature_strategy_api_serial', getLogger);
|
db = await dbInit('feature_strategy_api_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
app = await setupAppWithCustomConfig(
|
||||||
experimental: {
|
db.stores,
|
||||||
flags: {
|
{
|
||||||
strictSchemaValidation: true,
|
experimental: {
|
||||||
bulkOperations: true,
|
flags: {
|
||||||
|
strictSchemaValidation: true,
|
||||||
|
bulkOperations: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
db.rawDatabase,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
@ -29,6 +29,8 @@ const mockConstraints = (): IConstraint[] => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const irrelevantDate = new Date();
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const config = createTestConfig();
|
const config = createTestConfig();
|
||||||
db = await dbInit(
|
db = await dbInit(
|
||||||
@ -54,9 +56,14 @@ beforeAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
await db.rawDatabase('change_request_settings').del();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.rawDatabase('change_request_settings').del();
|
||||||
|
});
|
||||||
|
|
||||||
test('Should create feature toggle strategy configuration', async () => {
|
test('Should create feature toggle strategy configuration', async () => {
|
||||||
const projectId = 'default';
|
const projectId = 'default';
|
||||||
const username = 'feature-toggle';
|
const username = 'feature-toggle';
|
||||||
@ -263,7 +270,7 @@ test('adding and removing an environment preserves variants when variants per en
|
|||||||
|
|
||||||
const toggle = await service.getFeature({
|
const toggle = await service.getFeature({
|
||||||
featureName,
|
featureName,
|
||||||
projectId: null,
|
projectId: undefined,
|
||||||
environmentVariants: false,
|
environmentVariants: false,
|
||||||
});
|
});
|
||||||
expect(toggle.variants).toHaveLength(1);
|
expect(toggle.variants).toHaveLength(1);
|
||||||
@ -327,6 +334,26 @@ test('cloning a feature toggle copies variant environments correctly', async ()
|
|||||||
expect(newEnv.variants).toHaveLength(1);
|
expect(newEnv.variants).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cloning a feature toggle not allowed for change requests enabled', async () => {
|
||||||
|
await db.rawDatabase('change_request_settings').insert({
|
||||||
|
project: 'default',
|
||||||
|
environment: 'default',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
service.cloneFeatureToggle(
|
||||||
|
'newToggleName',
|
||||||
|
'default',
|
||||||
|
'clonedToggleName',
|
||||||
|
true,
|
||||||
|
'test-user',
|
||||||
|
),
|
||||||
|
).rejects.toEqual(
|
||||||
|
new NoAccessError(
|
||||||
|
`Cloning not allowed. Project default has change requests enabled.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('Cloning a feature toggle also clones segments correctly', async () => {
|
test('Cloning a feature toggle also clones segments correctly', async () => {
|
||||||
const featureName = 'ToggleWithSegments';
|
const featureName = 'ToggleWithSegments';
|
||||||
const clonedFeatureName = 'AWholeNewFeatureToggle';
|
const clonedFeatureName = 'AWholeNewFeatureToggle';
|
||||||
@ -372,7 +399,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => {
|
|||||||
|
|
||||||
let feature = await service.getFeature({ featureName: clonedFeatureName });
|
let feature = await service.getFeature({ featureName: clonedFeatureName });
|
||||||
expect(
|
expect(
|
||||||
feature.environments.find((x) => x.name === 'default').strategies[0]
|
feature.environments.find((x) => x.name === 'default')?.strategies[0]
|
||||||
.segments,
|
.segments,
|
||||||
).toHaveLength(1);
|
).toHaveLength(1);
|
||||||
});
|
});
|
||||||
@ -425,14 +452,14 @@ test('If change requests are enabled, cannot change variants without going via C
|
|||||||
'default',
|
'default',
|
||||||
[newVariant],
|
[newVariant],
|
||||||
{
|
{
|
||||||
createdAt: undefined,
|
createdAt: irrelevantDate,
|
||||||
email: '',
|
email: '',
|
||||||
id: 0,
|
id: 0,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
loginAttempts: 0,
|
loginAttempts: 0,
|
||||||
name: '',
|
name: '',
|
||||||
permissions: [],
|
permissions: [],
|
||||||
seenAt: undefined,
|
seenAt: irrelevantDate,
|
||||||
username: '',
|
username: '',
|
||||||
generateImageUrl(): string {
|
generateImageUrl(): string {
|
||||||
return '';
|
return '';
|
||||||
@ -532,14 +559,14 @@ test('If CRs are protected for any environment in the project stops bulk update
|
|||||||
[enabledEnv.name, disabledEnv.name],
|
[enabledEnv.name, disabledEnv.name],
|
||||||
newVariants,
|
newVariants,
|
||||||
{
|
{
|
||||||
createdAt: undefined,
|
createdAt: irrelevantDate,
|
||||||
email: '',
|
email: '',
|
||||||
id: 0,
|
id: 0,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
loginAttempts: 0,
|
loginAttempts: 0,
|
||||||
name: '',
|
name: '',
|
||||||
permissions: [],
|
permissions: [],
|
||||||
seenAt: undefined,
|
seenAt: irrelevantDate,
|
||||||
username: '',
|
username: '',
|
||||||
generateImageUrl(): string {
|
generateImageUrl(): string {
|
||||||
return '';
|
return '';
|
||||||
|
Loading…
Reference in New Issue
Block a user