1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00
unleash.unleash/frontend/src/component/impact-metrics/ImpactMetricsChart.tsx

221 lines
7.4 KiB
TypeScript

import type { FC, ReactNode } from 'react';
import { useMemo } from 'react';
import { Alert, Box, Typography } from '@mui/material';
import {
LineChart,
NotEnoughData,
} from '../insights/components/LineChart/LineChart.tsx';
import { useImpactMetricsData } from 'hooks/api/getters/useImpactMetricsData/useImpactMetricsData';
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 = {
metricName: string;
timeRange: 'hour' | 'day' | 'week' | 'month';
labelSelectors: Record<string, string[]>;
yAxisMin: 'auto' | 'zero';
aggregationMode?: AggregationMode;
aspectRatio?: number;
overrideOptions?: Record<string, unknown>;
errorTitle?: string;
emptyDataDescription?: string;
noSeriesPlaceholder?: ReactNode;
isPreview?: boolean;
};
export const ImpactMetricsChart: FC<ImpactMetricsChartProps> = ({
metricName,
timeRange,
labelSelectors,
yAxisMin,
aggregationMode,
aspectRatio,
overrideOptions = {},
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, debug },
loading: dataLoading,
error: dataError,
} = useImpactMetricsData(
metricName
? {
series: metricName,
range: timeRange,
aggregationMode,
labels:
Object.keys(labelSelectors).length > 0
? labelSelectors
: undefined,
}
: undefined,
);
const placeholderData = usePlaceholderData({
fill: true,
type: 'constant',
});
const data = useChartData(timeSeriesData, debug?.query);
const hasError = !!dataError;
const isLoading = dataLoading;
const shouldShowPlaceholder = !metricName || isLoading || hasError;
const notEnoughData = useMemo(
() =>
!isLoading &&
(!timeSeriesData ||
timeSeriesData.length === 0 ||
!data.datasets.some((d) => d.data.length > 1)),
[data, isLoading, timeSeriesData],
);
const minTime = start
? fromUnixTime(Number.parseInt(start, 10))
: undefined;
const maxTime = end ? fromUnixTime(Number.parseInt(end, 10)) : undefined;
const placeholder = metricName ? (
<NotEnoughData description={emptyDataDescription} />
) : noSeriesPlaceholder ? (
noSeriesPlaceholder
) : (
<NotEnoughData
title='Select a metric series to view the chart.'
description=''
/>
);
const hasManyLabels = Object.keys(labelSelectors).length > 0;
const cover = notEnoughData ? placeholder : isLoading;
const chartOptions = shouldShowPlaceholder
? overrideOptions
: {
...overrideOptions,
scales: {
x: {
type: 'time',
min: minTime?.getTime(),
max: maxTime?.getTime(),
time: {
unit: getTimeUnit(timeRange),
displayFormats: {
[getTimeUnit(timeRange)]:
getDisplayFormat(timeRange),
},
tooltipFormat: 'PPpp',
},
ticks: {
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 8,
},
},
y: {
beginAtZero: yAxisMin === 'zero',
title: {
display: aggregationMode === 'rps',
text:
aggregationMode === 'rps'
? 'Rate per second'
: '',
},
ticks: {
precision: 0,
callback: (value: unknown): string | number =>
typeof value === 'number'
? `${formatLargeNumbers(value)}${aggregationMode === 'rps' ? '/s' : ''}`
: (value as number),
},
},
},
plugins: {
legend: {
display:
timeSeriesData &&
(hasManyLabels || timeSeriesData.length > 1),
position: 'bottom' as const,
labels: {
usePointStyle: true,
boxWidth: 8,
padding: 12,
},
},
},
animations: {
x: { duration: 0 },
y: { duration: 0 },
},
};
return (
<>
<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'
sx={{ wordBreak: 'break-all' }}
>
<code>{debug.query}</code>
</Typography>
</Box>
) : null}
{isPreview && debug?.isTruncated ? (
<Box
sx={(theme) => ({
padding: theme.spacing(0, 2),
})}
>
<Alert severity='warning'>
Showing only {timeSeriesData.length} series due to
performance. Please change filters for more accurate
results.
</Alert>
</Box>
) : null}
</>
);
};