1
0
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:
Mateusz Kwasniewski 2024-07-01 15:05:44 +02:00 committed by GitHub
parent 2cfd71f34e
commit 57b253c050
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 150 additions and 11 deletions

View File

@ -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']);
});

View File

@ -6,6 +6,8 @@ import type React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader'; import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader';
import { parseParameterStrings } from 'utils/parseParameter'; import { parseParameterStrings } from 'utils/parseParameter';
import { useUiFlag } from 'hooks/useUiFlag';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
interface IFreeTextInputProps { interface IFreeTextInputProps {
values: string[]; values: string[];
@ -13,7 +15,7 @@ interface IFreeTextInputProps {
setValues: (values: string[]) => void; setValues: (values: string[]) => void;
beforeValues?: JSX.Element; beforeValues?: JSX.Element;
error: string; error: string;
setError: React.Dispatch<React.SetStateAction<string>>; setError: (error: string) => void;
} }
const useStyles = makeStyles()((theme) => ({ const useStyles = makeStyles()((theme) => ({
@ -62,6 +64,8 @@ export const FreeTextInput = ({
}: IFreeTextInputProps) => { }: IFreeTextInputProps) => {
const [inputValues, setInputValues] = useState(''); const [inputValues, setInputValues] = useState('');
const { classes: styles } = useStyles(); const { classes: styles } = useStyles();
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const { uiConfig, loading } = useUiConfig();
const onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => { const onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === ENTER) { if (event.key === ENTER) {
@ -75,8 +79,16 @@ export const FreeTextInput = ({
...values, ...values,
...parseParameterStrings(inputValues), ...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'); setError('values cannot be empty');
} else if (newValues.some((v) => v.length > 100)) { } else if (newValues.some((v) => v.length > 100)) {
setError('values cannot be longer than 100 characters'); setError('values cannot be longer than 100 characters');
@ -114,8 +126,9 @@ export const FreeTextInput = ({
className={styles.button} className={styles.button}
variant='outlined' variant='outlined'
color='primary' color='primary'
onClick={() => addValues()} onClick={addValues}
data-testid='CONSTRAINT_VALUES_ADD_BUTTON' data-testid='CONSTRAINT_VALUES_ADD_BUTTON'
disabled={loading}
> >
Add values Add values
</Button> </Button>

View File

@ -38,5 +38,6 @@ export const defaultValue: IUiConfig = {
actionSetFilterValues: 25, actionSetFilterValues: 25,
signalTokensPerEndpoint: 5, signalTokensPerEndpoint: 5,
featureEnvironmentStrategies: 30, featureEnvironmentStrategies: 30,
constraintValues: 250,
}, },
}; };

View File

@ -26,4 +26,6 @@ export interface ResourceLimitsSchema {
strategySegments: number; strategySegments: number;
/** The maximum number of feature environment strategies allowed. */ /** The maximum number of feature environment strategies allowed. */
featureEnvironmentStrategies: number; featureEnvironmentStrategies: number;
/** The maximum number of values for a single constraint. */
constraintValues: number;
} }

View File

@ -109,6 +109,7 @@ import { allSettledWithRejection } from '../../util/allSettledWithRejection';
import type EventEmitter from 'node:events'; import type EventEmitter from 'node:events';
import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type'; import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
import type { ResourceLimitsSchema } from '../../openapi'; import type { ResourceLimitsSchema } from '../../openapi';
import { ExceedsLimitError } from '../../error/exceeds-limit-error';
interface IFeatureContext { interface IFeatureContext {
featureName: string; featureName: string;
@ -382,11 +383,11 @@ class FeatureToggleService {
) )
).length; ).length;
if (existingCount >= limit) { 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; if (!this.flagResolver.isEnabled('resourceLimits')) return;
const limit = this.resourceLimits.constraintValues; const limit = this.resourceLimits.constraintValues;
@ -396,8 +397,9 @@ class FeatureToggleService {
constraint.values?.length > limit, constraint.values?.length > limit,
); );
if (constraintOverLimit) { if (constraintOverLimit) {
throw new BadDataError( throw new ExceedsLimitError(
`Constraint values limit of ${limit} is exceeded for ${constraintOverLimit.contextName}.`, `content values for ${constraintOverLimit.contextName}`,
limit,
); );
} }
} }
@ -647,7 +649,7 @@ class FeatureToggleService {
strategyConfig.constraints && strategyConfig.constraints &&
strategyConfig.constraints.length > 0 strategyConfig.constraints.length > 0
) { ) {
this.validateContraintValuesLimit(strategyConfig.constraints); this.validateConstraintValuesLimit(strategyConfig.constraints);
strategyConfig.constraints = await this.validateConstraints( strategyConfig.constraints = await this.validateConstraints(
strategyConfig.constraints, strategyConfig.constraints,
); );
@ -806,7 +808,7 @@ class FeatureToggleService {
if (existingStrategy.id === id) { if (existingStrategy.id === id) {
if (updates.constraints && updates.constraints.length > 0) { if (updates.constraints && updates.constraints.length > 0) {
this.validateContraintValuesLimit(updates.constraints); this.validateConstraintValuesLimit(updates.constraints);
updates.constraints = await this.validateConstraints( updates.constraints = await this.validateConstraints(
updates.constraints, updates.constraints,
); );

View File

@ -41,7 +41,7 @@ test('Should not allow to exceed strategy limit', async () => {
} }
await expect(addStrategy()).rejects.toThrow( 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( ).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",
); );
}); });