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:
parent
a4a2e7792b
commit
6c710f68a7
@ -12,7 +12,6 @@ export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
const data = useProjectChartData(projectFlagTrends);
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={data}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user