mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: average time to production chart (#6565)
Adds live data to TimeToProductionChart and AverageTimeToProduction gauge. Create a custom tooltip Changes the interaction mode for tooltips as per @nicolaesocaciu pairing session Improvement: Extract grouping by project to its own hook (3 charts that needed grouped data where handling it independently. <img width="1331" alt="Screenshot 2024-03-14 at 17 19 07" src="https://github.com/Unleash/unleash/assets/104830839/199c556c-8264-46e3-9dd5-9a864588de1f"> Closes # [1-2143](https://linear.app/unleash/issue/1-2143/time-to-production-total-aggregation) --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
4d78c6dadf
commit
45634689f8
@ -26,6 +26,10 @@ import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/Project
|
||||
import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart';
|
||||
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
|
||||
import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart';
|
||||
import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction';
|
||||
import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart';
|
||||
import { useGroupedProjectTrends } from './hooks/useGroupedProjectTrends';
|
||||
import { useAvgTimeToProduction } from './hooks/useAvgTimeToProduction';
|
||||
|
||||
const StyledGrid = styled(Box)(({ theme }) => ({
|
||||
display: 'grid',
|
||||
@ -74,14 +78,21 @@ export const ExecutiveDashboard: VFC = () => {
|
||||
executiveDashboardData.projectFlagTrends,
|
||||
projects,
|
||||
);
|
||||
|
||||
const groupedProjectsData = useGroupedProjectTrends(projectsData);
|
||||
|
||||
const metricsData = useFilteredTrends(
|
||||
executiveDashboardData.metricsSummaryTrends,
|
||||
projects,
|
||||
);
|
||||
const groupedMetricsData = useGroupedProjectTrends(metricsData);
|
||||
|
||||
const { users, environmentTypeTrends } = executiveDashboardData;
|
||||
|
||||
const summary = useFilteredFlagsSummary(projectsData);
|
||||
|
||||
const avgDaysToProduction = useAvgTimeToProduction(groupedProjectsData);
|
||||
|
||||
const isOneProjectSelected = projects.length === 1;
|
||||
|
||||
const handleScroll = () => {
|
||||
@ -147,7 +158,7 @@ export const ExecutiveDashboard: VFC = () => {
|
||||
elseShow={
|
||||
<ChartWidget title='Users per project'>
|
||||
<UsersPerProjectChart
|
||||
projectFlagTrends={projectsData}
|
||||
projectFlagTrends={groupedProjectsData}
|
||||
/>
|
||||
</ChartWidget>
|
||||
}
|
||||
@ -178,7 +189,7 @@ export const ExecutiveDashboard: VFC = () => {
|
||||
elseShow={
|
||||
<ChartWidget title='Flags per project'>
|
||||
<FlagsProjectChart
|
||||
projectFlagTrends={projectsData}
|
||||
projectFlagTrends={groupedProjectsData}
|
||||
/>
|
||||
</ChartWidget>
|
||||
}
|
||||
@ -200,25 +211,32 @@ export const ExecutiveDashboard: VFC = () => {
|
||||
}
|
||||
>
|
||||
<ProjectHealthChart
|
||||
projectFlagTrends={projectsData}
|
||||
projectFlagTrends={groupedProjectsData}
|
||||
isAggregate={showAllProjects}
|
||||
/>
|
||||
</ChartWidget>
|
||||
{/* <Widget title='Average time to production'>
|
||||
<TimeToProduction
|
||||
//FIXME: data from API
|
||||
daysToProduction={5.2}
|
||||
/>
|
||||
<Widget
|
||||
title='Average time to production'
|
||||
tooltip='How long did it take on average from a feature toggle was created until it was enabled in an environment of type production. This is calculated only from feature toggles with the type of "release". '
|
||||
>
|
||||
<TimeToProduction daysToProduction={avgDaysToProduction} />
|
||||
</Widget>
|
||||
<ChartWidget title='Time to production'>
|
||||
<TimeToProductionChart projectFlagTrends={projectsData} />
|
||||
</ChartWidget> */}
|
||||
<ChartWidget
|
||||
title='Time to production'
|
||||
tooltip='How the average time to production changes over time'
|
||||
>
|
||||
<TimeToProductionChart
|
||||
projectFlagTrends={groupedProjectsData}
|
||||
/>
|
||||
</ChartWidget>
|
||||
</StyledGrid>
|
||||
<Widget
|
||||
title='Metrics'
|
||||
tooltip='Summary of all flag evaluations reported by SDKs.'
|
||||
>
|
||||
<MetricsSummaryChart metricsSummaryTrends={metricsData} />
|
||||
<MetricsSummaryChart
|
||||
metricsSummaryTrends={groupedMetricsData}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget
|
||||
title='Updates per environment type'
|
||||
|
@ -28,14 +28,18 @@ export const createOptions = (
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
position: 'nearest',
|
||||
interaction: {
|
||||
axis: 'xy',
|
||||
mode: 'nearest',
|
||||
},
|
||||
external: createTooltip(setTooltip),
|
||||
},
|
||||
},
|
||||
locale: locationSettings.locale,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
axis: 'x',
|
||||
mode: 'index',
|
||||
axis: 'xy',
|
||||
mode: 'nearest',
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
|
@ -4,9 +4,12 @@ import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
projectFlagTrends: GroupedDataByProject<
|
||||
ExecutiveSummarySchema['projectFlagTrends']
|
||||
>;
|
||||
}
|
||||
|
||||
export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
|
||||
|
@ -5,9 +5,12 @@ import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
|
||||
import { useMetricsSummary } from '../../hooks/useMetricsSummary';
|
||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||
|
||||
interface IMetricsSummaryChartProps {
|
||||
metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends'];
|
||||
metricsSummaryTrends: GroupedDataByProject<
|
||||
ExecutiveSummarySchema['metricsSummaryTrends']
|
||||
>;
|
||||
}
|
||||
|
||||
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
||||
@ -15,7 +18,7 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
||||
}) => {
|
||||
const data = useMetricsSummary(metricsSummaryTrends);
|
||||
const notEnoughData = useMemo(
|
||||
() => (data.datasets.some((d) => d.data.length > 1) ? false : true),
|
||||
() => !data.datasets.some((d) => d.data.length > 1),
|
||||
[data],
|
||||
);
|
||||
const placeholderData = usePlaceholderData();
|
||||
|
@ -8,13 +8,16 @@ import {
|
||||
NotEnoughData,
|
||||
} from 'component/executiveDashboard/components/LineChart/LineChart';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
interface IProjectHealthChartProps {
|
||||
projectFlagTrends: GroupedDataByProject<
|
||||
ExecutiveSummarySchema['projectFlagTrends']
|
||||
>;
|
||||
isAggregate?: boolean;
|
||||
}
|
||||
|
||||
export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
|
||||
export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({
|
||||
projectFlagTrends,
|
||||
isAggregate,
|
||||
}) => {
|
||||
|
@ -1,21 +1,33 @@
|
||||
import { type VFC } from 'react';
|
||||
import { useMemo, type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart } from '../../components/LineChart/LineChart';
|
||||
import { useProjectChartData } from '../../hooks/useProjectChartData';
|
||||
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||
import { usePlaceholderData } from '../../hooks/usePlaceholderData';
|
||||
import { TimeToProductionTooltip } from './TimeToProductionTooltip/TimeToProductionTooltip';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
interface ITimeToProductionChartProps {
|
||||
projectFlagTrends: GroupedDataByProject<
|
||||
ExecutiveSummarySchema['projectFlagTrends']
|
||||
>;
|
||||
}
|
||||
|
||||
export const TimeToProductionChart: VFC<IFlagsProjectChartProps> = ({
|
||||
export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
const data = useProjectChartData(projectFlagTrends);
|
||||
const notEnoughData = useMemo(
|
||||
() => !data.datasets.some((d) => d.data.length > 1),
|
||||
[data],
|
||||
);
|
||||
|
||||
const placeholderData = usePlaceholderData();
|
||||
return (
|
||||
<LineChart
|
||||
data={data}
|
||||
data={notEnoughData ? placeholderData : data}
|
||||
isLocalTooltip
|
||||
TooltipComponent={TimeToProductionTooltip}
|
||||
overrideOptions={{
|
||||
parsing: {
|
||||
yAxisKey: 'timeToProduction',
|
||||
|
@ -0,0 +1,117 @@
|
||||
import { type VFC } from 'react';
|
||||
import { type ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
||||
import { Box, Paper, Typography, styled } from '@mui/material';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { TooltipState } from '../../../components/LineChart/ChartTooltip/ChartTooltip';
|
||||
|
||||
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledItemHeader = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const getInterval = (days?: number) => {
|
||||
if (!days) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
if (days > 11) {
|
||||
const weeks = days / 7;
|
||||
if (weeks > 6) {
|
||||
const months = weeks / 4.34524;
|
||||
return `${months.toFixed(2)} months`;
|
||||
} else {
|
||||
return `${weeks.toFixed(1)} weeks`;
|
||||
}
|
||||
} else {
|
||||
return `${days} days`;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveBadge = (input?: number) => {
|
||||
const ONE_MONTH = 30;
|
||||
const ONE_WEEK = 7;
|
||||
|
||||
if (!input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (input >= ONE_MONTH) {
|
||||
return <Badge color='error'>Low</Badge>;
|
||||
}
|
||||
|
||||
if (input <= ONE_MONTH && input >= ONE_WEEK + 1) {
|
||||
return <Badge>Medium</Badge>;
|
||||
}
|
||||
|
||||
if (input <= ONE_WEEK) {
|
||||
return <Badge color='success'>High</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
export const TimeToProductionTooltip: VFC<{ tooltip: TooltipState | null }> = ({
|
||||
tooltip,
|
||||
}) => {
|
||||
const data = tooltip?.dataPoints.map((point) => {
|
||||
return {
|
||||
label: point.label,
|
||||
title: point.dataset.label,
|
||||
color: point.dataset.borderColor,
|
||||
value: point.raw as ExecutiveSummarySchemaProjectFlagTrendsItem,
|
||||
};
|
||||
});
|
||||
|
||||
const limitedData = data?.slice(0, 5);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
width: '300px',
|
||||
})}
|
||||
>
|
||||
{limitedData?.map((point, index) => (
|
||||
<StyledTooltipItemContainer
|
||||
elevation={3}
|
||||
key={`${point.title}-${index}`}
|
||||
>
|
||||
<StyledItemHeader>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='textSecondary'
|
||||
component='span'
|
||||
>
|
||||
{point.label}
|
||||
</Typography>
|
||||
</StyledItemHeader>
|
||||
<StyledItemHeader>
|
||||
<Typography variant='body2' component='span'>
|
||||
<Typography
|
||||
sx={{ color: point.color }}
|
||||
component='span'
|
||||
>
|
||||
{'● '}
|
||||
</Typography>
|
||||
<strong>{point.title}</strong>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='body2'
|
||||
component='span'
|
||||
sx={{ mr: (theme) => theme.spacing(1), pt: 0.25 }}
|
||||
>
|
||||
{getInterval(point.value.timeToProduction)}
|
||||
</Typography>
|
||||
{resolveBadge(point.value.timeToProduction)}
|
||||
</StyledItemHeader>
|
||||
</StyledTooltipItemContainer>
|
||||
)) || null}
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -4,9 +4,12 @@ import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||
|
||||
interface IUsersPerProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
projectFlagTrends: GroupedDataByProject<
|
||||
ExecutiveSummarySchema['projectFlagTrends']
|
||||
>;
|
||||
}
|
||||
|
||||
export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { useAvgTimeToProduction } from './useAvgTimeToProduction';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
describe('useAvgTimeToProduction', () => {
|
||||
test('returns 0 when projectsData is empty', () => {
|
||||
const projectsData = {};
|
||||
const { result } = renderHook(() =>
|
||||
useAvgTimeToProduction(projectsData),
|
||||
);
|
||||
expect(result.current).toBe(0);
|
||||
});
|
||||
|
||||
test('calculates result.current time to production correctly', () => {
|
||||
const projectsData = {
|
||||
project1: [{ timeToProduction: 10 }, { timeToProduction: 20 }],
|
||||
project2: [{ timeToProduction: 15 }, { timeToProduction: 25 }],
|
||||
} as any;
|
||||
const { result } = renderHook(() =>
|
||||
useAvgTimeToProduction(projectsData),
|
||||
);
|
||||
expect(result.current).toBe(17.5);
|
||||
});
|
||||
|
||||
test('ignores projects without time to production data', () => {
|
||||
const projectsData = {
|
||||
project1: [{ timeToProduction: 10 }, { timeToProduction: 20 }],
|
||||
project2: [],
|
||||
} as any;
|
||||
const { result } = renderHook(() =>
|
||||
useAvgTimeToProduction(projectsData),
|
||||
);
|
||||
expect(result.current).toBe(7.5);
|
||||
});
|
||||
});
|
@ -0,0 +1,36 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ExecutiveSummarySchema } from 'openapi';
|
||||
import type { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||
|
||||
export const useAvgTimeToProduction = (
|
||||
projectsData: GroupedDataByProject<
|
||||
ExecutiveSummarySchema['projectFlagTrends']
|
||||
>,
|
||||
) =>
|
||||
useMemo(() => {
|
||||
const totalProjects = Object.keys(projectsData).length;
|
||||
|
||||
if (totalProjects === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const totalAvgTimeToProduction = Object.entries(projectsData).reduce(
|
||||
(acc, [_, trends]) => {
|
||||
const validTrends = trends.filter(
|
||||
(trend) => trend.timeToProduction !== undefined,
|
||||
);
|
||||
const avgTimeToProduction =
|
||||
validTrends.reduce(
|
||||
(sum, item) => sum + (item.timeToProduction || 0),
|
||||
0,
|
||||
) / (validTrends.length || 1);
|
||||
|
||||
return acc + (validTrends.length > 0 ? avgTimeToProduction : 0);
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
const overallAverage = totalAvgTimeToProduction / totalProjects;
|
||||
|
||||
return overallAverage;
|
||||
}, [projectsData]);
|
@ -0,0 +1,62 @@
|
||||
import { useGroupedProjectTrends } from './useGroupedProjectTrends';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
describe('useGroupedProjectTrends', () => {
|
||||
test('returns an empty object when input data is empty', () => {
|
||||
const input: any[] = [];
|
||||
const { result } = renderHook(() => useGroupedProjectTrends(input));
|
||||
expect(result.current).toEqual({});
|
||||
});
|
||||
|
||||
test('groups data by project correctly', () => {
|
||||
const input = [
|
||||
{ project: 'project1', data: 'data1' },
|
||||
{ project: 'project2', data: 'data2' },
|
||||
{ project: 'project1', data: 'data3' },
|
||||
];
|
||||
const { result } = renderHook(() => useGroupedProjectTrends(input));
|
||||
expect(result.current).toEqual({
|
||||
project1: [
|
||||
{ project: 'project1', data: 'data1' },
|
||||
{ project: 'project1', data: 'data3' },
|
||||
],
|
||||
project2: [{ project: 'project2', data: 'data2' }],
|
||||
});
|
||||
});
|
||||
|
||||
test('groups complex data by project correctly', () => {
|
||||
const input = [
|
||||
{
|
||||
project: 'project1',
|
||||
data: { some: { complex: { type: 'data1' } } },
|
||||
},
|
||||
{
|
||||
project: 'project2',
|
||||
data: { some: { complex: { type: 'data2' } } },
|
||||
},
|
||||
{
|
||||
project: 'project1',
|
||||
data: { some: { complex: { type: 'data3' } } },
|
||||
},
|
||||
];
|
||||
const { result } = renderHook(() => useGroupedProjectTrends(input));
|
||||
expect(result.current).toEqual({
|
||||
project1: [
|
||||
{
|
||||
project: 'project1',
|
||||
data: { some: { complex: { type: 'data1' } } },
|
||||
},
|
||||
{
|
||||
project: 'project1',
|
||||
data: { some: { complex: { type: 'data3' } } },
|
||||
},
|
||||
],
|
||||
project2: [
|
||||
{
|
||||
project: 'project2',
|
||||
data: { some: { complex: { type: 'data2' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,35 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type GroupedDataByProject<T> = Record<string, T>;
|
||||
|
||||
export function groupDataByProject<T extends { project: string }>(
|
||||
data: T[],
|
||||
): GroupedDataByProject<T[]> {
|
||||
if (!data || data.length === 0 || !('project' in data[0])) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const groupedData: GroupedDataByProject<T[]> = {};
|
||||
|
||||
data.forEach((item) => {
|
||||
const { project } = item;
|
||||
if (!groupedData[project]) {
|
||||
groupedData[project] = [];
|
||||
}
|
||||
groupedData[project].push(item);
|
||||
});
|
||||
|
||||
return groupedData;
|
||||
}
|
||||
|
||||
export const useGroupedProjectTrends = <
|
||||
T extends {
|
||||
project: string;
|
||||
},
|
||||
>(
|
||||
input: T[],
|
||||
) =>
|
||||
useMemo<GroupedDataByProject<T[]>>(
|
||||
() => groupDataByProject<T>(input),
|
||||
[JSON.stringify(input)],
|
||||
);
|
@ -1,43 +1,19 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import {
|
||||
ExecutiveSummarySchema,
|
||||
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
|
||||
} from 'openapi';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { useProjectColor } from './useProjectColor';
|
||||
import { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||
|
||||
type MetricsSummaryTrends = ExecutiveSummarySchema['metricsSummaryTrends'];
|
||||
|
||||
type GroupedDataByProject = Record<
|
||||
string,
|
||||
ExecutiveSummarySchemaMetricsSummaryTrendsItem[]
|
||||
>;
|
||||
|
||||
function groupDataByProject(
|
||||
data: ExecutiveSummarySchemaMetricsSummaryTrendsItem[],
|
||||
): GroupedDataByProject {
|
||||
const groupedData: GroupedDataByProject = {};
|
||||
|
||||
data.forEach((item) => {
|
||||
const { project } = item;
|
||||
if (!groupedData[project]) {
|
||||
groupedData[project] = [];
|
||||
}
|
||||
groupedData[project].push(item);
|
||||
});
|
||||
|
||||
return groupedData;
|
||||
}
|
||||
|
||||
export const useMetricsSummary = (
|
||||
metricsSummaryTrends: MetricsSummaryTrends,
|
||||
metricsSummaryTrends: GroupedDataByProject<MetricsSummaryTrends>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const getProjectColor = useProjectColor();
|
||||
|
||||
const data = useMemo(() => {
|
||||
const groupedMetrics = groupDataByProject(metricsSummaryTrends);
|
||||
const datasets = Object.entries(groupedMetrics).map(
|
||||
const datasets = Object.entries(metricsSummaryTrends).map(
|
||||
([project, trends]) => {
|
||||
const color = getProjectColor(project);
|
||||
return {
|
||||
|
@ -1,29 +1,19 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
ExecutiveSummarySchema,
|
||||
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
||||
} from '../../../openapi';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { useProjectColor } from './useProjectColor';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||
|
||||
type ProjectFlagTrends = ExecutiveSummarySchema['projectFlagTrends'];
|
||||
|
||||
export const useProjectChartData = (projectFlagTrends: ProjectFlagTrends) => {
|
||||
export const useProjectChartData = (
|
||||
projectFlagTrends: GroupedDataByProject<ProjectFlagTrends>,
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
const getProjectColor = useProjectColor();
|
||||
|
||||
const data = useMemo(() => {
|
||||
const groupedFlagTrends = projectFlagTrends.reduce<
|
||||
Record<string, ExecutiveSummarySchemaProjectFlagTrendsItem[]>
|
||||
>((groups, item) => {
|
||||
if (!groups[item.project]) {
|
||||
groups[item.project] = [];
|
||||
}
|
||||
groups[item.project].push(item);
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
const datasets = Object.entries(groupedFlagTrends).map(
|
||||
const datasets = Object.entries(projectFlagTrends).map(
|
||||
([project, trends]) => {
|
||||
const color = getProjectColor(project);
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user