diff --git a/frontend/src/component/executiveDashboard/Charts.tsx b/frontend/src/component/executiveDashboard/Charts.tsx index 85343f89b71..a5ddf79c59c 100644 --- a/frontend/src/component/executiveDashboard/Charts.tsx +++ b/frontend/src/component/executiveDashboard/Charts.tsx @@ -41,7 +41,7 @@ interface IChartsProps { averageHealth?: string; flagsPerUser?: string; }; - avgDaysToProduction: number; + medianTimeToProduction: number; loading: boolean; projects: string[]; } @@ -71,7 +71,7 @@ export const Charts: VFC = ({ userTrends, groupedProjectsData, flagTrends, - avgDaysToProduction, + medianTimeToProduction, groupedMetricsData, environmentTypeTrends, loading, @@ -165,8 +165,10 @@ export const Charts: VFC = ({ isAggregate={showAllProjects} /> - - + + { - test('returns 0 when projectsData is empty', () => { - const projectsData = {}; - const { result } = renderHook(() => - useAvgTimeToProduction(projectsData), - ); - expect(result.current).toBe(0); - }); - - test('calculates average time to production based on the latest date correctly', () => { - const projectsData = { - project1: [ - { timeToProduction: 10, date: '2023-01-01' }, - { timeToProduction: 20, date: '2023-02-01' }, - ], - project2: [ - { timeToProduction: 15, date: '2023-01-15' }, - { timeToProduction: 25, date: '2023-02-15' }, - ], - } as any; - const { result } = renderHook(() => - useAvgTimeToProduction(projectsData), - ); - // Expect average of the latest timeToProductions (20 from project1 and 25 from project2) - expect(result.current).toBe(22.5); - }); - - test('ignores projects without time to production data in their latest entries', () => { - const projectsData = { - project1: [ - { timeToProduction: 10, date: '2023-01-01' }, - { timeToProduction: 20, date: '2023-02-01' }, - ], - project2: [ - { date: '2023-01-15' }, - { timeToProduction: 25, date: '2023-01-10' }, - ], - } as any; - const { result } = renderHook(() => - useAvgTimeToProduction(projectsData), - ); - // Since project2's latest entry doesn't have timeToProduction, only project1's latest is considered - expect(result.current).toBe(20); - }); -}); diff --git a/frontend/src/component/executiveDashboard/hooks/useAvgTimeToProduction.ts b/frontend/src/component/executiveDashboard/hooks/useAvgTimeToProduction.ts deleted file mode 100644 index 8697d883fca..00000000000 --- a/frontend/src/component/executiveDashboard/hooks/useAvgTimeToProduction.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useMemo } from 'react'; -import type { - ExecutiveSummarySchema, - ExecutiveSummarySchemaProjectFlagTrendsItem, -} from 'openapi'; -import type { GroupedDataByProject } from './useGroupedProjectTrends'; - -const validTrend = (trend: ExecutiveSummarySchemaProjectFlagTrendsItem) => - Boolean(trend) && Boolean(trend.timeToProduction); - -export const useAvgTimeToProduction = ( - projectsData: GroupedDataByProject< - ExecutiveSummarySchema['projectFlagTrends'] - >, -) => - useMemo(() => { - let totalProjects = Object.keys(projectsData).length; - - if (totalProjects === 0) { - return 0; - } - - const totalAvgTimeToProduction = Object.entries(projectsData).reduce( - (acc, [_, trends]) => { - const latestTrend = trends.reduce( - (latest, current) => - new Date(latest.date) < new Date(current.date) - ? current - : latest, - trends[0], - ); - - // If there's no valid latest trend, this project won't contribute to the average - if (!validTrend(latestTrend)) { - totalProjects--; - return acc; - } - - const timeToProduction = latestTrend.timeToProduction || 0; - return acc + timeToProduction; - }, - 0, - ); - - const overallAverage = totalAvgTimeToProduction / totalProjects; - - return overallAverage; - }, [projectsData]); diff --git a/frontend/src/component/executiveDashboard/hooks/useDashboardData.ts b/frontend/src/component/executiveDashboard/hooks/useDashboardData.ts index 50cf54fb649..ade24fdaf30 100644 --- a/frontend/src/component/executiveDashboard/hooks/useDashboardData.ts +++ b/frontend/src/component/executiveDashboard/hooks/useDashboardData.ts @@ -3,7 +3,7 @@ import type { ExecutiveSummarySchema } from 'openapi'; import { useFilteredTrends } from './useFilteredTrends'; import { useGroupedProjectTrends } from './useGroupedProjectTrends'; import { useFilteredFlagsSummary } from './useFilteredFlagsSummary'; -import { useAvgTimeToProduction } from './useAvgTimeToProduction'; +import { useMedianTimeToProduction } from './useMedianTimeToProduction'; export const useDashboardData = ( executiveDashboardData: ExecutiveSummarySchema, @@ -27,7 +27,8 @@ export const useDashboardData = ( executiveDashboardData.users, ); - const avgDaysToProduction = useAvgTimeToProduction(groupedProjectsData); + const medianTimeToProduction = + useMedianTimeToProduction(groupedProjectsData); return useMemo( () => ({ @@ -39,7 +40,7 @@ export const useDashboardData = ( users: executiveDashboardData.users, environmentTypeTrends: executiveDashboardData.environmentTypeTrends, summary, - avgDaysToProduction, + medianTimeToProduction, }), [ executiveDashboardData, @@ -49,7 +50,7 @@ export const useDashboardData = ( metricsData, groupedMetricsData, summary, - avgDaysToProduction, + medianTimeToProduction, ], ); }; diff --git a/frontend/src/component/executiveDashboard/hooks/useMedianTimeToProduction.test.ts b/frontend/src/component/executiveDashboard/hooks/useMedianTimeToProduction.test.ts new file mode 100644 index 00000000000..9ea99efbade --- /dev/null +++ b/frontend/src/component/executiveDashboard/hooks/useMedianTimeToProduction.test.ts @@ -0,0 +1,63 @@ +import { useMedianTimeToProduction } from './useMedianTimeToProduction'; +import { renderHook } from '@testing-library/react-hooks'; + +describe('useMedianTimeToProduction', () => { + test('returns 0 when projectsData is empty', () => { + const projectsData = {}; + const { result } = renderHook(() => + useMedianTimeToProduction(projectsData), + ); + expect(result.current).toBe(0); + }); + + test('calculates median time to production correctly with even number of projects', () => { + const projectsData = { + project1: [ + { timeToProduction: 10, date: '2023-01-01' }, + { timeToProduction: 20, date: '2023-02-01' }, + ], + project2: [ + { timeToProduction: 15, date: '2023-01-15' }, + { timeToProduction: 25, date: '2023-02-15' }, + ], + project3: [{ timeToProduction: 30, date: '2023-01-20' }], + } as any; + const { result } = renderHook(() => + useMedianTimeToProduction(projectsData), + ); + // With sorted timeToProductions [10, 15, 20, 25, 30], median is the middle value, 20. + expect(result.current).toBe(20); + }); + + test('calculates median time to production correctly with odd number of projects', () => { + const projectsData = { + project1: [ + { timeToProduction: 10, date: '2023-01-01' }, + { timeToProduction: 20, date: '2023-02-01' }, + ], + project2: [{ timeToProduction: 15, date: '2023-01-15' }], + } as any; + const { result } = renderHook(() => + useMedianTimeToProduction(projectsData), + ); + // With sorted timeToProductions [10, 15, 20], median is the middle value, 15. + expect(result.current).toBe(15); + }); + + test('correctly handles all valid time to production values', () => { + const projectsData = { + project1: [{ timeToProduction: 10, date: '2023-01-01' }], + project2: [{ timeToProduction: 25, date: '2023-01-10' }], + project3: [{ date: '2023-01-15' }], + project4: [ + { timeToProduction: 30, date: '2023-02-01' }, + { timeToProduction: 20, date: '2023-02-02' }, + ], + } as any; + const { result } = renderHook(() => + useMedianTimeToProduction(projectsData), + ); + // With sorted timeToProductions [10, 20, 25, 30], the median is the average of 20 and 25, i.e., 22.5. + expect(result.current).toBe(22.5); + }); +}); diff --git a/frontend/src/component/executiveDashboard/hooks/useMedianTimeToProduction.ts b/frontend/src/component/executiveDashboard/hooks/useMedianTimeToProduction.ts new file mode 100644 index 00000000000..e4d689da1ae --- /dev/null +++ b/frontend/src/component/executiveDashboard/hooks/useMedianTimeToProduction.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'react'; +import type { + ExecutiveSummarySchema, + ExecutiveSummarySchemaProjectFlagTrendsItem, +} from 'openapi'; +import type { GroupedDataByProject } from './useGroupedProjectTrends'; + +const validTrend = (trend: ExecutiveSummarySchemaProjectFlagTrendsItem) => + Boolean(trend) && Boolean(trend.timeToProduction); + +export const useMedianTimeToProduction = ( + projectsData: GroupedDataByProject< + ExecutiveSummarySchema['projectFlagTrends'] + >, +) => + useMemo(() => { + const timesToProduction: number[] = []; + + Object.values(projectsData).forEach((trends) => { + trends.forEach((trend) => { + if (validTrend(trend)) { + timesToProduction.push(trend.timeToProduction!); + } + }); + }); + + if (timesToProduction.length === 0) { + return 0; + } + + timesToProduction.sort((a, b) => a - b); + + const midIndex = Math.floor(timesToProduction.length / 2); + + const median = + timesToProduction.length % 2 === 0 + ? (timesToProduction[midIndex - 1] + + timesToProduction[midIndex]) / + 2 + : timesToProduction[midIndex]; + + return median; + }, [projectsData]);