From c126ae130ddc85d6654c0622e1f2d44569d9b8c3 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Wed, 20 Mar 2024 09:24:56 +0200 Subject: [PATCH] fix: insights UI improvements and aggreated TTP (#6584) Various ui enhancements Aggregates the time to production and metrics summary by averaging by date across all projects to get the value. Creates a single dataset for the aggregation. This makes theme behave like eg the Health chart (showing aggregated graph when show all projects and per project when not) Gradient fill when all projects across all related charts Attached recording with generated data for 3 months https://github.com/Unleash/unleash/assets/104830839/7acd80a8-b799-4a35-9a2e-bf3798f56d32 --------- Signed-off-by: andreas-unleash --- .../component/executiveDashboard/Charts.tsx | 218 ++++++++++++++++++ .../executiveDashboard/ExecutiveDashboard.tsx | 211 ++--------------- .../MetricsChartTooltip.tsx | 42 ++-- .../aggregate-metrics-by-day.test.ts | 165 +++++++++++++ .../aggregate-metrics-by-day.ts | 33 +++ .../MetricsSummaryChart.tsx | 58 ++++- .../ProjectHealthChart/ProjectHealthChart.tsx | 4 +- .../TimeToProductionChart.tsx | 89 ++++++- .../TimeToProductionTooltip.tsx | 2 +- .../UpdatesPerEnvironmentTypeChart.tsx | 17 +- .../hooks/useDashboardData.ts | 43 ++++ 11 files changed, 644 insertions(+), 238 deletions(-) create mode 100644 frontend/src/component/executiveDashboard/Charts.tsx create mode 100644 frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.test.ts create mode 100644 frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.ts create mode 100644 frontend/src/component/executiveDashboard/hooks/useDashboardData.ts diff --git a/frontend/src/component/executiveDashboard/Charts.tsx b/frontend/src/component/executiveDashboard/Charts.tsx new file mode 100644 index 0000000000..2369b0d724 --- /dev/null +++ b/frontend/src/component/executiveDashboard/Charts.tsx @@ -0,0 +1,218 @@ +import { ConditionallyRender } from '../common/ConditionallyRender/ConditionallyRender'; +import { Widget } from './components/Widget/Widget'; +import { UserStats } from './componentsStat/UserStats/UserStats'; +import { UsersChart } from './componentsChart/UsersChart/UsersChart'; +import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart'; +import { FlagStats } from './componentsStat/FlagStats/FlagStats'; +import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart'; +import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart'; +import { HealthStats } from './componentsStat/HealthStats/HealthStats'; +import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart'; +import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction'; +import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart'; +import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart'; +import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart'; +import type { ExecutiveSummarySchema } from '../../openapi'; +import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends'; +import { Box, styled } from '@mui/material'; +import { allOption } from '../common/ProjectSelect/ProjectSelect'; +import type { VFC } from 'react'; + +interface IChartsProps { + flagTrends: ExecutiveSummarySchema['flagTrends']; + projectsData: ExecutiveSummarySchema['projectFlagTrends']; + groupedProjectsData: GroupedDataByProject< + ExecutiveSummarySchema['projectFlagTrends'] + >; + metricsData: ExecutiveSummarySchema['metricsSummaryTrends']; + groupedMetricsData: GroupedDataByProject< + ExecutiveSummarySchema['metricsSummaryTrends'] + >; + users: ExecutiveSummarySchema['users']; + userTrends: ExecutiveSummarySchema['userTrends']; + environmentTypeTrends: ExecutiveSummarySchema['environmentTypeTrends']; + summary: { + total: number; + active: number; + stale: number; + potentiallyStale: number; + averageUsers: number; + averageHealth?: string; + }; + avgDaysToProduction: number; + loading: boolean; + projects: string[]; +} + +const StyledGrid = styled(Box)(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: `repeat(2, 1fr)`, + gridAutoRows: 'auto', + gap: theme.spacing(2), + paddingBottom: theme.spacing(2), + [theme.breakpoints.up('md')]: { + gridTemplateColumns: `300px 1fr`, + }, +})); + +const ChartWidget = styled(Widget)(({ theme }) => ({ + [theme.breakpoints.down('md')]: { + gridColumnStart: 'span 2', + order: 2, + }, +})); + +export const Charts: VFC = ({ + projects, + users, + summary, + userTrends, + groupedProjectsData, + flagTrends, + avgDaysToProduction, + groupedMetricsData, + environmentTypeTrends, + loading, +}) => { + const showAllProjects = projects[0] === allOption.id; + const isOneProjectSelected = projects.length === 1; + + return ( + <> + + + + + } + elseShow={ + + + + } + /> + + + + } + elseShow={ + + + + } + /> + + + + + + + } + elseShow={ + + + + } + /> + + + + + + + + + + + + + + + + + theme.spacing(2) }} + > + + + + ); +}; diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx index b8f0c9e30c..f72968298f 100644 --- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx +++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx @@ -1,64 +1,26 @@ import { useState, type VFC } from 'react'; import { Box, styled } from '@mui/material'; import { ArrayParam, withDefault } from 'use-query-params'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { usePersistentTableState } from 'hooks/usePersistentTableState'; import { allOption, ProjectSelect, } from 'component/common/ProjectSelect/ProjectSelect'; import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary'; - -import { useFilteredFlagsSummary } from './hooks/useFilteredFlagsSummary'; -import { useFilteredTrends } from './hooks/useFilteredTrends'; - -import { Widget } from './components/Widget/Widget'; import { DashboardHeader } from './components/DashboardHeader/DashboardHeader'; +import { useDashboardData } from './hooks/useDashboardData'; +import { Charts } from './Charts'; -import { UserStats } from './componentsStat/UserStats/UserStats'; -import { FlagStats } from './componentsStat/FlagStats/FlagStats'; -import { HealthStats } from './componentsStat/HealthStats/HealthStats'; - -import { UsersChart } from './componentsChart/UsersChart/UsersChart'; -import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart'; -import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart'; -import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart'; -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', - gridTemplateColumns: `repeat(2, 1fr)`, - gridAutoRows: 'auto', - gap: theme.spacing(2), - paddingBottom: theme.spacing(2), - [theme.breakpoints.up('md')]: { - gridTemplateColumns: `300px 1fr`, - }, -})); - -const ChartWidget = styled(Widget)(({ theme }) => ({ - [theme.breakpoints.down('md')]: { - gridColumnStart: 'span 2', - order: 2, - }, -})); - -const StickyWrapper = styled(Box, { - shouldForwardProp: (prop) => prop !== 'scrolled', -})<{ scrolled?: boolean }>(({ theme, scrolled }) => ({ - position: 'sticky', - top: 0, - zIndex: 1000, - padding: scrolled ? theme.spacing(2, 0) : theme.spacing(0, 0, 2), - background: theme.palette.background.application, - transition: 'padding 0.3s ease', -})); +const StickyWrapper = styled(Box)<{ scrolled?: boolean }>( + ({ theme, scrolled }) => ({ + position: 'sticky', + top: 0, + zIndex: 1000, + padding: scrolled ? theme.spacing(2, 0) : theme.spacing(0, 0, 2), + background: theme.palette.background.application, + transition: 'padding 0.3s ease', + }), +); export const ExecutiveDashboard: VFC = () => { const [scrolled, setScrolled] = useState(false); @@ -73,27 +35,8 @@ export const ExecutiveDashboard: VFC = () => { const projects = state.projects ? (state.projects.filter(Boolean) as string[]) : []; - const showAllProjects = projects[0] === allOption.id; - const projectsData = useFilteredTrends( - 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 dashboardData = useDashboardData(executiveDashboardData, projects); const handleScroll = () => { if (!scrolled && window.scrollY > 0) { @@ -121,133 +64,7 @@ export const ExecutiveDashboard: VFC = () => { } /> - - - - - } - elseShow={ - - - - } - /> - - - - } - elseShow={ - - - - } - /> - - - - - - - } - elseShow={ - - - - } - /> - - - - - - - - - - - - - - - - - theme.spacing(2) }} - > - - + ); }; diff --git a/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/MetricsChartTooltip.tsx b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/MetricsChartTooltip.tsx index 316eab6c0a..4d0ace90cb 100644 --- a/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/MetricsChartTooltip.tsx +++ b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/MetricsChartTooltip.tsx @@ -35,24 +35,28 @@ const InfoLine = ({ ); -const InfoSummary = ({ data }: { data: { key: string; value: number }[] }) => ( +const InfoSummary = ({ + data, +}: { data: { key: string; value: string | number }[] }) => ( - {data.map(({ key, value }) => ( -
-
- - {key} - + {data + .filter(({ value }) => value !== 'N/A') + .map(({ key, value }) => ( +
+
+ + {key} + +
+
{value}
-
{value}
-
- ))} + ))} ); @@ -131,15 +135,15 @@ export const MetricsSummaryTooltip: VFC<{ tooltip: TooltipState | null }> = ({ data={[ { key: 'Flags', - value: point.value.totalFlags ?? 0, + value: point.value.totalFlags ?? 'N/A', }, { key: 'Environments', - value: point.value.totalEnvironments ?? 0, + value: point.value.totalEnvironments ?? 'N/A', }, { key: 'Apps', - value: point.value.totalApps ?? 0, + value: point.value.totalApps ?? 'N/A', }, ]} /> diff --git a/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.test.ts b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.test.ts new file mode 100644 index 0000000000..b7d0fbbfaa --- /dev/null +++ b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.test.ts @@ -0,0 +1,165 @@ +import { aggregateDataPerDate } from './aggregate-metrics-by-day'; + +describe('aggregateDataPerDate', () => { + it('should correctly aggregate data for a single item', () => { + const items = [ + { + date: '2024-03-19', + totalFlags: 5, + totalNo: 2, + totalRequests: 7, + totalYes: 3, + project: 'default', + totalApps: 2, + totalEnvironments: 3, + week: '2024-01', + }, + ]; + + const expected = { + '2024-03-19': { + totalFlags: 5, + totalNo: 2, + totalRequests: 7, + totalYes: 3, + }, + }; + + expect(aggregateDataPerDate(items)).toEqual(expected); + }); + + it('should aggregate multiple items for the same date correctly', () => { + const items = [ + { + date: '2024-03-19', + totalFlags: 1, + totalNo: 2, + totalRequests: 3, + totalYes: 4, + project: 'default', + totalApps: 2, + totalEnvironments: 3, + week: '2024-01', + }, + { + date: '2024-03-19', + totalFlags: 5, + totalNo: 6, + totalRequests: 7, + totalYes: 8, + project: 'default', + totalApps: 2, + totalEnvironments: 3, + week: '2024-01', + }, + ]; + + const expected = { + '2024-03-19': { + totalFlags: 6, + totalNo: 8, + totalRequests: 10, + totalYes: 12, + }, + }; + + expect(aggregateDataPerDate(items)).toEqual(expected); + }); + + it('should aggregate items across different dates correctly', () => { + const items = [ + { + date: '2024-03-18', + totalFlags: 10, + totalNo: 20, + totalRequests: 30, + totalYes: 40, + project: 'default', + totalApps: 2, + totalEnvironments: 3, + week: '2024-01', + }, + { + date: '2024-03-19', + totalFlags: 1, + totalNo: 2, + totalRequests: 3, + totalYes: 4, + project: 'default', + totalApps: 2, + totalEnvironments: 3, + week: '2024-01', + }, + ]; + + const expected = { + '2024-03-18': { + totalFlags: 10, + totalNo: 20, + totalRequests: 30, + totalYes: 40, + }, + '2024-03-19': { + totalFlags: 1, + totalNo: 2, + totalRequests: 3, + totalYes: 4, + }, + }; + + expect(aggregateDataPerDate(items)).toEqual(expected); + }); + + it('should correctly handle items with all metrics at zero', () => { + const items = [ + { + date: '2024-03-19', + totalFlags: 0, + totalNo: 0, + totalRequests: 0, + totalYes: 0, + project: 'default', + totalApps: 2, + totalEnvironments: 3, + week: '2024-01', + }, + ]; + + const expected = { + '2024-03-19': { + totalFlags: 0, + totalNo: 0, + totalRequests: 0, + totalYes: 0, + }, + }; + + expect(aggregateDataPerDate(items)).toEqual(expected); + }); + + it('should return an empty object for an empty array input', () => { + expect(aggregateDataPerDate([])).toEqual({}); + }); + + // Test for immutability of input + it('should not mutate the input array', () => { + const items = [ + { + date: '2024-03-19', + totalFlags: 1, + totalNo: 2, + totalRequests: 3, + totalYes: 4, + project: 'default', + totalApps: 2, + totalEnvironments: 3, + week: '2024-01', + }, + ]; + const itemsCopy = [...items]; + + aggregateDataPerDate(items); + + expect(items).toEqual(itemsCopy); + }); +}); diff --git a/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.ts b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.ts new file mode 100644 index 0000000000..84916011da --- /dev/null +++ b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsChartTooltip/aggregate-metrics-by-day.ts @@ -0,0 +1,33 @@ +import type { ExecutiveSummarySchema } from 'openapi'; + +export function aggregateDataPerDate( + items: ExecutiveSummarySchema['metricsSummaryTrends'], +) { + return items.reduce( + (acc, item) => { + if (!acc[item.date]) { + acc[item.date] = { + totalFlags: 0, + totalNo: 0, + totalRequests: 0, + totalYes: 0, + }; + } + + acc[item.date].totalFlags += item.totalFlags; + acc[item.date].totalNo += item.totalNo; + acc[item.date].totalRequests += item.totalRequests; + acc[item.date].totalYes += item.totalYes; + + return acc; + }, + {} as { + [date: string]: { + totalFlags: number; + totalNo: number; + totalRequests: number; + totalYes: number; + }; + }, + ); +} diff --git a/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx index 984720c642..cc2871a494 100644 --- a/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx +++ b/frontend/src/component/executiveDashboard/componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx @@ -1,36 +1,80 @@ import { useMemo, type VFC } from 'react'; import 'chartjs-adapter-date-fns'; + import type { ExecutiveSummarySchema } from 'openapi'; -import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart'; +import { + fillGradientPrimary, + LineChart, + NotEnoughData, +} from '../../components/LineChart/LineChart'; import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip'; import { useMetricsSummary } from '../../hooks/useMetricsSummary'; import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData'; import type { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends'; +import { useTheme } from '@mui/material'; +import { aggregateDataPerDate } from './MetricsChartTooltip/aggregate-metrics-by-day'; interface IMetricsSummaryChartProps { metricsSummaryTrends: GroupedDataByProject< ExecutiveSummarySchema['metricsSummaryTrends'] >; + isAggregate?: boolean; } export const MetricsSummaryChart: VFC = ({ metricsSummaryTrends, + isAggregate, }) => { - const data = useMetricsSummary(metricsSummaryTrends); + const theme = useTheme(); + const metricsSummary = useMetricsSummary(metricsSummaryTrends); const notEnoughData = useMemo( - () => !data.datasets.some((d) => d.data.length > 1), - [data], + () => !metricsSummary.datasets.some((d) => d.data.length > 1), + [metricsSummary], ); const placeholderData = usePlaceholderData(); + const aggregatedPerDay = useMemo(() => { + const result = aggregateDataPerDate( + Object.values(metricsSummary.datasets).flatMap((item) => item.data), + ); + const data = Object.entries(result) + .map(([date, trends]) => ({ date, ...trends })) + .sort( + (a, b) => + new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + + return { + datasets: [ + { + label: 'Total Requests', + data: data, + borderColor: theme.palette.primary.light, + backgroundColor: fillGradientPrimary, + fill: true, + order: 3, + }, + ], + }; + }, [JSON.stringify(metricsSummaryTrends), theme]); + + const data = isAggregate ? aggregatedPerDay : metricsSummary; + return ( : false} /> ); diff --git a/frontend/src/component/executiveDashboard/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx b/frontend/src/component/executiveDashboard/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx index 392ebba141..b6cec83424 100644 --- a/frontend/src/component/executiveDashboard/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx +++ b/frontend/src/component/executiveDashboard/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx @@ -4,6 +4,7 @@ import type { ExecutiveSummarySchema } from 'openapi'; import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip'; import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData'; import { + fillGradientPrimary, LineChart, NotEnoughData, } from 'component/executiveDashboard/components/LineChart/LineChart'; @@ -73,7 +74,8 @@ export const ProjectHealthChart: VFC = ({ date: item.date, })), borderColor: theme.palette.primary.light, - fill: false, + backgroundColor: fillGradientPrimary, + fill: true, order: 3, }, ], diff --git a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx index 9b4a305c43..d251604d87 100644 --- a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx +++ b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx @@ -1,39 +1,108 @@ import { useMemo, type VFC } from 'react'; import 'chartjs-adapter-date-fns'; import type { ExecutiveSummarySchema } from 'openapi'; -import { LineChart } from '../../components/LineChart/LineChart'; +import { + fillGradientPrimary, + LineChart, + NotEnoughData, +} from '../../components/LineChart/LineChart'; import { useProjectChartData } from '../../hooks/useProjectChartData'; import type { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends'; import { usePlaceholderData } from '../../hooks/usePlaceholderData'; import { TimeToProductionTooltip } from './TimeToProductionTooltip/TimeToProductionTooltip'; +import { useTheme } from '@mui/material'; interface ITimeToProductionChartProps { projectFlagTrends: GroupedDataByProject< ExecutiveSummarySchema['projectFlagTrends'] >; + isAggregate?: boolean; +} + +type GroupedDataByDate = Record; + +type DateResult = Record; + +function averageTimeToProduction( + projectsData: ExecutiveSummarySchema['projectFlagTrends'], +): DateResult { + // Group the data by date + const groupedData: GroupedDataByDate = {}; + projectsData.forEach((item) => { + const { date, timeToProduction } = item; + if (!groupedData[date]) { + groupedData[date] = []; + } + if (timeToProduction !== undefined) { + groupedData[date].push(timeToProduction); + } + }); + // Calculate the average time to production for each date + const averageByDate: DateResult = {}; + Object.entries(groupedData).forEach(([date, times]) => { + const sum = times.reduce((acc, curr) => acc + curr, 0); + const average = sum / times.length; + averageByDate[date] = average; + }); + return averageByDate; } export const TimeToProductionChart: VFC = ({ projectFlagTrends, + isAggregate, }) => { - const data = useProjectChartData(projectFlagTrends); + const theme = useTheme(); + const projectsDatasets = useProjectChartData(projectFlagTrends); const notEnoughData = useMemo( - () => !data.datasets.some((d) => d.data.length > 1), - [data], + () => !projectsDatasets.datasets.some((d) => d.data.length > 1), + [projectsDatasets], ); + const aggregatedPerDay = useMemo(() => { + const result = averageTimeToProduction( + Object.values(projectsDatasets.datasets).flatMap( + (item) => item.data, + ), + ); + const data = Object.entries(result) + .map(([date, timeToProduction]) => ({ date, timeToProduction })) + .sort( + (a, b) => + new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + + return { + datasets: [ + { + label: 'Time to production', + data, + borderColor: theme.palette.primary.light, + backgroundColor: fillGradientPrimary, + fill: true, + order: 3, + }, + ], + }; + }, [JSON.stringify(projectsDatasets), theme]); + + const data = isAggregate ? aggregatedPerDay : projectsDatasets; const placeholderData = usePlaceholderData(); return ( : false} /> ); }; diff --git a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionTooltip/TimeToProductionTooltip.tsx b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionTooltip/TimeToProductionTooltip.tsx index 20362fb8ed..15e9c7d180 100644 --- a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionTooltip/TimeToProductionTooltip.tsx +++ b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionTooltip/TimeToProductionTooltip.tsx @@ -29,7 +29,7 @@ const getInterval = (days?: number) => { return `${weeks.toFixed(1)} weeks`; } } else { - return `${days} days`; + return `${days.toFixed(2)} days`; } }; diff --git a/frontend/src/component/executiveDashboard/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx b/frontend/src/component/executiveDashboard/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx index 5e2d7fa846..ccf457d5b7 100644 --- a/frontend/src/component/executiveDashboard/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx +++ b/frontend/src/component/executiveDashboard/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx @@ -7,6 +7,7 @@ import type { } from 'openapi'; import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart'; import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData'; +import { UpdatesPerEnvironmentTypeChartTooltip } from './UpdatesPerEnvironmentTypeChartTooltip/UpdatesPerEnvironmentTypeChartTooltip'; interface IUpdatesPerEnvironmnetTypeChart { environmentTypeTrends: ExecutiveSummarySchema['environmentTypeTrends']; @@ -67,25 +68,35 @@ export const UpdatesPerEnvironmentTypeChart: VFC< const data = useMemo(() => { const grouped = groupByDate(environmentTypeTrends); - const labels = environmentTypeTrends?.map((item) => item.date); const datasets = Object.entries(grouped).map( ([environmentType, trends]) => { const color = getEnvironmentTypeColor(environmentType); return { label: environmentType, - data: trends.map((item) => item.totalUpdates), + data: trends, borderColor: color, backgroundColor: color, fill: false, }; }, ); - return { labels, datasets }; + return { datasets }; }, [theme, environmentTypeTrends]); return ( : isLoading} /> ); diff --git a/frontend/src/component/executiveDashboard/hooks/useDashboardData.ts b/frontend/src/component/executiveDashboard/hooks/useDashboardData.ts new file mode 100644 index 0000000000..82133e0df4 --- /dev/null +++ b/frontend/src/component/executiveDashboard/hooks/useDashboardData.ts @@ -0,0 +1,43 @@ +import { useMemo } from 'react'; +import type { ExecutiveSummarySchema } from 'openapi'; +import { useFilteredTrends } from './useFilteredTrends'; +import { useGroupedProjectTrends } from './useGroupedProjectTrends'; +import { useFilteredFlagsSummary } from './useFilteredFlagsSummary'; +import { useAvgTimeToProduction } from './useAvgTimeToProduction'; + +export const useDashboardData = ( + executiveDashboardData: ExecutiveSummarySchema, + projects: string[], +) => + useMemo(() => { + const projectsData = useFilteredTrends( + 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); + + return { + ...executiveDashboardData, + projectsData, + groupedProjectsData, + metricsData, + groupedMetricsData, + users, + environmentTypeTrends, + summary, + avgDaysToProduction, + }; + }, [executiveDashboardData, projects]);