diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx
index 1d801e4168..407ccf5805 100644
--- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx
+++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx
@@ -25,6 +25,7 @@ import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProj
import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart';
import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart';
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
+import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart';
const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid',
@@ -65,7 +66,8 @@ export const ExecutiveDashboard: VFC = () => {
executiveDashboardData.metricsSummaryTrends,
projects,
);
- const { users } = executiveDashboardData;
+
+ const { users, environmentTypeTrends } = executiveDashboardData;
const summary = useFilteredFlagsSummary(projectsData);
const isOneProjectSelected = projects.length === 1;
@@ -194,6 +196,16 @@ export const ExecutiveDashboard: VFC = () => {
>
+ theme.spacing(2) }}
+ >
+
+
>
);
};
diff --git a/frontend/src/component/executiveDashboard/components/Widget/Widget.tsx b/frontend/src/component/executiveDashboard/components/Widget/Widget.tsx
index ca2c4b599c..120ac49959 100644
--- a/frontend/src/component/executiveDashboard/components/Widget/Widget.tsx
+++ b/frontend/src/component/executiveDashboard/components/Widget/Widget.tsx
@@ -1,7 +1,8 @@
import { FC, ReactNode } from 'react';
-import { Paper, Typography, styled } from '@mui/material';
+import { Paper, Typography, styled, SxProps } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
+import { Theme } from '@mui/material/styles/createTheme';
const StyledPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(3),
@@ -13,6 +14,7 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
export const Widget: FC<{
title: ReactNode;
tooltip?: ReactNode;
+ sx?: SxProps;
}> = ({ title, children, tooltip, ...rest }) => (
=> {
+ if (!items) {
+ return {};
+ }
+
+ const grouped = items.reduce(
+ (acc, item) => {
+ const key = item.environmentType;
+
+ if (!acc[key]) {
+ acc[key] = [];
+ }
+
+ acc[key].push(item);
+
+ return acc;
+ },
+ {} as Record,
+ );
+
+ return grouped;
+};
+
+export const UpdatesPerEnvironmentTypeChart: VFC<
+ IUpdatesPerEnvironmnetTypeChart
+> = ({ environmentTypeTrends, isLoading }) => {
+ const theme = useTheme();
+ const notEnoughData = environmentTypeTrends?.length < 2;
+ const placeholderData = usePlaceholderData({ fill: true, type: 'double' });
+
+ const data = useMemo(() => {
+ const grouped = groupByDate(environmentTypeTrends);
+ const labels = environmentTypeTrends?.map((item) => item.date);
+ const datasets = Object.entries(grouped).map(
+ ([environmentType, trends]) => {
+ const color = getProjectColor(environmentType);
+ return {
+ label: environmentType,
+ data: trends.map((item) => item.totalUpdates),
+ borderColor: color,
+ backgroundColor: color,
+ fill: false,
+ };
+ },
+ );
+ return { labels, datasets };
+ }, [theme, environmentTypeTrends]);
+
+ return (
+ : isLoading}
+ />
+ );
+};
diff --git a/frontend/src/component/executiveDashboard/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChartTooltip/UpdatesPerEnvironmentTypeChartTooltip.tsx b/frontend/src/component/executiveDashboard/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChartTooltip/UpdatesPerEnvironmentTypeChartTooltip.tsx
new file mode 100644
index 0000000000..ae9d69c926
--- /dev/null
+++ b/frontend/src/component/executiveDashboard/componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChartTooltip/UpdatesPerEnvironmentTypeChartTooltip.tsx
@@ -0,0 +1,117 @@
+import { type VFC } from 'react';
+import { ExecutiveSummarySchemaEnvironmentTypeTrendsItem } from 'openapi';
+import { Box, Divider, Paper, styled, Typography } from '@mui/material';
+import { TooltipState } from '../../../components/LineChart/ChartTooltip/ChartTooltip';
+
+const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
+ padding: theme.spacing(2),
+}));
+
+const StyledItemHeader = styled(Box)(({ theme }) => ({
+ display: 'flex',
+ justifyContent: 'space-between',
+ gap: theme.spacing(2),
+ alignItems: 'center',
+}));
+
+const InfoLine = ({
+ iconChar,
+ title,
+ color,
+}: {
+ iconChar: string;
+ title: string;
+ color: 'info' | 'success' | 'error';
+}) => (
+ theme.palette[color].main,
+ }}
+ >
+ {iconChar}
+ {title}
+
+);
+
+const InfoSummary = ({ data }: { data: { key: string; value: number }[] }) => (
+
+
+ {data.map(({ key, value }) => (
+
+ ))}
+
+
+);
+
+export const UpdatesPerEnvironmentTypeChartTooltip: VFC<{
+ tooltip: TooltipState | null;
+}> = ({ tooltip }) => {
+ const data = tooltip?.dataPoints.map((point) => {
+ return {
+ label: point.label,
+ title: point.dataset.label,
+ color: point.dataset.borderColor,
+ value: point.raw as ExecutiveSummarySchemaEnvironmentTypeTrendsItem,
+ };
+ });
+
+ const limitedData = data?.slice(0, 5);
+
+ return (
+ ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(2),
+ width: '300px',
+ })}
+ >
+ {limitedData?.map((point, index) => (
+
+
+
+
+ {'● '}
+
+ {point.title}
+
+
+ {point.label}
+
+
+ ({ margin: theme.spacing(1.5, 0) })}
+ />
+
+
+ )) || null}
+
+ );
+};
diff --git a/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts b/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts
index 4ad7312fcd..ea5e753ea7 100644
--- a/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts
+++ b/frontend/src/hooks/api/getters/useExecutiveSummary/useExecutiveSummary.ts
@@ -34,6 +34,7 @@ export const useExecutiveDashboard = (
flagTrends: [],
projectFlagTrends: [],
metricsSummaryTrends: [],
+ environmentTypeTrends: [],
},
refetchExecutiveDashboard,
loading: !error && !data,
diff --git a/frontend/src/openapi/models/executiveSummarySchema.ts b/frontend/src/openapi/models/executiveSummarySchema.ts
index 5c150fd147..0f65daab6c 100644
--- a/frontend/src/openapi/models/executiveSummarySchema.ts
+++ b/frontend/src/openapi/models/executiveSummarySchema.ts
@@ -3,6 +3,7 @@
* Do not edit manually.
* See `gen:api` script in package.json
*/
+import type { ExecutiveSummarySchemaEnvironmentTypeTrendsItem } from './executiveSummarySchemaEnvironmentTypeTrendsItem';
import type { ExecutiveSummarySchemaFlags } from './executiveSummarySchemaFlags';
import type { ExecutiveSummarySchemaFlagTrendsItem } from './executiveSummarySchemaFlagTrendsItem';
import type { ExecutiveSummarySchemaMetricsSummaryTrendsItem } from './executiveSummarySchemaMetricsSummaryTrendsItem';
@@ -14,6 +15,8 @@ import type { ExecutiveSummarySchemaUserTrendsItem } from './executiveSummarySch
* Executive summary of Unleash usage
*/
export interface ExecutiveSummarySchema {
+ /** How updates per environment type changed over time */
+ environmentTypeTrends: ExecutiveSummarySchemaEnvironmentTypeTrendsItem[];
/** High level flag count statistics */
flags: ExecutiveSummarySchemaFlags;
/** How number of flags changed over time */
diff --git a/frontend/src/openapi/models/executiveSummarySchemaEnvironmentTypeTrendsItem.ts b/frontend/src/openapi/models/executiveSummarySchemaEnvironmentTypeTrendsItem.ts
new file mode 100644
index 0000000000..10fe5a5334
--- /dev/null
+++ b/frontend/src/openapi/models/executiveSummarySchemaEnvironmentTypeTrendsItem.ts
@@ -0,0 +1,16 @@
+/**
+ * Generated by Orval
+ * Do not edit manually.
+ * See `gen:api` script in package.json
+ */
+
+export type ExecutiveSummarySchemaEnvironmentTypeTrendsItem = {
+ /** A UTC date when the stats were captured. Time is the very end of a given day. */
+ date: string;
+ /** Environment type the data belongs too */
+ environmentType: string;
+ /** Total number of times configuration has been updated in the environment type */
+ totalUpdates: number;
+ /** Year and week in a given year for which the stats were calculated */
+ week: string;
+};
diff --git a/frontend/src/openapi/models/executiveSummarySchemaMetricsSummaryTrendsItem.ts b/frontend/src/openapi/models/executiveSummarySchemaMetricsSummaryTrendsItem.ts
index c776164339..7dd31b91af 100644
--- a/frontend/src/openapi/models/executiveSummarySchemaMetricsSummaryTrendsItem.ts
+++ b/frontend/src/openapi/models/executiveSummarySchemaMetricsSummaryTrendsItem.ts
@@ -5,6 +5,8 @@
*/
export type ExecutiveSummarySchemaMetricsSummaryTrendsItem = {
+ /** A UTC date when metrics summary was captured. Time is the very end of a given day. */
+ date: string;
/** Project id of the project the impressions summary belong to */
project: string;
/** Total number of applications the impression data belong to */
diff --git a/frontend/src/openapi/models/index.ts b/frontend/src/openapi/models/index.ts
index 70d33954a3..11b025f29f 100644
--- a/frontend/src/openapi/models/index.ts
+++ b/frontend/src/openapi/models/index.ts
@@ -515,6 +515,7 @@ export * from './eventsSchema';
export * from './eventsSchemaVersion';
export * from './executiveSummarySchema';
export * from './executiveSummarySchemaFlagTrendsItem';
+export * from './executiveSummarySchemaEnvironmentTypeTrendsItem';
export * from './executiveSummarySchemaFlags';
export * from './executiveSummarySchemaMetricsSummaryTrendsItem';
export * from './executiveSummarySchemaProjectFlagTrendsItem';