mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-24 20:06:55 +01:00
feat: create flags created vs archived chart (#10429)
This commit is contained in:
parent
2f1ca50bc4
commit
1d3aea47dc
@ -1,127 +1,288 @@
|
|||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import { type FC, useMemo } from 'react';
|
import { type FC, useMemo, useState } from 'react';
|
||||||
import type { InstanceInsightsSchema } from 'openapi';
|
import type { InstanceInsightsSchema } from 'openapi';
|
||||||
import { useProjectChartData } from 'component/insights/hooks/useProjectChartData';
|
import { useProjectChartData } from 'component/insights/hooks/useProjectChartData';
|
||||||
import {
|
|
||||||
fillGradientPrimary,
|
|
||||||
LineChart,
|
|
||||||
NotEnoughData,
|
|
||||||
} from 'component/insights/components/LineChart/LineChart';
|
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
|
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
|
||||||
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
|
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
|
||||||
|
import { Chart } from 'react-chartjs-2';
|
||||||
|
import {
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
TimeScale,
|
||||||
|
Chart as ChartJS,
|
||||||
|
Filler,
|
||||||
|
} from 'chart.js';
|
||||||
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import {
|
||||||
|
ChartTooltip,
|
||||||
|
type TooltipState,
|
||||||
|
} from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip';
|
||||||
|
import { createTooltip } from 'component/insights/components/LineChart/createTooltip';
|
||||||
|
import { CreationArchiveTooltip } from './CreationArchiveTooltip.tsx';
|
||||||
|
import { CreationArchiveRatioTooltip } from './CreationArchiveRatioTooltip.tsx';
|
||||||
|
import { getFlagTypeColors } from './flagTypeColors.ts';
|
||||||
|
import type { WeekData, RawWeekData } from './types.ts';
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
TimeScale,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Filler,
|
||||||
|
);
|
||||||
|
|
||||||
interface ICreationArchiveChartProps {
|
interface ICreationArchiveChartProps {
|
||||||
creationArchiveTrends: GroupedDataByProject<
|
creationArchiveTrends: GroupedDataByProject<
|
||||||
InstanceInsightsSchema['creationArchiveTrends']
|
InstanceInsightsSchema['creationArchiveTrends']
|
||||||
>;
|
>;
|
||||||
isAggregate?: boolean;
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WeekData = {
|
|
||||||
archivedFlags: number;
|
|
||||||
totalCreatedFlags: number;
|
|
||||||
week: string;
|
|
||||||
date?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type RawWeekData = {
|
|
||||||
archivedFlags: number;
|
|
||||||
createdFlags: Record<string, number>;
|
|
||||||
week: string;
|
|
||||||
date: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
|
export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
|
||||||
creationArchiveTrends,
|
creationArchiveTrends,
|
||||||
isAggregate,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
}) => {
|
}) => {
|
||||||
const creationArchiveData = useProjectChartData(creationArchiveTrends);
|
const creationVsArchivedChart = useProjectChartData(creationArchiveTrends);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const placeholderData = usePlaceholderData();
|
const placeholderData = usePlaceholderData();
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
const [tooltip, setTooltip] = useState<null | TooltipState>(null);
|
||||||
|
|
||||||
const aggregateHealthData = useMemo(() => {
|
const aggregateOrProjectData = useMemo(() => {
|
||||||
const labels: string[] = Array.from(
|
const labels: string[] = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
creationArchiveData.datasets.flatMap((d) =>
|
creationVsArchivedChart.datasets.flatMap((d) =>
|
||||||
d.data.map((item) => item.week),
|
d.data.map((item) => item.week),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const allFlagTypes = new Set<string>();
|
||||||
|
creationVsArchivedChart.datasets.forEach((d) =>
|
||||||
|
d.data.forEach((item: any) => {
|
||||||
|
if (item.createdFlags) {
|
||||||
|
Object.keys(item.createdFlags).forEach((type) =>
|
||||||
|
allFlagTypes.add(type),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const aggregateWeekData = (acc: WeekData, item: RawWeekData) => {
|
||||||
|
if (item) {
|
||||||
|
acc.archivedFlags += item.archivedFlags || 0;
|
||||||
|
|
||||||
|
if (item.createdFlags) {
|
||||||
|
Object.entries(item.createdFlags).forEach(
|
||||||
|
([type, count]) => {
|
||||||
|
acc.createdFlagsByType[type] =
|
||||||
|
(acc.createdFlagsByType[type] || 0) + count;
|
||||||
|
acc.totalCreatedFlags += count;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!acc.date) {
|
||||||
|
acc.date = item?.date;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInitialWeekData = (label: string): WeekData => ({
|
||||||
|
archivedFlags: 0,
|
||||||
|
totalCreatedFlags: 0,
|
||||||
|
createdFlagsByType: {},
|
||||||
|
archivePercentage: 0,
|
||||||
|
week: label,
|
||||||
|
});
|
||||||
|
|
||||||
const weeks: WeekData[] = labels
|
const weeks: WeekData[] = labels
|
||||||
.map((label) => {
|
.map((label) => {
|
||||||
return creationArchiveData.datasets
|
return creationVsArchivedChart.datasets
|
||||||
.map((d) => d.data.find((item) => item.week === label))
|
.map((d) => d.data.find((item) => item.week === label))
|
||||||
.reduce(
|
.reduce(aggregateWeekData, createInitialWeekData(label));
|
||||||
(acc: WeekData, item: RawWeekData) => {
|
|
||||||
if (item) {
|
|
||||||
acc.archivedFlags += item.archivedFlags || 0;
|
|
||||||
const createdFlagsSum = item.createdFlags
|
|
||||||
? Object.values(item.createdFlags).reduce(
|
|
||||||
(sum: number, count: number) =>
|
|
||||||
sum + count,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
acc.totalCreatedFlags += createdFlagsSum;
|
|
||||||
}
|
|
||||||
if (!acc.date) {
|
|
||||||
acc.date = item?.date;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
archivedFlags: 0,
|
|
||||||
totalCreatedFlags: 0,
|
|
||||||
week: label,
|
|
||||||
} as WeekData,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
|
.map((week) => ({
|
||||||
|
...week,
|
||||||
|
archivePercentage:
|
||||||
|
week.totalCreatedFlags > 0
|
||||||
|
? (week.archivedFlags / week.totalCreatedFlags) * 100
|
||||||
|
: 0,
|
||||||
|
}))
|
||||||
.sort((a, b) => (a.week > b.week ? 1 : -1));
|
.sort((a, b) => (a.week > b.week ? 1 : -1));
|
||||||
|
|
||||||
|
const flagTypeColors = getFlagTypeColors(theme);
|
||||||
|
|
||||||
|
const flagTypeDatasets = Array.from(allFlagTypes).map(
|
||||||
|
(flagType, index) => ({
|
||||||
|
label: flagType,
|
||||||
|
data: weeks,
|
||||||
|
backgroundColor: flagTypeColors[index % flagTypeColors.length],
|
||||||
|
borderColor: flagTypeColors[index % flagTypeColors.length],
|
||||||
|
type: 'bar' as const,
|
||||||
|
parsing: {
|
||||||
|
yAxisKey: `createdFlagsByType.${flagType}`,
|
||||||
|
xAxisKey: 'date',
|
||||||
|
},
|
||||||
|
yAxisID: 'y',
|
||||||
|
stack: 'created',
|
||||||
|
order: 2,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const flagTypeNames = Array.from(allFlagTypes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Number of created flags',
|
label: 'Archived flags',
|
||||||
|
data: weeks,
|
||||||
|
backgroundColor: theme.palette.background.application,
|
||||||
|
borderColor: theme.palette.background.application,
|
||||||
|
parsing: { yAxisKey: 'archivedFlags', xAxisKey: 'date' },
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
...flagTypeDatasets,
|
||||||
|
{
|
||||||
|
label: 'Flags archived / Flags created',
|
||||||
data: weeks,
|
data: weeks,
|
||||||
borderColor: theme.palette.primary.light,
|
borderColor: theme.palette.primary.light,
|
||||||
backgroundColor: fillGradientPrimary,
|
backgroundColor: theme.palette.primary.light,
|
||||||
fill: true,
|
type: 'line' as const,
|
||||||
order: 3,
|
parsing: {
|
||||||
|
yAxisKey: 'archivePercentage',
|
||||||
|
xAxisKey: 'date',
|
||||||
|
},
|
||||||
|
yAxisID: 'y1',
|
||||||
|
order: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
flagTypeNames,
|
||||||
};
|
};
|
||||||
}, [creationArchiveData, theme]);
|
}, [creationVsArchivedChart, theme]);
|
||||||
|
|
||||||
const aggregateOrProjectData = isAggregate
|
|
||||||
? aggregateHealthData
|
|
||||||
: creationArchiveData;
|
|
||||||
const notEnoughData = useMemo(
|
const notEnoughData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
!creationArchiveData.datasets.some((d) => d.data.length > 1),
|
!creationVsArchivedChart.datasets.some((d) => d.data.length > 1),
|
||||||
[creationArchiveData, isLoading],
|
[creationVsArchivedChart, isLoading],
|
||||||
);
|
);
|
||||||
const data =
|
const data =
|
||||||
notEnoughData || isLoading ? placeholderData : aggregateOrProjectData;
|
notEnoughData || isLoading ? placeholderData : aggregateOrProjectData;
|
||||||
|
|
||||||
|
const flagTypeNames = aggregateOrProjectData.flagTypeNames || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<>
|
||||||
key={isAggregate ? 'aggregate' : 'project'}
|
<Chart
|
||||||
data={data}
|
type='bar'
|
||||||
overrideOptions={
|
data={data}
|
||||||
notEnoughData
|
options={{
|
||||||
? {}
|
responsive: true,
|
||||||
: {
|
plugins: {
|
||||||
parsing: {
|
legend: {
|
||||||
yAxisKey: 'totalCreatedFlags',
|
position: 'bottom' as const,
|
||||||
xAxisKey: 'date',
|
labels: {
|
||||||
},
|
filter: (legendItem) => {
|
||||||
}
|
return !flagTypeNames.includes(
|
||||||
}
|
legendItem.text || '',
|
||||||
cover={notEnoughData ? <NotEnoughData /> : isLoading}
|
);
|
||||||
/>
|
},
|
||||||
|
generateLabels: (chart) => {
|
||||||
|
const original =
|
||||||
|
ChartJS.defaults.plugins.legend.labels.generateLabels(
|
||||||
|
chart,
|
||||||
|
);
|
||||||
|
const filtered = original.filter(
|
||||||
|
(item) =>
|
||||||
|
!flagTypeNames.includes(
|
||||||
|
item.text || '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
filtered.push({
|
||||||
|
text: 'Created Flags',
|
||||||
|
fillStyle: theme.palette.success.main,
|
||||||
|
strokeStyle: theme.palette.success.main,
|
||||||
|
lineWidth: 0,
|
||||||
|
hidden: false,
|
||||||
|
index: filtered.length,
|
||||||
|
datasetIndex: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: false,
|
||||||
|
position: 'nearest',
|
||||||
|
external: createTooltip(setTooltip),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
locale: locationSettings.locale,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'week',
|
||||||
|
tooltipFormat: 'PPP',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Number of flags',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Ratio',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => `${value}%`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
height={100}
|
||||||
|
width={250}
|
||||||
|
/>
|
||||||
|
{tooltip?.dataPoints?.some(
|
||||||
|
(point) =>
|
||||||
|
point.dataset.label !== 'Archived flags' &&
|
||||||
|
point.dataset.label !== 'Flags archived / Flags created',
|
||||||
|
) ? (
|
||||||
|
<CreationArchiveTooltip tooltip={tooltip} />
|
||||||
|
) : tooltip?.dataPoints?.some(
|
||||||
|
(point) =>
|
||||||
|
point.dataset.label === 'Flags archived / Flags created',
|
||||||
|
) ? (
|
||||||
|
<CreationArchiveRatioTooltip tooltip={tooltip} />
|
||||||
|
) : (
|
||||||
|
<ChartTooltip tooltip={tooltip} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { Box, Paper, Typography, styled, useTheme } from '@mui/material';
|
||||||
|
import type { TooltipState } from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip';
|
||||||
|
import { ChartTooltipContainer } from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip';
|
||||||
|
import type { Theme } from '@mui/material/styles/createTheme';
|
||||||
|
import type { WeekData } from './types.ts';
|
||||||
|
const getRatioTooltipColors = (theme: Theme) => ({
|
||||||
|
CREATED: theme.palette.success.main,
|
||||||
|
ARCHIVED: theme.palette.background.application,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
width: 200,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledFlagItem = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface CreationArchiveRatioTooltipProps {
|
||||||
|
tooltip: TooltipState | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreationArchiveRatioTooltip: FC<
|
||||||
|
CreationArchiveRatioTooltipProps
|
||||||
|
> = ({ tooltip }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const colors = getRatioTooltipColors(theme);
|
||||||
|
|
||||||
|
if (!tooltip?.dataPoints) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratioDataPoint = tooltip.dataPoints.find(
|
||||||
|
(point) => point.dataset.label === 'Flags archived / Flags created',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ratioDataPoint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = ratioDataPoint.raw as WeekData;
|
||||||
|
|
||||||
|
if (!rawData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const archivedCount = rawData.archivedFlags || 0;
|
||||||
|
const createdCount = rawData.totalCreatedFlags || 0;
|
||||||
|
const ratio = Math.round(ratioDataPoint.parsed.y as number);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartTooltipContainer tooltip={tooltip}>
|
||||||
|
<StyledTooltipItemContainer elevation={3}>
|
||||||
|
<Typography
|
||||||
|
variant='body2'
|
||||||
|
component='div'
|
||||||
|
fontWeight='bold'
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
>
|
||||||
|
Ratio {ratio}%
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<StyledFlagItem>
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
<Typography
|
||||||
|
sx={{ color: colors.CREATED }}
|
||||||
|
component='span'
|
||||||
|
>
|
||||||
|
{'● '}
|
||||||
|
</Typography>
|
||||||
|
Flags created
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
{createdCount}
|
||||||
|
</Typography>
|
||||||
|
</StyledFlagItem>
|
||||||
|
|
||||||
|
<StyledFlagItem>
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
<Typography
|
||||||
|
sx={{ color: colors.ARCHIVED }}
|
||||||
|
component='span'
|
||||||
|
>
|
||||||
|
{'● '}
|
||||||
|
</Typography>
|
||||||
|
Flags archived
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
{archivedCount}
|
||||||
|
</Typography>
|
||||||
|
</StyledFlagItem>
|
||||||
|
</StyledTooltipItemContainer>
|
||||||
|
</ChartTooltipContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { Box, Paper, Typography, styled, useTheme } from '@mui/material';
|
||||||
|
import type { TooltipState } from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip';
|
||||||
|
import { ChartTooltipContainer } from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip';
|
||||||
|
import { getFlagTypeColors } from './flagTypeColors.ts';
|
||||||
|
import type { WeekData } from './types.ts';
|
||||||
|
|
||||||
|
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
width: 240,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledFlagTypeItem = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface CreationArchiveTooltipProps {
|
||||||
|
tooltip: TooltipState | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreationArchiveTooltip: FC<CreationArchiveTooltipProps> = ({
|
||||||
|
tooltip,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (!tooltip?.dataPoints) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdFlagDataPoints = tooltip.dataPoints.filter(
|
||||||
|
(point) =>
|
||||||
|
point.dataset.label !== 'Archived flags' &&
|
||||||
|
point.dataset.label !== 'Flags archived / Flags created',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (createdFlagDataPoints.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = createdFlagDataPoints[0]?.raw as WeekData;
|
||||||
|
|
||||||
|
if (!rawData?.createdFlagsByType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagTypeNames = createdFlagDataPoints.map(
|
||||||
|
(point) => point.dataset.label || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const flagTypeColors = getFlagTypeColors(theme);
|
||||||
|
|
||||||
|
const flagTypeEntries = Object.entries(rawData.createdFlagsByType)
|
||||||
|
.filter(([, count]) => (count as number) > 0)
|
||||||
|
.map(([flagType, count], index) => ({
|
||||||
|
type: flagType,
|
||||||
|
count: count as number,
|
||||||
|
color:
|
||||||
|
flagTypeColors[flagTypeNames.indexOf(flagType)] ||
|
||||||
|
flagTypeColors[index % flagTypeColors.length],
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (flagTypeEntries.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartTooltipContainer tooltip={tooltip}>
|
||||||
|
<StyledTooltipItemContainer elevation={3}>
|
||||||
|
<Typography
|
||||||
|
variant='body2'
|
||||||
|
component='div'
|
||||||
|
fontWeight='bold'
|
||||||
|
sx={{ marginBottom: 1 }}
|
||||||
|
>
|
||||||
|
Flag type
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{flagTypeEntries.map(({ type, count, color }) => (
|
||||||
|
<StyledFlagTypeItem key={type}>
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
<Typography sx={{ color }} component='span'>
|
||||||
|
{'● '}
|
||||||
|
</Typography>
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2' component='span'>
|
||||||
|
{count}
|
||||||
|
</Typography>
|
||||||
|
</StyledFlagTypeItem>
|
||||||
|
))}
|
||||||
|
</StyledTooltipItemContainer>
|
||||||
|
</ChartTooltipContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import type { Theme } from '@mui/material';
|
||||||
|
|
||||||
|
export const getFlagTypeColors = (theme: Theme) => [
|
||||||
|
theme.palette.success.border,
|
||||||
|
theme.palette.success.main,
|
||||||
|
theme.palette.success.dark,
|
||||||
|
'#4D8007',
|
||||||
|
'#7D935E',
|
||||||
|
];
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
export type WeekData = {
|
||||||
|
archivedFlags: number;
|
||||||
|
totalCreatedFlags: number;
|
||||||
|
createdFlagsByType: Record<string, number>;
|
||||||
|
archivePercentage: number;
|
||||||
|
week: string;
|
||||||
|
date?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RawWeekData = {
|
||||||
|
archivedFlags: number;
|
||||||
|
createdFlags: Record<string, number>;
|
||||||
|
week: string;
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { Box, Typography, Link, styled } from '@mui/material';
|
||||||
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
|
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
||||||
|
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
||||||
|
import { StatsExplanation } from 'component/insights/InsightsCharts.styles';
|
||||||
|
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
|
||||||
|
import type { InstanceInsightsSchema } from 'openapi';
|
||||||
|
|
||||||
|
function getCurrentArchiveRatio(
|
||||||
|
groupedCreationArchiveData: GroupedDataByProject<
|
||||||
|
InstanceInsightsSchema['creationArchiveTrends']
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!groupedCreationArchiveData ||
|
||||||
|
Object.keys(groupedCreationArchiveData).length === 0
|
||||||
|
) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalArchived = 0;
|
||||||
|
let totalCreated = 0;
|
||||||
|
|
||||||
|
Object.values(groupedCreationArchiveData).forEach((projectData) => {
|
||||||
|
const latestData = projectData[projectData.length - 1];
|
||||||
|
if (latestData) {
|
||||||
|
totalArchived += latestData.archivedFlags || 0;
|
||||||
|
const createdSum = latestData.createdFlags
|
||||||
|
? Object.values(latestData.createdFlags).reduce(
|
||||||
|
(sum: number, count: number) => sum + count,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
totalCreated += createdSum;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return totalCreated > 0
|
||||||
|
? Math.round((totalArchived / totalCreated) * 100)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledRatioContainer = styled(Box)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.background.elevation1,
|
||||||
|
borderRadius: theme.spacing(2),
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(0.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledPercentageRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledRatioTypography = styled(Typography)(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontSize: theme.spacing(2.5),
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledInfoIcon = styled(InfoOutlined)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledLink = styled(Link)(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface CreationArchiveStatsProps {
|
||||||
|
groupedCreationArchiveData: GroupedDataByProject<
|
||||||
|
InstanceInsightsSchema['creationArchiveTrends']
|
||||||
|
>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreationArchiveStats: FC<CreationArchiveStatsProps> = ({
|
||||||
|
groupedCreationArchiveData,
|
||||||
|
isLoading,
|
||||||
|
}) => {
|
||||||
|
const currentRatio = getCurrentArchiveRatio(groupedCreationArchiveData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledRatioContainer>
|
||||||
|
<StyledPercentageRow>
|
||||||
|
<StyledRatioTypography>
|
||||||
|
{isLoading ? '...' : `${currentRatio}%`}
|
||||||
|
</StyledRatioTypography>
|
||||||
|
<HelpIcon tooltip='Ratio of archived flags to created flags'>
|
||||||
|
<StyledInfoIcon />
|
||||||
|
</HelpIcon>
|
||||||
|
</StyledPercentageRow>
|
||||||
|
<Typography variant='body2'>Current ratio</Typography>
|
||||||
|
</StyledRatioContainer>
|
||||||
|
<StatsExplanation>
|
||||||
|
<Lightbulb color='primary' />
|
||||||
|
Do you create more flags than you archive? Or do you have good
|
||||||
|
process for cleaning up?
|
||||||
|
</StatsExplanation>
|
||||||
|
<StyledLink href='/search?lifecycle=IS:completed' variant='body2'>
|
||||||
|
View flags in cleanup stage
|
||||||
|
</StyledLink>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -28,6 +28,7 @@ import { useUiFlag } from 'hooks/useUiFlag';
|
|||||||
import { NewProductionFlagsChart } from '../componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx';
|
import { NewProductionFlagsChart } from '../componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx';
|
||||||
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
||||||
import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx';
|
import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx';
|
||||||
|
import { CreationArchiveStats } from '../componentsStat/CreationArchiveStats/CreationArchiveStats.tsx';
|
||||||
|
|
||||||
export const PerformanceInsights: FC = () => {
|
export const PerformanceInsights: FC = () => {
|
||||||
const statePrefix = 'performance-';
|
const statePrefix = 'performance-';
|
||||||
@ -116,11 +117,16 @@ export const PerformanceInsights: FC = () => {
|
|||||||
<StyledWidget>
|
<StyledWidget>
|
||||||
<StyledWidgetStats width={275}>
|
<StyledWidgetStats width={275}>
|
||||||
<WidgetTitle title='Flags created vs archived' />
|
<WidgetTitle title='Flags created vs archived' />
|
||||||
|
<CreationArchiveStats
|
||||||
|
groupedCreationArchiveData={
|
||||||
|
groupedCreationArchiveData
|
||||||
|
}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
</StyledWidgetStats>
|
</StyledWidgetStats>
|
||||||
<StyledChartContainer>
|
<StyledChartContainer>
|
||||||
<CreationArchiveChart
|
<CreationArchiveChart
|
||||||
creationArchiveTrends={groupedCreationArchiveData}
|
creationArchiveTrends={groupedCreationArchiveData}
|
||||||
isAggregate={showAllProjects}
|
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
/>
|
/>
|
||||||
</StyledChartContainer>
|
</StyledChartContainer>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user