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:
parent
d8bb9f18de
commit
e7d07486a1
@ -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
|
||||
|
@ -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>
|
||||
|
75
frontend/src/component/segments/SegmentFormStepOne.test.tsx
Normal file
75
frontend/src/component/segments/SegmentFormStepOne.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
@ -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
|
||||
|
@ -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();
|
||||
});
|
@ -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 />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user