1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +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} selectedRange={formData.selectedRange}
selectedLabels={formData.selectedLabels} selectedLabels={formData.selectedLabels}
beginAtZero={formData.beginAtZero} beginAtZero={formData.beginAtZero}
showRate={formData.showRate}
/> />
</StyledPreviewPanel> </StyledPreviewPanel>
</Box> </Box>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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