1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-28 00:06:53 +01:00

feat: limit segments component (#7553)

This commit is contained in:
Mateusz Kwasniewski 2024-07-08 09:03:08 +02:00 committed by GitHub
parent d8bb9f18de
commit e7d07486a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 141 additions and 93 deletions

View File

@ -8,10 +8,7 @@ import { useNavigate } from 'react-router-dom';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import type { FC } from 'react';
export const CreateSegmentButton: FC<{
disabled: boolean;
tooltip?: string;
}> = ({ disabled, tooltip }) => {
export const CreateSegmentButton: FC = () => {
const projectId = useOptionalPathParam('projectId');
const navigate = useNavigate();
@ -26,10 +23,6 @@ export const CreateSegmentButton: FC<{
}}
permission={[CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT]}
projectId={projectId}
disabled={disabled}
tooltipProps={{
title: tooltip,
}}
data-testid={NAVIGATE_TO_CREATE_SEGMENT}
>
New segment

View File

@ -16,7 +16,8 @@ export const SegmentDocsValuesInfo = () => {
target='_blank'
rel='noreferrer'
>
at most {segmentValuesLimit} across all of its contraints
at most {segmentValuesLimit} values across all of its
constraints
</a>
. <SegmentLimitsLink />
</Alert>

View File

@ -0,0 +1,75 @@
import { render } from 'utils/testRenderer';
import { screen, waitFor } from '@testing-library/react';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { SegmentFormStepOne } from './SegmentFormStepOne';
const server = testServerSetup();
const setupRoutes = ({
limit,
segments,
}: { limit: number; segments: number }) => {
testServerRoute(server, 'api/admin/segments', {
segments: [...Array(segments).keys()].map((i) => ({
name: `segment${i}`,
})),
});
testServerRoute(server, '/api/admin/ui-config', {
flags: {
SE: true,
resourceLimits: true,
},
resourceLimits: {
segments: limit,
},
});
};
const irrelevant = () => {};
test('Do not allow next step when limit reached', async () => {
setupRoutes({ limit: 1, segments: 1 });
render(
<SegmentFormStepOne
name='irrelevant'
description='irrelevant'
clearErrors={irrelevant}
setCurrentStep={irrelevant}
setDescription={irrelevant}
setName={irrelevant}
setProject={irrelevant}
errors={{}}
project='irrelevent'
/>,
);
await screen.findByText('You have reached the limit for segments');
const nextStep = await screen.findByText('Next');
expect(nextStep).toBeDisabled();
});
test('Allows next step when approaching limit', async () => {
setupRoutes({ limit: 10, segments: 9 });
render(
<SegmentFormStepOne
name='name'
description='irrelevant'
clearErrors={irrelevant}
setCurrentStep={irrelevant}
setDescription={irrelevant}
setName={irrelevant}
setProject={irrelevant}
errors={{}}
project='irrelevent'
/>,
);
await screen.findByText('You are nearing the limit for segments');
await waitFor(async () => {
const nextStep = await screen.findByText('Next');
expect(nextStep).toBeEnabled();
});
});

View File

@ -1,4 +1,4 @@
import { Autocomplete, Button, styled, TextField } from '@mui/material';
import { Autocomplete, Box, Button, styled, TextField } from '@mui/material';
import Input from 'component/common/Input/Input';
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
@ -20,6 +20,10 @@ import {
import { SegmentProjectAlert } from './SegmentProjectAlert';
import { sortStrategiesByFeature } from './SegmentDelete/SegmentDeleteUsedSegment/sort-strategies';
import type { IFeatureStrategy } from 'interfaces/strategy';
import { useUiFlag } from 'hooks/useUiFlag';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import { Limit } from '../common/Limit/Limit';
interface ISegmentFormPartOneProps {
name: string;
@ -62,6 +66,32 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(3),
}));
const LimitContainer = styled(Box)(({ theme }) => ({
flex: 1,
display: 'flex',
alignItems: 'flex-end',
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3),
}));
const useSegmentLimit = () => {
const { segments, loading: loadingSegments } = useSegments();
const { uiConfig, loading: loadingConfig } = useUiConfig();
const segmentsLimit = uiConfig.resourceLimits.segments;
const segmentsCount = segments?.length || 0;
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const limitReached =
resourceLimitsEnabled && segmentsCount >= segmentsLimit;
return {
limit: segmentsLimit,
limitReached,
currentCount: segmentsCount,
loading: loadingSegments || loadingConfig,
resourceLimitsEnabled,
};
};
export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
name,
description,
@ -76,6 +106,13 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
const projectId = useOptionalPathParam('projectId');
const navigate = useNavigate();
const { projects, loading: loadingProjects } = useProjects();
const {
limitReached,
limit,
currentCount,
loading: loadingSegmentLimit,
resourceLimitsEnabled,
} = useSegmentLimit();
const {
strategies,
@ -106,7 +143,7 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
setSelectedProject(projects.find(({ id }) => id === project) ?? null);
}, [project, projects]);
const loading = loadingProjects && loadingStrategies;
const loading = loadingProjects || loadingStrategies || loadingSegmentLimit;
return (
<StyledForm>
@ -165,13 +202,32 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
}
/>
</StyledContainer>
<LimitContainer>
<ConditionallyRender
condition={resourceLimitsEnabled}
show={
<Limit
name='segments'
limit={limit}
currentValue={currentCount}
/>
}
/>
</LimitContainer>
<StyledButtonContainer>
<Button
type='button'
variant='contained'
color='primary'
onClick={() => setCurrentStep(2)}
disabled={name.length === 0 || Boolean(errors.name)}
disabled={
loading ||
limitReached ||
name.length === 0 ||
Boolean(errors.name)
}
data-testid={SEGMENT_NEXT_BTN_ID}
>
Next

View File

@ -1,48 +0,0 @@
import { render } from 'utils/testRenderer';
import { screen } from '@testing-library/react';
import { SegmentTable } from './SegmentTable';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { CREATE_SEGMENT } from '../../providers/AccessProvider/permissions';
const server = testServerSetup();
const setupRoutes = () => {
testServerRoute(server, 'api/admin/segments', {
segments: [
{
id: 2,
name: 'test2',
description: '',
usedInProjects: 3,
usedInFeatures: 2,
constraints: [],
createdBy: 'admin',
createdAt: '2023-05-24T06:23:07.797Z',
},
],
});
testServerRoute(server, '/api/admin/ui-config', {
flags: {
SE: true,
resourceLimits: true,
},
resourceLimits: {
segments: 2,
},
});
};
test('should show the count of projects and features used in', async () => {
setupRoutes();
render(<SegmentTable />, { permissions: [{ permission: CREATE_SEGMENT }] });
const loadingSegment = await screen.findByText('New segment');
expect(loadingSegment).toBeDisabled();
await screen.findByText('2 feature flags');
await screen.findByText('3 projects');
const segment = await screen.findByText('New segment');
expect(segment).not.toBeDisabled();
});

View File

@ -2,13 +2,13 @@ import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import {
SortableTableHeader,
TableCell,
TablePlaceholder,
Table,
TableBody,
TableCell,
TablePlaceholder,
TableRow,
} from 'component/common/Table';
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
import { useGlobalFilter, useSortBy, useTable } from 'react-table';
import { CreateSegmentButton } from 'component/segments/CreateSegmentButton/CreateSegmentButton';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { useMediaQuery } from '@mui/material';
@ -29,21 +29,6 @@ import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColum
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
import { UsedInCell } from 'component/context/ContextList/UsedInCell';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useUiFlag } from 'hooks/useUiFlag';
const useSegmentLimit = (segmentsLimit: number, segmentsCount: number) => {
const resourceLimitsEnabled = useUiFlag('resourceLimits');
const limitReached =
resourceLimitsEnabled && segmentsCount >= segmentsLimit;
return {
limitReached,
limitMessage: limitReached
? `Limit of ${segmentsCount} segments reached`
: undefined,
};
};
export const SegmentTable = () => {
const projectId = useOptionalPathParam('projectId');
@ -53,17 +38,6 @@ export const SegmentTable = () => {
sortBy: [{ id: 'createdAt' }],
hiddenColumns: ['description'],
});
const { uiConfig, loading: loadingConfig } = useUiConfig();
const segmentLimit = uiConfig.resourceLimits.segments;
const segmentCount = segments?.length || 0;
const { limitReached, limitMessage } = useSegmentLimit(
segmentLimit,
segmentCount,
);
const createSegmentDisabled =
loadingSegments || loadingConfig || limitReached;
const data = useMemo(() => {
if (!segments) {
@ -138,10 +112,7 @@ export const SegmentTable = () => {
onChange={setGlobalFilter}
/>
<PageHeader.Divider />
<CreateSegmentButton
disabled={createSegmentDisabled}
tooltip={limitMessage}
/>
<CreateSegmentButton />
</>
}
/>