1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-01 13:47:27 +02:00

feat: create flags created vs archived chart

This commit is contained in:
sjaanus 2025-07-29 15:36:57 +03:00
parent 8943cc0a3d
commit df01f53aed
No known key found for this signature in database
GPG Key ID: 20E007C0248BA7FF
3 changed files with 262 additions and 34 deletions

View File

@ -12,7 +12,6 @@ export const createTooltip =
setTooltip(null); setTooltip(null);
return; return;
} }
setTooltip({ setTooltip({
caretX: tooltip?.caretX, caretX: tooltip?.caretX,
caretY: tooltip?.caretY, caretY: tooltip?.caretY,

View File

@ -1,15 +1,43 @@
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';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
BarElement,
TimeScale,
Tooltip,
Legend,
Filler,
);
interface ICreationArchiveChartProps { interface ICreationArchiveChartProps {
creationArchiveTrends: GroupedDataByProject< creationArchiveTrends: GroupedDataByProject<
@ -22,6 +50,8 @@ interface ICreationArchiveChartProps {
type WeekData = { type WeekData = {
archivedFlags: number; archivedFlags: number;
totalCreatedFlags: number; totalCreatedFlags: number;
createdFlagsByType: Record<string, number>;
archivePercentage: number;
week: string; week: string;
date?: string; date?: string;
}; };
@ -41,6 +71,8 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
const creationArchiveData = useProjectChartData(creationArchiveTrends); const creationArchiveData = 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 aggregateHealthData = useMemo(() => {
const labels: string[] = Array.from( const labels: string[] = Array.from(
@ -51,6 +83,18 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
), ),
); );
// Get all unique flag types
const allFlagTypes = new Set<string>();
creationArchiveData.datasets.forEach((d) =>
d.data.forEach((item: any) => {
if (item.createdFlags) {
Object.keys(item.createdFlags).forEach((type) =>
allFlagTypes.add(type),
);
}
}),
);
const weeks: WeekData[] = labels const weeks: WeekData[] = labels
.map((label) => { .map((label) => {
return creationArchiveData.datasets return creationArchiveData.datasets
@ -59,14 +103,17 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
(acc: WeekData, item: RawWeekData) => { (acc: WeekData, item: RawWeekData) => {
if (item) { if (item) {
acc.archivedFlags += item.archivedFlags || 0; acc.archivedFlags += item.archivedFlags || 0;
const createdFlagsSum = item.createdFlags
? Object.values(item.createdFlags).reduce( if (item.createdFlags) {
(sum: number, count: number) => Object.entries(item.createdFlags).forEach(
sum + count, ([type, count]) => {
0, acc.createdFlagsByType[type] =
) (acc.createdFlagsByType[type] ||
: 0; 0) + count;
acc.totalCreatedFlags += createdFlagsSum; acc.totalCreatedFlags += count;
},
);
}
} }
if (!acc.date) { if (!acc.date) {
acc.date = item?.date; acc.date = item?.date;
@ -76,20 +123,74 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
{ {
archivedFlags: 0, archivedFlags: 0,
totalCreatedFlags: 0, totalCreatedFlags: 0,
createdFlagsByType: {},
archivePercentage: 0,
week: label, week: label,
} as WeekData, } 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));
// Create datasets for each flag type
const flagTypeColors = [
theme.palette.success.border,
theme.palette.success.main,
theme.palette.success.dark,
'#4D8007',
'#7D935E',
];
const flagTypeDatasets = Array.from(allFlagTypes).map(
(flagType, index) => ({
label: `Created: ${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,
}),
);
return { return {
datasets: [ datasets: [
{ {
label: 'Number of created flags', label: 'Archived flags',
data: weeks,
backgroundColor: theme.palette.background.application,
borderColor: theme.palette.background.application,
type: 'bar' as const,
parsing: { yAxisKey: 'archivedFlags', xAxisKey: 'date' },
yAxisID: 'y',
stack: 'archived',
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, fill: false,
order: 3, type: 'line' as const,
parsing: {
yAxisKey: 'archivePercentage',
xAxisKey: 'date',
},
yAxisID: 'y1',
order: 1,
}, },
], ],
}; };
@ -108,20 +209,110 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
notEnoughData || isLoading ? placeholderData : aggregateOrProjectData; notEnoughData || isLoading ? placeholderData : aggregateOrProjectData;
return ( return (
<LineChart <>
key={isAggregate ? 'aggregate' : 'project'} <Chart
data={data} key={isAggregate ? 'aggregate' : 'project'}
overrideOptions={ type='bar'
notEnoughData data={data as any}
? {} options={{
: { responsive: true,
parsing: { plugins: {
yAxisKey: 'totalCreatedFlags', legend: {
xAxisKey: 'date', position: 'bottom' as const,
}, labels: {
} filter: (legendItem) => {
} // Hide individual created flag type labels
cover={notEnoughData ? <NotEnoughData /> : isLoading} return !legendItem.text?.startsWith(
/> 'Created:',
);
},
generateLabels: (chart) => {
const original =
ChartJS.defaults.plugins.legend.labels.generateLabels(
chart,
);
const filtered = original.filter(
(item) =>
!item.text?.startsWith('Created:'),
);
// Add custom "Created Flags" legend item
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,
interaction: {
intersect: false,
axis: 'xy',
mode: 'nearest',
},
scales: {
x: {
type: 'time',
time: {
unit: 'week',
tooltipFormat: 'PPP',
},
stacked: true,
},
y: {
type: 'linear',
display: true,
position: 'left',
beginAtZero: true,
stacked: true,
title: {
display: true,
text: 'Number of Flags',
},
},
y1: {
type: 'linear',
display: true,
position: 'right',
beginAtZero: true,
title: {
display: true,
text: 'Archive Percentage (%)',
},
grid: {
drawOnChartArea: false,
},
},
},
}}
height={100}
width={250}
/>
{tooltip?.dataPoints?.some((point) =>
point.dataset.label?.startsWith('Created:'),
) ? (
<CreationArchiveTooltip tooltip={tooltip} />
) : tooltip?.dataPoints?.some(
(point) =>
point.dataset.label === 'Flags archived / Flags created',
) ? (
<CreationArchiveRatioTooltip tooltip={tooltip} />
) : (
<ChartTooltip tooltip={tooltip} />
)}
</>
); );
}; };

View File

@ -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-';
@ -80,6 +81,39 @@ export const PerformanceInsights: FC = () => {
: flagsPerUserCalculation.toFixed(2); : flagsPerUserCalculation.toFixed(2);
} }
// Calculate current archive ratio from latest data
function getCurrentArchiveRatio() {
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 currentRatio = getCurrentArchiveRatio();
const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs'); const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs');
return ( return (
@ -116,6 +150,10 @@ 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
currentRatio={currentRatio}
isLoading={loading}
/>
</StyledWidgetStats> </StyledWidgetStats>
<StyledChartContainer> <StyledChartContainer>
<CreationArchiveChart <CreationArchiveChart