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';