1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-23 13:46:45 +02:00

refactor: apply previous pr comments

This commit is contained in:
Tymoteusz Czech 2025-07-01 16:52:29 +02:00
parent 75d3a5d8d6
commit 41e0c3a653
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
11 changed files with 297 additions and 598 deletions

View File

@ -1,5 +1,5 @@
import type { FC } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { useState, useEffect } from 'react';
import {
Dialog,
DialogTitle,
@ -8,21 +8,11 @@ import {
Button,
TextField,
Box,
Typography,
Alert,
styled,
} from '@mui/material';
import { ImpactMetricsControls } from './ImpactMetricsControls/ImpactMetricsControls.tsx';
import {
LineChart,
NotEnoughData,
} from '../insights/components/LineChart/LineChart.tsx';
import { StyledChartContainer } from 'component/insights/InsightsCharts.styles';
import { ImpactMetricsChartPreview } from './ImpactMetricsChartPreview.tsx';
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts';
import { fromUnixTime } from 'date-fns';
import { useChartData } from './hooks/useChartData.ts';
import type { ChartConfig } from './types.ts';
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
@ -82,25 +72,6 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
Record<string, string[]>
>(initialConfig?.selectedLabels || {});
// Data for preview
const {
data: { start, end, series: timeSeriesData },
loading: dataLoading,
error: dataError,
} = useImpactMetricsData(
selectedSeries
? {
series: selectedSeries,
range: selectedRange,
labels:
Object.keys(selectedLabels).length > 0
? selectedLabels
: undefined,
}
: undefined,
);
// Fetch available labels for the currently selected series
const {
data: { labels: currentAvailableLabels },
} = useImpactMetricsData(
@ -112,40 +83,6 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
: undefined,
);
const placeholderData = usePlaceholderData({
fill: true,
type: 'constant',
});
const data = useChartData(timeSeriesData);
const hasError = !!dataError;
const isLoading = dataLoading;
const shouldShowPlaceholder = !selectedSeries || isLoading || hasError;
const notEnoughData = useMemo(
() =>
!isLoading &&
(!timeSeriesData ||
timeSeriesData.length === 0 ||
!data.datasets.some((d) => d.data.length > 1)),
[data, isLoading, timeSeriesData],
);
const minTime = start
? fromUnixTime(Number.parseInt(start, 10))
: undefined;
const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined;
const placeholder = selectedSeries ? (
<NotEnoughData description='Send impact metrics using Unleash SDK and select data series to view the chart.' />
) : (
<NotEnoughData
title='Select a metric series to view the chart.'
description=''
/>
);
const cover = notEnoughData ? placeholder : isLoading;
useEffect(() => {
if (open && initialConfig) {
setTitle(initialConfig.title || '');
@ -232,100 +169,13 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
availableLabels={currentAvailableLabels}
/>
</StyledConfigPanel>
{/* Preview Panel */}
<StyledPreviewPanel>
<Typography variant='h6' color='text.secondary'>
Preview
</Typography>
{!selectedSeries && !isLoading ? (
<Typography variant='body2' color='text.secondary'>
Select a metric series to view the preview
</Typography>
) : null}
<StyledChartContainer>
{hasError ? (
<Alert severity='error'>
Failed to load impact metrics. Please check
if Prometheus is configured and the feature
flag is enabled.
</Alert>
) : null}
<LineChart
data={
notEnoughData || isLoading
? placeholderData
: data
}
overrideOptions={
shouldShowPlaceholder
? {}
: {
scales: {
x: {
type: 'time',
min: minTime?.getTime(),
max: maxTime?.getTime(),
time: {
unit: getTimeUnit(
selectedRange,
),
displayFormats: {
[getTimeUnit(
selectedRange,
)]:
getDisplayFormat(
selectedRange,
),
},
tooltipFormat: 'PPpp',
},
},
y: {
beginAtZero,
title: {
display: false,
},
ticks: {
precision: 0,
callback: (
value: unknown,
): string | number =>
typeof value ===
'number'
? formatLargeNumbers(
value,
)
: (value as number),
},
},
},
plugins: {
legend: {
display:
timeSeriesData &&
timeSeriesData.length >
1,
position:
'bottom' as const,
labels: {
usePointStyle: true,
boxWidth: 8,
padding: 12,
},
},
},
animations: {
x: { duration: 0 },
y: { duration: 0 },
},
}
}
cover={cover}
/>
</StyledChartContainer>
<ImpactMetricsChartPreview
selectedSeries={selectedSeries}
selectedRange={selectedRange}
selectedLabels={selectedLabels}
beginAtZero={beginAtZero}
/>
</StyledPreviewPanel>
</Box>
</DialogContent>

View File

@ -1,25 +1,9 @@
import type { FC } from 'react';
import { useMemo } from 'react';
import {
Box,
Typography,
IconButton,
Alert,
styled,
Paper,
} from '@mui/material';
import { Box, Typography, IconButton, styled, Paper } from '@mui/material';
import Edit from '@mui/icons-material/Edit';
import Delete from '@mui/icons-material/Delete';
import DragHandle from '@mui/icons-material/DragHandle';
import {
LineChart,
NotEnoughData,
} from '../insights/components/LineChart/LineChart.tsx';
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts';
import { fromUnixTime } from 'date-fns';
import { useChartData } from './hooks/useChartData.ts';
import { ImpactMetricsChart } from './ImpactMetricsChart.tsx';
import type { ChartConfig } from './types.ts';
export interface ChartItemProps {
@ -32,46 +16,30 @@ const getConfigDescription = (config: ChartConfig): string => {
const parts: string[] = [];
if (config.selectedSeries) {
parts.push(`Series: ${config.selectedSeries}`);
parts.push(`${config.selectedSeries}`);
}
parts.push(`Time range: last ${config.selectedRange}`);
if (config.beginAtZero) {
parts.push('Begin at zero');
}
parts.push(`last ${config.selectedRange}`);
const labelCount = Object.keys(config.selectedLabels).length;
if (labelCount > 0) {
parts.push(`${labelCount} label filter${labelCount > 1 ? 's' : ''}`);
parts.push(`${labelCount} filter${labelCount > 1 ? 's' : ''}`);
}
return parts.join(' • ');
};
const StyledHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
padding: theme.spacing(2, 3),
borderBottom: `1px solid ${theme.palette.divider}`,
}));
const StyledDragHandle = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
cursor: 'move',
padding: theme.spacing(0.5),
borderRadius: theme.shape.borderRadius,
color: theme.palette.text.secondary,
'&:hover': {
backgroundColor: theme.palette.action.hover,
color: theme.palette.text.primary,
const StyledChartWrapper = styled(Box)({
height: '100%',
width: '100%',
'& > div': {
height: '100% !important',
width: '100% !important',
},
}));
});
const StyledWidget = styled(Paper)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusLarge}px`,
borderRadius: `${theme.shape.borderRadiusMedium}px`,
boxShadow: 'none',
display: 'flex',
flexDirection: 'column',
@ -97,182 +65,81 @@ const StyledImpactChartContainer = styled(Box)(({ theme }) => ({
padding: theme.spacing(3),
}));
const StyledChartWrapper = styled(Box)({
height: '100%',
width: '100%',
'& > div': {
height: '100% !important',
width: '100% !important',
const StyledHeader = styled(Box)(({ theme }) => ({
display: 'flex',
gap: theme.spacing(2),
alignItems: 'center',
padding: theme.spacing(1.5, 2),
borderBottom: `1px solid ${theme.palette.divider}`,
}));
const StyledDragHandle = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
cursor: 'move',
padding: theme.spacing(0.5),
borderRadius: theme.shape.borderRadius,
color: theme.palette.text.secondary,
'&:hover': {
backgroundColor: theme.palette.action.hover,
color: theme.palette.text.primary,
},
});
}));
export const ChartItem: FC<ChartItemProps> = ({ config, onEdit, onDelete }) => {
const {
data: { start, end, series: timeSeriesData },
loading: dataLoading,
error: dataError,
} = useImpactMetricsData({
series: config.selectedSeries,
range: config.selectedRange,
labels:
Object.keys(config.selectedLabels).length > 0
? config.selectedLabels
: undefined,
});
const StyledChartTitle = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
flexGrow: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
}));
const placeholderData = usePlaceholderData({
fill: true,
type: 'constant',
});
const StyledChartActions = styled(Box)(({ theme }) => ({
marginLeft: 'auto',
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
}));
const data = useChartData(timeSeriesData);
export const ChartItem: FC<ChartItemProps> = ({ config, onEdit, onDelete }) => (
<StyledWidget>
<StyledHeader>
<StyledDragHandle className='grid-item-drag-handle'>
<DragHandle fontSize='small' />
</StyledDragHandle>
<StyledChartTitle>
{config.title && (
<Typography variant='h6'>{config.title}</Typography>
)}
<Typography variant='body2' color='text.secondary'>
{getConfigDescription(config)}
</Typography>
</StyledChartTitle>
<StyledChartActions>
<IconButton onClick={() => onEdit(config)}>
<Edit />
</IconButton>
<IconButton onClick={() => onDelete(config.id)}>
<Delete />
</IconButton>
</StyledChartActions>
</StyledHeader>
const hasError = !!dataError;
const isLoading = dataLoading;
const shouldShowPlaceholder = isLoading || hasError;
const notEnoughData = useMemo(
() =>
!isLoading &&
(!timeSeriesData ||
timeSeriesData.length === 0 ||
!data.datasets.some((d) => d.data.length > 1)),
[data, isLoading, timeSeriesData],
);
const minTime = start
? fromUnixTime(Number.parseInt(start, 10))
: undefined;
const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined;
const placeholder = (
<NotEnoughData description='Send impact metrics using Unleash SDK for this series to view the chart.' />
);
const cover = notEnoughData ? placeholder : isLoading;
return (
<StyledWidget>
<StyledHeader>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<StyledDragHandle className='grid-item-drag-handle'>
<DragHandle fontSize='small' />
</StyledDragHandle>
<Box>
{config.title && (
<Typography variant='h6' gutterBottom>
{config.title}
</Typography>
)}
<Typography
variant='body2'
color='text.secondary'
sx={{ mb: 1 }}
>
{getConfigDescription(config)}
</Typography>
</Box>
</Box>
<Box>
<IconButton
onClick={() => onEdit(config)}
size='small'
sx={{ mr: 1 }}
>
<Edit fontSize='small' />
</IconButton>
<IconButton
onClick={() => onDelete(config.id)}
size='small'
>
<Delete fontSize='small' />
</IconButton>
</Box>
</StyledHeader>
<StyledChartContent>
<StyledImpactChartContainer>
{hasError ? (
<Alert severity='error'>
Failed to load impact metrics. Please check if
Prometheus is configured and the feature flag is
enabled.
</Alert>
) : null}
<StyledChartWrapper>
<LineChart
data={
notEnoughData || isLoading
? placeholderData
: data
}
aspectRatio={1.5}
overrideOptions={
shouldShowPlaceholder
? { maintainAspectRatio: false }
: {
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
min: minTime?.getTime(),
max: maxTime?.getTime(),
time: {
unit: getTimeUnit(
config.selectedRange,
),
displayFormats: {
[getTimeUnit(
config.selectedRange,
)]: getDisplayFormat(
config.selectedRange,
),
},
tooltipFormat: 'PPpp',
},
},
y: {
beginAtZero:
config.beginAtZero,
title: {
display: false,
},
ticks: {
precision: 0,
callback: (
value: unknown,
): string | number =>
typeof value ===
'number'
? formatLargeNumbers(
value,
)
: (value as number),
},
},
},
plugins: {
legend: {
display:
timeSeriesData &&
timeSeriesData.length > 1,
position: 'bottom' as const,
labels: {
usePointStyle: true,
boxWidth: 8,
padding: 12,
},
},
},
animations: {
x: { duration: 0 },
y: { duration: 0 },
},
}
}
cover={cover}
/>
</StyledChartWrapper>
</StyledImpactChartContainer>
</StyledChartContent>
</StyledWidget>
);
};
<StyledChartContent>
<StyledImpactChartContainer>
<StyledChartWrapper>
<ImpactMetricsChart
selectedSeries={config.selectedSeries}
selectedRange={config.selectedRange}
selectedLabels={config.selectedLabels}
beginAtZero={config.beginAtZero}
aspectRatio={1.5}
overrideOptions={{ maintainAspectRatio: false }}
emptyDataDescription='Send impact metrics using Unleash SDK for this series to view the chart.'
/>
</StyledChartWrapper>
</StyledImpactChartContainer>
</StyledChartContent>
</StyledWidget>
);

View File

@ -15,7 +15,7 @@ const StyledGridContainer = styled('div')(({ theme }) => ({
'& .react-grid-item': {
transition: 'all 200ms ease',
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
borderRadius: `${theme.shape.borderRadiusMedium}px`,
backgroundColor: theme.palette.background.paper,
overflow: 'hidden',
'&.react-grid-item--placeholder': {
@ -44,16 +44,15 @@ const StyledGridContainer = styled('div')(({ theme }) => ({
},
'& .react-resizable-handle': {
position: 'absolute',
width: '20px',
height: '20px',
width: theme.spacing(3),
height: theme.spacing(3),
bottom: '0px',
right: '0px',
cursor: 'se-resize',
backgroundImage: `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMTdMMTcgM00zIDEzTDEzIDNNNyAxN0wxNyA3IiBzdHJva2U9IiM5OTkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+Cjwvc3ZnPgo=')`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
'&:hover': {
backgroundImage: `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgMTdMMTcgM00zIDEzTDEzIDNNNyAxN0wxNyA3IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+Cjwvc3ZnPgo=')`,
'&::after': {
display: 'none',
},
},
}));
@ -93,7 +92,6 @@ export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
isResizable = true,
compactType = 'vertical',
}) => {
// Memoize layouts to prevent unnecessary re-renders
const layouts = useMemo(() => {
const baseLayout = items.map((item, index) => ({
i: item.id,
@ -133,7 +131,6 @@ export const GridLayoutWrapper: FC<GridLayoutWrapperProps> = ({
};
}, [items, cols]);
// Memoize children to improve performance
const children = useMemo(
() => items.map((item) => <div key={item.id}>{item.component}</div>),
[items],

View File

@ -7,7 +7,7 @@ import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMeta
import { ChartConfigModal } from './ChartConfigModal.tsx';
import { ChartItem } from './ChartItem.tsx';
import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx';
import { useUrlState } from './hooks/useUrlState.ts';
import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts';
import type { ChartConfig, LayoutItem } from './types.ts';
const StyledEmptyState = styled(Paper)(({ theme }) => ({
@ -23,7 +23,7 @@ export const ImpactMetrics: FC = () => {
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
const { charts, layout, addChart, updateChart, deleteChart, updateLayout } =
useUrlState();
useImpactMetricsState();
const {
metadata,

View File

@ -0,0 +1,150 @@
import type { FC, ReactNode } from 'react';
import { useMemo } from 'react';
import { Alert } from '@mui/material';
import {
LineChart,
NotEnoughData,
} from '../insights/components/LineChart/LineChart.tsx';
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
import { getDisplayFormat, getTimeUnit, formatLargeNumbers } from './utils.ts';
import { fromUnixTime } from 'date-fns';
import { useChartData } from './hooks/useChartData.ts';
type ImpactMetricsChartProps = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
selectedLabels: Record<string, string[]>;
beginAtZero: boolean;
aspectRatio?: number;
overrideOptions?: Record<string, unknown>;
errorTitle?: string;
emptyDataDescription?: string;
noSeriesPlaceholder?: ReactNode;
};
export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
selectedSeries,
selectedRange,
selectedLabels,
beginAtZero,
aspectRatio,
overrideOptions = {},
errorTitle = 'Failed to load impact metrics. Please check if Prometheus is configured and the feature flag is enabled.',
emptyDataDescription = 'Send impact metrics using Unleash SDK and select data series to view the chart.',
noSeriesPlaceholder,
}) => {
const {
data: { start, end, series: timeSeriesData },
loading: dataLoading,
error: dataError,
} = useImpactMetricsData(
selectedSeries
? {
series: selectedSeries,
range: selectedRange,
labels:
Object.keys(selectedLabels).length > 0
? selectedLabels
: undefined,
}
: undefined,
);
const placeholderData = usePlaceholderData({
fill: true,
type: 'constant',
});
const data = useChartData(timeSeriesData);
const hasError = !!dataError;
const isLoading = dataLoading;
const shouldShowPlaceholder = !selectedSeries || isLoading || hasError;
const notEnoughData = useMemo(
() =>
!isLoading &&
(!timeSeriesData ||
timeSeriesData.length === 0 ||
!data.datasets.some((d) => d.data.length > 1)),
[data, isLoading, timeSeriesData],
);
const minTime = start
? fromUnixTime(Number.parseInt(start, 10))
: undefined;
const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined;
const placeholder = selectedSeries ? (
<NotEnoughData description={emptyDataDescription} />
) : noSeriesPlaceholder ? (
noSeriesPlaceholder
) : (
<NotEnoughData
title='Select a metric series to view the chart.'
description=''
/>
);
const cover = notEnoughData ? placeholder : isLoading;
const chartOptions = shouldShowPlaceholder
? overrideOptions
: {
...overrideOptions,
scales: {
x: {
type: 'time',
min: minTime?.getTime(),
max: maxTime?.getTime(),
time: {
unit: getTimeUnit(selectedRange),
displayFormats: {
[getTimeUnit(selectedRange)]:
getDisplayFormat(selectedRange),
},
tooltipFormat: 'PPpp',
},
},
y: {
beginAtZero,
title: {
display: false,
},
ticks: {
precision: 0,
callback: (value: unknown): string | number =>
typeof value === 'number'
? formatLargeNumbers(value)
: (value as number),
},
},
},
plugins: {
legend: {
display: timeSeriesData && timeSeriesData.length > 1,
position: 'bottom' as const,
labels: {
usePointStyle: true,
boxWidth: 8,
padding: 12,
},
},
},
animations: {
x: { duration: 0 },
y: { duration: 0 },
},
};
return (
<>
{hasError ? <Alert severity='error'>{errorTitle}</Alert> : null}
<LineChart
data={notEnoughData || isLoading ? placeholderData : data}
aspectRatio={aspectRatio}
overrideOptions={chartOptions}
cover={cover}
/>
</>
);
};

View File

@ -0,0 +1,39 @@
import type { FC } from 'react';
import { Typography } from '@mui/material';
import { StyledChartContainer } from 'component/insights/InsightsCharts.styles';
import { ImpactMetricsChart } from './ImpactMetricsChart.tsx';
type ImpactMetricsChartPreviewProps = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
selectedLabels: Record<string, string[]>;
beginAtZero: boolean;
};
export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
selectedSeries,
selectedRange,
selectedLabels,
beginAtZero,
}) => (
<>
<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}
<StyledChartContainer>
<ImpactMetricsChart
selectedSeries={selectedSeries}
selectedRange={selectedRange}
selectedLabels={selectedLabels}
beginAtZero={beginAtZero}
/>
</StyledChartContainer>
</>
);

View File

@ -1,203 +0,0 @@
import type { FC } from 'react';
import {
FormControl,
InputLabel,
Select,
MenuItem,
FormControlLabel,
Checkbox,
Box,
Autocomplete,
TextField,
Typography,
Chip,
} from '@mui/material';
import type { ImpactMetricsSeries } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { Highlighter } from 'component/common/Highlighter/Highlighter';
export interface ImpactMetricsControlsProps {
selectedSeries: string;
onSeriesChange: (series: string) => void;
selectedRange: 'hour' | 'day' | 'week' | 'month';
onRangeChange: (range: 'hour' | 'day' | 'week' | 'month') => void;
beginAtZero: boolean;
onBeginAtZeroChange: (beginAtZero: boolean) => void;
metricSeries: (ImpactMetricsSeries & { name: string })[];
loading?: boolean;
selectedLabels: Record<string, string[]>;
onLabelsChange: (labels: Record<string, string[]>) => void;
availableLabels?: ImpactMetricsLabels;
}
export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({
selectedSeries,
onSeriesChange,
selectedRange,
onRangeChange,
beginAtZero,
onBeginAtZeroChange,
metricSeries,
loading = false,
selectedLabels,
onLabelsChange,
availableLabels,
}) => {
const handleLabelChange = (labelKey: string, values: string[]) => {
const newLabels = { ...selectedLabels };
if (values.length === 0) {
delete newLabels[labelKey];
} else {
newLabels[labelKey] = values;
}
onLabelsChange(newLabels);
};
const clearAllLabels = () => {
onLabelsChange({});
};
return (
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
maxWidth: 400,
})}
>
<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>
<Autocomplete
options={metricSeries}
getOptionLabel={(option) => option.name}
value={
metricSeries.find(
(option) => option.name === selectedSeries,
) || null
}
onChange={(_, newValue) => onSeriesChange(newValue?.name || '')}
disabled={loading}
renderOption={(props, option, { inputValue }) => (
<Box component='li' {...props}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant='body2'>
<Highlighter search={inputValue}>
{option.name}
</Highlighter>
</Typography>
<Typography
variant='caption'
color='text.secondary'
>
<Highlighter search={inputValue}>
{option.help}
</Highlighter>
</Typography>
</Box>
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label='Data series'
placeholder='Search for a metric…'
variant='outlined'
size='small'
/>
)}
noOptionsText='No metrics available'
sx={{ minWidth: 300 }}
/>
<FormControl variant='outlined' size='small' sx={{ minWidth: 200 }}>
<InputLabel id='range-select-label'>Time</InputLabel>
<Select
labelId='range-select-label'
value={selectedRange}
onChange={(e) =>
onRangeChange(
e.target.value as 'hour' | 'day' | 'week' | 'month',
)
}
label='Time Range'
>
<MenuItem value='hour'>Last hour</MenuItem>
<MenuItem value='day'>Last 24 hours</MenuItem>
<MenuItem value='week'>Last 7 days</MenuItem>
<MenuItem value='month'>Last 30 days</MenuItem>
</Select>
</FormControl>
<FormControlLabel
control={
<Checkbox
checked={beginAtZero}
onChange={(e) => onBeginAtZeroChange(e.target.checked)}
/>
}
label='Begin at zero'
/>
{availableLabels && Object.keys(availableLabels).length > 0 ? (
<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]) => (
<Autocomplete
key={labelKey}
multiple
options={values}
value={selectedLabels[labelKey] || []}
onChange={(_, newValues) =>
handleLabelChange(labelKey, newValues)
}
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='Select values...'
variant='outlined'
size='small'
/>
)}
sx={{ minWidth: 300 }}
/>
),
)}
</Box>
) : null}
</Box>
);
};

View File

@ -1,5 +1,7 @@
import { lazy } from 'react';
export const LazyImpactMetricsPage = lazy(
() => import('./LazyImpactMetricsPageExport.tsx'),
export const LazyImpactMetricsPage = lazy(() =>
import('./ImpactMetricsPage.tsx').then((module) => ({
default: module.ImpactMetricsPage,
})),
);

View File

@ -1,3 +0,0 @@
import { ImpactMetricsPage } from './ImpactMetricsPage.tsx';
export default ImpactMetricsPage;

View File

@ -20,7 +20,7 @@ const createArrayParam = <T>() => ({
const ChartsParam = createArrayParam<ChartConfig>();
const LayoutParam = createArrayParam<LayoutItem>();
export const useUrlState = () => {
export const useImpactMetricsState = () => {
const stateConfig = {
charts: withDefault(ChartsParam, []),
layout: withDefault(LayoutParam, []),

View File

@ -1,12 +1,12 @@
import { render } from 'utils/testRenderer';
import { useUrlState } from './useUrlState.ts';
import { useImpactMetricsState } from './useImpactMetricsState.ts';
import { Route, Routes } from 'react-router-dom';
import { createLocalStorage } from '../../../utils/createLocalStorage.ts';
import type { FC } from 'react';
import type { ImpactMetricsState } from '../types.ts';
const TestComponent: FC = () => {
const { charts, layout } = useUrlState();
const { charts, layout } = useImpactMetricsState();
return (
<div>