1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

Feat: metrics chart tooltip (#6409)

Creates the Metrics chart tooltip

Data are summarised per week and grouped per project

<img width="1454" alt="Screenshot 2024-03-01 at 18 14 11"
src="https://github.com/Unleash/unleash/assets/104830839/0707f764-b067-4171-86bb-0592b846fcda">

Post refactoring image (matches)

<img width="1357" alt="Screenshot 2024-03-04 at 12 14 28"
src="https://github.com/Unleash/unleash/assets/104830839/324981e3-ace2-4198-9620-23d7182a9702">

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
This commit is contained in:
andreas-unleash 2024-03-04 12:17:48 +02:00 committed by GitHub
parent a4a2e7792b
commit 6c710f68a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 221 additions and 47 deletions

View File

@ -12,7 +12,6 @@ export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
projectFlagTrends,
}) => {
const data = useProjectChartData(projectFlagTrends);
return (
<LineChart
data={data}

View File

@ -0,0 +1,142 @@
import { type VFC } from 'react';
import { ExecutiveSummarySchemaMetricsSummaryTrendsItem } from 'openapi';
import { Box, Divider, Paper, styled, Typography } from '@mui/material';
import { TooltipState } from '../../LineChart/ChartTooltip/ChartTooltip';
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2),
}));
const StyledItemHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
gap: theme.spacing(2),
alignItems: 'center',
}));
const InfoLine = ({
iconChar,
title,
color,
}: {
iconChar: string;
title: string;
color: 'info' | 'success' | 'error';
}) => (
<Typography
variant='body2'
component='p'
sx={{
color: (theme) => theme.palette[color].main,
}}
>
<Typography component='span'>{iconChar}</Typography>
<strong>{title}</strong>
</Typography>
);
const InfoSummary = ({ data }: { data: { key: string; value: number }[] }) => (
<Typography variant={'body1'} component={'p'}>
<Box display={'flex'} flexDirection={'row'}>
{data.map(({ key, value }) => (
<div style={{ flex: 1, flexDirection: 'column' }}>
<div
style={{
flex: 1,
textAlign: 'center',
marginBottom: '4px',
}}
>
{key}
</div>
<div style={{ flex: 1, textAlign: 'center' }}>{value}</div>
</div>
))}
</Box>
</Typography>
);
export const MetricsSummaryTooltip: VFC<{ tooltip: TooltipState | null }> = ({
tooltip,
}) => {
const data = tooltip?.dataPoints.map((point) => {
return {
label: point.label,
title: point.dataset.label,
color: point.dataset.borderColor,
value: point.raw as ExecutiveSummarySchemaMetricsSummaryTrendsItem & {
total: number;
},
};
});
const limitedData = data?.slice(0, 5);
return (
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
width: '300px',
})}
>
{limitedData?.map((point, index) => (
<StyledTooltipItemContainer
elevation={3}
key={`${point.title}-${index}`}
>
<StyledItemHeader>
<Typography variant='body2' component='span'>
<Typography
sx={{ color: point.color }}
component='span'
>
{'● '}
</Typography>
<strong>{point.title}</strong>
</Typography>
<Typography
variant='body2'
color='textSecondary'
component='span'
>
{point.label}
</Typography>
</StyledItemHeader>
<Divider
sx={(theme) => ({ margin: theme.spacing(1.5, 0) })}
/>
<InfoLine
iconChar={'▣ '}
title={`Total requests: ${point.value.total}`}
color={'info'}
/>
<InfoLine
iconChar={'▲ '}
title={`Exposed: ${point.value.totalYes}`}
color={'success'}
/>
<InfoLine
iconChar={'▼ '}
title={`Not exposed: ${point.value.totalNo}`}
color={'error'}
/>
<Divider
sx={(theme) => ({ margin: theme.spacing(1.5, 0) })}
/>
<InfoSummary
data={[
{ key: 'Flags', value: point.value.totalFlags },
{
key: 'Environments',
value: point.value.totalEnvironments,
},
{ key: 'Apps', value: point.value.totalApps },
]}
/>
</StyledTooltipItemContainer>
)) || null}
</Box>
);
};

View File

@ -3,6 +3,7 @@ import 'chartjs-adapter-date-fns';
import { ExecutiveSummarySchema } from 'openapi';
import { LineChart } from '../LineChart/LineChart';
import { useMetricsSummary } from '../useMetricsSummary';
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
interface IMetricsSummaryChartProps {
metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends'];
@ -11,7 +12,15 @@ interface IMetricsSummaryChartProps {
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
metricsSummaryTrends,
}) => {
const data = useMetricsSummary(metricsSummaryTrends, 'total');
return <LineChart data={data} />;
const data = useMetricsSummary(metricsSummaryTrends);
return (
<LineChart
data={data}
isLocalTooltip
TooltipComponent={MetricsSummaryTooltip}
overrideOptions={{
parsing: { yAxisKey: 'total', xAxisKey: 'weekId' },
}}
/>
);
};

View File

@ -1,57 +1,81 @@
import { useMemo } from 'react';
import { getProjectColor } from './executive-dashboard-utils';
import { useTheme } from '@mui/material';
import {
ExecutiveSummarySchema,
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
} from '../../openapi';
import { ExecutiveSummarySchema } from '../../openapi';
import { parseISO, getISOWeek, format } from 'date-fns';
import { getProjectColor } from './executive-dashboard-utils';
type MetricsSummaryTrends = ExecutiveSummarySchema['metricsSummaryTrends'];
interface GroupedData {
[key: string]: {
[week: string]: {
total: number;
totalYes: number;
totalNo: number;
totalApps: number;
totalEnvironments: number;
totalFlags: number;
};
};
}
function groupAndSumData(data: MetricsSummaryTrends): any {
const groupedData: GroupedData = {};
data.forEach((item) => {
const weekNumber = getISOWeek(parseISO(item.date));
const year = format(parseISO(item.date), 'yyyy');
const weekId = `${year}-${weekNumber.toString().padStart(2, '0')}`;
const project = item.project;
if (!groupedData[project]) {
groupedData[project] = {};
}
if (!groupedData[project][weekId]) {
groupedData[project][weekId] = {
total: 0,
totalYes: 0,
totalNo: 0,
totalApps: 0,
totalEnvironments: 0,
totalFlags: 0,
};
}
groupedData[project][weekId].total += item.totalYes + item.totalNo;
groupedData[project][weekId].totalYes += item.totalYes;
groupedData[project][weekId].totalNo += item.totalNo;
groupedData[project][weekId].totalApps += item.totalApps;
groupedData[project][weekId].totalEnvironments +=
item.totalEnvironments;
groupedData[project][weekId].totalFlags += item.totalFlags;
});
return Object.entries(groupedData).map(([project, weeks]) => {
const color = getProjectColor(project);
return {
label: project,
borderColor: color,
backgroundColor: color,
fill: false,
data: Object.entries(weeks)
.sort(([weekA], [weekB]) => weekA.localeCompare(weekB))
.map(([weekId, values]) => ({
weekId,
...values,
})),
};
});
}
export const useMetricsSummary = (
metricsSummaryTrends: MetricsSummaryTrends,
field: 'total' | 'totalYes' | 'totalNo' | 'totalApps',
) => {
const theme = useTheme();
const data = useMemo(() => {
const groupedFlagTrends = metricsSummaryTrends.reduce<
Record<string, ExecutiveSummarySchemaMetricsSummaryTrendsItem[]>
>((groups, item) => {
if (!groups[item.project]) {
groups[item.project] = [];
}
groups[item.project].push(item);
return groups;
}, {});
const datasets = Object.entries(groupedFlagTrends).map(
([project, metricsSummaryTrends]) => {
const color = getProjectColor(project);
return {
label: project,
data: metricsSummaryTrends.map((item) => {
if (field !== 'total') {
return item[field] || 0;
}
return item.totalYes + item.totalNo || 0;
}),
borderColor: color,
backgroundColor: color,
fill: false,
};
},
);
const objectKeys = Object.keys(groupedFlagTrends);
const firstElementSummary = groupedFlagTrends[objectKeys[0]] || [];
const firstElementsDates = firstElementSummary.map((item) => item.date);
return {
labels: firstElementsDates,
datasets,
};
return { datasets: groupAndSumData(metricsSummaryTrends) };
}, [theme, metricsSummaryTrends]);
return data;