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 { 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>
|
||||||
|
@ -38,5 +38,6 @@ export const defaultValue: IUiConfig = {
|
|||||||
actionSetFilterValues: 25,
|
actionSetFilterValues: 25,
|
||||||
signalTokensPerEndpoint: 5,
|
signalTokensPerEndpoint: 5,
|
||||||
featureEnvironmentStrategies: 30,
|
featureEnvironmentStrategies: 30,
|
||||||
|
constraintValues: 250,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user