mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-23 01:16:27 +02:00
feat: limit component used in strategies (#7542)
This commit is contained in:
parent
233bf0757e
commit
cad8a3c2df
@ -12,6 +12,7 @@ const StyledBox = styled(Box)(({ theme }) => ({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
border: `2px solid ${theme.palette.background.application}`,
|
border: `2px solid ${theme.palette.background.application}`,
|
||||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
|
width: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
|
const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
|
||||||
@ -32,7 +33,7 @@ const Header = styled(Box)(({ theme }) => ({
|
|||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
borderBottom: `2px solid ${theme.palette.background.application}`,
|
borderBottom: `1px solid ${theme.palette.background.application}`,
|
||||||
padding: theme.spacing(3, 4),
|
padding: theme.spacing(3, 4),
|
||||||
fontSize: theme.typography.h2.fontSize,
|
fontSize: theme.typography.h2.fontSize,
|
||||||
}));
|
}));
|
||||||
@ -42,7 +43,7 @@ const Footer = styled(Box)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const Main = styled(Box)(({ theme }) => ({
|
const Main = styled(Box)(({ theme }) => ({
|
||||||
borderBottom: `2px solid ${theme.palette.background.application}`,
|
borderBottom: `1px solid ${theme.palette.background.application}`,
|
||||||
padding: theme.spacing(3, 4),
|
padding: theme.spacing(3, 4),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -95,6 +95,8 @@ describe('NewFeatureStrategyCreate', () => {
|
|||||||
|
|
||||||
const titleEl = await screen.findByText('Gradual rollout');
|
const titleEl = await screen.findByText('Gradual rollout');
|
||||||
expect(titleEl).toBeInTheDocument();
|
expect(titleEl).toBeInTheDocument();
|
||||||
|
const saveButton = await screen.findByText('Save strategy');
|
||||||
|
expect(saveButton).not.toBeDisabled();
|
||||||
|
|
||||||
const slider = await screen.findByRole('slider', { name: /rollout/i });
|
const slider = await screen.findByRole('slider', { name: /rollout/i });
|
||||||
expect(slider).toHaveValue('100');
|
expect(slider).toHaveValue('100');
|
||||||
|
@ -36,6 +36,25 @@ import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
|||||||
import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
|
import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
|
||||||
import { FeatureStrategyForm } from '../FeatureStrategyForm/FeatureStrategyForm';
|
import { FeatureStrategyForm } from '../FeatureStrategyForm/FeatureStrategyForm';
|
||||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
|
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { Limit } from 'component/common/Limit/Limit';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
|
const useStrategyLimit = (strategyCount: number) => {
|
||||||
|
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const featureEnvironmentStrategiesLimit =
|
||||||
|
uiConfig.resourceLimits?.featureEnvironmentStrategies || 100;
|
||||||
|
const limitReached =
|
||||||
|
resourceLimitsEnabled &&
|
||||||
|
strategyCount >= featureEnvironmentStrategiesLimit;
|
||||||
|
|
||||||
|
return {
|
||||||
|
resourceLimitsEnabled,
|
||||||
|
limit: featureEnvironmentStrategiesLimit,
|
||||||
|
limitReached,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const FeatureStrategyCreate = () => {
|
export const FeatureStrategyCreate = () => {
|
||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(0);
|
||||||
@ -70,6 +89,12 @@ export const FeatureStrategyCreate = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||||
|
const featureEnvironment = feature?.environments.find(
|
||||||
|
(featureEnvironment) => featureEnvironment.name === environmentId,
|
||||||
|
);
|
||||||
|
const strategyCount = featureEnvironment?.strategies.length || 0;
|
||||||
|
const { limit, limitReached, resourceLimitsEnabled } =
|
||||||
|
useStrategyLimit(strategyCount);
|
||||||
const ref = useRef<IFeatureToggle>(feature);
|
const ref = useRef<IFeatureToggle>(feature);
|
||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
const { refetch: refetchChangeRequests } =
|
const { refetch: refetchChangeRequests } =
|
||||||
@ -221,6 +246,20 @@ export const FeatureStrategyCreate = () => {
|
|||||||
editable
|
editable
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
Limit={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={resourceLimitsEnabled}
|
||||||
|
show={
|
||||||
|
<Limit
|
||||||
|
name='strategies in this environment'
|
||||||
|
shortName='strategies'
|
||||||
|
currentValue={strategyCount}
|
||||||
|
limit={limit}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={limitReached}
|
||||||
/>
|
/>
|
||||||
{staleDataNotification}
|
{staleDataNotification}
|
||||||
</FormTemplate>
|
</FormTemplate>
|
||||||
|
@ -86,6 +86,10 @@ export const setupUiConfigEndpoint = () => {
|
|||||||
environment: 'enterprise',
|
environment: 'enterprise',
|
||||||
flags: {
|
flags: {
|
||||||
newStrategyConfiguration: true,
|
newStrategyConfiguration: true,
|
||||||
|
resourceLimits: true,
|
||||||
|
},
|
||||||
|
resourceLimits: {
|
||||||
|
featureEnvironmentStrategies: 2,
|
||||||
},
|
},
|
||||||
unleashUrl: 'example.com',
|
unleashUrl: 'example.com',
|
||||||
});
|
});
|
||||||
|
@ -66,6 +66,8 @@ interface IFeatureStrategyFormProps {
|
|||||||
tab: number;
|
tab: number;
|
||||||
setTab: React.Dispatch<React.SetStateAction<number>>;
|
setTab: React.Dispatch<React.SetStateAction<number>>;
|
||||||
StrategyVariants: JSX.Element;
|
StrategyVariants: JSX.Element;
|
||||||
|
Limit?: JSX.Element;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledDividerContent = styled(Box)(({ theme }) => ({
|
const StyledDividerContent = styled(Box)(({ theme }) => ({
|
||||||
@ -205,6 +207,8 @@ export const FeatureStrategyForm = ({
|
|||||||
tab,
|
tab,
|
||||||
setTab,
|
setTab,
|
||||||
StrategyVariants,
|
StrategyVariants,
|
||||||
|
Limit,
|
||||||
|
disabled,
|
||||||
}: IFeatureStrategyFormProps) => {
|
}: IFeatureStrategyFormProps) => {
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
const [showProdGuard, setShowProdGuard] = useState(false);
|
const [showProdGuard, setShowProdGuard] = useState(false);
|
||||||
@ -532,6 +536,10 @@ export const FeatureStrategyForm = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ flex: 1, display: 'flex', alignItems: 'flex-end' }}>
|
||||||
|
{Limit}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<StyledButtons>
|
<StyledButtons>
|
||||||
<PermissionButton
|
<PermissionButton
|
||||||
permission={permission}
|
permission={permission}
|
||||||
@ -541,6 +549,7 @@ export const FeatureStrategyForm = ({
|
|||||||
color='primary'
|
color='primary'
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={
|
disabled={
|
||||||
|
disabled ||
|
||||||
loading ||
|
loading ||
|
||||||
!hasValidConstraints ||
|
!hasValidConstraints ||
|
||||||
errors.hasFormErrors()
|
errors.hasFormErrors()
|
||||||
|
@ -1,42 +1,10 @@
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import FeatureOverviewEnvironment from './FeatureOverviewEnvironment';
|
import FeatureOverviewEnvironment from './FeatureOverviewEnvironment';
|
||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
import type { IFeatureStrategy } from 'interfaces/strategy';
|
||||||
|
|
||||||
const server = testServerSetup();
|
|
||||||
|
|
||||||
const LIMIT = 3;
|
|
||||||
|
|
||||||
const setupApi = () => {
|
|
||||||
testServerRoute(server, '/api/admin/ui-config', {
|
|
||||||
flags: {
|
|
||||||
resourceLimits: true,
|
|
||||||
},
|
|
||||||
resourceLimits: {
|
|
||||||
featureEnvironmentStrategies: LIMIT,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testServerRoute(
|
|
||||||
server,
|
|
||||||
'/api/admin/projects/default/features/featureWithoutStrategies',
|
|
||||||
{
|
|
||||||
environments: [environmentWithoutStrategies],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testServerRoute(
|
|
||||||
server,
|
|
||||||
'/api/admin/projects/default/features/featureWithManyStrategies',
|
|
||||||
{
|
|
||||||
environments: [environmentWithManyStrategies],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const strategy = {
|
const strategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
} as IFeatureStrategy;
|
} as IFeatureStrategy;
|
||||||
@ -46,15 +14,8 @@ const environmentWithoutStrategies = {
|
|||||||
type: 'production',
|
type: 'production',
|
||||||
strategies: [],
|
strategies: [],
|
||||||
};
|
};
|
||||||
const environmentWithManyStrategies = {
|
|
||||||
name: 'production',
|
|
||||||
enabled: true,
|
|
||||||
type: 'production',
|
|
||||||
strategies: [...Array(LIMIT).keys()].map(() => strategy),
|
|
||||||
};
|
|
||||||
|
|
||||||
test('should allow to add strategy when no strategies', async () => {
|
test('should allow to add strategy', async () => {
|
||||||
setupApi();
|
|
||||||
render(
|
render(
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
@ -75,28 +36,3 @@ test('should allow to add strategy when no strategies', async () => {
|
|||||||
const button = await screen.findByText('Add strategy');
|
const button = await screen.findByText('Add strategy');
|
||||||
expect(button).toBeEnabled();
|
expect(button).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not allow to add strategy when limit reached', async () => {
|
|
||||||
setupApi();
|
|
||||||
render(
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path='/projects/:projectId/features/:featureId/strategies/create'
|
|
||||||
element={
|
|
||||||
<FeatureOverviewEnvironment
|
|
||||||
env={environmentWithManyStrategies}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>,
|
|
||||||
{
|
|
||||||
route: '/projects/default/features/featureWithManyStrategies/strategies/create',
|
|
||||||
permissions: [{ permission: CREATE_FEATURE_STRATEGY }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(async () => {
|
|
||||||
const button = await screen.findByText('Add strategy');
|
|
||||||
expect(button).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -22,8 +22,6 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|||||||
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
|
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
|
||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
|
|
||||||
interface IFeatureOverviewEnvironmentProps {
|
interface IFeatureOverviewEnvironmentProps {
|
||||||
env: IFeatureEnvironment;
|
env: IFeatureEnvironment;
|
||||||
@ -117,19 +115,6 @@ const StyledButtonContainer = styled('div')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const useStrategyLimit = (strategyCount: number) => {
|
|
||||||
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const featureEnvironmentStrategiesLimit =
|
|
||||||
uiConfig.resourceLimits?.featureEnvironmentStrategies || 100;
|
|
||||||
const limitReached =
|
|
||||||
resourceLimitsEnabled &&
|
|
||||||
strategyCount >= featureEnvironmentStrategiesLimit;
|
|
||||||
const limitMessage = `Limit of ${featureEnvironmentStrategiesLimit} strategies reached`;
|
|
||||||
|
|
||||||
return { limitReached, limitMessage };
|
|
||||||
};
|
|
||||||
|
|
||||||
const FeatureOverviewEnvironment = ({
|
const FeatureOverviewEnvironment = ({
|
||||||
env,
|
env,
|
||||||
}: IFeatureOverviewEnvironmentProps) => {
|
}: IFeatureOverviewEnvironmentProps) => {
|
||||||
@ -147,10 +132,6 @@ const FeatureOverviewEnvironment = ({
|
|||||||
(featureEnvironment) => featureEnvironment.name === env.name,
|
(featureEnvironment) => featureEnvironment.name === env.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { limitMessage, limitReached } = useStrategyLimit(
|
|
||||||
featureEnvironment?.strategies.length || 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!new Set(globalStore.hiddenEnvironments).has(env.name)}
|
condition={!new Set(globalStore.hiddenEnvironments).has(env.name)}
|
||||||
@ -198,11 +179,6 @@ const FeatureOverviewEnvironment = ({
|
|||||||
environmentId={env.name}
|
environmentId={env.name}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
size='small'
|
size='small'
|
||||||
disableReason={
|
|
||||||
limitReached
|
|
||||||
? limitMessage
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<FeatureStrategyIcons
|
<FeatureStrategyIcons
|
||||||
strategies={
|
strategies={
|
||||||
@ -245,11 +221,6 @@ const FeatureOverviewEnvironment = ({
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
environmentId={env.name}
|
environmentId={env.name}
|
||||||
disableReason={
|
|
||||||
limitReached
|
|
||||||
? limitMessage
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<EnvironmentFooter
|
<EnvironmentFooter
|
||||||
|
Loading…
Reference in New Issue
Block a user