1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

feat: add limit warning for feature flags (#7556)

This PR adds the Limit component to the feature flag creation form.

At the limit: 

![image](https://github.com/Unleash/unleash/assets/17786332/86f17565-5c75-4265-8e3b-5200222345ec)

Approaching the limit:

![image](https://github.com/Unleash/unleash/assets/17786332/af041d78-fcd3-4aa6-b415-9738cbfbae1b)

Below the limit threshold (no change):

![image](https://github.com/Unleash/unleash/assets/17786332/79ddc3ee-6e52-44d3-8d0b-0ebae90707a7)
This commit is contained in:
Thomas Heartman 2024-07-08 15:06:21 +02:00 committed by GitHub
parent 5ed4ccc981
commit 8f8ff13cc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 80 additions and 40 deletions

View File

@ -28,50 +28,66 @@ const setupApi = ({
}); });
}; };
test("should allow you to create feature flags when you're below the global limit", async () => { describe('button states', () => {
setupApi({ flagLimit: 3, flagCount: 2 }); test("should allow you to create feature flags when you're below the global limit", async () => {
setupApi({ flagLimit: 3, flagCount: 2 });
render( render(
<Routes> <Routes>
<Route <Route
path='/projects/:projectId/create-toggle' path='/projects/:projectId/create-toggle'
element={<CreateFeature />} element={<CreateFeature />}
/> />
</Routes>, </Routes>,
{ {
route: '/projects/default/create-toggle', route: '/projects/default/create-toggle',
permissions: [{ permission: CREATE_FEATURE }], permissions: [{ permission: CREATE_FEATURE }],
}, },
); );
await waitFor(async () => {
const button = await screen.findByRole('button', { const button = await screen.findByRole('button', {
name: /create feature flag/i, name: /create feature flag/i,
}); });
expect(button).not.toBeDisabled(); await waitFor(() => {
}); expect(button).not.toBeDisabled();
});
test("should not allow you to create API tokens when you're at the global limit", async () => {
setupApi({ flagLimit: 3, flagCount: 3 });
render(
<Routes>
<Route
path='/projects/:projectId/create-toggle'
element={<CreateFeature />}
/>
</Routes>,
{
route: '/projects/default/create-toggle',
permissions: [{ permission: CREATE_FEATURE }],
},
);
await waitFor(async () => {
const button = await screen.findByRole('button', {
name: /create feature flag/i,
}); });
expect(button).toBeDisabled(); });
});
describe('limit component', () => {
test('should show limit reached info', async () => {
setupApi({ flagLimit: 1, flagCount: 1 });
render(
<Routes>
<Route
path='/projects/:projectId/create-toggle'
element={<CreateFeature />}
/>
</Routes>,
{
route: '/projects/default/create-toggle',
permissions: [{ permission: CREATE_FEATURE }],
},
);
await screen.findByText('You have reached the limit for feature flags');
});
test('should show approaching limit info', async () => {
setupApi({ flagLimit: 10, flagCount: 9 });
render(
<Routes>
<Route
path='/projects/:projectId/create-toggle'
element={<CreateFeature />}
/>
</Routes>,
{
route: '/projects/default/create-toggle',
permissions: [{ permission: CREATE_FEATURE }],
},
);
await screen.findByText('You are nearing the limit for feature flags');
}); });
}); });

View File

@ -19,6 +19,7 @@ import useProjectOverview, {
} from 'hooks/api/getters/useProjectOverview/useProjectOverview'; } from 'hooks/api/getters/useProjectOverview/useProjectOverview';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { useGlobalFeatureSearch } from '../FeatureToggleList/useGlobalFeatureSearch'; import { useGlobalFeatureSearch } from '../FeatureToggleList/useGlobalFeatureSearch';
import { Limit } from 'component/common/Limit/Limit';
const StyledAlert = styled(Alert)(({ theme }) => ({ const StyledAlert = styled(Alert)(({ theme }) => ({
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
@ -106,6 +107,8 @@ const CreateFeature = () => {
const { total: totalFlags, loading: loadingTotalFlagCount } = const { total: totalFlags, loading: loadingTotalFlagCount } =
useGlobalFeatureSearch(); useGlobalFeatureSearch();
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const { globalFlagLimitReached, projectFlagLimitReached, limitMessage } = const { globalFlagLimitReached, projectFlagLimitReached, limitMessage } =
useFlagLimits({ useFlagLimits({
global: { global: {
@ -196,6 +199,18 @@ const CreateFeature = () => {
mode='Create' mode='Create'
clearErrors={clearErrors} clearErrors={clearErrors}
featureNaming={projectInfo.featureNaming} featureNaming={projectInfo.featureNaming}
Limit={
<ConditionallyRender
condition={resourceLimitsEnabled}
show={
<Limit
name='feature flags'
limit={uiConfig.resourceLimits.featureFlags}
currentValue={totalFlags ?? 0}
/>
}
/>
}
> >
<CreateButton <CreateButton
name='feature flag' name='feature flag'

View File

@ -7,6 +7,7 @@ import {
type Theme, type Theme,
Typography, Typography,
Link, Link,
Box,
} from '@mui/material'; } from '@mui/material';
import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect'; import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from 'utils/testIds'; import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from 'utils/testIds';
@ -43,6 +44,7 @@ interface IFeatureToggleForm {
mode: 'Create' | 'Edit'; mode: 'Create' | 'Edit';
clearErrors: () => void; clearErrors: () => void;
children?: React.ReactNode; children?: React.ReactNode;
Limit?: React.ReactNode;
} }
const StyledForm = styled('form')({ const StyledForm = styled('form')({
@ -79,7 +81,6 @@ const StyledTypeDescription = styled('p')(({ theme }) => ({
})); }));
const StyledButtonContainer = styled('div')({ const StyledButtonContainer = styled('div')({
marginTop: 'auto',
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
}); });
@ -98,6 +99,12 @@ const styledTypography = (theme: Theme) => ({
margin: theme.spacing(1, 0), margin: theme.spacing(1, 0),
}); });
const LimitContainer = styled(Box)(({ theme }) => ({
'&:has(*)': {
marginBottom: theme.spacing(2),
},
}));
const FeatureForm: React.FC<IFeatureToggleForm> = ({ const FeatureForm: React.FC<IFeatureToggleForm> = ({
children, children,
type, type,
@ -117,6 +124,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
errors, errors,
mode, mode,
clearErrors, clearErrors,
Limit,
}) => { }) => {
const { featureTypes } = useFeatureTypes(); const { featureTypes } = useFeatureTypes();
const navigate = useNavigate(); const navigate = useNavigate();
@ -256,6 +264,7 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
/> />
</StyledRow> </StyledRow>
</StyledFormControl> </StyledFormControl>
<LimitContainer>{Limit}</LimitContainer>
<StyledButtonContainer> <StyledButtonContainer>
{children} {children}
<StyledCancelButton onClick={handleCancel}> <StyledCancelButton onClick={handleCancel}>