1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-17 13:46:47 +02:00

Chore(1-3755)/split insights in three (#10035)

Creates sections for the insights dashboard and moves charts around into
the same order as the sketches and into the right sections. There's no
charts for the top section (lifecycle currently) yet, and the sections
also don't have their own filters.

To make this re-ordering easier, I've also moved the previous insights
chart into a legacy file and set up a proxy component that handles
switching based on the flag.


![image](https://github.com/user-attachments/assets/f0929998-def3-4643-babd-ab53f4ea8e98)


Next step is separating the filters.
This commit is contained in:
Thomas Heartman 2025-05-27 14:06:48 +01:00 committed by GitHub
parent 5df074bd14
commit 3e57c4803c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 468 additions and 165 deletions

View File

@ -1,4 +1,4 @@
import type { FC } from 'react';
import type { FC, PropsWithChildren } from 'react';
import { Box, Paper, styled } from '@mui/material';
import { UserStats } from './componentsStat/UserStats/UserStats.tsx';
import { UsersChart } from './componentsChart/UsersChart/UsersChart.tsx';
@ -8,8 +8,6 @@ import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart.tsx';
import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart.tsx';
import { HealthStats } from './componentsStat/HealthStats/HealthStats.tsx';
import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart.tsx';
import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction.tsx';
import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart.tsx';
import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx';
import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx';
import type { InstanceInsightsSchema } from 'openapi';
@ -17,8 +15,8 @@ import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends.ts';
import { allOption } from 'component/common/ProjectSelect/ProjectSelect';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { WidgetTitle } from './components/WidgetTitle/WidgetTitle.tsx';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useUiFlag } from 'hooks/useUiFlag.ts';
import { LegacyInsightsCharts } from './LegacyInsightsCharts.tsx';
export interface IChartsProps {
flagTrends: InstanceInsightsSchema['flagTrends'];
@ -50,7 +48,7 @@ export interface IChartsProps {
const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
gap: theme.spacing(4),
}));
const StyledWidget = styled(Paper)(({ theme }) => ({
@ -91,7 +89,23 @@ const StyledChartContainer = styled(Box)(({ theme }) => ({
padding: theme.spacing(3),
}));
export const InsightsCharts: FC<IChartsProps> = ({
const StyledSection = styled('section')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
const Section: FC<PropsWithChildren<{ title: string }>> = ({
title,
children,
}) => (
<StyledSection>
<h2>{title}</h2>
{children}
</StyledSection>
);
const NewInsightsCharts: FC<IChartsProps> = ({
projects,
summary,
userTrends,
@ -105,7 +119,6 @@ export const InsightsCharts: FC<IChartsProps> = ({
const showAllProjects = projects[0] === allOption.id;
const isOneProjectSelected = projects.length === 1;
const { isEnterprise } = useUiConfig();
const showMedianTimeToProduction = !useUiFlag('lifecycleMetrics');
const lastUserTrend = userTrends[userTrends.length - 1];
const lastFlagTrend = flagTrends[flagTrends.length - 1];
@ -124,163 +137,74 @@ export const InsightsCharts: FC<IChartsProps> = ({
return (
<StyledContainer>
<ConditionallyRender
condition={showAllProjects}
show={
<>
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle title='Total users' />
<UserStats
count={usersTotal}
active={usersActive}
inactive={usersInactive}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<UsersChart
userTrends={userTrends}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
elseShow={
<>
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle
title={
isOneProjectSelected
? 'Users in project'
: 'Users per project on average'
}
tooltip={
isOneProjectSelected
? 'Number of users in selected projects.'
: 'Average number of users for selected projects.'
}
/>
<UserStats
count={summary.averageUsers}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<UsersPerProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<>
<StyledWidget>
<StyledWidgetStats width={350} padding={0}>
<HealthStats
value={summary.averageHealth}
healthy={summary.active}
stale={summary.stale}
potentiallyStale={summary.potentiallyStale}
title={
<WidgetTitle
title='Health'
tooltip={
'Percentage of flags that are not stale or potentially stale.'
}
/>
}
/>
</StyledWidgetStats>
<StyledChartContainer>
<ProjectHealthChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
{showMedianTimeToProduction ? (
<StyledWidget>
<StyledWidgetStats>
<Section title='Flags lifecycle currently' />
<Section title='Performance insights'>
{showAllProjects ? (
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags' />
<FlagStats
count={flagsTotal}
flagsPerUser={getFlagsPerUser(
flagsTotal,
usersTotal,
)}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<FlagsChart
flagTrends={flagTrends}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
) : (
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags' />
<FlagStats
count={summary.total}
flagsPerUser={''}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<FlagsProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
)}
{isEnterprise() ? (
<StyledWidget>
<StyledWidgetStats width={350} padding={0}>
<HealthStats
value={summary.averageHealth}
healthy={summary.active}
stale={summary.stale}
potentiallyStale={summary.potentiallyStale}
title={
<WidgetTitle
title='Median time to production'
tooltip={`How long does it currently take on average from when a feature flag was created until it was enabled in a "production" type environment. This is calculated only from feature flags of the type "release" and is the median across the selected projects.`}
/>
<TimeToProduction
daysToProduction={
summary.medianTimeToProduction
title='Health'
tooltip={
'Percentage of flags that are not stale or potentially stale.'
}
/>
</StyledWidgetStats>
<StyledChartContainer>
<TimeToProductionChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
) : null}
</>
}
/>
<ConditionallyRender
condition={showAllProjects}
show={
<>
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags' />
<FlagStats
count={flagsTotal}
flagsPerUser={getFlagsPerUser(
flagsTotal,
usersTotal,
)}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<FlagsChart
flagTrends={flagTrends}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
elseShow={
<>
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags' />
<FlagStats
count={summary.total}
flagsPerUser={''}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<FlagsProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
}
/>
</StyledWidgetStats>
<StyledChartContainer>
<ProjectHealthChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
) : null}
{isEnterprise() ? (
<>
<StyledWidget>
<StyledWidgetContent>
@ -317,8 +241,67 @@ export const InsightsCharts: FC<IChartsProps> = ({
</StyledWidgetContent>
</StyledWidget>
</>
}
/>
) : null}
</Section>
<Section title='User insights'>
{showAllProjects ? (
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle title='Total users' />
<UserStats
count={usersTotal}
active={usersActive}
inactive={usersInactive}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<UsersChart
userTrends={userTrends}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
) : (
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle
title={
isOneProjectSelected
? 'Users in project'
: 'Users per project on average'
}
tooltip={
isOneProjectSelected
? 'Number of users in selected projects.'
: 'Average number of users for selected projects.'
}
/>
<UserStats
count={summary.averageUsers}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<UsersPerProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
)}
</Section>
</StyledContainer>
);
};
export const InsightsCharts: FC<IChartsProps> = (props) => {
const useNewInsightsCharts = useUiFlag('lifecycleMetrics');
return useNewInsightsCharts ? (
<NewInsightsCharts {...props} />
) : (
<LegacyInsightsCharts {...props} />
);
};

View File

@ -0,0 +1,320 @@
import type { FC } from 'react';
import { Box, Paper, styled } from '@mui/material';
import { UserStats } from './componentsStat/UserStats/UserStats.tsx';
import { UsersChart } from './componentsChart/UsersChart/UsersChart.tsx';
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart.tsx';
import { FlagStats } from './componentsStat/FlagStats/FlagStats.tsx';
import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart.tsx';
import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart.tsx';
import { HealthStats } from './componentsStat/HealthStats/HealthStats.tsx';
import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart.tsx';
import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction.tsx';
import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart.tsx';
import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart.tsx';
import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart.tsx';
import type { InstanceInsightsSchema } from 'openapi';
import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends.ts';
import { allOption } from 'component/common/ProjectSelect/ProjectSelect';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { WidgetTitle } from './components/WidgetTitle/WidgetTitle.tsx';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
export interface IChartsProps {
flagTrends: InstanceInsightsSchema['flagTrends'];
projectsData: InstanceInsightsSchema['projectFlagTrends'];
groupedProjectsData: GroupedDataByProject<
InstanceInsightsSchema['projectFlagTrends']
>;
metricsData: InstanceInsightsSchema['metricsSummaryTrends'];
groupedMetricsData: GroupedDataByProject<
InstanceInsightsSchema['metricsSummaryTrends']
>;
userTrends: InstanceInsightsSchema['userTrends'];
environmentTypeTrends: InstanceInsightsSchema['environmentTypeTrends'];
summary: {
total: number;
active: number;
stale: number;
potentiallyStale: number;
averageUsers: number;
averageHealth?: string;
flagsPerUser?: string;
medianTimeToProduction?: number;
};
loading: boolean;
projects: string[];
allMetricsDatapoints: string[];
}
const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
const StyledWidget = styled(Paper)(({ theme }) => ({
borderRadius: `${theme.shape.borderRadiusLarge}px`,
boxShadow: 'none',
display: 'flex',
flexWrap: 'wrap',
[theme.breakpoints.up('md')]: {
flexDirection: 'row',
flexWrap: 'nowrap',
},
}));
const StyledWidgetContent = styled(Box)(({ theme }) => ({
padding: theme.spacing(3),
width: '100%',
}));
const StyledWidgetStats = styled(Box)<{ width?: number; padding?: number }>(
({ theme, width = 300, padding = 3 }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
padding: theme.spacing(padding),
minWidth: '100%',
[theme.breakpoints.up('md')]: {
minWidth: `${width}px`,
borderRight: `1px solid ${theme.palette.background.application}`,
},
}),
);
const StyledChartContainer = styled(Box)(({ theme }) => ({
position: 'relative',
minWidth: 0, // bugfix, see: https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128
flexGrow: 1,
margin: 'auto 0',
padding: theme.spacing(3),
}));
export const LegacyInsightsCharts: FC<IChartsProps> = ({
projects,
summary,
userTrends,
groupedProjectsData,
flagTrends,
groupedMetricsData,
environmentTypeTrends,
allMetricsDatapoints,
loading,
}) => {
const showAllProjects = projects[0] === allOption.id;
const isOneProjectSelected = projects.length === 1;
const { isEnterprise } = useUiConfig();
const lastUserTrend = userTrends[userTrends.length - 1];
const lastFlagTrend = flagTrends[flagTrends.length - 1];
const usersTotal = lastUserTrend?.total ?? 0;
const usersActive = lastUserTrend?.active ?? 0;
const usersInactive = lastUserTrend?.inactive ?? 0;
const flagsTotal = lastFlagTrend?.total ?? 0;
function getFlagsPerUser(flagsTotal: number, usersTotal: number) {
const flagsPerUserCalculation = flagsTotal / usersTotal;
return Number.isNaN(flagsPerUserCalculation)
? 'N/A'
: flagsPerUserCalculation.toFixed(2);
}
return (
<StyledContainer>
<ConditionallyRender
condition={showAllProjects}
show={
<>
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle title='Total users' />
<UserStats
count={usersTotal}
active={usersActive}
inactive={usersInactive}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<UsersChart
userTrends={userTrends}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
elseShow={
<>
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle
title={
isOneProjectSelected
? 'Users in project'
: 'Users per project on average'
}
tooltip={
isOneProjectSelected
? 'Number of users in selected projects.'
: 'Average number of users for selected projects.'
}
/>
<UserStats
count={summary.averageUsers}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<UsersPerProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<>
<StyledWidget>
<StyledWidgetStats width={350} padding={0}>
<HealthStats
value={summary.averageHealth}
healthy={summary.active}
stale={summary.stale}
potentiallyStale={summary.potentiallyStale}
title={
<WidgetTitle
title='Health'
tooltip={
'Percentage of flags that are not stale or potentially stale.'
}
/>
}
/>
</StyledWidgetStats>
<StyledChartContainer>
<ProjectHealthChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
<StyledWidget>
<StyledWidgetStats>
<WidgetTitle
title='Median time to production'
tooltip={`How long does it currently take on average from when a feature flag was created until it was enabled in a "production" type environment. This is calculated only from feature flags of the type "release" and is the median across the selected projects.`}
/>
<TimeToProduction
daysToProduction={
summary.medianTimeToProduction
}
/>
</StyledWidgetStats>
<StyledChartContainer>
<TimeToProductionChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
/>
<ConditionallyRender
condition={showAllProjects}
show={
<>
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags' />
<FlagStats
count={flagsTotal}
flagsPerUser={getFlagsPerUser(
flagsTotal,
usersTotal,
)}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<FlagsChart
flagTrends={flagTrends}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
elseShow={
<>
<StyledWidget>
<StyledWidgetStats width={275}>
<WidgetTitle title='Flags' />
<FlagStats
count={summary.total}
flagsPerUser={''}
isLoading={loading}
/>
</StyledWidgetStats>
<StyledChartContainer>
<FlagsProjectChart
projectFlagTrends={groupedProjectsData}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidget>
</>
}
/>
<ConditionallyRender
condition={isEnterprise()}
show={
<>
<StyledWidget>
<StyledWidgetContent>
<WidgetTitle
title='Flag evaluation metrics'
tooltip='Summary of all flag evaluations reported by SDKs.'
/>
<StyledChartContainer>
<MetricsSummaryChart
metricsSummaryTrends={
groupedMetricsData
}
allDatapointsSorted={
allMetricsDatapoints
}
isAggregate={showAllProjects}
isLoading={loading}
/>
</StyledChartContainer>
</StyledWidgetContent>
</StyledWidget>
<StyledWidget>
<StyledWidgetContent>
<WidgetTitle
title='Updates per environment type'
tooltip='Summary of all configuration updates per environment type.'
/>
<UpdatesPerEnvironmentTypeChart
environmentTypeTrends={
environmentTypeTrends
}
isLoading={loading}
/>
</StyledWidgetContent>
</StyledWidget>
</>
}
/>
</StyledContainer>
);
};

View File

@ -73,14 +73,14 @@ export const InsightsHeader: VFC<DashboardHeaderProps> = ({ actions }) => {
titleElement={
<Typography
variant='h1'
component='div'
component='span'
sx={(theme) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
})}
>
<span>Insights</span>{' '}
Insights
</Typography>
}
actions={