1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

Impact metrics errors with rate per second option (#10337)

- checkbox to select 'rate' vs 'increase' - always available for now,
but does nothing for gauge. I can improve it later on
- better preview - it will show resolved query underneath
- cleaner error handling that doesn't overflow widgets
This commit is contained in:
Tymoteusz Czech 2025-07-10 16:43:55 +02:00 committed by GitHub
parent ada4431957
commit 69905185c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 114 additions and 41 deletions

View File

@ -120,6 +120,7 @@ export const ChartConfigModal: FC<ChartConfigModalProps> = ({
selectedRange={formData.selectedRange}
selectedLabels={formData.selectedLabels}
beginAtZero={formData.beginAtZero}
showRate={formData.showRate}
/>
</StyledPreviewPanel>
</Box>

View File

@ -21,6 +21,10 @@ const getConfigDescription = (config: ChartConfig): string => {
parts.push(`last ${config.selectedRange}`);
if (config.showRate) {
parts.push('rate per second');
}
const labelCount = Object.keys(config.selectedLabels).length;
if (labelCount > 0) {
parts.push(`${labelCount} filter${labelCount > 1 ? 's' : ''}`);
@ -29,15 +33,6 @@ const getConfigDescription = (config: ChartConfig): string => {
return parts.join(' • ');
};
const StyledChartWrapper = styled(Box)({
height: '100%',
width: '100%',
'& > div': {
height: '100% !important',
width: '100% !important',
},
});
const StyledWidget = styled(Paper)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusMedium}px`,
boxShadow: 'none',
@ -127,17 +122,16 @@ export const ChartItem: FC<ChartItemProps> = ({ config, onEdit, onDelete }) => (
<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>
<ImpactMetricsChart
selectedSeries={config.selectedSeries}
selectedRange={config.selectedRange}
selectedLabels={config.selectedLabels}
beginAtZero={config.beginAtZero}
showRate={config.showRate}
aspectRatio={1.5}
overrideOptions={{ maintainAspectRatio: false }}
emptyDataDescription='Send impact metrics using Unleash SDK for this series to view the chart.'
/>
</StyledImpactChartContainer>
</StyledChartContent>
</StyledWidget>

View File

@ -1,6 +1,6 @@
import type { FC, ReactNode } from 'react';
import { useMemo } from 'react';
import { Alert } from '@mui/material';
import { Alert, Box, Typography } from '@mui/material';
import {
LineChart,
NotEnoughData,
@ -16,11 +16,13 @@ type ImpactMetricsChartProps = {
selectedRange: 'hour' | 'day' | 'week' | 'month';
selectedLabels: Record<string, string[]>;
beginAtZero: boolean;
showRate?: boolean;
aspectRatio?: number;
overrideOptions?: Record<string, unknown>;
errorTitle?: string;
emptyDataDescription?: string;
noSeriesPlaceholder?: ReactNode;
isPreview?: boolean;
};
export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
@ -28,14 +30,16 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
selectedRange,
selectedLabels,
beginAtZero,
showRate,
aspectRatio,
overrideOptions = {},
errorTitle = 'Failed to load impact metrics. Please check if Prometheus is configured and the feature flag is enabled.',
errorTitle = 'Failed to load impact metrics.',
emptyDataDescription = 'Send impact metrics using Unleash SDK and select data series to view the chart.',
noSeriesPlaceholder,
isPreview,
}) => {
const {
data: { start, end, series: timeSeriesData },
data: { start, end, series: timeSeriesData, debug },
loading: dataLoading,
error: dataError,
} = useImpactMetricsData(
@ -43,6 +47,7 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
? {
series: selectedSeries,
range: selectedRange,
showRate,
labels:
Object.keys(selectedLabels).length > 0
? selectedLabels
@ -113,13 +118,14 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
y: {
beginAtZero,
title: {
display: false,
display: !!showRate,
text: showRate ? 'Rate per second' : '',
},
ticks: {
precision: 0,
callback: (value: unknown): string | number =>
typeof value === 'number'
? formatLargeNumbers(value)
? `${formatLargeNumbers(value)}${showRate ? '/s' : ''}`
: (value as number),
},
},
@ -143,13 +149,46 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
return (
<>
{hasError ? <Alert severity='error'>{errorTitle}</Alert> : null}
<LineChart
data={notEnoughData || isLoading ? placeholderData : data}
aspectRatio={aspectRatio}
overrideOptions={chartOptions}
cover={cover}
/>
<Box
sx={
!isPreview
? {
height: '100%',
width: '100%',
'& > div': {
height: '100% !important',
width: '100% !important',
},
}
: {}
}
>
<LineChart
data={notEnoughData || isLoading ? placeholderData : data}
aspectRatio={aspectRatio}
overrideOptions={chartOptions}
cover={
hasError ? (
<Alert severity='error'>{errorTitle}</Alert>
) : (
cover
)
}
/>
</Box>
{isPreview && debug?.query ? (
<Box
sx={(theme) => ({
margin: theme.spacing(2),
padding: theme.spacing(2),
background: theme.palette.background.elevation1,
})}
>
<Typography variant='caption' color='text.secondary'>
<code>{debug.query}</code>
</Typography>
</Box>
) : null}
</>
);
};

View File

@ -8,6 +8,7 @@ type ImpactMetricsChartPreviewProps = {
selectedRange: 'hour' | 'day' | 'week' | 'month';
selectedLabels: Record<string, string[]>;
beginAtZero: boolean;
showRate?: boolean;
};
export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
@ -15,6 +16,7 @@ export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
selectedRange,
selectedLabels,
beginAtZero,
showRate,
}) => (
<>
<Typography variant='h6' color='text.secondary'>
@ -33,6 +35,8 @@ export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
selectedRange={selectedRange}
selectedLabels={selectedLabels}
beginAtZero={beginAtZero}
showRate={showRate}
isPreview
/>
</StyledChartContainer>
</>

View File

@ -15,6 +15,7 @@ export type ImpactMetricsControlsProps = {
| 'setSelectedRange'
| 'setBeginAtZero'
| 'setSelectedLabels'
| 'setShowRate'
>;
metricSeries: (ImpactMetricsSeries & { name: string })[];
loading?: boolean;
@ -54,16 +55,29 @@ export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({
onChange={actions.setSelectedRange}
/>
<FormControlLabel
control={
<Checkbox
checked={formData.beginAtZero}
onChange={(e) => actions.setBeginAtZero(e.target.checked)}
/>
}
label='Begin at zero'
/>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<FormControlLabel
control={
<Checkbox
checked={formData.beginAtZero}
onChange={(e) =>
actions.setBeginAtZero(e.target.checked)
}
/>
}
label='Begin at zero'
/>
<FormControlLabel
control={
<Checkbox
checked={formData.showRate}
onChange={(e) => actions.setShowRate(e.target.checked)}
/>
}
label='Show rate per second'
/>
</Box>
{availableLabels && (
<LabelsFilter
selectedLabels={formData.selectedLabels}

View File

@ -14,6 +14,7 @@ export type ChartFormState = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
beginAtZero: boolean;
showRate: boolean;
selectedLabels: Record<string, string[]>;
};
actions: {
@ -21,6 +22,7 @@ export type ChartFormState = {
setSelectedSeries: (series: string) => void;
setSelectedRange: (range: 'hour' | 'day' | 'week' | 'month') => void;
setBeginAtZero: (beginAtZero: boolean) => void;
setShowRate: (showRate: boolean) => void;
setSelectedLabels: (labels: Record<string, string[]>) => void;
handleSeriesChange: (series: string) => void;
getConfigToSave: () => Omit<ChartConfig, 'id'>;
@ -46,6 +48,7 @@ export const useChartFormState = ({
const [selectedLabels, setSelectedLabels] = useState<
Record<string, string[]>
>(initialConfig?.selectedLabels || {});
const [showRate, setShowRate] = useState(initialConfig?.showRate || false);
const {
data: { labels: currentAvailableLabels },
@ -54,6 +57,7 @@ export const useChartFormState = ({
? {
series: selectedSeries,
range: selectedRange,
showRate,
}
: undefined,
);
@ -65,12 +69,14 @@ export const useChartFormState = ({
setSelectedRange(initialConfig.selectedRange);
setBeginAtZero(initialConfig.beginAtZero);
setSelectedLabels(initialConfig.selectedLabels);
setShowRate(initialConfig.showRate || false);
} else if (open && !initialConfig) {
setTitle('');
setSelectedSeries('');
setSelectedRange('day');
setBeginAtZero(false);
setSelectedLabels({});
setShowRate(false);
}
}, [open, initialConfig]);
@ -85,6 +91,7 @@ export const useChartFormState = ({
selectedRange,
beginAtZero,
selectedLabels,
showRate,
});
const isValid = selectedSeries.length > 0;
@ -95,6 +102,7 @@ export const useChartFormState = ({
selectedSeries,
selectedRange,
beginAtZero,
showRate,
selectedLabels,
},
actions: {
@ -102,6 +110,7 @@ export const useChartFormState = ({
setSelectedSeries,
setSelectedRange,
setBeginAtZero,
setShowRate,
setSelectedLabels,
handleSeriesChange,
getConfigToSave,

View File

@ -43,6 +43,7 @@ describe('useImpactMetricsState', () => {
selectedSeries: 'test-series',
selectedRange: 'day' as const,
beginAtZero: true,
showRate: false,
selectedLabels: {},
title: 'Test Chart',
},
@ -84,6 +85,7 @@ describe('useImpactMetricsState', () => {
selectedSeries: 'old-series',
selectedRange: 'day' as const,
beginAtZero: true,
showRate: false,
selectedLabels: {},
title: 'Old Chart',
},
@ -98,6 +100,7 @@ describe('useImpactMetricsState', () => {
selectedSeries: 'url-series',
selectedRange: 'day',
beginAtZero: true,
showRate: false,
selectedLabels: {},
title: 'URL Chart',
},

View File

@ -3,6 +3,7 @@ export type ChartConfig = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
beginAtZero: boolean;
showRate: boolean;
selectedLabels: Record<string, string[]>;
title?: string;
};

View File

@ -16,12 +16,16 @@ export type ImpactMetricsResponse = {
step?: string;
series: ImpactMetricsSeries[];
labels?: ImpactMetricsLabels;
debug?: {
query?: string;
};
};
export type ImpactMetricsQuery = {
series: string;
range: 'hour' | 'day' | 'week' | 'month';
labels?: Record<string, string[]>;
showRate?: boolean;
};
export const useImpactMetricsData = (query?: ImpactMetricsQuery) => {
@ -34,6 +38,10 @@ export const useImpactMetricsData = (query?: ImpactMetricsQuery) => {
range: query.range,
});
if (query.showRate !== undefined) {
params.append('showRate', query.showRate.toString());
}
if (query.labels && Object.keys(query.labels).length > 0) {
// Send labels as they are - the backend will handle the formatting
const labelsParam = Object.entries(query.labels).reduce(