1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02: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:
andreas-unleash 2024-03-01 14:28:57 +02:00 committed by GitHub
parent a4a604aebb
commit 7b67f218eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 198 additions and 214 deletions

View 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'}
/>
);
};

View File

@ -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'

View File

@ -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>
);
};

View File

@ -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 {

View File

@ -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 }}