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);
|
setTooltip(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTooltip({
|
setTooltip({
|
||||||
caretX: tooltip?.caretX,
|
caretX: tooltip?.caretX,
|
||||||
caretY: tooltip?.caretY,
|
caretY: tooltip?.caretY,
|
||||||
|
@ -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} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user