diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx index bc8e69da8f..05f36cef13 100644 --- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx +++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx @@ -13,6 +13,7 @@ import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/use import { UserStats } from './UserStats/UserStats'; import { FlagStats } from './FlagStats/FlagStats'; import { Widget } from './Widget/Widget'; +import { FlagsProjectChart } from './FlagsProjectChart/FlagsProjectChart'; const StyledGrid = styled(Box)(({ theme }) => ({ display: 'grid', @@ -107,6 +108,10 @@ export const ExecutiveDashboard: VFC = () => { /> + + ); }; diff --git a/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx b/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx index e818ea5f0b..bcb52b0db9 100644 --- a/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx +++ b/frontend/src/component/executiveDashboard/FlagsChart/FlagsChartComponent.tsx @@ -64,7 +64,11 @@ const createOptions = (theme: Theme, locationSettings: ILocationSettings) => const date = item?.chart?.data?.labels?.[item.dataIndex]; return date - ? formatDateYMD(date, locationSettings.locale) + ? formatDateYMD( + date, + locationSettings.locale, + 'UTC', + ) : ''; }, }, diff --git a/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx new file mode 100644 index 0000000000..0200b68fd5 --- /dev/null +++ b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx @@ -0,0 +1,5 @@ +import { lazy } from 'react'; + +export const FlagsProjectChart = lazy( + () => import('./FlagsProjectChartComponent'), +); diff --git a/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChartComponent.tsx b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChartComponent.tsx new file mode 100644 index 0000000000..07b5424126 --- /dev/null +++ b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChartComponent.tsx @@ -0,0 +1,162 @@ +import { useMemo, type VFC } from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + TimeScale, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; +import 'chartjs-adapter-date-fns'; +import { Paper, Theme, Typography, useTheme } from '@mui/material'; +import { + useLocationSettings, + type ILocationSettings, +} from 'hooks/useLocationSettings'; +import { formatDateYMD } from 'utils/formatDate'; +import { + ExecutiveSummarySchema, + ExecutiveSummarySchemaProjectFlagTrendsItem, +} from 'openapi'; + +const getRandomColor = () => { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; +}; + +const createData = ( + theme: Theme, + flagTrends: ExecutiveSummarySchema['projectFlagTrends'] = [], +) => { + const groupedFlagTrends = flagTrends.reduce< + Record + >((groups, item) => { + if (!groups[item.project]) { + groups[item.project] = []; + } + groups[item.project].push(item); + return groups; + }, {}); + + const datasets = Object.entries(groupedFlagTrends).map( + ([project, trends]) => { + const color = getRandomColor(); + return { + label: project, + data: trends.map((item) => item.total), + borderColor: color, + backgroundColor: color, + fill: true, + }; + }, + ); + + return { + labels: flagTrends.map((item) => item.date), + datasets, + }; +}; + +const createOptions = (theme: Theme, locationSettings: ILocationSettings) => + ({ + responsive: true, + plugins: { + legend: { + position: 'bottom', + }, + tooltip: { + callbacks: { + title: (tooltipItems: any) => { + const item = tooltipItems?.[0]; + const date = + item?.chart?.data?.labels?.[item.dataIndex]; + return date + ? formatDateYMD( + date, + locationSettings.locale, + 'UTC', + ) + : ''; + }, + }, + }, + }, + locale: locationSettings.locale, + interaction: { + intersect: false, + axis: 'x', + }, + color: theme.palette.text.secondary, + scales: { + y: { + type: 'linear', + grid: { + color: theme.palette.divider, + borderColor: theme.palette.divider, + }, + ticks: { color: theme.palette.text.secondary }, + }, + x: { + type: 'time', + time: { + unit: 'month', + }, + grid: { + color: theme.palette.divider, + borderColor: theme.palette.divider, + }, + ticks: { + color: theme.palette.text.secondary, + }, + }, + }, + }) as const; + +interface IFlagsChartComponentProps { + projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends']; +} + +const FlagsProjectChart: VFC = ({ + projectFlagTrends, +}) => { + const theme = useTheme(); + const { locationSettings } = useLocationSettings(); + const data = useMemo( + () => createData(theme, projectFlagTrends), + [theme, projectFlagTrends], + ); + const options = createOptions(theme, locationSettings); + + return ( + ({ padding: theme.spacing(4) })}> + ({ marginBottom: theme.spacing(3) })} + > + Number of flags per project + + + + ); +}; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + TimeScale, + Title, + Tooltip, + Legend, +); + +export default FlagsProjectChart; diff --git a/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts b/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts index 9a29d66071..eda954d196 100644 --- a/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts +++ b/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts @@ -32,6 +32,7 @@ export const useExecutiveDashboard = ( flags: { total: 0 }, userTrends: [], flagTrends: [], + projectFlagTrends: [], }, refetchExecutiveDashboard, loading: !error && !data, diff --git a/frontend/src/openapi/models/executiveSummarySchema.ts b/frontend/src/openapi/models/executiveSummarySchema.ts index fec4155686..65dcad10bc 100644 --- a/frontend/src/openapi/models/executiveSummarySchema.ts +++ b/frontend/src/openapi/models/executiveSummarySchema.ts @@ -5,6 +5,7 @@ */ import type { ExecutiveSummarySchemaFlags } from './executiveSummarySchemaFlags'; import type { ExecutiveSummarySchemaFlagTrendsItem } from './executiveSummarySchemaFlagTrendsItem'; +import type { ExecutiveSummarySchemaProjectFlagTrendsItem } from './executiveSummarySchemaProjectFlagTrendsItem'; import type { ExecutiveSummarySchemaUsers } from './executiveSummarySchemaUsers'; import type { ExecutiveSummarySchemaUserTrendsItem } from './executiveSummarySchemaUserTrendsItem'; @@ -16,6 +17,8 @@ export interface ExecutiveSummarySchema { flags: ExecutiveSummarySchemaFlags; /** How number of flags changed over time */ flagTrends: ExecutiveSummarySchemaFlagTrendsItem[]; + /** How number of flags per project changed over time */ + projectFlagTrends: ExecutiveSummarySchemaProjectFlagTrendsItem[]; /** High level user count statistics */ users: ExecutiveSummarySchemaUsers; /** How number of users changed over time */ diff --git a/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts b/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts index 72de2a7849..a3e8360041 100644 --- a/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts +++ b/frontend/src/openapi/models/executiveSummarySchemaFlagTrendsItem.ts @@ -10,7 +10,7 @@ export type ExecutiveSummarySchemaFlagTrendsItem = { /** A UTC date when the stats were captured. Time is the very end of a given day. */ date: string; /** The number of time calculated potentially stale flags on a particular day */ - potentiallyStale?: number; + potentiallyStale: number; /** The number of user marked stale flags on a particular day */ stale: number; /** The number of all flags on a particular day */ diff --git a/frontend/src/openapi/models/executiveSummarySchemaProjectFlagTrendsItem.ts b/frontend/src/openapi/models/executiveSummarySchemaProjectFlagTrendsItem.ts new file mode 100644 index 0000000000..75fde6c2fe --- /dev/null +++ b/frontend/src/openapi/models/executiveSummarySchemaProjectFlagTrendsItem.ts @@ -0,0 +1,24 @@ +/** + * Generated by Orval + * Do not edit manually. + * See `gen:api` script in package.json + */ + +export type ExecutiveSummarySchemaProjectFlagTrendsItem = { + /** The number of active flags on a particular day */ + active: number; + /** A UTC date when the stats were captured. Time is the very end of a given day. */ + date: string; + /** An indicator of the [project's health](https://docs.getunleash.io/reference/technical-debt#health-rating) on a scale from 0 to 100 */ + health?: number; + /** The number of time calculated potentially stale flags on a particular day */ + potentiallyStale: number; + /** Project id of the project the flag trends belong to */ + project: string; + /** The number of user marked stale flags on a particular day */ + stale: number; + /** The average time from when a feature was created to when it was enabled in the "production" environment during the current window */ + timeToProduction?: number; + /** The number of all flags on a particular day */ + total: number; +}; diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts index 3732a99d7e..55b9ed1edf 100644 --- a/frontend/src/openapi/models/index.ts +++ b/frontend/src/openapi/models/index.ts @@ -497,6 +497,7 @@ export * from './eventsSchemaVersion'; export * from './executiveSummarySchema'; export * from './executiveSummarySchemaFlagTrendsItem'; export * from './executiveSummarySchemaFlags'; +export * from './executiveSummarySchemaProjectFlagTrendsItem'; export * from './executiveSummarySchemaUserTrendsItem'; export * from './executiveSummarySchemaUsers'; export * from './exportFeatures404';