1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: display median calculation (#10763)

Adds the median calculation to the new flags in production widget and
allows the median to take batched data into account:

<img width="1288" height="530" alt="image"
src="https://github.com/user-attachments/assets/5052dad1-03fa-4ce6-8626-ff4f6a5d818f"
/>

To achieve this, it was necessary to extract the data aggregation method
from the NewProductionFlagsChart component, so that we could use the
data (to calculate the median) at a higher level in the tree. Because
the data is now passed around outside of the chart component, I've also
updated the `ChartDataResult` type to a proper union type that contains
the state and the data.
This commit is contained in:
Thomas Heartman 2025-10-20 13:49:42 +02:00 committed by GitHub
parent b18b128e52
commit ca4ec203c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 391 additions and 255 deletions

View File

@ -30,6 +30,8 @@ import { GraphCover } from 'component/insights/GraphCover.tsx';
import { batchCreationArchiveData } from './batchCreationArchiveData.ts'; import { batchCreationArchiveData } from './batchCreationArchiveData.ts';
import { useBatchedTooltipDate } from '../useBatchedTooltipDate.ts'; import { useBatchedTooltipDate } from '../useBatchedTooltipDate.ts';
import { aggregateCreationArchiveData } from './aggregateCreationArchiveData.ts'; import { aggregateCreationArchiveData } from './aggregateCreationArchiveData.ts';
import type { Theme } from '@mui/material/styles/createTheme';
import type { ChartData } from '../chartData.ts';
ChartJS.register( ChartJS.register(
CategoryScale, CategoryScale,
@ -51,6 +53,42 @@ interface ICreationArchiveChartProps {
labels: { week: string; date: string }[]; labels: { week: string; date: string }[];
} }
const batchSize = 4;
const sharedDatasetOptions = (lineColor: string) => ({
backgroundColor: lineColor,
borderColor: lineColor,
hoverBackgroundColor: lineColor,
hoverBorderColor: lineColor,
});
const makeChartData = (
data: FinalizedWeekData[] | BatchedWeekData[],
theme: Theme,
) => ({
datasets: [
{
label: 'Flags archived',
data: data,
order: 1,
parsing: {
yAxisKey: 'archivedFlags',
xAxisKey: 'date',
},
...sharedDatasetOptions(theme.palette.charts.A2),
},
{
label: 'Flags created',
data: data,
order: 2,
parsing: {
yAxisKey: 'totalCreatedFlags',
xAxisKey: 'date',
},
...sharedDatasetOptions(theme.palette.charts.A1),
},
],
});
export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
creationArchiveTrends, creationArchiveTrends,
isLoading, isLoading,
@ -61,62 +99,42 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
const { locationSettings } = useLocationSettings(); const { locationSettings } = useLocationSettings();
const [tooltip, setTooltip] = useState<null | TooltipState>(null); const [tooltip, setTooltip] = useState<null | TooltipState>(null);
const { dataResult, aggregateOrProjectData } = useMemo(() => { const chartData: ChartData<FinalizedWeekData | BatchedWeekData> =
const weeklyData = aggregateCreationArchiveData( useMemo(() => {
labels, if (isLoading) {
creationVsArchivedChart.datasets, return { state: 'Loading', value: placeholderData };
); }
let dataResult: ChartDataResult = 'Weekly'; const weeklyData = aggregateCreationArchiveData(
let displayData: FinalizedWeekData[] | (BatchedWeekData | null)[] = labels,
weeklyData; creationVsArchivedChart.datasets,
);
if (weeklyData.length < 2) { if (weeklyData.length < 2) {
dataResult = 'Not Enough Data'; return { state: 'Not Enough Data', value: placeholderData };
} else if (weeklyData.length >= 12) { }
dataResult = 'Batched';
displayData = batchCreationArchiveData(weeklyData);
}
return { if (weeklyData.length >= 12) {
dataResult, return {
aggregateOrProjectData: { state: 'Batched',
datasets: [ batchSize,
{ value: makeChartData(
label: 'Flags archived', batchCreationArchiveData(weeklyData, batchSize),
data: displayData, theme,
backgroundColor: theme.palette.charts.A2, ),
borderColor: theme.palette.charts.A2, };
hoverBackgroundColor: theme.palette.charts.A2, }
hoverBorderColor: theme.palette.charts.A2,
parsing: {
yAxisKey: 'archivedFlags',
xAxisKey: 'date',
},
order: 1,
},
{
label: 'Flags created',
data: displayData,
backgroundColor: theme.palette.charts.A1,
borderColor: theme.palette.charts.A1,
hoverBackgroundColor: theme.palette.charts.A1,
hoverBorderColor: theme.palette.charts.A1,
parsing: {
yAxisKey: 'totalCreatedFlags',
xAxisKey: 'date',
},
order: 2,
},
],
},
};
}, [creationVsArchivedChart, theme]);
const useGraphCover = dataResult === 'Not Enough Data' || isLoading; return {
const showNotEnoughDataText = state: 'Weekly',
dataResult === 'Not Enough Data' && !isLoading; value: makeChartData(weeklyData, theme),
const data = useGraphCover ? placeholderData : aggregateOrProjectData; };
}, [creationVsArchivedChart, theme]);
const useGraphCover = ['Loading', 'Not Enough Data'].includes(
chartData.state,
);
const showNotEnoughDataText = chartData.state === 'Not Enough Data';
const locale = getDateFnsLocale(locationSettings.locale); const locale = getDateFnsLocale(locationSettings.locale);
const batchedTooltipTitle = useBatchedTooltipDate(); const batchedTooltipTitle = useBatchedTooltipDate();
@ -153,7 +171,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
external: createTooltip(setTooltip), external: createTooltip(setTooltip),
callbacks: { callbacks: {
title: title:
dataResult === 'Batched' chartData.state === 'Batched'
? batchedTooltipTitle ? batchedTooltipTitle
: undefined, : undefined,
}, },
@ -171,7 +189,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
display: true, display: true,
time: { time: {
unit: unit:
dataResult === 'Batched' chartData.state === 'Batched'
? ('month' as const) ? ('month' as const)
: ('week' as const), : ('week' as const),
tooltipFormat: 'P', tooltipFormat: 'P',
@ -181,7 +199,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
}, },
ticks: { ticks: {
source: source:
dataResult === 'Batched' chartData.state === 'Batched'
? ('auto' as const) ? ('auto' as const)
: ('data' as const), : ('data' as const),
display: !useGraphCover, display: !useGraphCover,
@ -208,7 +226,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
return ( return (
<> <>
<Bar <Bar
data={data} data={chartData.value}
options={options} options={options}
height={100} height={100}
width={250} width={250}

View File

@ -6,7 +6,7 @@ import type {
BatchedWeekDataWithRatio, BatchedWeekDataWithRatio,
} from './types.ts'; } from './types.ts';
export const batchCreationArchiveData = batchData({ const batchArgs = (batchSize?: number) => ({
merge: (accumulated: BatchedWeekData, next: FinalizedWeekData) => { merge: (accumulated: BatchedWeekData, next: FinalizedWeekData) => {
if (next.state === 'empty') { if (next.state === 'empty') {
return { return {
@ -53,4 +53,10 @@ export const batchCreationArchiveData = batchData({
endDate: item.date, endDate: item.date,
}; };
}, },
batchSize,
}); });
export const batchCreationArchiveData = (
data: FinalizedWeekData[],
batchSize?: number,
) => batchData(batchArgs(batchSize))(data);

View File

@ -1,29 +1,18 @@
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { type FC, useMemo } from 'react'; import type { FC } from 'react';
import type { InstanceInsightsSchema } from 'openapi';
import { useProjectChartData } from 'component/insights/hooks/useProjectChartData';
import { import {
fillGradientPrimary,
LineChart, LineChart,
NotEnoughData, NotEnoughData,
} from 'component/insights/components/LineChart/LineChart'; } 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 type { BatchedWeekData, WeekData } from './types.ts';
import { batchProductionFlagsData } from './batchProductionFlagsData.ts';
import { useBatchedTooltipDate } from '../useBatchedTooltipDate.ts'; import { useBatchedTooltipDate } from '../useBatchedTooltipDate.ts';
import type { WeekData } from './types.ts';
import type { ChartData } from '../chartData.ts';
interface IProjectHealthChartProps { interface INewProductionFlagsChartProps {
lifecycleTrends: GroupedDataByProject< chartData: ChartData<WeekData, number>;
InstanceInsightsSchema['lifecycleTrends']
>;
isAggregate?: boolean;
isLoading?: boolean;
labels: { week: string; date: string }[];
} }
const useOverrideOptions = (chartDataResult: ChartDataResult) => { const useOverrideOptions = (chartData: ChartData<WeekData, number>) => {
const batchedTooltipTitle = useBatchedTooltipDate(); const batchedTooltipTitle = useBatchedTooltipDate();
const sharedOptions = { const sharedOptions = {
parsing: { parsing: {
@ -31,7 +20,7 @@ const useOverrideOptions = (chartDataResult: ChartDataResult) => {
xAxisKey: 'date', xAxisKey: 'date',
}, },
}; };
switch (chartDataResult) { switch (chartData.state) {
case 'Batched': { case 'Batched': {
return { return {
...sharedOptions, ...sharedOptions,
@ -58,112 +47,27 @@ const useOverrideOptions = (chartDataResult: ChartDataResult) => {
case 'Weekly': case 'Weekly':
return sharedOptions; return sharedOptions;
case 'Not Enough Data': case 'Not Enough Data':
case 'Loading':
return {}; return {};
} }
}; };
export const NewProductionFlagsChart: FC<IProjectHealthChartProps> = ({ export const NewProductionFlagsChart: FC<INewProductionFlagsChartProps> = ({
labels, chartData,
lifecycleTrends,
isAggregate,
isLoading,
}) => { }) => {
const lifecycleData = useProjectChartData(lifecycleTrends); const overrideOptions = useOverrideOptions(chartData);
const theme = useTheme();
const placeholderData = usePlaceholderData();
const shouldBatchData = labels.length >= 12;
const { aggregateProductionFlagsData, chartDataResult } = useMemo(() => {
const weeks: WeekData[] = labels.map(({ week, date }) => {
return lifecycleData.datasets
.map((d) => d.data.find((item) => item.week === week))
.reduce(
(acc: WeekData, item: WeekData) => {
if (item) {
acc.newProductionFlags =
(acc.newProductionFlags ?? 0) +
(item.newProductionFlags ?? 0);
}
return acc;
},
{ date, week },
);
});
let chartDataResult: ChartDataResult = 'Weekly';
let displayData: WeekData[] | (BatchedWeekData | null)[] = weeks;
if (!isLoading && labels.length < 2) {
chartDataResult = 'Not Enough Data';
} else if (shouldBatchData) {
chartDataResult = 'Batched';
displayData = batchProductionFlagsData(weeks);
}
return {
chartDataResult,
aggregateProductionFlagsData: {
datasets: [
{
label: 'Number of new flags',
data: displayData,
borderColor: theme.palette.primary.light,
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
},
],
},
};
}, [lifecycleData, theme, isLoading]);
const padProjectData = () => {
if (lifecycleData.datasets.length === 0) {
// fallback for when there's no data in the selected time period for the selected projects
return [
{
label: 'No data',
data: labels,
},
];
}
const padData = (data: WeekData[]) => {
const padded = labels.map(
({ date, week }) =>
data.find((item) => item?.week === week) ?? {
date,
week,
},
);
return shouldBatchData ? batchProductionFlagsData(padded) : padded;
};
return lifecycleData.datasets.map(({ data, ...dataset }) => ({
...dataset,
data: padData(data),
}));
};
const aggregateOrProjectData = isAggregate
? aggregateProductionFlagsData
: {
datasets: padProjectData(),
};
const notEnoughData = chartDataResult === 'Not Enough Data';
const data =
notEnoughData || isLoading ? placeholderData : aggregateOrProjectData;
const overrideOptions = useOverrideOptions(chartDataResult);
return ( return (
<LineChart <LineChart
key={isAggregate ? 'aggregate' : 'project'} data={chartData.value}
data={data}
overrideOptions={overrideOptions} overrideOptions={overrideOptions}
cover={notEnoughData ? <NotEnoughData /> : isLoading} cover={
chartData.state === 'Not Enough Data' ? (
<NotEnoughData />
) : (
chartData.state === 'Loading'
)
}
/> />
); );
}; };

View File

@ -11,6 +11,7 @@ it('handles a single data point', () => {
newProductionFlags: 5, newProductionFlags: 5,
date: input.date, date: input.date,
endDate: input.date, endDate: input.date,
week: '50',
}, },
]); ]);
}); });
@ -33,6 +34,7 @@ it('adds data in the expected way', () => {
newProductionFlags: 11, newProductionFlags: 11,
date: '2022-01-01', date: '2022-01-01',
endDate: '2022-02-01', endDate: '2022-02-01',
week: '50',
}, },
]); ]);
}); });

View File

@ -1,7 +1,7 @@
import { batchData } from '../batchData.ts'; import { batchData } from '../batchData.ts';
import type { BatchedWeekData, WeekData } from './types.ts'; import type { BatchedWeekData, WeekData } from './types.ts';
export const batchProductionFlagsData = batchData({ const batchArgs = (batchSize?: number) => ({
merge: (accumulated: BatchedWeekData, next: WeekData) => { merge: (accumulated: BatchedWeekData, next: WeekData) => {
if (next.newProductionFlags) if (next.newProductionFlags)
accumulated.newProductionFlags = accumulated.newProductionFlags =
@ -16,11 +16,15 @@ export const batchProductionFlagsData = batchData({
return accumulated; return accumulated;
}, },
map: (item: WeekData) => { map: (item: WeekData) => {
const { week: _, ...shared } = item;
return { return {
...shared, ...item,
endDate: item.date, endDate: item.date,
}; };
}, },
batchSize,
}); });
export const batchProductionFlagsData = (
data: WeekData[],
batchSize?: number,
): BatchedWeekData[] => batchData(batchArgs(batchSize))(data);

View File

@ -4,6 +4,6 @@ export type WeekData = {
date: string; date: string;
}; };
export type BatchedWeekData = Omit<WeekData, 'week'> & { export type BatchedWeekData = WeekData & {
endDate: string; endDate: string;
}; };

View File

@ -0,0 +1,169 @@
import { useTheme } from '@mui/material';
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
import { useProjectChartData } from 'component/insights/hooks/useProjectChartData';
import type { InstanceInsightsSchema } from 'openapi';
import type { BatchedWeekData, WeekData } from './types.ts';
import { useMemo } from 'react';
import { batchProductionFlagsData } from './batchProductionFlagsData.ts';
import { fillGradientPrimary } from 'component/insights/components/LineChart/LineChart';
import { calculateMedian } from 'component/insights/componentsStat/NewProductionFlagsStats/calculateMedian.ts';
import { hasRealData, type ChartData } from '../chartData.ts';
import type { ChartData as ChartJsData } from 'chart.js';
const batchSize = 4;
type UseProductionFlagsDataProps = {
groupedLifecycleData: GroupedDataByProject<
InstanceInsightsSchema['lifecycleTrends']
>;
showAllProjects?: boolean;
loading?: boolean;
labels: { week: string; date: string }[];
};
type UseProductionFlagsDataResult = {
chartData: ChartData<WeekData, number>;
median: string | number;
};
const padProjectData = ({
labels,
lifecycleData,
shouldBatchData,
}: {
labels: UseProductionFlagsDataProps['labels'];
lifecycleData: ChartJsData<'line', WeekData[]>;
shouldBatchData: boolean;
}) => {
if (lifecycleData.datasets.length === 0) {
// fallback for when there's no data in the selected time period for the selected projects
return [
{
label: 'No data',
data: labels,
},
];
}
const padData = (data: WeekData[]) => {
const padded = labels.map(
({ date, week }) =>
data.find((item) => item?.week === week) ?? {
date,
week,
},
);
return shouldBatchData
? batchProductionFlagsData(padded, batchSize)
: padded;
};
return lifecycleData.datasets.map(({ data, ...dataset }) => ({
...dataset,
data: padData(data),
}));
};
export const useProductionFlagsData = ({
groupedLifecycleData,
showAllProjects,
loading,
labels,
}: UseProductionFlagsDataProps): UseProductionFlagsDataResult => {
const lifecycleData = useProjectChartData(groupedLifecycleData);
const theme = useTheme();
const placeholderData = usePlaceholderData();
const shouldBatchData = labels.length >= 12;
const chartData = useMemo((): ChartData<
WeekData | BatchedWeekData,
number
> => {
if (loading) {
return {
state: 'Loading',
value: placeholderData,
};
}
if (labels.length < 2) {
return {
state: 'Not Enough Data',
value: placeholderData,
};
}
const weeks: WeekData[] = labels.map(({ week, date }) => {
return lifecycleData.datasets
.map((d) => d.data.find((item: WeekData) => item.week === week))
.reduce(
(acc: WeekData, item: WeekData) => {
if (item && item.newProductionFlags !== undefined) {
acc.newProductionFlags =
(acc.newProductionFlags ?? 0) +
item.newProductionFlags;
}
return acc;
},
{ date, week },
);
});
const chartArgs = (data: WeekData[] | BatchedWeekData[]) => ({
datasets: [
{
label: 'Number of new flags',
data,
borderColor: theme.palette.primary.light,
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
},
],
});
if (shouldBatchData) {
return {
state: 'Batched',
value: chartArgs(batchProductionFlagsData(weeks, batchSize)),
batchSize: batchSize,
};
}
return {
state: 'Weekly',
value: chartArgs(weeks),
};
}, [lifecycleData, theme, loading]);
if (!hasRealData(chartData)) {
return {
chartData,
median: 'N/A',
};
}
const { value: allProjectsData, ...metadata } = chartData;
const value = showAllProjects
? allProjectsData
: {
datasets: padProjectData({
labels,
lifecycleData,
shouldBatchData,
}),
};
const median = calculateMedian(value.datasets);
return {
chartData: {
...metadata,
value,
},
median,
};
};

View File

@ -13,38 +13,3 @@ it('batches by 4, starting from the first entry', () => {
), ),
).toStrictEqual([50, 23]); ).toStrictEqual([50, 23]);
}); });
describe('null entry handling', () => {
it('creates a new entry from the next item, if the accumulated is null', () => {
const input = [null, 9];
const result = batchData<number, number>({
merge: (x, y) => x * y,
map: Math.sqrt,
})(input);
expect(result).toStrictEqual([3]);
});
it('merges the accumulated entry with the next item if they are both present', () => {
const input = [4, 9];
const result = batchData<number, number>({
merge: (x, y) => x + y,
map: (x) => x,
})(input);
expect(result).toStrictEqual([13]);
});
it('it returns null if both the accumulated and the next item are null', () => {
const input = [null, null];
const result = batchData<number, number>({
merge: (x, y) => x + y,
map: (x) => x,
})(input);
expect(result).toStrictEqual([null]);
});
it('it returns the accumulated entry if the next item is null', () => {
const input = [7, null];
const result = batchData<number, number>({
merge: (x, y) => x * y,
map: (x) => x,
})(input);
expect(result).toStrictEqual([7]);
});
});

View File

@ -6,33 +6,22 @@ export type BatchDataOptions<T, TBatched> = {
batchSize?: number; batchSize?: number;
}; };
const nullOrUndefined = (value: any): value is null | undefined =>
value === null || value === undefined;
export const batchData = export const batchData =
<T, TBatched>({ <T, TBatched>({
merge, merge,
map, map,
batchSize = defaultBatchSize, batchSize = defaultBatchSize,
}: BatchDataOptions<T, TBatched>) => }: BatchDataOptions<T, TBatched>) =>
(xs: (T | null)[]): (TBatched | null)[] => (xs: T[]): TBatched[] =>
xs.reduce( xs.reduce((acc, curr, index) => {
(acc, curr, index) => { const currentAggregatedIndex = Math.floor(index / batchSize);
const currentAggregatedIndex = Math.floor(index / batchSize); const data = acc[currentAggregatedIndex];
const data = acc[currentAggregatedIndex];
const hasData = !nullOrUndefined(data); if (data) {
const hasCurr = curr !== null; acc[currentAggregatedIndex] = merge(data, curr);
} else {
acc[currentAggregatedIndex] = map(curr);
}
if (!hasData && !hasCurr) { return acc;
acc[currentAggregatedIndex] = null; }, [] as TBatched[]);
} else if (hasData && hasCurr) {
acc[currentAggregatedIndex] = merge(data, curr);
} else if (!hasData && hasCurr) {
acc[currentAggregatedIndex] = map(curr);
}
return acc;
},
[] as (TBatched | null)[],
);

View File

@ -0,0 +1,33 @@
import type { ChartTypeRegistry, ChartData as ChartJsData } from 'chart.js';
export type ChartData<
T,
TPlaceholder = T,
TChartType extends keyof ChartTypeRegistry = any,
> =
| { state: 'Loading'; value: ChartJsData<TChartType, TPlaceholder[]> }
| {
state: 'Not Enough Data';
value: ChartJsData<TChartType, TPlaceholder[]>;
}
| { state: 'Weekly'; value: ChartJsData<TChartType, T[]> }
| {
state: 'Batched';
batchSize: number;
value: ChartJsData<TChartType, T[]>;
};
export const hasRealData = <
T,
TPlaceholder = T,
TChartType extends keyof ChartTypeRegistry = any,
>(
chartDataStatus: ChartData<T, TPlaceholder, TChartType>,
): chartDataStatus is
| { state: 'Weekly'; value: ChartJsData<TChartType, T[]> }
| {
state: 'Batched';
batchSize: number;
value: ChartJsData<TChartType, T[]>;
} =>
chartDataStatus.state === 'Weekly' || chartDataStatus.state === 'Batched';

View File

@ -1 +0,0 @@
type ChartDataResult = 'Not Enough Data' | 'Batched' | 'Weekly';

View File

@ -0,0 +1,28 @@
import type { FC } from 'react';
import { StatCard } from '../shared/StatCard.tsx';
import type { ChartData } from 'component/insights/componentsChart/chartData.ts';
interface NewProductionFlagsStatsProps {
median: number | string;
chartData: ChartData<unknown, unknown>;
}
export const NewProductionFlagsStats: FC<NewProductionFlagsStatsProps> = ({
median,
chartData,
}) => {
const label =
chartData.state === 'Batched'
? `Median per ${chartData.batchSize} weeks`
: 'Median per week';
return (
<StatCard
value={median}
label={label}
tooltip='The median number of flags that have entered production for the selected time range.'
explanation='How often do flags go live in production?'
isLoading={chartData.state === 'Loading'}
/>
);
};

View File

@ -1,6 +1,7 @@
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { fillGradientPrimary } from '../components/LineChart/LineChart.jsx'; import { fillGradientPrimary } from '../components/LineChart/LineChart.jsx';
import type { ChartData } from 'chart.js';
type PlaceholderDataOptions = { type PlaceholderDataOptions = {
fill?: boolean; fill?: boolean;
@ -9,7 +10,7 @@ type PlaceholderDataOptions = {
export const usePlaceholderData = ( export const usePlaceholderData = (
placeholderDataOptions?: PlaceholderDataOptions, placeholderDataOptions?: PlaceholderDataOptions,
) => { ): ChartData<'line', number[]> => {
const { fill = false, type = 'constant' } = placeholderDataOptions || {}; const { fill = false, type = 'constant' } = placeholderDataOptions || {};
const theme = useTheme(); const theme = useTheme();

View File

@ -22,13 +22,42 @@ import {
StyledWidget, StyledWidget,
StyledWidgetContent, StyledWidgetContent,
StyledWidgetStats, StyledWidgetStats,
StatsExplanation,
} from '../InsightsCharts.styles'; } from '../InsightsCharts.styles';
import { useUiFlag } from 'hooks/useUiFlag'; 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 { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx'; import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx';
import { CreationArchiveStats } from '../componentsStat/CreationArchiveStats/CreationArchiveStats.tsx'; import { CreationArchiveStats } from '../componentsStat/CreationArchiveStats/CreationArchiveStats.tsx';
import { NewProductionFlagsStats } from '../componentsStat/NewProductionFlagsStats/NewProductionFlagsStats.tsx';
import { useProductionFlagsData } from '../componentsChart/NewProductionFlagsChart/useNewProductionFlagsData.ts';
const NewProductionFlagsWidget = ({
groupedLifecycleData,
loading,
showAllProjects,
labels,
}) => {
const { median, chartData } = useProductionFlagsData({
groupedLifecycleData,
labels,
loading,
showAllProjects,
});
return (
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='New flags in production' />
<NewProductionFlagsStats
chartData={chartData}
median={median}
/>
</StyledWidgetStats>
<StyledChartContainer>
<NewProductionFlagsChart chartData={chartData} />
</StyledChartContainer>
</StyledWidget>
);
};
export const PerformanceInsights: FC = () => { export const PerformanceInsights: FC = () => {
const statePrefix = 'performance-'; const statePrefix = 'performance-';
@ -85,23 +114,12 @@ export const PerformanceInsights: FC = () => {
} }
> >
{isLifecycleGraphsEnabled && isEnterprise() ? ( {isLifecycleGraphsEnabled && isEnterprise() ? (
<StyledWidget> <NewProductionFlagsWidget
<StyledWidgetStats width={275}> groupedLifecycleData={groupedLifecycleData}
<WidgetTitle title='New flags in production' /> loading={loading}
<StatsExplanation> showAllProjects={showAllProjects}
<Lightbulb color='primary' /> labels={insights.labels}
How often do flags go live in production? />
</StatsExplanation>
</StyledWidgetStats>
<StyledChartContainer>
<NewProductionFlagsChart
labels={insights.labels}
lifecycleTrends={groupedLifecycleData}
isAggregate={showAllProjects}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
) : null} ) : null}
{isLifecycleGraphsEnabled && isEnterprise() ? ( {isLifecycleGraphsEnabled && isEnterprise() ? (