diff --git a/frontend/src/component/insights/InsightsCharts.tsx b/frontend/src/component/insights/InsightsCharts.tsx index 25ea374877..da2ff60780 100644 --- a/frontend/src/component/insights/InsightsCharts.tsx +++ b/frontend/src/component/insights/InsightsCharts.tsx @@ -49,6 +49,7 @@ interface IChartsProps { }; loading: boolean; projects: string[]; + allMetricsDatapoints: string[]; } const StyledGrid = styled(Box)(({ theme }) => ({ @@ -79,6 +80,7 @@ export const InsightsCharts: VFC = ({ flagTrends, groupedMetricsData, environmentTypeTrends, + allMetricsDatapoints, loading, }) => { const showAllProjects = projects[0] === allOption.id; @@ -203,6 +205,7 @@ export const InsightsCharts: VFC = ({ > diff --git a/frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx b/frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx index ebf35279c4..f48f6abd52 100644 --- a/frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx +++ b/frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx @@ -8,25 +8,30 @@ import { NotEnoughData, } from 'component/insights/components/LineChart/LineChart'; import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip'; -import { useMetricsSummary } from 'component/insights/hooks/useMetricsSummary'; import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData'; import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; import { useTheme } from '@mui/material'; -import { aggregateDataPerDate } from './MetricsChartTooltip/aggregate-metrics-by-day'; +import { aggregateDataPerDate } from './aggregate-metrics-by-day'; +import { useFilledMetricsSummary } from '../../hooks/useFilledMetricsSummary'; interface IMetricsSummaryChartProps { metricsSummaryTrends: GroupedDataByProject< InstanceInsightsSchema['metricsSummaryTrends'] >; isAggregate?: boolean; + allDatapointsSorted: string[]; } export const MetricsSummaryChart: VFC = ({ metricsSummaryTrends, isAggregate, + allDatapointsSorted, }) => { const theme = useTheme(); - const metricsSummary = useMetricsSummary(metricsSummaryTrends); + const metricsSummary = useFilledMetricsSummary( + metricsSummaryTrends, + allDatapointsSorted, + ); const notEnoughData = useMemo( () => !metricsSummary.datasets.some((d) => d.data.length > 1), [metricsSummary], diff --git a/frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.test.ts b/frontend/src/component/insights/componentsChart/MetricsSummaryChart/aggregate-metrics-by-day.test.ts similarity index 100% rename from frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.test.ts rename to frontend/src/component/insights/componentsChart/MetricsSummaryChart/aggregate-metrics-by-day.test.ts diff --git a/frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.ts b/frontend/src/component/insights/componentsChart/MetricsSummaryChart/aggregate-metrics-by-day.ts similarity index 100% rename from frontend/src/component/insights/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.ts rename to frontend/src/component/insights/componentsChart/MetricsSummaryChart/aggregate-metrics-by-day.ts diff --git a/frontend/src/component/insights/hooks/useAllDatapoints.ts b/frontend/src/component/insights/hooks/useAllDatapoints.ts new file mode 100644 index 0000000000..e8086dbbb4 --- /dev/null +++ b/frontend/src/component/insights/hooks/useAllDatapoints.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react'; + +export const useAllDatapoints = (data: T[]) => + useMemo(() => { + const allDataPoints = new Set(); + + data.forEach((item) => { + allDataPoints.add(item.date); + }); + + return Array.from(allDataPoints).sort(); + }, [data]); diff --git a/frontend/src/component/insights/hooks/useFilledMetricsSummary.test.ts b/frontend/src/component/insights/hooks/useFilledMetricsSummary.test.ts new file mode 100644 index 0000000000..4c3e1af814 --- /dev/null +++ b/frontend/src/component/insights/hooks/useFilledMetricsSummary.test.ts @@ -0,0 +1,54 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useTheme } from '@mui/material'; +import { useProjectColor } from './useProjectColor'; +import { useFilledMetricsSummary } from './useFilledMetricsSummary'; +import type { Theme } from '@mui/material/styles'; +import type { InstanceInsightsSchema } from '../../../openapi'; +import type { GroupedDataByProject } from './useGroupedProjectTrends'; +import { vi, type Mock } from 'vitest'; + +vi.mock('@mui/material', () => ({ + useTheme: vi.fn(), +})); + +vi.mock('./useProjectColor', () => ({ + useProjectColor: vi.fn(), +})); + +// Mock data +const mockTheme: Partial = {}; +const mockGetProjectColor = (project: string) => `color-${project}`; +const mockFilteredMetricsSummaryTrends = { + Project1: [{ date: '2024-01-01', totalRequests: 5 }], + Project2: [{ date: '2024-01-02', totalRequests: 10 }], +} as unknown as GroupedDataByProject< + InstanceInsightsSchema['metricsSummaryTrends'] +>; +const mockAllDataPointsSorted = ['2024-01-01', '2024-01-02']; + +beforeEach(() => { + (useTheme as Mock).mockReturnValue(mockTheme); + (useProjectColor as Mock).mockImplementation(() => mockGetProjectColor); +}); + +describe('useFilledMetricsSummary', () => { + it('returns datasets with normalized data for each project', () => { + const { result } = renderHook(() => + useFilledMetricsSummary( + mockFilteredMetricsSummaryTrends, + mockAllDataPointsSorted, + ), + ); + + expect(result.current.datasets).toHaveLength(2); // Expect two projects + expect(result.current.datasets[0].data).toHaveLength(2); // Each dataset should have data for both dates + + // Check for normalized missing data + const project1DataFor20240102 = result.current.datasets + .find((dataset) => dataset.label === 'Project1') + ?.data.find((data) => data.date === '2024-01-02'); + expect(project1DataFor20240102).toEqual( + expect.objectContaining({ totalRequests: 0 }), + ); // Missing data should be filled with 0 + }); +}); diff --git a/frontend/src/component/insights/hooks/useFilledMetricsSummary.ts b/frontend/src/component/insights/hooks/useFilledMetricsSummary.ts new file mode 100644 index 0000000000..5cf14b665c --- /dev/null +++ b/frontend/src/component/insights/hooks/useFilledMetricsSummary.ts @@ -0,0 +1,63 @@ +import { useMemo } from 'react'; +import { useTheme } from '@mui/material'; +import type { + InstanceInsightsSchema, + InstanceInsightsSchemaMetricsSummaryTrendsItem, +} from 'openapi'; +import { useProjectColor } from './useProjectColor'; +import type { GroupedDataByProject } from './useGroupedProjectTrends'; +import { format } from 'date-fns'; + +type MetricsSummaryTrends = InstanceInsightsSchema['metricsSummaryTrends']; + +const weekIdFromDate = (dateString: string) => { + return format(new Date(dateString), 'yyyy-ww'); +}; + +export const useFilledMetricsSummary = ( + filteredMetricsSummaryTrends: GroupedDataByProject, + allDataPointsSorted: string[], +) => { + const theme = useTheme(); + const getProjectColor = useProjectColor(); + + const data = useMemo(() => { + const datasets = Object.entries(filteredMetricsSummaryTrends).map( + ([project, trends]) => { + const trendsMap = new Map< + string, + InstanceInsightsSchemaMetricsSummaryTrendsItem + >(trends.map((trend) => [trend.date, trend])); + + const normalizedData = allDataPointsSorted.map((date) => { + return ( + trendsMap.get(date) || { + date, + totalRequests: 0, + totalNo: 0, + project, + totalApps: 0, + totalYes: 0, + totalEnvironments: 0, + totalFlags: 0, + week: weekIdFromDate(date), + } + ); + }); + + const color = getProjectColor(project); + return { + label: project, + data: normalizedData, + borderColor: color, + backgroundColor: color, + fill: false, + }; + }, + ); + + return { datasets }; + }, [theme, filteredMetricsSummaryTrends, getProjectColor]); + + return data; +}; diff --git a/frontend/src/component/insights/hooks/useInsightsData.ts b/frontend/src/component/insights/hooks/useInsightsData.ts index 7c1b72b1b6..b203e93500 100644 --- a/frontend/src/component/insights/hooks/useInsightsData.ts +++ b/frontend/src/component/insights/hooks/useInsightsData.ts @@ -3,42 +3,47 @@ import type { InstanceInsightsSchema } from 'openapi'; import { useFilteredTrends } from './useFilteredTrends'; import { useGroupedProjectTrends } from './useGroupedProjectTrends'; import { useFilteredFlagsSummary } from './useFilteredFlagsSummary'; +import { useAllDatapoints } from './useAllDatapoints'; export const useInsightsData = ( - executiveDashboardData: InstanceInsightsSchema, + instanceInsights: InstanceInsightsSchema, projects: string[], ) => { + const allMetricsDatapoints = useAllDatapoints( + instanceInsights.metricsSummaryTrends, + ); const projectsData = useFilteredTrends( - executiveDashboardData.projectFlagTrends, + instanceInsights.projectFlagTrends, projects, ); const groupedProjectsData = useGroupedProjectTrends(projectsData); const metricsData = useFilteredTrends( - executiveDashboardData.metricsSummaryTrends, + instanceInsights.metricsSummaryTrends, projects, ); const groupedMetricsData = useGroupedProjectTrends(metricsData); const summary = useFilteredFlagsSummary( projectsData, - executiveDashboardData.users, + instanceInsights.users, ); return useMemo( () => ({ - ...executiveDashboardData, + ...instanceInsights, projectsData, groupedProjectsData, metricsData, groupedMetricsData, - users: executiveDashboardData.users, - environmentTypeTrends: executiveDashboardData.environmentTypeTrends, + users: instanceInsights.users, + environmentTypeTrends: instanceInsights.environmentTypeTrends, summary, + allMetricsDatapoints, }), [ - executiveDashboardData, + instanceInsights, projects, projectsData, groupedProjectsData, diff --git a/frontend/src/component/insights/hooks/useMetricsSummary.ts b/frontend/src/component/insights/hooks/useMetricsSummary.ts deleted file mode 100644 index 935c271116..0000000000 --- a/frontend/src/component/insights/hooks/useMetricsSummary.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useMemo } from 'react'; -import { useTheme } from '@mui/material'; -import type { InstanceInsightsSchema } from 'openapi'; -import { useProjectColor } from './useProjectColor'; -import type { GroupedDataByProject } from './useGroupedProjectTrends'; - -type MetricsSummaryTrends = InstanceInsightsSchema['metricsSummaryTrends']; - -export const useMetricsSummary = ( - metricsSummaryTrends: GroupedDataByProject, -) => { - const theme = useTheme(); - const getProjectColor = useProjectColor(); - - const data = useMemo(() => { - const datasets = Object.entries(metricsSummaryTrends).map( - ([project, trends]) => { - const color = getProjectColor(project); - return { - label: project, - data: trends, - borderColor: color, - backgroundColor: color, - fill: false, - }; - }, - ); - return { datasets }; - }, [theme, metricsSummaryTrends]); - - return data; -};