mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: impact metrics modal - label filtering and UX (#10377)
Modal for editing a chart now follows design of other parts of the app more closely.
This commit is contained in:
		
							parent
							
								
									51f8244a5d
								
							
						
					
					
						commit
						f54305c8b7
					
				| @ -8,12 +8,16 @@ import { | ||||
|     TextField, | ||||
|     Box, | ||||
|     styled, | ||||
|     useTheme, | ||||
|     useMediaQuery, | ||||
|     Divider, | ||||
| } from '@mui/material'; | ||||
| import { ImpactMetricsControls } from './ImpactMetricsControls/ImpactMetricsControls.tsx'; | ||||
| import { ImpactMetricsChartPreview } from './ImpactMetricsChartPreview.tsx'; | ||||
| import { useChartFormState } from './hooks/useChartFormState.ts'; | ||||
| import type { ChartConfig } from './types.ts'; | ||||
| import { useChartFormState } from '../hooks/useChartFormState.ts'; | ||||
| import type { ChartConfig } from '../types.ts'; | ||||
| import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; | ||||
| import { LabelsFilter } from './LabelFilter/LabelsFilter.tsx'; | ||||
| import { ImpactMetricsChart } from '../ImpactMetricsChart.tsx'; | ||||
| 
 | ||||
| export const StyledConfigPanel = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
| @ -62,6 +66,8 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({ | ||||
|             open, | ||||
|             initialConfig, | ||||
|         }); | ||||
|     const theme = useTheme(); | ||||
|     const screenBreakpoint = useMediaQuery(theme.breakpoints.down('lg')); | ||||
| 
 | ||||
|     const handleSave = () => { | ||||
|         if (!isValid) return; | ||||
| @ -111,21 +117,33 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({ | ||||
|                             actions={actions} | ||||
|                             metricSeries={metricSeries} | ||||
|                             loading={loading} | ||||
|                             availableLabels={currentAvailableLabels} | ||||
|                         /> | ||||
|                     </StyledConfigPanel> | ||||
|                     <StyledPreviewPanel> | ||||
|                         <ImpactMetricsChartPreview | ||||
|                             selectedSeries={formData.selectedSeries} | ||||
|                             selectedRange={formData.selectedRange} | ||||
|                             selectedLabels={formData.selectedLabels} | ||||
|                             beginAtZero={formData.beginAtZero} | ||||
|                             aggregationMode={formData.aggregationMode} | ||||
|                         /> | ||||
|                         <Box sx={(theme) => ({ padding: theme.spacing(1) })}> | ||||
|                             <ImpactMetricsChart | ||||
|                                 key={screenBreakpoint ? 'small' : 'large'} | ||||
|                                 selectedSeries={formData.selectedSeries} | ||||
|                                 selectedRange={formData.selectedRange} | ||||
|                                 selectedLabels={formData.selectedLabels} | ||||
|                                 beginAtZero={formData.beginAtZero} | ||||
|                                 aggregationMode={formData.aggregationMode} | ||||
|                                 isPreview | ||||
|                             /> | ||||
|                         </Box> | ||||
|                     </StyledPreviewPanel> | ||||
|                 </Box> | ||||
| 
 | ||||
|                 {currentAvailableLabels ? ( | ||||
|                     <LabelsFilter | ||||
|                         selectedLabels={formData.selectedLabels} | ||||
|                         onChange={actions.setSelectedLabels} | ||||
|                         availableLabels={currentAvailableLabels} | ||||
|                     /> | ||||
|                 ) : null} | ||||
|             </DialogContent> | ||||
|             <DialogActions> | ||||
|             <Divider /> | ||||
|             <DialogActions sx={(theme) => ({ margin: theme.spacing(2, 3, 3) })}> | ||||
|                 <Button onClick={onClose}>Cancel</Button> | ||||
|                 <Button | ||||
|                     onClick={handleSave} | ||||
| @ -0,0 +1,81 @@ | ||||
| import type { FC } from 'react'; | ||||
| import { Box, Typography, FormControlLabel, Checkbox } from '@mui/material'; | ||||
| import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; | ||||
| import { SeriesSelector } from './SeriesSelector/SeriesSelector.tsx'; | ||||
| import { RangeSelector } from './RangeSelector/RangeSelector.tsx'; | ||||
| import { ModeSelector } from './ModeSelector/ModeSelector.tsx'; | ||||
| import type { ChartFormState } from '../../hooks/useChartFormState.ts'; | ||||
| import { getMetricType } from '../../utils.ts'; | ||||
| 
 | ||||
| export type ImpactMetricsControlsProps = { | ||||
|     formData: ChartFormState['formData']; | ||||
|     actions: Pick< | ||||
|         ChartFormState['actions'], | ||||
|         | 'handleSeriesChange' | ||||
|         | 'setSelectedRange' | ||||
|         | 'setBeginAtZero' | ||||
|         | 'setSelectedLabels' | ||||
|         | 'setAggregationMode' | ||||
|     >; | ||||
|     metricSeries: (ImpactMetricsSeries & { name: string })[]; | ||||
|     loading?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({ | ||||
|     formData, | ||||
|     actions, | ||||
|     metricSeries, | ||||
|     loading, | ||||
| }) => ( | ||||
|     <Box> | ||||
|         <Box | ||||
|             sx={(theme) => ({ | ||||
|                 display: 'flex', | ||||
|                 flexDirection: 'column', | ||||
|                 gap: theme.spacing(3), | ||||
|             })} | ||||
|         > | ||||
|             <Typography variant='body2' color='text.secondary'> | ||||
|                 Select a custom metric to see its value over time. This can help | ||||
|                 you understand the impact of your feature rollout on key | ||||
|                 outcomes, such as system performance, usage patterns or error | ||||
|                 rates. | ||||
|             </Typography> | ||||
| 
 | ||||
|             <SeriesSelector | ||||
|                 value={formData.selectedSeries} | ||||
|                 onChange={actions.handleSeriesChange} | ||||
|                 options={metricSeries} | ||||
|                 loading={loading} | ||||
|             /> | ||||
| 
 | ||||
|             {formData.selectedSeries ? ( | ||||
|                 <> | ||||
|                     <RangeSelector | ||||
|                         value={formData.selectedRange} | ||||
|                         onChange={actions.setSelectedRange} | ||||
|                     /> | ||||
|                     <ModeSelector | ||||
|                         value={formData.aggregationMode} | ||||
|                         onChange={actions.setAggregationMode} | ||||
|                         seriesType={getMetricType(formData.selectedSeries)!} | ||||
|                     /> | ||||
|                 </> | ||||
|             ) : null} | ||||
|         </Box> | ||||
|         {formData.selectedSeries ? ( | ||||
|             <FormControlLabel | ||||
|                 sx={(theme) => ({ margin: theme.spacing(1.5, 0) })} | ||||
|                 control={ | ||||
|                     <Checkbox | ||||
|                         checked={formData.beginAtZero} | ||||
|                         onChange={(e) => | ||||
|                             actions.setBeginAtZero(e.target.checked) | ||||
|                         } | ||||
|                     /> | ||||
|                 } | ||||
|                 label='Begin at zero' | ||||
|             /> | ||||
|         ) : null} | ||||
|     </Box> | ||||
| ); | ||||
| @ -1,6 +1,6 @@ | ||||
| import type { FC } from 'react'; | ||||
| import { FormControl, InputLabel, Select, MenuItem } from '@mui/material'; | ||||
| import type { AggregationMode } from '../../types.ts'; | ||||
| import type { AggregationMode } from '../../../types.ts'; | ||||
| 
 | ||||
| export type ModeSelectorProps = { | ||||
|     value: AggregationMode; | ||||
| @ -15,11 +15,7 @@ export const ModeSelector: FC<ModeSelectorProps> = ({ | ||||
| }) => { | ||||
|     if (seriesType === 'unknown') return null; | ||||
|     return ( | ||||
|         <FormControl | ||||
|             variant='outlined' | ||||
|             size='small' | ||||
|             sx={{ minWidth: 200, mt: 1 }} | ||||
|         > | ||||
|         <FormControl variant='outlined' size='small' sx={{ minWidth: 200 }}> | ||||
|             <InputLabel id='mode-select-label'>Mode</InputLabel> | ||||
|             <Select | ||||
|                 labelId='mode-select-label' | ||||
| @ -47,6 +47,7 @@ export const SeriesSelector: FC<SeriesSelectorProps> = ({ | ||||
|                 placeholder='Search for a metric…' | ||||
|                 variant='outlined' | ||||
|                 size='small' | ||||
|                 required | ||||
|             /> | ||||
|         )} | ||||
|         noOptionsText='No metrics available' | ||||
| @ -0,0 +1,104 @@ | ||||
| import { | ||||
|     Autocomplete, | ||||
|     Box, | ||||
|     Checkbox, | ||||
|     Chip, | ||||
|     FormControlLabel, | ||||
|     TextField, | ||||
| } from '@mui/material'; | ||||
| import type { FC } from 'react'; | ||||
| 
 | ||||
| type LabelFilterItemProps = { | ||||
|     labelKey: string; | ||||
|     options: string[]; | ||||
|     value: string[]; | ||||
|     onChange: (values: string[]) => void; | ||||
|     handleAllToggle: (labelKey: string, checked: boolean) => void; | ||||
| }; | ||||
| 
 | ||||
| export const LabelFilterItem: FC<LabelFilterItemProps> = ({ | ||||
|     labelKey, | ||||
|     options, | ||||
|     value, | ||||
|     onChange, | ||||
| }) => { | ||||
|     const isAllSelected = value.includes('*'); | ||||
|     const autocompleteId = `autocomplete-${labelKey}`; | ||||
|     const selectAllId = `select-all-${labelKey}`; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <FormControlLabel | ||||
|                 sx={(theme) => ({ | ||||
|                     marginLeft: theme.spacing(0), | ||||
|                 })} | ||||
|                 control={ | ||||
|                     <Checkbox | ||||
|                         id={selectAllId} | ||||
|                         size='small' | ||||
|                         checked={isAllSelected} | ||||
|                         onChange={(e) => | ||||
|                             onChange(e.target.checked ? ['*'] : []) | ||||
|                         } | ||||
|                         inputProps={{ | ||||
|                             'aria-describedby': autocompleteId, | ||||
|                             'aria-label': `Select all ${labelKey} options`, | ||||
|                         }} | ||||
|                     /> | ||||
|                 } | ||||
|                 label={ | ||||
|                     <Box | ||||
|                         component='span' | ||||
|                         sx={(theme) => ({ | ||||
|                             fontSize: theme.fontSizes.smallBody, | ||||
|                         })} | ||||
|                     > | ||||
|                         Select all | ||||
|                     </Box> | ||||
|                 } | ||||
|             /> | ||||
|             <Autocomplete | ||||
|                 multiple | ||||
|                 id={autocompleteId} | ||||
|                 options={options} | ||||
|                 value={isAllSelected ? options : value} | ||||
|                 onChange={(_, newValues) => { | ||||
|                     onChange(newValues); | ||||
|                 }} | ||||
|                 disabled={isAllSelected} | ||||
|                 renderTags={(value, getTagProps) => | ||||
|                     value.map((option, index) => { | ||||
|                         const { key, ...chipProps } = getTagProps({ | ||||
|                             index, | ||||
|                         }); | ||||
|                         return ( | ||||
|                             <Chip | ||||
|                                 {...chipProps} | ||||
|                                 key={key} | ||||
|                                 label={option} | ||||
|                                 size='small' | ||||
|                             /> | ||||
|                         ); | ||||
|                     }) | ||||
|                 } | ||||
|                 renderInput={(params) => ( | ||||
|                     <TextField | ||||
|                         {...params} | ||||
|                         label={labelKey} | ||||
|                         placeholder={ | ||||
|                             isAllSelected ? undefined : 'Select values…' | ||||
|                         } | ||||
|                         variant='outlined' | ||||
|                         size='small' | ||||
|                         inputProps={{ | ||||
|                             ...params.inputProps, | ||||
|                             'aria-describedby': isAllSelected | ||||
|                                 ? `${selectAllId}-description` | ||||
|                                 : undefined, | ||||
|                         }} | ||||
|                     /> | ||||
|                 )} | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,102 @@ | ||||
| import type { FC } from 'react'; | ||||
| import { Box, Typography, Chip } from '@mui/material'; | ||||
| import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; | ||||
| import { LabelFilterItem } from './LabelFilterItem/LabelFilterItem.tsx'; | ||||
| 
 | ||||
| export type LabelsFilterProps = { | ||||
|     selectedLabels: Record<string, string[]>; | ||||
|     onChange: (labels: Record<string, string[]>) => void; | ||||
|     availableLabels: ImpactMetricsLabels; | ||||
| }; | ||||
| 
 | ||||
| export const LabelsFilter: FC<LabelsFilterProps> = ({ | ||||
|     selectedLabels, | ||||
|     onChange, | ||||
|     availableLabels, | ||||
| }) => { | ||||
|     const handleLabelChange = (labelKey: string, values: string[]) => { | ||||
|         const newLabels = { ...selectedLabels }; | ||||
|         if (values.length === 0) { | ||||
|             delete newLabels[labelKey]; | ||||
|         } else { | ||||
|             newLabels[labelKey] = values; | ||||
|         } | ||||
|         onChange(newLabels); | ||||
|     }; | ||||
| 
 | ||||
|     const handleAllToggle = (labelKey: string, checked: boolean) => { | ||||
|         const newLabels = { ...selectedLabels }; | ||||
|         if (checked) { | ||||
|             newLabels[labelKey] = ['*']; | ||||
|         } else { | ||||
|             delete newLabels[labelKey]; | ||||
|         } | ||||
|         onChange(newLabels); | ||||
|     }; | ||||
| 
 | ||||
|     const clearAllLabels = () => { | ||||
|         onChange({}); | ||||
|     }; | ||||
| 
 | ||||
|     if (!availableLabels || Object.keys(availableLabels).length === 0) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}> | ||||
|             <Box | ||||
|                 sx={{ | ||||
|                     display: 'flex', | ||||
|                     alignItems: 'center', | ||||
|                     gap: 1, | ||||
|                     width: '100%', | ||||
|                 }} | ||||
|             > | ||||
|                 <Typography variant='subtitle2'>Filter by labels</Typography> | ||||
|                 {Object.keys(selectedLabels).length > 0 && ( | ||||
|                     <Chip | ||||
|                         label='Clear all' | ||||
|                         size='small' | ||||
|                         variant='outlined' | ||||
|                         onClick={clearAllLabels} | ||||
|                     /> | ||||
|                 )} | ||||
|             </Box> | ||||
| 
 | ||||
|             <Box | ||||
|                 sx={{ | ||||
|                     display: 'grid', | ||||
|                     gridTemplateColumns: | ||||
|                         'repeat(auto-fill, minmax(300px, 1fr))', | ||||
|                     gap: 2, | ||||
|                     flexGrow: 1, | ||||
|                 }} | ||||
|             > | ||||
|                 {Object.entries(availableLabels).map(([labelKey, values]) => { | ||||
|                     const currentSelection = selectedLabels[labelKey] || []; | ||||
| 
 | ||||
|                     return ( | ||||
|                         <Box | ||||
|                             key={labelKey} | ||||
|                             sx={{ | ||||
|                                 display: 'flex', | ||||
|                                 flexDirection: 'column', | ||||
|                                 flexGrow: 1, | ||||
|                             }} | ||||
|                         > | ||||
|                             <LabelFilterItem | ||||
|                                 labelKey={labelKey} | ||||
|                                 options={values} | ||||
|                                 value={currentSelection} | ||||
|                                 onChange={(newValues) => | ||||
|                                     handleLabelChange(labelKey, newValues) | ||||
|                                 } | ||||
|                                 handleAllToggle={handleAllToggle} | ||||
|                             /> | ||||
|                         </Box> | ||||
|                     ); | ||||
|                 })} | ||||
|             </Box> | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
| @ -4,7 +4,7 @@ import { Typography, Button, Paper, styled } from '@mui/material'; | ||||
| import Add from '@mui/icons-material/Add'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx'; | ||||
| import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; | ||||
| import { ChartConfigModal } from './ChartConfigModal.tsx'; | ||||
| import { ChartConfigModal } from './ChartConfigModal/ChartConfigModal.tsx'; | ||||
| import { ChartItem } from './ChartItem.tsx'; | ||||
| import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx'; | ||||
| import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts'; | ||||
|  | ||||
| @ -1,50 +0,0 @@ | ||||
| import type { FC } from 'react'; | ||||
| import { Box, Typography, useMediaQuery, useTheme } from '@mui/material'; | ||||
| import { ImpactMetricsChart } from './ImpactMetricsChart.tsx'; | ||||
| import type { AggregationMode } from './types.ts'; | ||||
| 
 | ||||
| type ImpactMetricsChartPreviewProps = { | ||||
|     selectedSeries: string; | ||||
|     selectedRange: 'hour' | 'day' | 'week' | 'month'; | ||||
|     selectedLabels: Record<string, string[]>; | ||||
|     beginAtZero: boolean; | ||||
|     aggregationMode?: AggregationMode; | ||||
| }; | ||||
| 
 | ||||
| export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({ | ||||
|     selectedSeries, | ||||
|     selectedRange, | ||||
|     selectedLabels, | ||||
|     beginAtZero, | ||||
|     aggregationMode, | ||||
| }) => { | ||||
|     const theme = useTheme(); | ||||
|     const screenBreakpoint = useMediaQuery(theme.breakpoints.down('lg')); | ||||
|     const key = screenBreakpoint ? 'small' : 'large'; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Typography variant='h6' color='text.secondary'> | ||||
|                 Preview | ||||
|             </Typography> | ||||
| 
 | ||||
|             {!selectedSeries ? ( | ||||
|                 <Typography variant='body2' color='text.secondary'> | ||||
|                     Select a metric series to view the preview | ||||
|                 </Typography> | ||||
|             ) : null} | ||||
| 
 | ||||
|             <Box sx={(theme) => ({ padding: theme.spacing(1) })}> | ||||
|                 <ImpactMetricsChart | ||||
|                     key={key} | ||||
|                     selectedSeries={selectedSeries} | ||||
|                     selectedRange={selectedRange} | ||||
|                     selectedLabels={selectedLabels} | ||||
|                     beginAtZero={beginAtZero} | ||||
|                     aggregationMode={aggregationMode} | ||||
|                     isPreview | ||||
|                 /> | ||||
|             </Box> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -1,88 +0,0 @@ | ||||
| import type { FC } from 'react'; | ||||
| import { Box, Typography, FormControlLabel, Checkbox } from '@mui/material'; | ||||
| import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata'; | ||||
| import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; | ||||
| import { SeriesSelector } from './components/SeriesSelector.tsx'; | ||||
| import { RangeSelector } from './components/RangeSelector.tsx'; | ||||
| import { LabelsFilter } from './components/LabelsFilter.tsx'; | ||||
| import { ModeSelector } from './components/ModeSelector.tsx'; | ||||
| import type { ChartFormState } from '../hooks/useChartFormState.ts'; | ||||
| import { getMetricType } from '../utils.ts'; | ||||
| 
 | ||||
| export type ImpactMetricsControlsProps = { | ||||
|     formData: ChartFormState['formData']; | ||||
|     actions: Pick< | ||||
|         ChartFormState['actions'], | ||||
|         | 'handleSeriesChange' | ||||
|         | 'setSelectedRange' | ||||
|         | 'setBeginAtZero' | ||||
|         | 'setSelectedLabels' | ||||
|         | 'setAggregationMode' | ||||
|     >; | ||||
|     metricSeries: (ImpactMetricsSeries & { name: string })[]; | ||||
|     loading?: boolean; | ||||
|     availableLabels?: ImpactMetricsLabels; | ||||
| }; | ||||
| 
 | ||||
| export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({ | ||||
|     formData, | ||||
|     actions, | ||||
|     metricSeries, | ||||
|     loading, | ||||
|     availableLabels, | ||||
| }) => ( | ||||
|     <Box | ||||
|         sx={(theme) => ({ | ||||
|             display: 'flex', | ||||
|             flexDirection: 'column', | ||||
|             gap: theme.spacing(3), | ||||
|         })} | ||||
|     > | ||||
|         <Typography variant='body2' color='text.secondary'> | ||||
|             Select a custom metric to see its value over time. This can help you | ||||
|             understand the impact of your feature rollout on key outcomes, such | ||||
|             as system performance, usage patterns or error rates. | ||||
|         </Typography> | ||||
| 
 | ||||
|         <SeriesSelector | ||||
|             value={formData.selectedSeries} | ||||
|             onChange={actions.handleSeriesChange} | ||||
|             options={metricSeries} | ||||
|             loading={loading} | ||||
|         /> | ||||
| 
 | ||||
|         <RangeSelector | ||||
|             value={formData.selectedRange} | ||||
|             onChange={actions.setSelectedRange} | ||||
|         /> | ||||
| 
 | ||||
|         <Box sx={{ display: 'flex', flexDirection: 'column' }}> | ||||
|             <FormControlLabel | ||||
|                 control={ | ||||
|                     <Checkbox | ||||
|                         checked={formData.beginAtZero} | ||||
|                         onChange={(e) => | ||||
|                             actions.setBeginAtZero(e.target.checked) | ||||
|                         } | ||||
|                     /> | ||||
|                 } | ||||
|                 label='Begin at zero' | ||||
|             /> | ||||
| 
 | ||||
|             {formData.selectedSeries ? ( | ||||
|                 <ModeSelector | ||||
|                     value={formData.aggregationMode} | ||||
|                     onChange={actions.setAggregationMode} | ||||
|                     seriesType={getMetricType(formData.selectedSeries)!} | ||||
|                 /> | ||||
|             ) : null} | ||||
|         </Box> | ||||
|         {availableLabels && ( | ||||
|             <LabelsFilter | ||||
|                 selectedLabels={formData.selectedLabels} | ||||
|                 onChange={actions.setSelectedLabels} | ||||
|                 availableLabels={availableLabels} | ||||
|             /> | ||||
|         )} | ||||
|     </Box> | ||||
| ); | ||||
| @ -1,136 +0,0 @@ | ||||
| import type { FC } from 'react'; | ||||
| import { | ||||
|     Box, | ||||
|     Autocomplete, | ||||
|     TextField, | ||||
|     Typography, | ||||
|     Chip, | ||||
|     Checkbox, | ||||
|     FormControlLabel, | ||||
| } from '@mui/material'; | ||||
| import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData'; | ||||
| 
 | ||||
| export type LabelsFilterProps = { | ||||
|     selectedLabels: Record<string, string[]>; | ||||
|     onChange: (labels: Record<string, string[]>) => void; | ||||
|     availableLabels: ImpactMetricsLabels; | ||||
| }; | ||||
| 
 | ||||
| export const LabelsFilter: FC<LabelsFilterProps> = ({ | ||||
|     selectedLabels, | ||||
|     onChange, | ||||
|     availableLabels, | ||||
| }) => { | ||||
|     const handleLabelChange = (labelKey: string, values: string[]) => { | ||||
|         const newLabels = { ...selectedLabels }; | ||||
|         if (values.length === 0) { | ||||
|             delete newLabels[labelKey]; | ||||
|         } else { | ||||
|             newLabels[labelKey] = values; | ||||
|         } | ||||
|         onChange(newLabels); | ||||
|     }; | ||||
| 
 | ||||
|     const handleAllToggle = (labelKey: string, checked: boolean) => { | ||||
|         const newLabels = { ...selectedLabels }; | ||||
|         if (checked) { | ||||
|             newLabels[labelKey] = ['*']; | ||||
|         } else { | ||||
|             delete newLabels[labelKey]; | ||||
|         } | ||||
|         onChange(newLabels); | ||||
|     }; | ||||
| 
 | ||||
|     const clearAllLabels = () => { | ||||
|         onChange({}); | ||||
|     }; | ||||
| 
 | ||||
|     if (!availableLabels || Object.keys(availableLabels).length === 0) { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||
|             <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | ||||
|                 <Typography variant='subtitle2'>Filter by labels</Typography> | ||||
|                 {Object.keys(selectedLabels).length > 0 && ( | ||||
|                     <Chip | ||||
|                         label='Clear all' | ||||
|                         size='small' | ||||
|                         variant='outlined' | ||||
|                         onClick={clearAllLabels} | ||||
|                     /> | ||||
|                 )} | ||||
|             </Box> | ||||
| 
 | ||||
|             {Object.entries(availableLabels).map(([labelKey, values]) => { | ||||
|                 const currentSelection = selectedLabels[labelKey] || []; | ||||
|                 const isAllSelected = currentSelection.includes('*'); | ||||
| 
 | ||||
|                 return ( | ||||
|                     <Box | ||||
|                         key={labelKey} | ||||
|                         sx={(theme) => ({ | ||||
|                             display: 'flex', | ||||
|                             alignItems: 'center', | ||||
|                             gap: theme.spacing(3), | ||||
|                         })} | ||||
|                     > | ||||
|                         <Autocomplete | ||||
|                             multiple | ||||
|                             options={values} | ||||
|                             value={isAllSelected ? [] : currentSelection} | ||||
|                             onChange={(_, newValues) => { | ||||
|                                 handleLabelChange(labelKey, newValues); | ||||
|                             }} | ||||
|                             disabled={isAllSelected} | ||||
|                             renderTags={(value, getTagProps) => | ||||
|                                 value.map((option, index) => { | ||||
|                                     const { key, ...chipProps } = getTagProps({ | ||||
|                                         index, | ||||
|                                     }); | ||||
|                                     return ( | ||||
|                                         <Chip | ||||
|                                             {...chipProps} | ||||
|                                             key={key} | ||||
|                                             label={option} | ||||
|                                             size='small' | ||||
|                                         /> | ||||
|                                     ); | ||||
|                                 }) | ||||
|                             } | ||||
|                             renderInput={(params) => ( | ||||
|                                 <TextField | ||||
|                                     {...params} | ||||
|                                     label={labelKey} | ||||
|                                     placeholder={ | ||||
|                                         isAllSelected | ||||
|                                             ? 'All values selected' | ||||
|                                             : 'Select values…' | ||||
|                                     } | ||||
|                                     variant='outlined' | ||||
|                                     size='small' | ||||
|                                 /> | ||||
|                             )} | ||||
|                             sx={{ minWidth: 300, flexGrow: 1 }} | ||||
|                         /> | ||||
|                         <FormControlLabel | ||||
|                             control={ | ||||
|                                 <Checkbox | ||||
|                                     checked={isAllSelected} | ||||
|                                     onChange={(e) => | ||||
|                                         handleAllToggle( | ||||
|                                             labelKey, | ||||
|                                             e.target.checked, | ||||
|                                         ) | ||||
|                                     } | ||||
|                                 /> | ||||
|                             } | ||||
|                             label='All' | ||||
|                         /> | ||||
|                     </Box> | ||||
|                 ); | ||||
|             })} | ||||
|         </Box> | ||||
|     ); | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user