1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

fix: insights UI improvements and aggreated TTP (#6584)

Various ui enhancements
Aggregates the time to production and metrics summary by averaging by
date across all projects to get the value. Creates a single dataset for
the aggregation. This makes theme behave like eg the Health chart
(showing aggregated graph when show all projects and per project when
not)

Gradient fill when all projects across all related charts
Attached recording with generated data for 3 months 




https://github.com/Unleash/unleash/assets/104830839/7acd80a8-b799-4a35-9a2e-bf3798f56d32

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
andreas-unleash 2024-03-20 09:24:56 +02:00 committed by GitHub
parent 646a8f0192
commit c126ae130d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 644 additions and 238 deletions

View File

@ -0,0 +1,218 @@
import { ConditionallyRender } from '../common/ConditionallyRender/ConditionallyRender';
import { Widget } from './components/Widget/Widget';
import { UserStats } from './componentsStat/UserStats/UserStats';
import { UsersChart } from './componentsChart/UsersChart/UsersChart';
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
import { FlagStats } from './componentsStat/FlagStats/FlagStats';
import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart';
import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart';
import { HealthStats } from './componentsStat/HealthStats/HealthStats';
import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart';
import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction';
import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart';
import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart';
import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart';
import type { ExecutiveSummarySchema } from '../../openapi';
import type { GroupedDataByProject } from './hooks/useGroupedProjectTrends';
import { Box, styled } from '@mui/material';
import { allOption } from '../common/ProjectSelect/ProjectSelect';
import type { VFC } from 'react';
interface IChartsProps {
flagTrends: ExecutiveSummarySchema['flagTrends'];
projectsData: ExecutiveSummarySchema['projectFlagTrends'];
groupedProjectsData: GroupedDataByProject<
ExecutiveSummarySchema['projectFlagTrends']
>;
metricsData: ExecutiveSummarySchema['metricsSummaryTrends'];
groupedMetricsData: GroupedDataByProject<
ExecutiveSummarySchema['metricsSummaryTrends']
>;
users: ExecutiveSummarySchema['users'];
userTrends: ExecutiveSummarySchema['userTrends'];
environmentTypeTrends: ExecutiveSummarySchema['environmentTypeTrends'];
summary: {
total: number;
active: number;
stale: number;
potentiallyStale: number;
averageUsers: number;
averageHealth?: string;
};
avgDaysToProduction: number;
loading: boolean;
projects: string[];
}
const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: `repeat(2, 1fr)`,
gridAutoRows: 'auto',
gap: theme.spacing(2),
paddingBottom: theme.spacing(2),
[theme.breakpoints.up('md')]: {
gridTemplateColumns: `300px 1fr`,
},
}));
const ChartWidget = styled(Widget)(({ theme }) => ({
[theme.breakpoints.down('md')]: {
gridColumnStart: 'span 2',
order: 2,
},
}));
export const Charts: VFC<IChartsProps> = ({
projects,
users,
summary,
userTrends,
groupedProjectsData,
flagTrends,
avgDaysToProduction,
groupedMetricsData,
environmentTypeTrends,
loading,
}) => {
const showAllProjects = projects[0] === allOption.id;
const isOneProjectSelected = projects.length === 1;
return (
<>
<StyledGrid>
<ConditionallyRender
condition={showAllProjects}
show={
<Widget title='Total users'>
<UserStats
count={users.total}
active={users.active}
inactive={users.inactive}
/>
</Widget>
}
elseShow={
<Widget
title={
isOneProjectSelected
? 'Users in project'
: 'Users per project on average'
}
>
<UserStats count={summary.averageUsers} />
</Widget>
}
/>
<ConditionallyRender
condition={showAllProjects}
show={
<ChartWidget title='Users'>
<UsersChart
userTrends={userTrends}
isLoading={loading}
/>
</ChartWidget>
}
elseShow={
<ChartWidget title='Users per project'>
<UsersPerProjectChart
projectFlagTrends={groupedProjectsData}
/>
</ChartWidget>
}
/>
<Widget
title='Total flags'
tooltip='Active flags (not archived) that currently exist across selected projects.'
>
<FlagStats
count={summary.total}
flagsPerUser={
showAllProjects
? (summary.total / users.total).toFixed(2)
: ''
}
/>
</Widget>
<ConditionallyRender
condition={showAllProjects}
show={
<ChartWidget title='Number of flags'>
<FlagsChart
flagTrends={flagTrends}
isLoading={loading}
/>
</ChartWidget>
}
elseShow={
<ChartWidget title='Flags per project'>
<FlagsProjectChart
projectFlagTrends={groupedProjectsData}
/>
</ChartWidget>
}
/>
<Widget
title='Average health'
tooltip='Average health is a percentage of flags that are not stale nor potencially stale.'
>
<HealthStats
value={summary.averageHealth}
healthy={summary.active}
stale={summary.stale}
potentiallyStale={summary.potentiallyStale}
/>
</Widget>
<ChartWidget
title={
showAllProjects ? 'Healthy flags' : 'Health per project'
}
tooltip='How the health changes over time'
>
<ProjectHealthChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
/>
</ChartWidget>
<Widget
title='Average time to production'
tooltip='How long did it take on average from a feature toggle was created until it was enabled in an environment of type production. This is calculated only from feature toggles with the type of "release" and averaged across selected projects. '
>
<TimeToProduction daysToProduction={avgDaysToProduction} />
</Widget>
<ChartWidget
title={
showAllProjects
? 'Time to production'
: 'Time to production per project'
}
tooltip='How the time to production changes over time'
>
<TimeToProductionChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
/>
</ChartWidget>
</StyledGrid>
<Widget
title={showAllProjects ? 'Metrics' : 'Metrics per project'}
tooltip='Summary of all flag evaluations reported by SDKs.'
>
<MetricsSummaryChart
metricsSummaryTrends={groupedMetricsData}
isAggregate={showAllProjects}
/>
</Widget>
<Widget
title='Updates per environment type'
tooltip='Summary of all configuration updates per environment type'
sx={{ mt: (theme) => theme.spacing(2) }}
>
<UpdatesPerEnvironmentTypeChart
environmentTypeTrends={environmentTypeTrends}
isLoading={loading}
/>
</Widget>
</>
);
};

View File

@ -1,64 +1,26 @@
import { useState, type VFC } from 'react';
import { Box, styled } from '@mui/material';
import { ArrayParam, withDefault } from 'use-query-params';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { usePersistentTableState } from 'hooks/usePersistentTableState';
import {
allOption,
ProjectSelect,
} from 'component/common/ProjectSelect/ProjectSelect';
import { useExecutiveDashboard } from 'hooks/api/getters/useExecutiveSummary/useExecutiveSummary';
import { useFilteredFlagsSummary } from './hooks/useFilteredFlagsSummary';
import { useFilteredTrends } from './hooks/useFilteredTrends';
import { Widget } from './components/Widget/Widget';
import { DashboardHeader } from './components/DashboardHeader/DashboardHeader';
import { useDashboardData } from './hooks/useDashboardData';
import { Charts } from './Charts';
import { UserStats } from './componentsStat/UserStats/UserStats';
import { FlagStats } from './componentsStat/FlagStats/FlagStats';
import { HealthStats } from './componentsStat/HealthStats/HealthStats';
import { UsersChart } from './componentsChart/UsersChart/UsersChart';
import { FlagsChart } from './componentsChart/FlagsChart/FlagsChart';
import { FlagsProjectChart } from './componentsChart/FlagsProjectChart/FlagsProjectChart';
import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/ProjectHealthChart';
import { MetricsSummaryChart } from './componentsChart/MetricsSummaryChart/MetricsSummaryChart';
import { UsersPerProjectChart } from './componentsChart/UsersPerProjectChart/UsersPerProjectChart';
import { UpdatesPerEnvironmentTypeChart } from './componentsChart/UpdatesPerEnvironmentTypeChart/UpdatesPerEnvironmentTypeChart';
import { TimeToProduction } from './componentsStat/TimeToProduction/TimeToProduction';
import { TimeToProductionChart } from './componentsChart/TimeToProductionChart/TimeToProductionChart';
import { useGroupedProjectTrends } from './hooks/useGroupedProjectTrends';
import { useAvgTimeToProduction } from './hooks/useAvgTimeToProduction';
const StyledGrid = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: `repeat(2, 1fr)`,
gridAutoRows: 'auto',
gap: theme.spacing(2),
paddingBottom: theme.spacing(2),
[theme.breakpoints.up('md')]: {
gridTemplateColumns: `300px 1fr`,
},
}));
const ChartWidget = styled(Widget)(({ theme }) => ({
[theme.breakpoints.down('md')]: {
gridColumnStart: 'span 2',
order: 2,
},
}));
const StickyWrapper = styled(Box, {
shouldForwardProp: (prop) => prop !== 'scrolled',
})<{ scrolled?: boolean }>(({ theme, scrolled }) => ({
position: 'sticky',
top: 0,
zIndex: 1000,
padding: scrolled ? theme.spacing(2, 0) : theme.spacing(0, 0, 2),
background: theme.palette.background.application,
transition: 'padding 0.3s ease',
}));
const StickyWrapper = styled(Box)<{ scrolled?: boolean }>(
({ theme, scrolled }) => ({
position: 'sticky',
top: 0,
zIndex: 1000,
padding: scrolled ? theme.spacing(2, 0) : theme.spacing(0, 0, 2),
background: theme.palette.background.application,
transition: 'padding 0.3s ease',
}),
);
export const ExecutiveDashboard: VFC = () => {
const [scrolled, setScrolled] = useState(false);
@ -73,27 +35,8 @@ export const ExecutiveDashboard: VFC = () => {
const projects = state.projects
? (state.projects.filter(Boolean) as string[])
: [];
const showAllProjects = projects[0] === allOption.id;
const projectsData = useFilteredTrends(
executiveDashboardData.projectFlagTrends,
projects,
);
const groupedProjectsData = useGroupedProjectTrends(projectsData);
const metricsData = useFilteredTrends(
executiveDashboardData.metricsSummaryTrends,
projects,
);
const groupedMetricsData = useGroupedProjectTrends(metricsData);
const { users, environmentTypeTrends } = executiveDashboardData;
const summary = useFilteredFlagsSummary(projectsData);
const avgDaysToProduction = useAvgTimeToProduction(groupedProjectsData);
const isOneProjectSelected = projects.length === 1;
const dashboardData = useDashboardData(executiveDashboardData, projects);
const handleScroll = () => {
if (!scrolled && window.scrollY > 0) {
@ -121,133 +64,7 @@ export const ExecutiveDashboard: VFC = () => {
}
/>
</StickyWrapper>
<StyledGrid>
<ConditionallyRender
condition={showAllProjects}
show={
<Widget title='Total users'>
<UserStats
count={users.total}
active={users.active}
inactive={users.inactive}
/>
</Widget>
}
elseShow={
<Widget
title={
isOneProjectSelected
? 'Users in project'
: 'Users per project on average'
}
>
<UserStats count={summary.averageUsers} />
</Widget>
}
/>
<ConditionallyRender
condition={showAllProjects}
show={
<ChartWidget title='Users'>
<UsersChart
userTrends={executiveDashboardData.userTrends}
isLoading={loading}
/>
</ChartWidget>
}
elseShow={
<ChartWidget title='Users per project'>
<UsersPerProjectChart
projectFlagTrends={groupedProjectsData}
/>
</ChartWidget>
}
/>
<Widget
title='Total flags'
tooltip='Active flags (not archived) that currently exist across selected projects.'
>
<FlagStats
count={summary.total}
flagsPerUser={
showAllProjects
? (summary.total / users.total).toFixed(2)
: ''
}
/>
</Widget>
<ConditionallyRender
condition={showAllProjects}
show={
<ChartWidget title='Number of flags'>
<FlagsChart
flagTrends={executiveDashboardData.flagTrends}
isLoading={loading}
/>
</ChartWidget>
}
elseShow={
<ChartWidget title='Flags per project'>
<FlagsProjectChart
projectFlagTrends={groupedProjectsData}
/>
</ChartWidget>
}
/>
<Widget
title='Average health'
tooltip='Average health is a percentage of flags that are not stale nor potencially stale.'
>
<HealthStats
value={summary.averageHealth}
healthy={summary.active}
stale={summary.stale}
potentiallyStale={summary.potentiallyStale}
/>
</Widget>
<ChartWidget
title={
showAllProjects ? 'Healthy flags' : 'Health per project'
}
>
<ProjectHealthChart
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
/>
</ChartWidget>
<Widget
title='Average time to production'
tooltip='How long did it take on average from a feature toggle was created until it was enabled in an environment of type production. This is calculated only from feature toggles with the type of "release". '
>
<TimeToProduction daysToProduction={avgDaysToProduction} />
</Widget>
<ChartWidget
title='Time to production'
tooltip='How the average time to production changes over time'
>
<TimeToProductionChart
projectFlagTrends={groupedProjectsData}
/>
</ChartWidget>
</StyledGrid>
<Widget
title='Metrics'
tooltip='Summary of all flag evaluations reported by SDKs.'
>
<MetricsSummaryChart
metricsSummaryTrends={groupedMetricsData}
/>
</Widget>
<Widget
title='Updates per environment type'
tooltip='Summary of all configuration updates per environment type'
sx={{ mt: (theme) => theme.spacing(2) }}
>
<UpdatesPerEnvironmentTypeChart
environmentTypeTrends={environmentTypeTrends}
isLoading={loading}
/>
</Widget>
<Charts loading={loading} projects={projects} {...dashboardData} />
</>
);
};

View File

@ -35,24 +35,28 @@ const InfoLine = ({
</Typography>
);
const InfoSummary = ({ data }: { data: { key: string; value: number }[] }) => (
const InfoSummary = ({
data,
}: { data: { key: string; value: string | number }[] }) => (
<Box display={'flex'} flexDirection={'row'}>
{data.map(({ key, value }) => (
<div style={{ flex: 1, flexDirection: 'column' }} key={key}>
<div
style={{
flex: 1,
textAlign: 'center',
marginBottom: '4px',
}}
>
<Typography variant={'body1'} component={'p'}>
{key}
</Typography>
{data
.filter(({ value }) => value !== 'N/A')
.map(({ key, value }) => (
<div style={{ flex: 1, flexDirection: 'column' }} key={key}>
<div
style={{
flex: 1,
textAlign: 'center',
marginBottom: '4px',
}}
>
<Typography variant={'body1'} component={'p'}>
{key}
</Typography>
</div>
<div style={{ flex: 1, textAlign: 'center' }}>{value}</div>
</div>
<div style={{ flex: 1, textAlign: 'center' }}>{value}</div>
</div>
))}
))}
</Box>
);
@ -131,15 +135,15 @@ export const MetricsSummaryTooltip: VFC<{ tooltip: TooltipState | null }> = ({
data={[
{
key: 'Flags',
value: point.value.totalFlags ?? 0,
value: point.value.totalFlags ?? 'N/A',
},
{
key: 'Environments',
value: point.value.totalEnvironments ?? 0,
value: point.value.totalEnvironments ?? 'N/A',
},
{
key: 'Apps',
value: point.value.totalApps ?? 0,
value: point.value.totalApps ?? 'N/A',
},
]}
/>

View File

@ -0,0 +1,165 @@
import { aggregateDataPerDate } from './aggregate-metrics-by-day';
describe('aggregateDataPerDate', () => {
it('should correctly aggregate data for a single item', () => {
const items = [
{
date: '2024-03-19',
totalFlags: 5,
totalNo: 2,
totalRequests: 7,
totalYes: 3,
project: 'default',
totalApps: 2,
totalEnvironments: 3,
week: '2024-01',
},
];
const expected = {
'2024-03-19': {
totalFlags: 5,
totalNo: 2,
totalRequests: 7,
totalYes: 3,
},
};
expect(aggregateDataPerDate(items)).toEqual(expected);
});
it('should aggregate multiple items for the same date correctly', () => {
const items = [
{
date: '2024-03-19',
totalFlags: 1,
totalNo: 2,
totalRequests: 3,
totalYes: 4,
project: 'default',
totalApps: 2,
totalEnvironments: 3,
week: '2024-01',
},
{
date: '2024-03-19',
totalFlags: 5,
totalNo: 6,
totalRequests: 7,
totalYes: 8,
project: 'default',
totalApps: 2,
totalEnvironments: 3,
week: '2024-01',
},
];
const expected = {
'2024-03-19': {
totalFlags: 6,
totalNo: 8,
totalRequests: 10,
totalYes: 12,
},
};
expect(aggregateDataPerDate(items)).toEqual(expected);
});
it('should aggregate items across different dates correctly', () => {
const items = [
{
date: '2024-03-18',
totalFlags: 10,
totalNo: 20,
totalRequests: 30,
totalYes: 40,
project: 'default',
totalApps: 2,
totalEnvironments: 3,
week: '2024-01',
},
{
date: '2024-03-19',
totalFlags: 1,
totalNo: 2,
totalRequests: 3,
totalYes: 4,
project: 'default',
totalApps: 2,
totalEnvironments: 3,
week: '2024-01',
},
];
const expected = {
'2024-03-18': {
totalFlags: 10,
totalNo: 20,
totalRequests: 30,
totalYes: 40,
},
'2024-03-19': {
totalFlags: 1,
totalNo: 2,
totalRequests: 3,
totalYes: 4,
},
};
expect(aggregateDataPerDate(items)).toEqual(expected);
});
it('should correctly handle items with all metrics at zero', () => {
const items = [
{
date: '2024-03-19',
totalFlags: 0,
totalNo: 0,
totalRequests: 0,
totalYes: 0,
project: 'default',
totalApps: 2,
totalEnvironments: 3,
week: '2024-01',
},
];
const expected = {
'2024-03-19': {
totalFlags: 0,
totalNo: 0,
totalRequests: 0,
totalYes: 0,
},
};
expect(aggregateDataPerDate(items)).toEqual(expected);
});
it('should return an empty object for an empty array input', () => {
expect(aggregateDataPerDate([])).toEqual({});
});
// Test for immutability of input
it('should not mutate the input array', () => {
const items = [
{
date: '2024-03-19',
totalFlags: 1,
totalNo: 2,
totalRequests: 3,
totalYes: 4,
project: 'default',
totalApps: 2,
totalEnvironments: 3,
week: '2024-01',
},
];
const itemsCopy = [...items];
aggregateDataPerDate(items);
expect(items).toEqual(itemsCopy);
});
});

View File

@ -0,0 +1,33 @@
import type { ExecutiveSummarySchema } from 'openapi';
export function aggregateDataPerDate(
items: ExecutiveSummarySchema['metricsSummaryTrends'],
) {
return items.reduce(
(acc, item) => {
if (!acc[item.date]) {
acc[item.date] = {
totalFlags: 0,
totalNo: 0,
totalRequests: 0,
totalYes: 0,
};
}
acc[item.date].totalFlags += item.totalFlags;
acc[item.date].totalNo += item.totalNo;
acc[item.date].totalRequests += item.totalRequests;
acc[item.date].totalYes += item.totalYes;
return acc;
},
{} as {
[date: string]: {
totalFlags: number;
totalNo: number;
totalRequests: number;
totalYes: number;
};
},
);
}

View File

@ -1,36 +1,80 @@
import { useMemo, type VFC } from 'react';
import 'chartjs-adapter-date-fns';
import type { ExecutiveSummarySchema } from 'openapi';
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
import {
fillGradientPrimary,
LineChart,
NotEnoughData,
} from '../../components/LineChart/LineChart';
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
import { useMetricsSummary } from '../../hooks/useMetricsSummary';
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
import type { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
import { useTheme } from '@mui/material';
import { aggregateDataPerDate } from './MetricsChartTooltip/aggregate-metrics-by-day';
interface IMetricsSummaryChartProps {
metricsSummaryTrends: GroupedDataByProject<
ExecutiveSummarySchema['metricsSummaryTrends']
>;
isAggregate?: boolean;
}
export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
metricsSummaryTrends,
isAggregate,
}) => {
const data = useMetricsSummary(metricsSummaryTrends);
const theme = useTheme();
const metricsSummary = useMetricsSummary(metricsSummaryTrends);
const notEnoughData = useMemo(
() => !data.datasets.some((d) => d.data.length > 1),
[data],
() => !metricsSummary.datasets.some((d) => d.data.length > 1),
[metricsSummary],
);
const placeholderData = usePlaceholderData();
const aggregatedPerDay = useMemo(() => {
const result = aggregateDataPerDate(
Object.values(metricsSummary.datasets).flatMap((item) => item.data),
);
const data = Object.entries(result)
.map(([date, trends]) => ({ date, ...trends }))
.sort(
(a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime(),
);
return {
datasets: [
{
label: 'Total Requests',
data: data,
borderColor: theme.palette.primary.light,
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
},
],
};
}, [JSON.stringify(metricsSummaryTrends), theme]);
const data = isAggregate ? aggregatedPerDay : metricsSummary;
return (
<LineChart
data={notEnoughData ? placeholderData : data}
isLocalTooltip
TooltipComponent={MetricsSummaryTooltip}
overrideOptions={{
parsing: { yAxisKey: 'totalRequests', xAxisKey: 'date' },
}}
overrideOptions={
notEnoughData
? {}
: {
parsing: {
yAxisKey: 'totalRequests',
xAxisKey: 'date',
},
}
}
cover={notEnoughData ? <NotEnoughData /> : false}
/>
);

View File

@ -4,6 +4,7 @@ import type { ExecutiveSummarySchema } from 'openapi';
import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip';
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
import {
fillGradientPrimary,
LineChart,
NotEnoughData,
} from 'component/executiveDashboard/components/LineChart/LineChart';
@ -73,7 +74,8 @@ export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({
date: item.date,
})),
borderColor: theme.palette.primary.light,
fill: false,
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
},
],

View File

@ -1,39 +1,108 @@
import { useMemo, type VFC } from 'react';
import 'chartjs-adapter-date-fns';
import type { ExecutiveSummarySchema } from 'openapi';
import { LineChart } from '../../components/LineChart/LineChart';
import {
fillGradientPrimary,
LineChart,
NotEnoughData,
} from '../../components/LineChart/LineChart';
import { useProjectChartData } from '../../hooks/useProjectChartData';
import type { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
import { usePlaceholderData } from '../../hooks/usePlaceholderData';
import { TimeToProductionTooltip } from './TimeToProductionTooltip/TimeToProductionTooltip';
import { useTheme } from '@mui/material';
interface ITimeToProductionChartProps {
projectFlagTrends: GroupedDataByProject<
ExecutiveSummarySchema['projectFlagTrends']
>;
isAggregate?: boolean;
}
type GroupedDataByDate<T> = Record<string, T[]>;
type DateResult<T> = Record<string, T>;
function averageTimeToProduction(
projectsData: ExecutiveSummarySchema['projectFlagTrends'],
): DateResult<number> {
// Group the data by date
const groupedData: GroupedDataByDate<number> = {};
projectsData.forEach((item) => {
const { date, timeToProduction } = item;
if (!groupedData[date]) {
groupedData[date] = [];
}
if (timeToProduction !== undefined) {
groupedData[date].push(timeToProduction);
}
});
// Calculate the average time to production for each date
const averageByDate: DateResult<number> = {};
Object.entries(groupedData).forEach(([date, times]) => {
const sum = times.reduce((acc, curr) => acc + curr, 0);
const average = sum / times.length;
averageByDate[date] = average;
});
return averageByDate;
}
export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({
projectFlagTrends,
isAggregate,
}) => {
const data = useProjectChartData(projectFlagTrends);
const theme = useTheme();
const projectsDatasets = useProjectChartData(projectFlagTrends);
const notEnoughData = useMemo(
() => !data.datasets.some((d) => d.data.length > 1),
[data],
() => !projectsDatasets.datasets.some((d) => d.data.length > 1),
[projectsDatasets],
);
const aggregatedPerDay = useMemo(() => {
const result = averageTimeToProduction(
Object.values(projectsDatasets.datasets).flatMap(
(item) => item.data,
),
);
const data = Object.entries(result)
.map(([date, timeToProduction]) => ({ date, timeToProduction }))
.sort(
(a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime(),
);
return {
datasets: [
{
label: 'Time to production',
data,
borderColor: theme.palette.primary.light,
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
},
],
};
}, [JSON.stringify(projectsDatasets), theme]);
const data = isAggregate ? aggregatedPerDay : projectsDatasets;
const placeholderData = usePlaceholderData();
return (
<LineChart
data={notEnoughData ? placeholderData : data}
isLocalTooltip
TooltipComponent={TimeToProductionTooltip}
overrideOptions={{
parsing: {
yAxisKey: 'timeToProduction',
xAxisKey: 'date',
},
}}
overrideOptions={
notEnoughData
? {}
: {
parsing: {
yAxisKey: 'timeToProduction',
xAxisKey: 'date',
},
}
}
cover={notEnoughData ? <NotEnoughData /> : false}
/>
);
};

View File

@ -29,7 +29,7 @@ const getInterval = (days?: number) => {
return `${weeks.toFixed(1)} weeks`;
}
} else {
return `${days} days`;
return `${days.toFixed(2)} days`;
}
};

View File

@ -7,6 +7,7 @@ import type {
} from 'openapi';
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
import { UpdatesPerEnvironmentTypeChartTooltip } from './UpdatesPerEnvironmentTypeChartTooltip/UpdatesPerEnvironmentTypeChartTooltip';
interface IUpdatesPerEnvironmnetTypeChart {
environmentTypeTrends: ExecutiveSummarySchema['environmentTypeTrends'];
@ -67,25 +68,35 @@ export const UpdatesPerEnvironmentTypeChart: VFC<
const data = useMemo(() => {
const grouped = groupByDate(environmentTypeTrends);
const labels = environmentTypeTrends?.map((item) => item.date);
const datasets = Object.entries(grouped).map(
([environmentType, trends]) => {
const color = getEnvironmentTypeColor(environmentType);
return {
label: environmentType,
data: trends.map((item) => item.totalUpdates),
data: trends,
borderColor: color,
backgroundColor: color,
fill: false,
};
},
);
return { labels, datasets };
return { datasets };
}, [theme, environmentTypeTrends]);
return (
<LineChart
data={notEnoughData || isLoading ? placeholderData : data}
overrideOptions={
notEnoughData
? {}
: {
parsing: {
yAxisKey: 'totalUpdates',
xAxisKey: 'date',
},
}
}
TooltipComponent={UpdatesPerEnvironmentTypeChartTooltip}
cover={notEnoughData ? <NotEnoughData /> : isLoading}
/>
);

View File

@ -0,0 +1,43 @@
import { useMemo } from 'react';
import type { ExecutiveSummarySchema } from 'openapi';
import { useFilteredTrends } from './useFilteredTrends';
import { useGroupedProjectTrends } from './useGroupedProjectTrends';
import { useFilteredFlagsSummary } from './useFilteredFlagsSummary';
import { useAvgTimeToProduction } from './useAvgTimeToProduction';
export const useDashboardData = (
executiveDashboardData: ExecutiveSummarySchema,
projects: string[],
) =>
useMemo(() => {
const projectsData = useFilteredTrends(
executiveDashboardData.projectFlagTrends,
projects,
);
const groupedProjectsData = useGroupedProjectTrends(projectsData);
const metricsData = useFilteredTrends(
executiveDashboardData.metricsSummaryTrends,
projects,
);
const groupedMetricsData = useGroupedProjectTrends(metricsData);
const { users, environmentTypeTrends } = executiveDashboardData;
const summary = useFilteredFlagsSummary(projectsData);
const avgDaysToProduction = useAvgTimeToProduction(groupedProjectsData);
return {
...executiveDashboardData,
projectsData,
groupedProjectsData,
metricsData,
groupedMetricsData,
users,
environmentTypeTrends,
summary,
avgDaysToProduction,
};
}, [executiveDashboardData, projects]);