diff --git a/frontend/src/component/impact-metrics/ChartConfigModal.tsx b/frontend/src/component/impact-metrics/ChartConfigModal.tsx index 1e8202e118..17a1db9fea 100644 --- a/frontend/src/component/impact-metrics/ChartConfigModal.tsx +++ b/frontend/src/component/impact-metrics/ChartConfigModal.tsx @@ -120,7 +120,7 @@ export const ChartConfigModal: FC = ({ selectedRange={formData.selectedRange} selectedLabels={formData.selectedLabels} beginAtZero={formData.beginAtZero} - showRate={formData.showRate} + aggregationMode={formData.aggregationMode} /> diff --git a/frontend/src/component/impact-metrics/ChartItem.tsx b/frontend/src/component/impact-metrics/ChartItem.tsx index 7223f0f740..7d69d67d61 100644 --- a/frontend/src/component/impact-metrics/ChartItem.tsx +++ b/frontend/src/component/impact-metrics/ChartItem.tsx @@ -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 = ({ 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.' diff --git a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx index ff18d44b83..9d0280a27d 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx @@ -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; beginAtZero: boolean; - showRate?: boolean; + aggregationMode?: AggregationMode; aspectRatio?: number; overrideOptions?: Record; errorTitle?: string; @@ -30,7 +31,7 @@ export const ImpactMetricsChart: FC = ({ selectedRange, selectedLabels, beginAtZero, - showRate, + aggregationMode, aspectRatio, overrideOptions = {}, errorTitle = 'Failed to load impact metrics.', @@ -47,7 +48,7 @@ export const ImpactMetricsChart: FC = ({ ? { series: selectedSeries, range: selectedRange, - showRate, + aggregationMode, labels: Object.keys(selectedLabels).length > 0 ? selectedLabels @@ -121,14 +122,17 @@ export const ImpactMetricsChart: FC = ({ 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), }, }, diff --git a/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx b/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx index fc07edd009..1280ae06b8 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx @@ -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; beginAtZero: boolean; - showRate?: boolean; + aggregationMode?: AggregationMode; }; export const ImpactMetricsChartPreview: FC = ({ @@ -16,7 +17,7 @@ export const ImpactMetricsChartPreview: FC = ({ selectedRange, selectedLabels, beginAtZero, - showRate, + aggregationMode, }) => ( <> @@ -35,7 +36,7 @@ export const ImpactMetricsChartPreview: FC = ({ selectedRange={selectedRange} selectedLabels={selectedLabels} beginAtZero={beginAtZero} - showRate={showRate} + aggregationMode={aggregationMode} isPreview /> diff --git a/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx b/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx index 2a4f68fb24..96f60ceaec 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx @@ -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 = ({ label='Begin at zero' /> - {formData.selectedSeries.startsWith('unleash_counter_') ? ( - - actions.setShowRate(e.target.checked) - } - /> - } - label='Show rate per second' + {formData.selectedSeries ? ( + ) : null} diff --git a/frontend/src/component/impact-metrics/ImpactMetricsControls/components/ModeSelector.tsx b/frontend/src/component/impact-metrics/ImpactMetricsControls/components/ModeSelector.tsx new file mode 100644 index 0000000000..77223cf894 --- /dev/null +++ b/frontend/src/component/impact-metrics/ImpactMetricsControls/components/ModeSelector.tsx @@ -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 = ({ + value, + onChange, + seriesType, +}) => { + if (seriesType === 'unknown') return null; + return ( + + Mode + + + ); +}; diff --git a/frontend/src/component/impact-metrics/hooks/useChartFormState.ts b/frontend/src/component/impact-metrics/hooks/useChartFormState.ts index 6526e1a35d..93a7854001 100644 --- a/frontend/src/component/impact-metrics/hooks/useChartFormState.ts +++ b/frontend/src/component/impact-metrics/hooks/useChartFormState.ts @@ -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; }; 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) => void; handleSeriesChange: (series: string) => void; getConfigToSave: () => Omit; @@ -48,7 +49,12 @@ export const useChartFormState = ({ const [selectedLabels, setSelectedLabels] = useState< Record >(initialConfig?.selectedLabels || {}); - const [showRate, setShowRate] = useState(initialConfig?.showRate || false); + const [aggregationMode, setAggregationMode] = useState( + (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 => ({ @@ -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, diff --git a/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx b/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx index f113df5347..13b936ada3 100644 --- a/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx +++ b/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx @@ -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', }, diff --git a/frontend/src/component/impact-metrics/types.ts b/frontend/src/component/impact-metrics/types.ts index 96abb0c3bc..2b945e6957 100644 --- a/frontend/src/component/impact-metrics/types.ts +++ b/frontend/src/component/impact-metrics/types.ts @@ -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; 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 diff --git a/frontend/src/component/impact-metrics/utils.ts b/frontend/src/component/impact-metrics/utils.ts index 47a580a735..db50dfcf15 100644 --- a/frontend/src/component/impact-metrics/utils.ts +++ b/frontend/src/component/impact-metrics/utils.ts @@ -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'; +}; diff --git a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts index 737d62a44b..fae49fa1b8 100644 --- a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts +++ b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts @@ -25,7 +25,7 @@ export type ImpactMetricsQuery = { series: string; range: 'hour' | 'day' | 'week' | 'month'; labels?: Record; - 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) {