mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-28 00:06:53 +01:00
feat: constraint values limit UI (#7501)
This commit is contained in:
parent
2cfd71f34e
commit
57b253c050
@ -0,0 +1,121 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||
import { FreeTextInput } from './FreeTextInput';
|
||||
|
||||
const server = testServerSetup();
|
||||
|
||||
const LIMIT = 3;
|
||||
|
||||
const setupApi = () => {
|
||||
testServerRoute(server, '/api/admin/ui-config', {
|
||||
flags: {
|
||||
resourceLimits: true,
|
||||
},
|
||||
resourceLimits: {
|
||||
constraintValues: LIMIT,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
test('should set error when new constraint values exceed the limit', async () => {
|
||||
setupApi();
|
||||
const values: string[] = [];
|
||||
const errors: string[] = [];
|
||||
render(
|
||||
<FreeTextInput
|
||||
error=''
|
||||
values={[]}
|
||||
setValues={(newValues) => {
|
||||
values.push(...newValues);
|
||||
}}
|
||||
setError={(newError: string) => {
|
||||
errors.push(newError);
|
||||
}}
|
||||
removeValue={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
const button = await screen.findByText('Add values');
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
|
||||
const input = await screen.findByLabelText('Values');
|
||||
fireEvent.change(input, {
|
||||
target: { value: '1, 2, 3, 4' },
|
||||
});
|
||||
const button = await screen.findByText('Add values');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(errors).toEqual(['constraints cannot have more than 3 values']);
|
||||
expect(values).toEqual([]);
|
||||
});
|
||||
|
||||
test('should set error when old and new constraint values exceed the limit', async () => {
|
||||
setupApi();
|
||||
const values: string[] = [];
|
||||
const errors: string[] = [];
|
||||
render(
|
||||
<FreeTextInput
|
||||
error=''
|
||||
values={['1', '2']}
|
||||
setValues={(newValues) => {
|
||||
values.push(...newValues);
|
||||
}}
|
||||
setError={(newError: string) => {
|
||||
errors.push(newError);
|
||||
}}
|
||||
removeValue={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
const button = await screen.findByText('Add values');
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
|
||||
const input = await screen.findByLabelText('Values');
|
||||
fireEvent.change(input, {
|
||||
target: { value: '3, 4' },
|
||||
});
|
||||
const button = await screen.findByText('Add values');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(errors).toEqual(['constraints cannot have more than 3 values']);
|
||||
expect(values).toEqual([]);
|
||||
});
|
||||
|
||||
test('should set values', async () => {
|
||||
setupApi();
|
||||
const values: string[] = [];
|
||||
const errors: string[] = [];
|
||||
render(
|
||||
<FreeTextInput
|
||||
error=''
|
||||
values={['1', '2']}
|
||||
setValues={(newValues) => {
|
||||
values.push(...newValues);
|
||||
}}
|
||||
setError={(newError: string) => {
|
||||
errors.push(newError);
|
||||
}}
|
||||
removeValue={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(async () => {
|
||||
const button = await screen.findByText('Add values');
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
|
||||
const input = await screen.findByLabelText('Values');
|
||||
fireEvent.change(input, {
|
||||
target: { value: '2, 3' },
|
||||
});
|
||||
const button = await screen.findByText('Add values');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(errors).toEqual(['']);
|
||||
expect(values).toEqual(['1', '2', '3']);
|
||||
});
|
@ -6,6 +6,8 @@ import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader';
|
||||
import { parseParameterStrings } from 'utils/parseParameter';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
|
||||
interface IFreeTextInputProps {
|
||||
values: string[];
|
||||
@ -13,7 +15,7 @@ interface IFreeTextInputProps {
|
||||
setValues: (values: string[]) => void;
|
||||
beforeValues?: JSX.Element;
|
||||
error: string;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
setError: (error: string) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme) => ({
|
||||
@ -62,6 +64,8 @@ export const FreeTextInput = ({
|
||||
}: IFreeTextInputProps) => {
|
||||
const [inputValues, setInputValues] = useState('');
|
||||
const { classes: styles } = useStyles();
|
||||
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
||||
const { uiConfig, loading } = useUiConfig();
|
||||
|
||||
const onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === ENTER) {
|
||||
@ -75,8 +79,16 @@ export const FreeTextInput = ({
|
||||
...values,
|
||||
...parseParameterStrings(inputValues),
|
||||
]);
|
||||
const limitReached = Boolean(
|
||||
resourceLimitsEnabled &&
|
||||
newValues.length > uiConfig.resourceLimits.constraintValues,
|
||||
);
|
||||
|
||||
if (newValues.length === 0) {
|
||||
if (limitReached) {
|
||||
setError(
|
||||
`constraints cannot have more than ${uiConfig.resourceLimits.constraintValues} values`,
|
||||
);
|
||||
} else if (newValues.length === 0) {
|
||||
setError('values cannot be empty');
|
||||
} else if (newValues.some((v) => v.length > 100)) {
|
||||
setError('values cannot be longer than 100 characters');
|
||||
@ -114,8 +126,9 @@ export const FreeTextInput = ({
|
||||
className={styles.button}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={() => addValues()}
|
||||
onClick={addValues}
|
||||
data-testid='CONSTRAINT_VALUES_ADD_BUTTON'
|
||||
disabled={loading}
|
||||
>
|
||||
Add values
|
||||
</Button>
|
||||
|
@ -38,5 +38,6 @@ export const defaultValue: IUiConfig = {
|
||||
actionSetFilterValues: 25,
|
||||
signalTokensPerEndpoint: 5,
|
||||
featureEnvironmentStrategies: 30,
|
||||
constraintValues: 250,
|
||||
},
|
||||
};
|
||||
|
@ -26,4 +26,6 @@ export interface ResourceLimitsSchema {
|
||||
strategySegments: number;
|
||||
/** The maximum number of feature environment strategies allowed. */
|
||||
featureEnvironmentStrategies: number;
|
||||
/** The maximum number of values for a single constraint. */
|
||||
constraintValues: number;
|
||||
}
|
||||
|
@ -109,6 +109,7 @@ import { allSettledWithRejection } from '../../util/allSettledWithRejection';
|
||||
import type EventEmitter from 'node:events';
|
||||
import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
|
||||
import type { ResourceLimitsSchema } from '../../openapi';
|
||||
import { ExceedsLimitError } from '../../error/exceeds-limit-error';
|
||||
|
||||
interface IFeatureContext {
|
||||
featureName: string;
|
||||
@ -382,11 +383,11 @@ class FeatureToggleService {
|
||||
)
|
||||
).length;
|
||||
if (existingCount >= limit) {
|
||||
throw new BadDataError(`Strategy limit of ${limit} exceeded}.`);
|
||||
throw new ExceedsLimitError('strategy', limit);
|
||||
}
|
||||
}
|
||||
|
||||
validateContraintValuesLimit(updatedConstrains: IConstraint[]) {
|
||||
validateConstraintValuesLimit(updatedConstrains: IConstraint[]) {
|
||||
if (!this.flagResolver.isEnabled('resourceLimits')) return;
|
||||
|
||||
const limit = this.resourceLimits.constraintValues;
|
||||
@ -396,8 +397,9 @@ class FeatureToggleService {
|
||||
constraint.values?.length > limit,
|
||||
);
|
||||
if (constraintOverLimit) {
|
||||
throw new BadDataError(
|
||||
`Constraint values limit of ${limit} is exceeded for ${constraintOverLimit.contextName}.`,
|
||||
throw new ExceedsLimitError(
|
||||
`content values for ${constraintOverLimit.contextName}`,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -647,7 +649,7 @@ class FeatureToggleService {
|
||||
strategyConfig.constraints &&
|
||||
strategyConfig.constraints.length > 0
|
||||
) {
|
||||
this.validateContraintValuesLimit(strategyConfig.constraints);
|
||||
this.validateConstraintValuesLimit(strategyConfig.constraints);
|
||||
strategyConfig.constraints = await this.validateConstraints(
|
||||
strategyConfig.constraints,
|
||||
);
|
||||
@ -806,7 +808,7 @@ class FeatureToggleService {
|
||||
|
||||
if (existingStrategy.id === id) {
|
||||
if (updates.constraints && updates.constraints.length > 0) {
|
||||
this.validateContraintValuesLimit(updates.constraints);
|
||||
this.validateConstraintValuesLimit(updates.constraints);
|
||||
updates.constraints = await this.validateConstraints(
|
||||
updates.constraints,
|
||||
);
|
||||
|
@ -41,7 +41,7 @@ test('Should not allow to exceed strategy limit', async () => {
|
||||
}
|
||||
|
||||
await expect(addStrategy()).rejects.toThrow(
|
||||
`Strategy limit of ${LIMIT} exceeded`,
|
||||
"Failed to create strategy. You can't create more than the established limit of 3",
|
||||
);
|
||||
});
|
||||
|
||||
@ -79,6 +79,6 @@ test('Should not allow to exceed constraint values limit', async () => {
|
||||
},
|
||||
]),
|
||||
).rejects.toThrow(
|
||||
`Constraint values limit of ${LIMIT} is exceeded for userId`,
|
||||
"Failed to create content values for userId. You can't create more than the established limit of 3",
|
||||
);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user