1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-18 13:48:58 +02:00

feat: aggregation mode selection (#10367)

This commit is contained in:
Mateusz Kwasniewski 2025-07-17 18:36:25 +02:00 committed by GitHub
parent 2d96becb28
commit 89f5f79836
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 124 additions and 42 deletions

View File

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

View File

@ -21,8 +21,14 @@ const getConfigDescription = (config: DisplayChartConfig): string => {
parts.push(`last ${config.selectedRange}`);
if (config.showRate) {
if (config.aggregationMode === 'rps') {
parts.push('rate per second');
} else if (config.aggregationMode === 'count') {
parts.push('count');
} else if (config.aggregationMode === 'avg') {
parts.push('average');
} else if (config.aggregationMode === 'sum') {
parts.push('sum');
}
const labelCount = Object.keys(config.selectedLabels).length;
@ -127,7 +133,7 @@ export const ChartItem: FC<ChartItemProps> = ({ config, onEdit, onDelete }) => (
selectedRange={config.selectedRange}
selectedLabels={config.selectedLabels}
beginAtZero={config.beginAtZero}
showRate={config.showRate}
aggregationMode={config.aggregationMode}
aspectRatio={1.5}
overrideOptions={{ maintainAspectRatio: false }}
emptyDataDescription='Send impact metrics using Unleash SDK for this series to view the chart.'

View File

@ -10,13 +10,14 @@ 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 { AggregationMode } from './types.ts';
type ImpactMetricsChartProps = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
selectedLabels: Record<string, string[]>;
beginAtZero: boolean;
showRate?: boolean;
aggregationMode?: AggregationMode;
aspectRatio?: number;
overrideOptions?: Record<string, unknown>;
errorTitle?: string;
@ -30,7 +31,7 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
selectedRange,
selectedLabels,
beginAtZero,
showRate,
aggregationMode,
aspectRatio,
overrideOptions = {},
errorTitle = 'Failed to load impact metrics.',
@ -47,7 +48,7 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
? {
series: selectedSeries,
range: selectedRange,
showRate,
aggregationMode,
labels:
Object.keys(selectedLabels).length > 0
? selectedLabels
@ -121,14 +122,17 @@ export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
y: {
beginAtZero,
title: {
display: !!showRate,
text: showRate ? 'Rate per second' : '',
display: aggregationMode === 'rps',
text:
aggregationMode === 'rps'
? 'Rate per second'
: '',
},
ticks: {
precision: 0,
callback: (value: unknown): string | number =>
typeof value === 'number'
? `${formatLargeNumbers(value)}${showRate ? '/s' : ''}`
? `${formatLargeNumbers(value)}${aggregationMode === 'rps' ? '/s' : ''}`
: (value as number),
},
},

View File

@ -2,13 +2,14 @@ import type { FC } from 'react';
import { Typography } from '@mui/material';
import { StyledChartContainer } from 'component/insights/InsightsCharts.styles';
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;
showRate?: boolean;
aggregationMode?: AggregationMode;
};
export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
@ -16,7 +17,7 @@ export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
selectedRange,
selectedLabels,
beginAtZero,
showRate,
aggregationMode,
}) => (
<>
<Typography variant='h6' color='text.secondary'>
@ -35,7 +36,7 @@ export const ImpactMetricsChartPreview: FC<ImpactMetricsChartPreviewProps> = ({
selectedRange={selectedRange}
selectedLabels={selectedLabels}
beginAtZero={beginAtZero}
showRate={showRate}
aggregationMode={aggregationMode}
isPreview
/>
</StyledChartContainer>

View File

@ -5,7 +5,9 @@ import type { ImpactMetricsLabels } from 'hooks/api/getters/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'];
@ -15,7 +17,7 @@ export type ImpactMetricsControlsProps = {
| 'setSelectedRange'
| 'setBeginAtZero'
| 'setSelectedLabels'
| 'setShowRate'
| 'setAggregationMode'
>;
metricSeries: (ImpactMetricsSeries & { name: string })[];
loading?: boolean;
@ -68,17 +70,11 @@ export const ImpactMetricsControls: FC<ImpactMetricsControlsProps> = ({
label='Begin at zero'
/>
{formData.selectedSeries.startsWith('unleash_counter_') ? (
<FormControlLabel
control={
<Checkbox
checked={formData.showRate}
onChange={(e) =>
actions.setShowRate(e.target.checked)
}
/>
}
label='Show rate per second'
{formData.selectedSeries ? (
<ModeSelector
value={formData.aggregationMode}
onChange={actions.setAggregationMode}
seriesType={getMetricType(formData.selectedSeries)!}
/>
) : null}
</Box>

View File

@ -0,0 +1,50 @@
import type { FC } from 'react';
import { FormControl, InputLabel, Select, MenuItem } from '@mui/material';
import type { AggregationMode } from '../../types.ts';
export type ModeSelectorProps = {
value: AggregationMode;
onChange: (mode: AggregationMode) => void;
seriesType: 'counter' | 'gauge' | 'unknown';
};
export const ModeSelector: FC<ModeSelectorProps> = ({
value,
onChange,
seriesType,
}) => {
if (seriesType === 'unknown') return null;
return (
<FormControl
variant='outlined'
size='small'
sx={{ minWidth: 200, mt: 1 }}
>
<InputLabel id='mode-select-label'>Mode</InputLabel>
<Select
labelId='mode-select-label'
value={value}
onChange={(e) => onChange(e.target.value as AggregationMode)}
label='Mode'
>
{seriesType === 'counter'
? [
<MenuItem key='rps' value='rps'>
Rate per second
</MenuItem>,
<MenuItem key='count' value='count'>
Count
</MenuItem>,
]
: [
<MenuItem key='avg' value='avg'>
Average
</MenuItem>,
<MenuItem key='sum' value='sum'>
Sum
</MenuItem>,
]}
</Select>
</FormControl>
);
};

View File

@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import type { ChartConfig } from '../types.ts';
import type { AggregationMode, ChartConfig } from '../types.ts';
import type { ImpactMetricsLabels } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
import { getMetricType } from '../utils.ts';
type UseChartConfigParams = {
open: boolean;
@ -14,7 +15,7 @@ export type ChartFormState = {
selectedSeries: string;
selectedRange: 'hour' | 'day' | 'week' | 'month';
beginAtZero: boolean;
showRate: boolean;
aggregationMode: AggregationMode;
selectedLabels: Record<string, string[]>;
};
actions: {
@ -22,7 +23,7 @@ export type ChartFormState = {
setSelectedSeries: (series: string) => void;
setSelectedRange: (range: 'hour' | 'day' | 'week' | 'month') => void;
setBeginAtZero: (beginAtZero: boolean) => void;
setShowRate: (showRate: boolean) => void;
setAggregationMode: (mode: AggregationMode) => void;
setSelectedLabels: (labels: Record<string, string[]>) => void;
handleSeriesChange: (series: string) => void;
getConfigToSave: () => Omit<ChartConfig, 'id'>;
@ -48,7 +49,12 @@ export const useChartFormState = ({
const [selectedLabels, setSelectedLabels] = useState<
Record<string, string[]>
>(initialConfig?.selectedLabels || {});
const [showRate, setShowRate] = useState(initialConfig?.showRate || false);
const [aggregationMode, setAggregationMode] = useState<AggregationMode>(
(initialConfig?.aggregationMode || getMetricType(selectedSeries)) ===
'counter'
? 'count'
: 'avg',
);
const {
data: { labels: currentAvailableLabels },
@ -57,7 +63,7 @@ export const useChartFormState = ({
? {
series: selectedSeries,
range: selectedRange,
showRate,
aggregationMode,
}
: undefined,
);
@ -69,20 +75,31 @@ export const useChartFormState = ({
setSelectedRange(initialConfig.selectedRange);
setBeginAtZero(initialConfig.beginAtZero);
setSelectedLabels(initialConfig.selectedLabels);
setShowRate(initialConfig.showRate || false);
setAggregationMode(
initialConfig.aggregationMode ||
(getMetricType(initialConfig.selectedSeries) === 'counter'
? 'count'
: 'avg'),
);
} else if (open && !initialConfig) {
setTitle('');
setSelectedSeries('');
setSelectedRange('day');
setBeginAtZero(false);
setSelectedLabels({});
setShowRate(false);
setAggregationMode('count');
}
}, [open, initialConfig]);
const handleSeriesChange = (series: string) => {
setSelectedSeries(series);
setSelectedLabels({});
const metric = getMetricType(series);
if (metric === 'counter') {
setAggregationMode('count');
} else if (metric === 'gauge') {
setAggregationMode('avg');
}
};
const getConfigToSave = (): Omit<ChartConfig, 'id'> => ({
@ -91,7 +108,7 @@ export const useChartFormState = ({
selectedRange,
beginAtZero,
selectedLabels,
showRate,
aggregationMode,
});
const isValid = selectedSeries.length > 0;
@ -102,7 +119,7 @@ export const useChartFormState = ({
selectedSeries,
selectedRange,
beginAtZero,
showRate,
aggregationMode,
selectedLabels,
},
actions: {
@ -110,7 +127,7 @@ export const useChartFormState = ({
setSelectedSeries,
setSelectedRange,
setBeginAtZero,
setShowRate,
setAggregationMode,
setSelectedLabels,
handleSeriesChange,
getConfigToSave,

View File

@ -37,7 +37,7 @@ const TestComponent: FC<{
selectedSeries: 'test-series',
selectedRange: 'day',
beginAtZero: true,
showRate: false,
aggregationMode: 'count',
selectedLabels: {},
title: 'Test Chart',
})
@ -79,7 +79,7 @@ const mockSettings: ImpactMetricsState = {
selectedSeries: 'test-series',
selectedRange: 'day' as const,
beginAtZero: true,
showRate: false,
aggregationMode: 'count',
selectedLabels: {},
title: 'Test Chart',
},
@ -182,7 +182,7 @@ describe('useImpactMetricsState', () => {
selectedSeries: 'test-series',
selectedRange: 'day',
beginAtZero: true,
showRate: false,
mode: 'count',
selectedLabels: {},
title: 'Test Chart',
},
@ -215,7 +215,7 @@ describe('useImpactMetricsState', () => {
selectedSeries: 'test-series',
selectedRange: 'day',
beginAtZero: true,
showRate: false,
mode: 'count',
selectedLabels: {},
title: 'Test Chart',
},

View File

@ -3,11 +3,13 @@ export type ChartConfig = {
selectedSeries: string; // e.g. unleash_counter_my_metric
selectedRange: 'hour' | 'day' | 'week' | 'month';
beginAtZero: boolean;
showRate: boolean;
aggregationMode: AggregationMode;
selectedLabels: Record<string, string[]>;
title?: string;
};
export type AggregationMode = 'rps' | 'count' | 'avg' | 'sum';
export type DisplayChartConfig = ChartConfig & {
type: 'counter' | 'gauge';
displayName: string; // e.g. my_metric with unleash_counter stripped

View File

@ -60,3 +60,9 @@ export const formatLargeNumbers = (value: number): string => {
}
return value.toString();
};
export const getMetricType = (seriesName: string) => {
if (seriesName.startsWith('unleash_counter_')) return 'counter';
if (seriesName.startsWith('unleash_gauge_')) return 'gauge';
return 'unknown';
};

View File

@ -25,7 +25,7 @@ export type ImpactMetricsQuery = {
series: string;
range: 'hour' | 'day' | 'week' | 'month';
labels?: Record<string, string[]>;
showRate?: boolean;
aggregationMode?: 'rps' | 'count' | 'avg' | 'sum';
};
export const useImpactMetricsData = (query?: ImpactMetricsQuery) => {
@ -38,8 +38,8 @@ export const useImpactMetricsData = (query?: ImpactMetricsQuery) => {
range: query.range,
});
if (query.showRate !== undefined) {
params.append('showRate', query.showRate.toString());
if (query.aggregationMode !== undefined) {
params.append('aggregationMode', query.aggregationMode);
}
if (query.labels && Object.keys(query.labels).length > 0) {