diff --git a/frontend/src/component/insights/Insights.test.tsx b/frontend/src/component/insights/Insights.test.tsx index b5d531c26b..481922d47d 100644 --- a/frontend/src/component/insights/Insights.test.tsx +++ b/frontend/src/component/insights/Insights.test.tsx @@ -15,6 +15,7 @@ const setupApi = () => { flags: { total: 0 }, flagTrends: [], environmentTypeTrends: [], + lifecycleTrends: [], }); testServerRoute(server, '/api/admin/projects', { diff --git a/frontend/src/component/insights/InsightsCharts.tsx b/frontend/src/component/insights/InsightsCharts.tsx index ec29147ee8..ec93c658df 100644 --- a/frontend/src/component/insights/InsightsCharts.tsx +++ b/frontend/src/component/insights/InsightsCharts.tsx @@ -31,6 +31,7 @@ export interface IChartsProps { >; userTrends: InstanceInsightsSchema['userTrends']; environmentTypeTrends: InstanceInsightsSchema['environmentTypeTrends']; + lifecycleTrends: InstanceInsightsSchema['lifecycleTrends']; summary: { total: number; active: number; diff --git a/frontend/src/component/insights/componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx b/frontend/src/component/insights/componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx new file mode 100644 index 0000000000..bcb9f62c39 --- /dev/null +++ b/frontend/src/component/insights/componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx @@ -0,0 +1,116 @@ +import 'chartjs-adapter-date-fns'; +import { type FC, useMemo } from 'react'; +import type { InstanceInsightsSchema } from 'openapi'; +import { useProjectChartData } from 'component/insights/hooks/useProjectChartData'; +import { + fillGradientPrimary, + LineChart, + NotEnoughData, +} from 'component/insights/components/LineChart/LineChart'; +import { useTheme } from '@mui/material'; +import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; +import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData'; + +interface IProjectHealthChartProps { + lifecycleTrends: GroupedDataByProject< + InstanceInsightsSchema['lifecycleTrends'] + >; + isAggregate?: boolean; + isLoading?: boolean; +} + +type WeekData = { + newProductionFlags: number; + week: string; + date?: string; +}; + +export const NewProductionFlagsChart: FC = ({ + lifecycleTrends, + isAggregate, + isLoading, +}) => { + const lifecycleData = useProjectChartData(lifecycleTrends); + const theme = useTheme(); + const placeholderData = usePlaceholderData(); + + const aggregateHealthData = useMemo(() => { + const labels: string[] = Array.from( + new Set( + lifecycleData.datasets.flatMap((d) => + d.data.map((item) => item.week), + ), + ), + ); + + const weeks: WeekData[] = labels + .map((label) => { + return lifecycleData.datasets + .map((d) => d.data.find((item) => item.week === label)) + .reduce( + (acc: WeekData, item: WeekData) => { + if (item) { + acc.newProductionFlags += + item.newProductionFlags; + } + if (!acc.date) { + acc.date = item?.date; + } + return acc; + }, + { + newProductionFlags: 0, + week: label, + } as WeekData, + ); + }) + .sort((a, b) => (a.week > b.week ? 1 : -1)); + return { + datasets: [ + { + label: 'New production flags', + data: weeks, + borderColor: theme.palette.primary.light, + backgroundColor: fillGradientPrimary, + fill: true, + order: 3, + }, + ], + }; + }, [lifecycleData, theme]); + + const aggregateOrProjectData = isAggregate + ? aggregateHealthData + : lifecycleData; + const notEnoughData = useMemo( + () => + !isLoading && + !lifecycleData.datasets.some((d) => d.data.length > 1), + [lifecycleData, isLoading], + ); + const data = + notEnoughData || isLoading ? placeholderData : aggregateOrProjectData; + + return ( + : isLoading} + /> + ); +}; diff --git a/frontend/src/component/insights/hooks/useInsightsData.ts b/frontend/src/component/insights/hooks/useInsightsData.ts index 21c0d7d117..7b20ff5cdd 100644 --- a/frontend/src/component/insights/hooks/useInsightsData.ts +++ b/frontend/src/component/insights/hooks/useInsightsData.ts @@ -17,6 +17,11 @@ export const useInsightsData = ( projects, ); + const lifecycleData = useFilteredTrends( + instanceInsights.lifecycleTrends, + projects, + ); + const groupedProjectsData = useGroupedProjectTrends(projectsData); const metricsData = useFilteredTrends( @@ -27,6 +32,8 @@ export const useInsightsData = ( const summary = useFilteredFlagsSummary(projectsData); + const groupedLifecycleData = useGroupedProjectTrends(lifecycleData); + return useMemo( () => ({ ...instanceInsights, @@ -37,6 +44,8 @@ export const useInsightsData = ( environmentTypeTrends: instanceInsights.environmentTypeTrends, summary, allMetricsDatapoints, + lifecycleData, + groupedLifecycleData, }), [ instanceInsights, @@ -46,6 +55,8 @@ export const useInsightsData = ( metricsData, groupedMetricsData, summary, + lifecycleData, + groupedLifecycleData, ], ); }; diff --git a/frontend/src/component/insights/hooks/useProjectChartData.ts b/frontend/src/component/insights/hooks/useProjectChartData.ts index 2f13c74c4f..a09ed2cce2 100644 --- a/frontend/src/component/insights/hooks/useProjectChartData.ts +++ b/frontend/src/component/insights/hooks/useProjectChartData.ts @@ -6,6 +6,7 @@ import type { GroupedDataByProject } from './useGroupedProjectTrends.js'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; type ProjectFlagTrends = InstanceInsightsSchema['projectFlagTrends']; +type LifecycleTrends = InstanceInsightsSchema['lifecycleTrends']; export const calculateTechDebt = (item: { total: number; @@ -22,7 +23,9 @@ export const calculateTechDebt = (item: { }; export const useProjectChartData = ( - projectFlagTrends: GroupedDataByProject, + projectFlagTrends: + | GroupedDataByProject + | GroupedDataByProject, ) => { const theme = useTheme(); const getProjectColor = useProjectColor(); diff --git a/frontend/src/component/insights/sections/PerformanceInsights.tsx b/frontend/src/component/insights/sections/PerformanceInsights.tsx index bae9edf399..4b4beddca0 100644 --- a/frontend/src/component/insights/sections/PerformanceInsights.tsx +++ b/frontend/src/component/insights/sections/PerformanceInsights.tsx @@ -23,6 +23,8 @@ import { StyledWidgetContent, StyledWidgetStats, } from '../InsightsCharts.styles'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { NewProductionFlagsChart } from '../componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx'; export const PerformanceInsights: FC = () => { const statePrefix = 'performance-'; @@ -54,6 +56,7 @@ export const PerformanceInsights: FC = () => { flagTrends, summary, groupedProjectsData, + groupedLifecycleData, userTrends, groupedMetricsData, allMetricsDatapoints, @@ -73,6 +76,8 @@ export const PerformanceInsights: FC = () => { : flagsPerUserCalculation.toFixed(2); } + const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs'); + return ( { /> } > + {isLifecycleGraphsEnabled && isEnterprise() ? ( + + + + + + + + + ) : null} + {showAllProjects ? ( diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index c8d7fdd947..7ccf976eeb 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -93,6 +93,7 @@ export type UiFlags = { changeRequestApproverEmails?: boolean; eventGrouping?: boolean; reportUnknownFlags?: boolean; + lifecycleGraphs?: boolean; }; export interface IVersionInfo { diff --git a/frontend/src/openapi/models/instanceInsightsSchemaLifecycleTrendsItem.ts b/frontend/src/openapi/models/instanceInsightsSchemaLifecycleTrendsItem.ts index 8ec7ed8828..6ce39d2232 100644 --- a/frontend/src/openapi/models/instanceInsightsSchemaLifecycleTrendsItem.ts +++ b/frontend/src/openapi/models/instanceInsightsSchemaLifecycleTrendsItem.ts @@ -10,7 +10,7 @@ export type InstanceInsightsSchemaLifecycleTrendsItem = { /** Number of flags that entered production during this week */ newProductionFlags: number; /** Project id that the flags belong to */ - project?: string; + project: string; /** Year and week in a given year for which the stats were calculated */ week: string; };