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:
parent
8943cc0a3d
commit
df01f53aed
@ -12,7 +12,6 @@ export const createTooltip =
|
||||
setTooltip(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltip({
|
||||
caretX: tooltip?.caretX,
|
||||
caretY: tooltip?.caretY,
|
||||
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user