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);
return;
}
setTooltip({
caretX: tooltip?.caretX,
caretY: tooltip?.caretY,

View File

@ -1,15 +1,43 @@
import 'chartjs-adapter-date-fns';
import { type FC, useMemo } from 'react';
import { type FC, useMemo, useState } from 'react';
import type { InstanceInsightsSchema } from 'openapi';
import { useProjectChartData } from 'component/insights/hooks/useProjectChartData';
import {
fillGradientPrimary,
LineChart,
NotEnoughData,
} from 'component/insights/components/LineChart/LineChart';
import { useTheme } from '@mui/material';
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
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 {
creationArchiveTrends: GroupedDataByProject<
@ -22,6 +50,8 @@ interface ICreationArchiveChartProps {
type WeekData = {
archivedFlags: number;
totalCreatedFlags: number;
createdFlagsByType: Record<string, number>;
archivePercentage: number;
week: string;
date?: string;
};
@ -41,6 +71,8 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
const creationArchiveData = useProjectChartData(creationArchiveTrends);
const theme = useTheme();
const placeholderData = usePlaceholderData();
const { locationSettings } = useLocationSettings();
const [tooltip, setTooltip] = useState<null | TooltipState>(null);
const aggregateHealthData = useMemo(() => {
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
.map((label) => {
return creationArchiveData.datasets
@ -59,14 +103,17 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
(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 (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;
@ -76,20 +123,74 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
{
archivedFlags: 0,
totalCreatedFlags: 0,
createdFlagsByType: {},
archivePercentage: 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));
// 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 {
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,
borderColor: theme.palette.primary.light,
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
backgroundColor: theme.palette.primary.light,
fill: false,
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;
return (
<LineChart
key={isAggregate ? 'aggregate' : 'project'}
data={data}
overrideOptions={
notEnoughData
? {}
: {
parsing: {
yAxisKey: 'totalCreatedFlags',
xAxisKey: 'date',
},
}
}
cover={notEnoughData ? <NotEnoughData /> : isLoading}
/>
<>
<Chart
key={isAggregate ? 'aggregate' : 'project'}
type='bar'
data={data as any}
options={{
responsive: true,
plugins: {
legend: {
position: 'bottom' as const,
labels: {
filter: (legendItem) => {
// Hide individual created flag type labels
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 Lightbulb from '@mui/icons-material/LightbulbOutlined';
import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx';
import { CreationArchiveStats } from '../componentsStat/CreationArchiveStats/CreationArchiveStats.tsx';
export const PerformanceInsights: FC = () => {
const statePrefix = 'performance-';
@ -80,6 +81,39 @@ export const PerformanceInsights: FC = () => {
: 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');
return (
@ -116,6 +150,10 @@ export const PerformanceInsights: FC = () => {
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags created vs archived' />
<CreationArchiveStats
currentRatio={currentRatio}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<CreationArchiveChart