diff --git a/frontend/src/component/impact-metrics/ChartConfigModal.tsx b/frontend/src/component/impact-metrics/ChartConfigModal.tsx index 40a480aa77..1e8202e118 100644 --- a/frontend/src/component/impact-metrics/ChartConfigModal.tsx +++ b/frontend/src/component/impact-metrics/ChartConfigModal.tsx @@ -120,6 +120,7 @@ export const ChartConfigModal: FC = ({ selectedRange={formData.selectedRange} selectedLabels={formData.selectedLabels} beginAtZero={formData.beginAtZero} + showRate={formData.showRate} /> diff --git a/frontend/src/component/impact-metrics/ChartItem.tsx b/frontend/src/component/impact-metrics/ChartItem.tsx index 6793670a1e..029637eb40 100644 --- a/frontend/src/component/impact-metrics/ChartItem.tsx +++ b/frontend/src/component/impact-metrics/ChartItem.tsx @@ -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 = ({ config, onEdit, onDelete }) => ( - - - + diff --git a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx index fc9b7f871a..eacb969c2d 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx @@ -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; beginAtZero: boolean; + showRate?: boolean; aspectRatio?: number; overrideOptions?: Record; errorTitle?: string; emptyDataDescription?: string; noSeriesPlaceholder?: ReactNode; + isPreview?: boolean; }; export const ImpactMetricsChart: FC = ({ @@ -28,14 +30,16 @@ export const ImpactMetricsChart: FC = ({ 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 = ({ ? { series: selectedSeries, range: selectedRange, + showRate, labels: Object.keys(selectedLabels).length > 0 ? selectedLabels @@ -113,13 +118,14 @@ export const ImpactMetricsChart: FC = ({ 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 = ({ return ( <> - {hasError ? {errorTitle} : null} - + div': { + height: '100% !important', + width: '100% !important', + }, + } + : {} + } + > + {errorTitle} + ) : ( + cover + ) + } + /> + + {isPreview && debug?.query ? ( + ({ + margin: theme.spacing(2), + padding: theme.spacing(2), + background: theme.palette.background.elevation1, + })} + > + + {debug.query} + + + ) : null} ); }; diff --git a/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx b/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx index 087c9b80ee..fc07edd009 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsChartPreview.tsx @@ -8,6 +8,7 @@ type ImpactMetricsChartPreviewProps = { selectedRange: 'hour' | 'day' | 'week' | 'month'; selectedLabels: Record; beginAtZero: boolean; + showRate?: boolean; }; export const ImpactMetricsChartPreview: FC = ({ @@ -15,6 +16,7 @@ export const ImpactMetricsChartPreview: FC = ({ selectedRange, selectedLabels, beginAtZero, + showRate, }) => ( <> @@ -33,6 +35,8 @@ export const ImpactMetricsChartPreview: FC = ({ selectedRange={selectedRange} selectedLabels={selectedLabels} beginAtZero={beginAtZero} + showRate={showRate} + isPreview /> diff --git a/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx b/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx index 1c166e9be2..907e0d5621 100644 --- a/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx +++ b/frontend/src/component/impact-metrics/ImpactMetricsControls/ImpactMetricsControls.tsx @@ -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 = ({ onChange={actions.setSelectedRange} /> - actions.setBeginAtZero(e.target.checked)} - /> - } - label='Begin at zero' - /> + + + actions.setBeginAtZero(e.target.checked) + } + /> + } + label='Begin at zero' + /> + actions.setShowRate(e.target.checked)} + /> + } + label='Show rate per second' + /> + {availableLabels && ( ; }; 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) => void; handleSeriesChange: (series: string) => void; getConfigToSave: () => Omit; @@ -46,6 +48,7 @@ export const useChartFormState = ({ const [selectedLabels, setSelectedLabels] = useState< Record >(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, diff --git a/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx b/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx index 3ac0899420..c546ad4f32 100644 --- a/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx +++ b/frontend/src/component/impact-metrics/hooks/useImpactMetricsState.test.tsx @@ -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', }, diff --git a/frontend/src/component/impact-metrics/types.ts b/frontend/src/component/impact-metrics/types.ts index 177975ed78..e979ca58c7 100644 --- a/frontend/src/component/impact-metrics/types.ts +++ b/frontend/src/component/impact-metrics/types.ts @@ -3,6 +3,7 @@ export type ChartConfig = { selectedSeries: string; selectedRange: 'hour' | 'day' | 'week' | 'month'; beginAtZero: boolean; + showRate: boolean; selectedLabels: Record; title?: string; }; diff --git a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts index 930611435b..737d62a44b 100644 --- a/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts +++ b/frontend/src/hooks/api/getters/useImpactMetricsData/useImpactMetricsData.ts @@ -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; + 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(