mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			221 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			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}
 | |
|         </>
 | |
|     );
 | |
| };
 |