mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +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 { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart';
|
||||||
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
|
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
|
||||||
import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart';
|
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 }) => ({
|
const StyledGrid = styled(Box)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -74,14 +78,21 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
executiveDashboardData.projectFlagTrends,
|
executiveDashboardData.projectFlagTrends,
|
||||||
projects,
|
projects,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groupedProjectsData = useGroupedProjectTrends(projectsData);
|
||||||
|
|
||||||
const metricsData = useFilteredTrends(
|
const metricsData = useFilteredTrends(
|
||||||
executiveDashboardData.metricsSummaryTrends,
|
executiveDashboardData.metricsSummaryTrends,
|
||||||
projects,
|
projects,
|
||||||
);
|
);
|
||||||
|
const groupedMetricsData = useGroupedProjectTrends(metricsData);
|
||||||
|
|
||||||
const { users, environmentTypeTrends } = executiveDashboardData;
|
const { users, environmentTypeTrends } = executiveDashboardData;
|
||||||
|
|
||||||
const summary = useFilteredFlagsSummary(projectsData);
|
const summary = useFilteredFlagsSummary(projectsData);
|
||||||
|
|
||||||
|
const avgDaysToProduction = useAvgTimeToProduction(groupedProjectsData);
|
||||||
|
|
||||||
const isOneProjectSelected = projects.length === 1;
|
const isOneProjectSelected = projects.length === 1;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@ -147,7 +158,7 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
elseShow={
|
elseShow={
|
||||||
<ChartWidget title='Users per project'>
|
<ChartWidget title='Users per project'>
|
||||||
<UsersPerProjectChart
|
<UsersPerProjectChart
|
||||||
projectFlagTrends={projectsData}
|
projectFlagTrends={groupedProjectsData}
|
||||||
/>
|
/>
|
||||||
</ChartWidget>
|
</ChartWidget>
|
||||||
}
|
}
|
||||||
@ -178,7 +189,7 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
elseShow={
|
elseShow={
|
||||||
<ChartWidget title='Flags per project'>
|
<ChartWidget title='Flags per project'>
|
||||||
<FlagsProjectChart
|
<FlagsProjectChart
|
||||||
projectFlagTrends={projectsData}
|
projectFlagTrends={groupedProjectsData}
|
||||||
/>
|
/>
|
||||||
</ChartWidget>
|
</ChartWidget>
|
||||||
}
|
}
|
||||||
@ -200,25 +211,32 @@ export const ExecutiveDashboard: VFC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ProjectHealthChart
|
<ProjectHealthChart
|
||||||
projectFlagTrends={projectsData}
|
projectFlagTrends={groupedProjectsData}
|
||||||
isAggregate={showAllProjects}
|
isAggregate={showAllProjects}
|
||||||
/>
|
/>
|
||||||
</ChartWidget>
|
</ChartWidget>
|
||||||
{/* <Widget title='Average time to production'>
|
<Widget
|
||||||
<TimeToProduction
|
title='Average time to production'
|
||||||
//FIXME: data from API
|
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". '
|
||||||
daysToProduction={5.2}
|
>
|
||||||
/>
|
<TimeToProduction daysToProduction={avgDaysToProduction} />
|
||||||
</Widget>
|
</Widget>
|
||||||
<ChartWidget title='Time to production'>
|
<ChartWidget
|
||||||
<TimeToProductionChart projectFlagTrends={projectsData} />
|
title='Time to production'
|
||||||
</ChartWidget> */}
|
tooltip='How the average time to production changes over time'
|
||||||
|
>
|
||||||
|
<TimeToProductionChart
|
||||||
|
projectFlagTrends={groupedProjectsData}
|
||||||
|
/>
|
||||||
|
</ChartWidget>
|
||||||
</StyledGrid>
|
</StyledGrid>
|
||||||
<Widget
|
<Widget
|
||||||
title='Metrics'
|
title='Metrics'
|
||||||
tooltip='Summary of all flag evaluations reported by SDKs.'
|
tooltip='Summary of all flag evaluations reported by SDKs.'
|
||||||
>
|
>
|
||||||
<MetricsSummaryChart metricsSummaryTrends={metricsData} />
|
<MetricsSummaryChart
|
||||||
|
metricsSummaryTrends={groupedMetricsData}
|
||||||
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
<Widget
|
<Widget
|
||||||
title='Updates per environment type'
|
title='Updates per environment type'
|
||||||
|
@ -28,14 +28,18 @@ export const createOptions = (
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
position: 'nearest',
|
position: 'nearest',
|
||||||
|
interaction: {
|
||||||
|
axis: 'xy',
|
||||||
|
mode: 'nearest',
|
||||||
|
},
|
||||||
external: createTooltip(setTooltip),
|
external: createTooltip(setTooltip),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
locale: locationSettings.locale,
|
locale: locationSettings.locale,
|
||||||
interaction: {
|
interaction: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
axis: 'x',
|
axis: 'xy',
|
||||||
mode: 'index',
|
mode: 'nearest',
|
||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
point: {
|
point: {
|
||||||
|
@ -4,9 +4,12 @@ import { ExecutiveSummarySchema } from 'openapi';
|
|||||||
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||||
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
||||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||||
|
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||||
|
|
||||||
interface IFlagsProjectChartProps {
|
interface IFlagsProjectChartProps {
|
||||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
projectFlagTrends: GroupedDataByProject<
|
||||||
|
ExecutiveSummarySchema['projectFlagTrends']
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
|
export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
|
||||||
|
@ -5,9 +5,12 @@ import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
|||||||
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
|
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
|
||||||
import { useMetricsSummary } from '../../hooks/useMetricsSummary';
|
import { useMetricsSummary } from '../../hooks/useMetricsSummary';
|
||||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||||
|
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||||
|
|
||||||
interface IMetricsSummaryChartProps {
|
interface IMetricsSummaryChartProps {
|
||||||
metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends'];
|
metricsSummaryTrends: GroupedDataByProject<
|
||||||
|
ExecutiveSummarySchema['metricsSummaryTrends']
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
||||||
@ -15,7 +18,7 @@ export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const data = useMetricsSummary(metricsSummaryTrends);
|
const data = useMetricsSummary(metricsSummaryTrends);
|
||||||
const notEnoughData = useMemo(
|
const notEnoughData = useMemo(
|
||||||
() => (data.datasets.some((d) => d.data.length > 1) ? false : true),
|
() => !data.datasets.some((d) => d.data.length > 1),
|
||||||
[data],
|
[data],
|
||||||
);
|
);
|
||||||
const placeholderData = usePlaceholderData();
|
const placeholderData = usePlaceholderData();
|
||||||
|
@ -8,13 +8,16 @@ import {
|
|||||||
NotEnoughData,
|
NotEnoughData,
|
||||||
} from 'component/executiveDashboard/components/LineChart/LineChart';
|
} from 'component/executiveDashboard/components/LineChart/LineChart';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
|
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||||
|
|
||||||
interface IFlagsProjectChartProps {
|
interface IProjectHealthChartProps {
|
||||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
projectFlagTrends: GroupedDataByProject<
|
||||||
|
ExecutiveSummarySchema['projectFlagTrends']
|
||||||
|
>;
|
||||||
isAggregate?: boolean;
|
isAggregate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
|
export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({
|
||||||
projectFlagTrends,
|
projectFlagTrends,
|
||||||
isAggregate,
|
isAggregate,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -1,21 +1,33 @@
|
|||||||
import { type VFC } from 'react';
|
import { useMemo, type VFC } from 'react';
|
||||||
import 'chartjs-adapter-date-fns';
|
import 'chartjs-adapter-date-fns';
|
||||||
import { ExecutiveSummarySchema } from 'openapi';
|
import { ExecutiveSummarySchema } from 'openapi';
|
||||||
import { LineChart } from '../../components/LineChart/LineChart';
|
import { LineChart } from '../../components/LineChart/LineChart';
|
||||||
import { useProjectChartData } from '../../hooks/useProjectChartData';
|
import { useProjectChartData } from '../../hooks/useProjectChartData';
|
||||||
|
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||||
|
import { usePlaceholderData } from '../../hooks/usePlaceholderData';
|
||||||
|
import { TimeToProductionTooltip } from './TimeToProductionTooltip/TimeToProductionTooltip';
|
||||||
|
|
||||||
interface IFlagsProjectChartProps {
|
interface ITimeToProductionChartProps {
|
||||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
projectFlagTrends: GroupedDataByProject<
|
||||||
|
ExecutiveSummarySchema['projectFlagTrends']
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeToProductionChart: VFC<IFlagsProjectChartProps> = ({
|
export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({
|
||||||
projectFlagTrends,
|
projectFlagTrends,
|
||||||
}) => {
|
}) => {
|
||||||
const data = useProjectChartData(projectFlagTrends);
|
const data = useProjectChartData(projectFlagTrends);
|
||||||
|
const notEnoughData = useMemo(
|
||||||
|
() => !data.datasets.some((d) => d.data.length > 1),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholderData = usePlaceholderData();
|
||||||
return (
|
return (
|
||||||
<LineChart
|
<LineChart
|
||||||
data={data}
|
data={notEnoughData ? placeholderData : data}
|
||||||
isLocalTooltip
|
isLocalTooltip
|
||||||
|
TooltipComponent={TimeToProductionTooltip}
|
||||||
overrideOptions={{
|
overrideOptions={{
|
||||||
parsing: {
|
parsing: {
|
||||||
yAxisKey: 'timeToProduction',
|
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 { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
|
||||||
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
|
||||||
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
|
||||||
|
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
|
||||||
|
|
||||||
interface IUsersPerProjectChartProps {
|
interface IUsersPerProjectChartProps {
|
||||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
projectFlagTrends: GroupedDataByProject<
|
||||||
|
ExecutiveSummarySchema['projectFlagTrends']
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({
|
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 { useMemo } from 'react';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import {
|
import { ExecutiveSummarySchema } from 'openapi';
|
||||||
ExecutiveSummarySchema,
|
|
||||||
ExecutiveSummarySchemaMetricsSummaryTrendsItem,
|
|
||||||
} from 'openapi';
|
|
||||||
import { useProjectColor } from './useProjectColor';
|
import { useProjectColor } from './useProjectColor';
|
||||||
|
import { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||||
|
|
||||||
type MetricsSummaryTrends = ExecutiveSummarySchema['metricsSummaryTrends'];
|
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 = (
|
export const useMetricsSummary = (
|
||||||
metricsSummaryTrends: MetricsSummaryTrends,
|
metricsSummaryTrends: GroupedDataByProject<MetricsSummaryTrends>,
|
||||||
) => {
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const getProjectColor = useProjectColor();
|
const getProjectColor = useProjectColor();
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const groupedMetrics = groupDataByProject(metricsSummaryTrends);
|
const datasets = Object.entries(metricsSummaryTrends).map(
|
||||||
const datasets = Object.entries(groupedMetrics).map(
|
|
||||||
([project, trends]) => {
|
([project, trends]) => {
|
||||||
const color = getProjectColor(project);
|
const color = getProjectColor(project);
|
||||||
return {
|
return {
|
||||||
|
@ -1,29 +1,19 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import { ExecutiveSummarySchema } from 'openapi';
|
||||||
ExecutiveSummarySchema,
|
|
||||||
ExecutiveSummarySchemaProjectFlagTrendsItem,
|
|
||||||
} from '../../../openapi';
|
|
||||||
import { useProjectColor } from './useProjectColor';
|
import { useProjectColor } from './useProjectColor';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
|
import { GroupedDataByProject } from './useGroupedProjectTrends';
|
||||||
|
|
||||||
type ProjectFlagTrends = ExecutiveSummarySchema['projectFlagTrends'];
|
type ProjectFlagTrends = ExecutiveSummarySchema['projectFlagTrends'];
|
||||||
|
|
||||||
export const useProjectChartData = (projectFlagTrends: ProjectFlagTrends) => {
|
export const useProjectChartData = (
|
||||||
|
projectFlagTrends: GroupedDataByProject<ProjectFlagTrends>,
|
||||||
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const getProjectColor = useProjectColor();
|
const getProjectColor = useProjectColor();
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
const groupedFlagTrends = projectFlagTrends.reduce<
|
const datasets = Object.entries(projectFlagTrends).map(
|
||||||
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(
|
|
||||||
([project, trends]) => {
|
([project, trends]) => {
|
||||||
const color = getProjectColor(project);
|
const color = getProjectColor(project);
|
||||||
return {
|
return {
|
||||||
|
Loading…
Reference in New Issue
Block a user