mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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 { 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