mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: Project select bug with duplicate values (#6405)
Project select fix for Executive Dashboard and Playground Extract the select to it's own component and move to `common` Re-use for both Dashboard and Playground Adds the id in parenthesis when there are duplicate names <img width="1406" alt="Screenshot 2024-03-01 at 12 04 22" src="https://github.com/Unleash/unleash/assets/104830839/379ea11f-d627-493e-8088-a739d58fba61"> <img width="1434" alt="Screenshot 2024-03-01 at 12 36 46" src="https://github.com/Unleash/unleash/assets/104830839/9c5cf863-002c-4630-ac3a-4a869303a308"> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									a4a604aebb
								
							
						
					
					
						commit
						7b67f218eb
					
				
							
								
								
									
										112
									
								
								frontend/src/component/common/ProjectSelect/ProjectSelect.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								frontend/src/component/common/ProjectSelect/ProjectSelect.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,112 @@ | ||||
| import { ComponentProps, Dispatch, SetStateAction, VFC } from 'react'; | ||||
| import { Autocomplete, SxProps, TextField } from '@mui/material'; | ||||
| import { renderOption } from 'component/playground/Playground/PlaygroundForm/renderOption'; | ||||
| import useProjects from 'hooks/api/getters/useProjects/useProjects'; | ||||
| 
 | ||||
| interface IOption { | ||||
|     label: string; | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| export const allOption = { label: 'ALL', id: '*' }; | ||||
| 
 | ||||
| interface IProjectSelectProps { | ||||
|     selectedProjects: string[]; | ||||
|     onChange: Dispatch<SetStateAction<string[]>>; | ||||
|     dataTestId?: string; | ||||
|     sx?: SxProps; | ||||
|     disabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| function findAllIndexes(arr: string[], name: string): number[] { | ||||
|     const indexes: number[] = []; | ||||
|     arr.forEach((currentValue, index) => { | ||||
|         if (currentValue === name) { | ||||
|             indexes.push(index); | ||||
|         } | ||||
|     }); | ||||
|     return indexes; | ||||
| } | ||||
| 
 | ||||
| export const ProjectSelect: VFC<IProjectSelectProps> = ({ | ||||
|     selectedProjects, | ||||
|     onChange, | ||||
|     dataTestId, | ||||
|     sx, | ||||
|     disabled, | ||||
| }) => { | ||||
|     const { projects: availableProjects } = useProjects(); | ||||
| 
 | ||||
|     const projectNames = availableProjects.map(({ name }) => name); | ||||
| 
 | ||||
|     const projectsOptions = [ | ||||
|         allOption, | ||||
|         ...availableProjects.map(({ name, id }) => { | ||||
|             const indexes = findAllIndexes(projectNames, name); | ||||
|             const isDuplicate = indexes.length > 1; | ||||
| 
 | ||||
|             return { | ||||
|                 label: isDuplicate ? `${name} - (${id})` : name, | ||||
|                 id, | ||||
|             }; | ||||
|         }), | ||||
|     ]; | ||||
| 
 | ||||
|     const isAllProjects = | ||||
|         selectedProjects && | ||||
|         (selectedProjects.length === 0 || | ||||
|             (selectedProjects.length === 1 && selectedProjects[0] === '*')); | ||||
| 
 | ||||
|     const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = ( | ||||
|         event, | ||||
|         value, | ||||
|         reason, | ||||
|     ) => { | ||||
|         const newProjects = value as IOption | IOption[]; | ||||
|         if (reason === 'clear' || newProjects === null) { | ||||
|             return onChange([allOption.id]); | ||||
|         } | ||||
|         if (Array.isArray(newProjects)) { | ||||
|             if (newProjects.length === 0) { | ||||
|                 return onChange([allOption.id]); | ||||
|             } | ||||
|             if ( | ||||
|                 newProjects.find(({ id }) => id === allOption.id) !== undefined | ||||
|             ) { | ||||
|                 return onChange([allOption.id]); | ||||
|             } | ||||
|             return onChange(newProjects.map(({ id }) => id)); | ||||
|         } | ||||
|         if (newProjects.id === allOption.id) { | ||||
|             return onChange([allOption.id]); | ||||
|         } | ||||
| 
 | ||||
|         return onChange([newProjects.id]); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <Autocomplete | ||||
|             disablePortal | ||||
|             id='projects' | ||||
|             limitTags={3} | ||||
|             multiple={!isAllProjects} | ||||
|             options={projectsOptions} | ||||
|             sx={sx} | ||||
|             renderInput={(params) => <TextField {...params} label='Projects' />} | ||||
|             renderOption={renderOption} | ||||
|             getOptionLabel={({ label }) => label} | ||||
|             disableCloseOnSelect | ||||
|             size='small' | ||||
|             disabled={disabled} | ||||
|             value={ | ||||
|                 isAllProjects | ||||
|                     ? allOption | ||||
|                     : projectsOptions.filter(({ id }) => | ||||
|                           selectedProjects.includes(id), | ||||
|                       ) | ||||
|             } | ||||
|             onChange={onProjectsChange} | ||||
|             data-testid={dataTestId ? dataTestId : 'PROJECT_SELECT'} | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @ -1,5 +1,11 @@ | ||||
| import { useMemo, useState, VFC } from 'react'; | ||||
| import { Box, styled, useMediaQuery, useTheme } from '@mui/material'; | ||||
| import { | ||||
|     Box, | ||||
|     styled, | ||||
|     Typography, | ||||
|     useMediaQuery, | ||||
|     useTheme, | ||||
| } from '@mui/material'; | ||||
| import { UsersChart } from './UsersChart/UsersChart'; | ||||
| import { FlagsChart } from './FlagsChart/FlagsChart'; | ||||
| import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary'; | ||||
| @ -10,7 +16,10 @@ import { FlagsProjectChart } from './FlagsProjectChart/FlagsProjectChart'; | ||||
| import { ProjectHealthChart } from './ProjectHealthChart/ProjectHealthChart'; | ||||
| import { TimeToProductionChart } from './TimeToProductionChart/TimeToProductionChart'; | ||||
| import { TimeToProduction } from './TimeToProduction/TimeToProduction'; | ||||
| import { ProjectSelect, allOption } from './ProjectSelect/ProjectSelect'; | ||||
| import { | ||||
|     ProjectSelect, | ||||
|     allOption, | ||||
| } from '../common/ProjectSelect/ProjectSelect'; | ||||
| import { MetricsSummaryChart } from './MetricsSummaryChart/MetricsSummaryChart'; | ||||
| import { | ||||
|     ExecutiveSummarySchemaMetricsSummaryTrendsItem, | ||||
| @ -26,6 +35,18 @@ const StyledGrid = styled(Box)(({ theme }) => ({ | ||||
|     gap: theme.spacing(2), | ||||
| })); | ||||
| 
 | ||||
| const StyledBox = styled(Box)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(4), | ||||
|     marginTop: theme.spacing(4), | ||||
|     [theme.breakpoints.down('lg')]: { | ||||
|         width: '100%', | ||||
|         marginLeft: 0, | ||||
|     }, | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
| })); | ||||
| 
 | ||||
| const useDashboardGrid = () => { | ||||
|     const theme = useTheme(); | ||||
|     const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg')); | ||||
| @ -153,7 +174,17 @@ export const ExecutiveDashboard: VFC = () => { | ||||
|                     /> | ||||
|                 </Widget> | ||||
|             </StyledGrid> | ||||
|             <ProjectSelect selectedProjects={projects} onChange={setProjects} /> | ||||
|             <StyledBox> | ||||
|                 <Typography variant='h2' component='span'> | ||||
|                     Insights per project | ||||
|                 </Typography> | ||||
|                 <ProjectSelect | ||||
|                     selectedProjects={projects} | ||||
|                     onChange={setProjects} | ||||
|                     dataTestId={'DASHBOARD_PROJECT_SELECT'} | ||||
|                     sx={{ flex: 1, maxWidth: '360px' }} | ||||
|                 /> | ||||
|             </StyledBox> | ||||
|             <StyledGrid> | ||||
|                 <Widget | ||||
|                     title='Number of flags per project' | ||||
|  | ||||
| @ -1,113 +0,0 @@ | ||||
| import { ComponentProps, Dispatch, SetStateAction, VFC } from 'react'; | ||||
| import { | ||||
|     Autocomplete, | ||||
|     Box, | ||||
|     styled, | ||||
|     TextField, | ||||
|     Typography, | ||||
| } from '@mui/material'; | ||||
| import { renderOption } from '../../playground/Playground/PlaygroundForm/renderOption'; | ||||
| import useProjects from '../../../hooks/api/getters/useProjects/useProjects'; | ||||
| 
 | ||||
| const StyledBox = styled(Box)(({ theme }) => ({ | ||||
|     marginBottom: theme.spacing(4), | ||||
|     marginTop: theme.spacing(4), | ||||
|     [theme.breakpoints.down('lg')]: { | ||||
|         width: '100%', | ||||
|         marginLeft: 0, | ||||
|     }, | ||||
|     display: 'flex', | ||||
|     justifyContent: 'space-between', | ||||
|     alignItems: 'center', | ||||
| })); | ||||
| 
 | ||||
| interface IOption { | ||||
|     label: string; | ||||
|     id: string; | ||||
| } | ||||
| 
 | ||||
| export const allOption = { label: 'ALL', id: '*' }; | ||||
| 
 | ||||
| interface IProjectSelectProps { | ||||
|     selectedProjects: string[]; | ||||
|     onChange: Dispatch<SetStateAction<string[]>>; | ||||
| } | ||||
| 
 | ||||
| export const ProjectSelect: VFC<IProjectSelectProps> = ({ | ||||
|     selectedProjects, | ||||
|     onChange, | ||||
| }) => { | ||||
|     const { projects: availableProjects } = useProjects(); | ||||
| 
 | ||||
|     const projectsOptions = [ | ||||
|         allOption, | ||||
|         ...availableProjects.map(({ name: label, id }) => ({ | ||||
|             label, | ||||
|             id, | ||||
|         })), | ||||
|     ]; | ||||
| 
 | ||||
|     const isAllProjects = | ||||
|         selectedProjects && | ||||
|         (selectedProjects.length === 0 || | ||||
|             (selectedProjects.length === 1 && selectedProjects[0] === '*')); | ||||
| 
 | ||||
|     const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = ( | ||||
|         event, | ||||
|         value, | ||||
|         reason, | ||||
|     ) => { | ||||
|         const newProjects = value as IOption | IOption[]; | ||||
|         if (reason === 'clear' || newProjects === null) { | ||||
|             return onChange([allOption.id]); | ||||
|         } | ||||
|         if (Array.isArray(newProjects)) { | ||||
|             if (newProjects.length === 0) { | ||||
|                 return onChange([allOption.id]); | ||||
|             } | ||||
|             if ( | ||||
|                 newProjects.find(({ id }) => id === allOption.id) !== undefined | ||||
|             ) { | ||||
|                 return onChange([allOption.id]); | ||||
|             } | ||||
|             return onChange(newProjects.map(({ id }) => id)); | ||||
|         } | ||||
|         if (newProjects.id === allOption.id) { | ||||
|             return onChange([allOption.id]); | ||||
|         } | ||||
| 
 | ||||
|         return onChange([newProjects.id]); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledBox> | ||||
|             <Typography variant='h2' component='span'> | ||||
|                 Insights per project | ||||
|             </Typography> | ||||
|             <Autocomplete | ||||
|                 disablePortal | ||||
|                 id='projects' | ||||
|                 limitTags={3} | ||||
|                 multiple={!isAllProjects} | ||||
|                 options={projectsOptions} | ||||
|                 sx={{ flex: 1, maxWidth: 360 }} | ||||
|                 renderInput={(params) => ( | ||||
|                     <TextField {...params} label='Projects' /> | ||||
|                 )} | ||||
|                 renderOption={renderOption} | ||||
|                 getOptionLabel={({ label }) => label} | ||||
|                 disableCloseOnSelect | ||||
|                 size='small' | ||||
|                 value={ | ||||
|                     isAllProjects | ||||
|                         ? allOption | ||||
|                         : projectsOptions.filter(({ id }) => | ||||
|                               selectedProjects.includes(id), | ||||
|                           ) | ||||
|                 } | ||||
|                 onChange={onProjectsChange} | ||||
|                 data-testid={'DASHBOARD_PROJECT_SELECT'} | ||||
|             /> | ||||
|         </StyledBox> | ||||
|     ); | ||||
| }; | ||||
| @ -1,5 +1,5 @@ | ||||
| import { useMemo } from 'react'; | ||||
| import { allOption } from '../ProjectSelect/ProjectSelect'; | ||||
| import { allOption } from 'component/common/ProjectSelect/ProjectSelect'; | ||||
| 
 | ||||
| export const useFilteredTrends = < | ||||
|     T extends { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { ComponentProps, useState, VFC } from 'react'; | ||||
| import { ComponentProps, Dispatch, SetStateAction, useState, VFC } from 'react'; | ||||
| import { | ||||
|     Autocomplete, | ||||
|     Box, | ||||
| @ -22,14 +22,15 @@ import { | ||||
|     validateTokenFormat, | ||||
| } from '../../playground.utils'; | ||||
| import { Clear } from '@mui/icons-material'; | ||||
| import { ProjectSelect } from '../../../../common/ProjectSelect/ProjectSelect'; | ||||
| 
 | ||||
| interface IPlaygroundConnectionFieldsetProps { | ||||
|     environments: string[]; | ||||
|     projects: string[]; | ||||
|     token?: string; | ||||
|     setProjects: (projects: string[]) => void; | ||||
|     setEnvironments: (environments: string[]) => void; | ||||
|     setToken?: (token: string) => void; | ||||
|     setProjects: Dispatch<SetStateAction<string[]>>; | ||||
|     setEnvironments: Dispatch<SetStateAction<string[]>>; | ||||
|     setToken?: Dispatch<SetStateAction<string | undefined>>; | ||||
|     availableEnvironments: string[]; | ||||
| } | ||||
| 
 | ||||
| @ -76,33 +77,6 @@ export const PlaygroundConnectionFieldset: VFC< | ||||
|         })), | ||||
|     ]; | ||||
| 
 | ||||
|     const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = ( | ||||
|         event, | ||||
|         value, | ||||
|         reason, | ||||
|     ) => { | ||||
|         const newProjects = value as IOption | IOption[]; | ||||
|         if (reason === 'clear' || newProjects === null) { | ||||
|             return setProjects([allOption.id]); | ||||
|         } | ||||
|         if (Array.isArray(newProjects)) { | ||||
|             if (newProjects.length === 0) { | ||||
|                 return setProjects([allOption.id]); | ||||
|             } | ||||
|             if ( | ||||
|                 newProjects.find(({ id }) => id === allOption.id) !== undefined | ||||
|             ) { | ||||
|                 return setProjects([allOption.id]); | ||||
|             } | ||||
|             return setProjects(newProjects.map(({ id }) => id)); | ||||
|         } | ||||
|         if (newProjects.id === allOption.id) { | ||||
|             return setProjects([allOption.id]); | ||||
|         } | ||||
| 
 | ||||
|         return setProjects([newProjects.id]); | ||||
|     }; | ||||
| 
 | ||||
|     const onEnvironmentsChange: ComponentProps< | ||||
|         typeof Autocomplete | ||||
|     >['onChange'] = (event, value, reason) => { | ||||
| @ -120,11 +94,6 @@ export const PlaygroundConnectionFieldset: VFC< | ||||
|         return setEnvironments([newEnvironments.id]); | ||||
|     }; | ||||
| 
 | ||||
|     const isAllProjects = | ||||
|         projects && | ||||
|         (projects.length === 0 || | ||||
|             (projects.length === 1 && projects[0] === '*')); | ||||
| 
 | ||||
|     const envValue = environmentOptions.filter(({ id }) => | ||||
|         environments.includes(id), | ||||
|     ); | ||||
| @ -235,68 +204,53 @@ export const PlaygroundConnectionFieldset: VFC< | ||||
|                 </Typography> | ||||
|             </Box> | ||||
|             <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}> | ||||
|                 <Tooltip | ||||
|                     arrow | ||||
|                     title={ | ||||
|                         token | ||||
|                             ? 'Environment is automatically selected because you are using a token' | ||||
|                             : 'Select environments to use in the playground' | ||||
|                     } | ||||
|                 > | ||||
|                     <Autocomplete | ||||
|                         disablePortal | ||||
|                         limitTags={3} | ||||
|                         id='environment' | ||||
|                         multiple={true} | ||||
|                         options={environmentOptions} | ||||
|                         sx={{ flex: 1 }} | ||||
|                         renderInput={(params) => ( | ||||
|                             <TextField {...params} label='Environments' /> | ||||
|                         )} | ||||
|                         renderOption={renderOption} | ||||
|                         getOptionLabel={({ label }) => label} | ||||
|                         disableCloseOnSelect={false} | ||||
|                         size='small' | ||||
|                         value={envValue} | ||||
|                         onChange={onEnvironmentsChange} | ||||
|                         disabled={Boolean(token)} | ||||
|                         data-testid={'PLAYGROUND_ENVIRONMENT_SELECT'} | ||||
|                     /> | ||||
|                 </Tooltip> | ||||
|                 <Tooltip | ||||
|                     arrow | ||||
|                     title={ | ||||
|                         token | ||||
|                             ? 'Project is automatically selected because you are using a token' | ||||
|                             : 'Select projects to use in the playground' | ||||
|                     } | ||||
|                 > | ||||
|                     <Autocomplete | ||||
|                         disablePortal | ||||
|                         id='projects' | ||||
|                         limitTags={3} | ||||
|                         multiple={!isAllProjects} | ||||
|                         options={projectsOptions} | ||||
|                         sx={{ flex: 1 }} | ||||
|                         renderInput={(params) => ( | ||||
|                             <TextField {...params} label='Projects' /> | ||||
|                         )} | ||||
|                         renderOption={renderOption} | ||||
|                         getOptionLabel={({ label }) => label} | ||||
|                         disableCloseOnSelect | ||||
|                         size='small' | ||||
|                         value={ | ||||
|                             isAllProjects | ||||
|                                 ? allOption | ||||
|                                 : projectsOptions.filter(({ id }) => | ||||
|                                       projects.includes(id), | ||||
|                                   ) | ||||
|                 <Box flex={1}> | ||||
|                     <Tooltip | ||||
|                         arrow | ||||
|                         title={ | ||||
|                             token | ||||
|                                 ? 'Environment is automatically selected because you are using a token' | ||||
|                                 : 'Select environments to use in the playground' | ||||
|                         } | ||||
|                         onChange={onProjectsChange} | ||||
|                         disabled={Boolean(token)} | ||||
|                         data-testid={'PLAYGROUND_PROJECT_SELECT'} | ||||
|                     /> | ||||
|                 </Tooltip> | ||||
|                     > | ||||
|                         <Autocomplete | ||||
|                             disablePortal | ||||
|                             limitTags={3} | ||||
|                             id='environment' | ||||
|                             multiple={true} | ||||
|                             options={environmentOptions} | ||||
|                             sx={{ flex: 1 }} | ||||
|                             renderInput={(params) => ( | ||||
|                                 <TextField {...params} label='Environments' /> | ||||
|                             )} | ||||
|                             renderOption={renderOption} | ||||
|                             getOptionLabel={({ label }) => label} | ||||
|                             disableCloseOnSelect={false} | ||||
|                             size='small' | ||||
|                             value={envValue} | ||||
|                             onChange={onEnvironmentsChange} | ||||
|                             disabled={Boolean(token)} | ||||
|                             data-testid={'PLAYGROUND_ENVIRONMENT_SELECT'} | ||||
|                         /> | ||||
|                     </Tooltip> | ||||
|                 </Box> | ||||
|                 <Box flex={1}> | ||||
|                     <Tooltip | ||||
|                         arrow | ||||
|                         title={ | ||||
|                             token | ||||
|                                 ? 'Project is automatically selected because you are using a token' | ||||
|                                 : 'Select projects to use in the playground' | ||||
|                         } | ||||
|                     > | ||||
|                         <ProjectSelect | ||||
|                             selectedProjects={projects} | ||||
|                             onChange={setProjects} | ||||
|                             dataTestId={'PLAYGROUND_PROJECT_SELECT'} | ||||
|                             disabled={Boolean(token)} | ||||
|                         /> | ||||
|                     </Tooltip> | ||||
|                 </Box> | ||||
|             </Box> | ||||
|             <Input | ||||
|                 sx={{ mt: 2, width: '50%', pr: 1 }} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user