mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02: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 { useOptionalPathParam } from 'hooks/useOptionalPathParam';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
export const CreateSegmentButton: FC<{
|
export const CreateSegmentButton: FC = () => {
|
||||||
disabled: boolean;
|
|
||||||
tooltip?: string;
|
|
||||||
}> = ({ disabled, tooltip }) => {
|
|
||||||
const projectId = useOptionalPathParam('projectId');
|
const projectId = useOptionalPathParam('projectId');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -26,10 +23,6 @@ export const CreateSegmentButton: FC<{
|
|||||||
}}
|
}}
|
||||||
permission={[CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT]}
|
permission={[CREATE_SEGMENT, UPDATE_PROJECT_SEGMENT]}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
disabled={disabled}
|
|
||||||
tooltipProps={{
|
|
||||||
title: tooltip,
|
|
||||||
}}
|
|
||||||
data-testid={NAVIGATE_TO_CREATE_SEGMENT}
|
data-testid={NAVIGATE_TO_CREATE_SEGMENT}
|
||||||
>
|
>
|
||||||
New segment
|
New segment
|
||||||
|
@ -16,7 +16,8 @@ export const SegmentDocsValuesInfo = () => {
|
|||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
at most {segmentValuesLimit} across all of its contraints
|
at most {segmentValuesLimit} values across all of its
|
||||||
|
constraints
|
||||||
</a>
|
</a>
|
||||||
. <SegmentLimitsLink />
|
. <SegmentLimitsLink />
|
||||||
</Alert>
|
</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 Input from 'component/common/Input/Input';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@ -20,6 +20,10 @@ import {
|
|||||||
import { SegmentProjectAlert } from './SegmentProjectAlert';
|
import { SegmentProjectAlert } from './SegmentProjectAlert';
|
||||||
import { sortStrategiesByFeature } from './SegmentDelete/SegmentDeleteUsedSegment/sort-strategies';
|
import { sortStrategiesByFeature } from './SegmentDelete/SegmentDeleteUsedSegment/sort-strategies';
|
||||||
import type { IFeatureStrategy } from 'interfaces/strategy';
|
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 {
|
interface ISegmentFormPartOneProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -62,6 +66,32 @@ const StyledCancelButton = styled(Button)(({ theme }) => ({
|
|||||||
marginLeft: theme.spacing(3),
|
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> = ({
|
export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@ -76,6 +106,13 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
|
|||||||
const projectId = useOptionalPathParam('projectId');
|
const projectId = useOptionalPathParam('projectId');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { projects, loading: loadingProjects } = useProjects();
|
const { projects, loading: loadingProjects } = useProjects();
|
||||||
|
const {
|
||||||
|
limitReached,
|
||||||
|
limit,
|
||||||
|
currentCount,
|
||||||
|
loading: loadingSegmentLimit,
|
||||||
|
resourceLimitsEnabled,
|
||||||
|
} = useSegmentLimit();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
strategies,
|
strategies,
|
||||||
@ -106,7 +143,7 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
|
|||||||
setSelectedProject(projects.find(({ id }) => id === project) ?? null);
|
setSelectedProject(projects.find(({ id }) => id === project) ?? null);
|
||||||
}, [project, projects]);
|
}, [project, projects]);
|
||||||
|
|
||||||
const loading = loadingProjects && loadingStrategies;
|
const loading = loadingProjects || loadingStrategies || loadingSegmentLimit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledForm>
|
<StyledForm>
|
||||||
@ -165,13 +202,32 @@ export const SegmentFormStepOne: React.FC<ISegmentFormPartOneProps> = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|
||||||
|
<LimitContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={resourceLimitsEnabled}
|
||||||
|
show={
|
||||||
|
<Limit
|
||||||
|
name='segments'
|
||||||
|
limit={limit}
|
||||||
|
currentValue={currentCount}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</LimitContainer>
|
||||||
|
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='contained'
|
variant='contained'
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={() => setCurrentStep(2)}
|
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}
|
data-testid={SEGMENT_NEXT_BTN_ID}
|
||||||
>
|
>
|
||||||
Next
|
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 { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import {
|
import {
|
||||||
SortableTableHeader,
|
SortableTableHeader,
|
||||||
TableCell,
|
|
||||||
TablePlaceholder,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TablePlaceholder,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from 'component/common/Table';
|
} 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 { CreateSegmentButton } from 'component/segments/CreateSegmentButton/CreateSegmentButton';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { useMediaQuery } from '@mui/material';
|
import { useMediaQuery } from '@mui/material';
|
||||||
@ -29,21 +29,6 @@ import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColum
|
|||||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||||
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
|
import { useOptionalPathParam } from 'hooks/useOptionalPathParam';
|
||||||
import { UsedInCell } from 'component/context/ContextList/UsedInCell';
|
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 = () => {
|
export const SegmentTable = () => {
|
||||||
const projectId = useOptionalPathParam('projectId');
|
const projectId = useOptionalPathParam('projectId');
|
||||||
@ -53,17 +38,6 @@ export const SegmentTable = () => {
|
|||||||
sortBy: [{ id: 'createdAt' }],
|
sortBy: [{ id: 'createdAt' }],
|
||||||
hiddenColumns: ['description'],
|
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(() => {
|
const data = useMemo(() => {
|
||||||
if (!segments) {
|
if (!segments) {
|
||||||
@ -138,10 +112,7 @@ export const SegmentTable = () => {
|
|||||||
onChange={setGlobalFilter}
|
onChange={setGlobalFilter}
|
||||||
/>
|
/>
|
||||||
<PageHeader.Divider />
|
<PageHeader.Divider />
|
||||||
<CreateSegmentButton
|
<CreateSegmentButton />
|
||||||
disabled={createSegmentDisabled}
|
|
||||||
tooltip={limitMessage}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user