mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
feat: Query complexity validation (#4017)
This commit is contained in:
parent
3acb116ab2
commit
a0862cfc10
@ -1,6 +1,6 @@
|
|||||||
import { FormEventHandler, useEffect, useState, VFC } from 'react';
|
import { FormEventHandler, useEffect, useState, VFC } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Box, Paper, useTheme } from '@mui/material';
|
import { Box, Paper, useTheme, styled, Alert } from '@mui/material';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
@ -21,6 +21,11 @@ import Loader from '../../common/Loader/Loader';
|
|||||||
import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable';
|
import { AdvancedPlaygroundResultsTable } from './AdvancedPlaygroundResultsTable/AdvancedPlaygroundResultsTable';
|
||||||
import { AdvancedPlaygroundResponseSchema } from 'openapi';
|
import { AdvancedPlaygroundResponseSchema } from 'openapi';
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
import { BadRequestError } from '../../../utils/apiUtils';
|
||||||
|
|
||||||
|
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||||
|
marginBottom: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
export const AdvancedPlayground: VFC<{
|
export const AdvancedPlayground: VFC<{
|
||||||
FormComponent?: typeof PlaygroundForm;
|
FormComponent?: typeof PlaygroundForm;
|
||||||
@ -39,6 +44,7 @@ export const AdvancedPlayground: VFC<{
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const matches = true;
|
const matches = true;
|
||||||
|
|
||||||
|
const [configurationError, setConfigurationError] = useState<string>();
|
||||||
const [environments, setEnvironments] = useState<string[]>(
|
const [environments, setEnvironments] = useState<string[]>(
|
||||||
value.environments
|
value.environments
|
||||||
);
|
);
|
||||||
@ -137,12 +143,24 @@ export const AdvancedPlayground: VFC<{
|
|||||||
if (action && typeof action === 'function') {
|
if (action && typeof action === 'function') {
|
||||||
action();
|
action();
|
||||||
}
|
}
|
||||||
|
setConfigurationError(undefined);
|
||||||
setResults(response);
|
setResults(response);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastData({
|
if (error instanceof BadRequestError) {
|
||||||
type: 'error',
|
setConfigurationError(error.message);
|
||||||
title: `Error parsing context: ${formatUnknownError(error)}`,
|
} else if (error instanceof SyntaxError) {
|
||||||
});
|
setToastData({
|
||||||
|
type: 'error',
|
||||||
|
title: `Error parsing context: ${formatUnknownError(
|
||||||
|
error
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setToastData({
|
||||||
|
type: 'error',
|
||||||
|
title: formatUnknownError(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -242,6 +260,14 @@ export const AdvancedPlayground: VFC<{
|
|||||||
padding: theme.spacing(4, 2),
|
padding: theme.spacing(4, 2),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(configurationError)}
|
||||||
|
show={
|
||||||
|
<StyledAlert severity="warning">
|
||||||
|
{configurationError}
|
||||||
|
</StyledAlert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={loading}
|
condition={loading}
|
||||||
show={<Loader />}
|
show={<Loader />}
|
||||||
|
@ -15,6 +15,7 @@ import groupBy from 'lodash.groupby';
|
|||||||
import { omitKeys } from '../../util';
|
import { omitKeys } from '../../util';
|
||||||
import { AdvancedPlaygroundFeatureSchema } from '../../openapi/spec/advanced-playground-feature-schema';
|
import { AdvancedPlaygroundFeatureSchema } from '../../openapi/spec/advanced-playground-feature-schema';
|
||||||
import { AdvancedPlaygroundEnvironmentFeatureSchema } from '../../openapi/spec/advanced-playground-environment-feature-schema';
|
import { AdvancedPlaygroundEnvironmentFeatureSchema } from '../../openapi/spec/advanced-playground-environment-feature-schema';
|
||||||
|
import { validateQueryComplexity } from './validateQueryComplexity';
|
||||||
|
|
||||||
type EvaluationInput = {
|
type EvaluationInput = {
|
||||||
features: FeatureConfigurationClient[];
|
features: FeatureConfigurationClient[];
|
||||||
@ -54,6 +55,12 @@ export class PlaygroundService {
|
|||||||
);
|
);
|
||||||
const contexts = generateObjectCombinations(context);
|
const contexts = generateObjectCombinations(context);
|
||||||
|
|
||||||
|
validateQueryComplexity(
|
||||||
|
environments.length,
|
||||||
|
environmentFeatures[0]?.features.length ?? 0,
|
||||||
|
contexts.length,
|
||||||
|
);
|
||||||
|
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
environmentFeatures.flatMap(
|
environmentFeatures.flatMap(
|
||||||
({ features, featureProject, environment }) =>
|
({ features, featureProject, environment }) =>
|
||||||
|
39
src/lib/features/playground/validateQueryComplexity.test.ts
Normal file
39
src/lib/features/playground/validateQueryComplexity.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { validateQueryComplexity } from './validateQueryComplexity';
|
||||||
|
import { BadDataError } from '../../error';
|
||||||
|
|
||||||
|
test('should not throw error when total combinations are under MAX_COMPLEXITY', () => {
|
||||||
|
const environmentsCount = 10;
|
||||||
|
const featuresCount = 10;
|
||||||
|
const contextCombinationsCount = 10;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
validateQueryComplexity(
|
||||||
|
environmentsCount,
|
||||||
|
featuresCount,
|
||||||
|
contextCombinationsCount,
|
||||||
|
),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw BadDataError when total combinations are over MAX_COMPLEXITY', () => {
|
||||||
|
const environmentsCount = 2;
|
||||||
|
const featuresCount = 200;
|
||||||
|
const contextCombinationsCount = 10000;
|
||||||
|
|
||||||
|
const expectedMessage = `Rejecting evaluation as it would generate 4000000 combinations exceeding 30000 limit. Please reduce the number of selected environments (2), features (200), context field combinations (10000).`;
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
validateQueryComplexity(
|
||||||
|
environmentsCount,
|
||||||
|
featuresCount,
|
||||||
|
contextCombinationsCount,
|
||||||
|
),
|
||||||
|
).toThrow(BadDataError);
|
||||||
|
expect(() =>
|
||||||
|
validateQueryComplexity(
|
||||||
|
environmentsCount,
|
||||||
|
featuresCount,
|
||||||
|
contextCombinationsCount,
|
||||||
|
),
|
||||||
|
).toThrow(expectedMessage);
|
||||||
|
});
|
19
src/lib/features/playground/validateQueryComplexity.ts
Normal file
19
src/lib/features/playground/validateQueryComplexity.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { BadDataError } from '../../error';
|
||||||
|
|
||||||
|
const MAX_COMPLEXITY = 30000;
|
||||||
|
|
||||||
|
export const validateQueryComplexity = (
|
||||||
|
environmentsCount: number,
|
||||||
|
featuresCount: number,
|
||||||
|
contextCombinationsCount: number,
|
||||||
|
): void => {
|
||||||
|
const totalCount =
|
||||||
|
environmentsCount * featuresCount * contextCombinationsCount;
|
||||||
|
|
||||||
|
const reason = `Rejecting evaluation as it would generate ${totalCount} combinations exceeding ${MAX_COMPLEXITY} limit. `;
|
||||||
|
const action = `Please reduce the number of selected environments (${environmentsCount}), features (${featuresCount}), context field combinations (${contextCombinationsCount}).`;
|
||||||
|
|
||||||
|
if (totalCount > MAX_COMPLEXITY) {
|
||||||
|
throw new BadDataError(reason + action);
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user