Skip to content

Commit

Permalink
feat: average time to production chart (#6565)
Browse files Browse the repository at this point in the history
Adds live data to TimeToProductionChart and AverageTimeToProduction
gauge.
Create a custom tooltip

Changes the interaction mode for tooltips as per @nicolaesocaciu pairing
session

Improvement:
Extract grouping by project to its own hook (3 charts that needed
grouped data where handling it independently.

<img width="1331" alt="Screenshot 2024-03-14 at 17 19 07"
src="https://github.com/Unleash/unleash/assets/104830839/199c556c-8264-46e3-9dd5-9a864588de1f">

Closes #
[1-2143](https://linear.app/unleash/issue/1-2143/time-to-production-total-aggregation)

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
  • Loading branch information
andreas-unleash committed Mar 15, 2024
1 parent 4d78c6d commit 4563468
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 70 deletions.
42 changes: 30 additions & 12 deletions frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx
Expand Up @@ -26,6 +26,10 @@ import { ProjectHealthChart } from './componentsChart/ProjectHealthChart/Project
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',
Expand Down Expand Up @@ -74,14 +78,21 @@ export const ExecutiveDashboard: VFC = () => {
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 handleScroll = () => {
Expand Down Expand Up @@ -147,7 +158,7 @@ export const ExecutiveDashboard: VFC = () => {
elseShow={
<ChartWidget title='Users per project'>
<UsersPerProjectChart
projectFlagTrends={projectsData}
projectFlagTrends={groupedProjectsData}
/>
</ChartWidget>
}
Expand Down Expand Up @@ -178,7 +189,7 @@ export const ExecutiveDashboard: VFC = () => {
elseShow={
<ChartWidget title='Flags per project'>
<FlagsProjectChart
projectFlagTrends={projectsData}
projectFlagTrends={groupedProjectsData}
/>
</ChartWidget>
}
Expand All @@ -200,25 +211,32 @@ export const ExecutiveDashboard: VFC = () => {
}
>
<ProjectHealthChart
projectFlagTrends={projectsData}
projectFlagTrends={groupedProjectsData}
isAggregate={showAllProjects}
/>
</ChartWidget>
{/* <Widget title='Average time to production'>
<TimeToProduction
//FIXME: data from API
daysToProduction={5.2}
/>
<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'>
<TimeToProductionChart projectFlagTrends={projectsData} />
</ChartWidget> */}
<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={metricsData} />
<MetricsSummaryChart
metricsSummaryTrends={groupedMetricsData}
/>
</Widget>
<Widget
title='Updates per environment type'
Expand Down
Expand Up @@ -28,14 +28,18 @@ export const createOptions = (
tooltip: {
enabled: false,
position: 'nearest',
interaction: {
axis: 'xy',
mode: 'nearest',
},
external: createTooltip(setTooltip),
},
},
locale: locationSettings.locale,
interaction: {
intersect: false,
axis: 'x',
mode: 'index',
axis: 'xy',
mode: 'nearest',
},
elements: {
point: {
Expand Down
Expand Up @@ -4,9 +4,12 @@ import { ExecutiveSummarySchema } from 'openapi';
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';

interface IFlagsProjectChartProps {
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
projectFlagTrends: GroupedDataByProject<
ExecutiveSummarySchema['projectFlagTrends']
>;
}

export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
Expand Down
Expand Up @@ -5,17 +5,20 @@ import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
import { MetricsSummaryTooltip } from './MetricsChartTooltip/MetricsChartTooltip';
import { useMetricsSummary } from '../../hooks/useMetricsSummary';
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';

interface IMetricsSummaryChartProps {
metricsSummaryTrends: ExecutiveSummarySchema['metricsSummaryTrends'];
metricsSummaryTrends: GroupedDataByProject<
ExecutiveSummarySchema['metricsSummaryTrends']
>;
}

export const MetricsSummaryChart: VFC<IMetricsSummaryChartProps> = ({
metricsSummaryTrends,
}) => {
const data = useMetricsSummary(metricsSummaryTrends);
const notEnoughData = useMemo(
() => (data.datasets.some((d) => d.data.length > 1) ? false : true),
() => !data.datasets.some((d) => d.data.length > 1),
[data],
);
const placeholderData = usePlaceholderData();
Expand Down
Expand Up @@ -8,13 +8,16 @@ import {
NotEnoughData,
} from 'component/executiveDashboard/components/LineChart/LineChart';
import { useTheme } from '@mui/material';
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';

interface IFlagsProjectChartProps {
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
interface IProjectHealthChartProps {
projectFlagTrends: GroupedDataByProject<
ExecutiveSummarySchema['projectFlagTrends']
>;
isAggregate?: boolean;
}

export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
export const ProjectHealthChart: VFC<IProjectHealthChartProps> = ({
projectFlagTrends,
isAggregate,
}) => {
Expand Down
@@ -1,21 +1,33 @@
import { type VFC } from 'react';
import { useMemo, type VFC } from 'react';
import 'chartjs-adapter-date-fns';
import { ExecutiveSummarySchema } from 'openapi';
import { LineChart } from '../../components/LineChart/LineChart';
import { useProjectChartData } from '../../hooks/useProjectChartData';
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';
import { usePlaceholderData } from '../../hooks/usePlaceholderData';
import { TimeToProductionTooltip } from './TimeToProductionTooltip/TimeToProductionTooltip';

interface IFlagsProjectChartProps {
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
interface ITimeToProductionChartProps {
projectFlagTrends: GroupedDataByProject<
ExecutiveSummarySchema['projectFlagTrends']
>;
}

export const TimeToProductionChart: VFC<IFlagsProjectChartProps> = ({
export const TimeToProductionChart: VFC<ITimeToProductionChartProps> = ({
projectFlagTrends,
}) => {
const data = useProjectChartData(projectFlagTrends);
const notEnoughData = useMemo(
() => !data.datasets.some((d) => d.data.length > 1),
[data],
);

const placeholderData = usePlaceholderData();
return (
<LineChart
data={data}
data={notEnoughData ? placeholderData : data}
isLocalTooltip
TooltipComponent={TimeToProductionTooltip}
overrideOptions={{
parsing: {
yAxisKey: 'timeToProduction',
Expand Down
@@ -0,0 +1,117 @@
import { type VFC } from 'react';
import { type ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
import { Box, Paper, Typography, styled } from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';
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 getInterval = (days?: number) => {
if (!days) {
return 'N/A';
}

if (days > 11) {
const weeks = days / 7;
if (weeks > 6) {
const months = weeks / 4.34524;
return `${months.toFixed(2)} months`;
} else {
return `${weeks.toFixed(1)} weeks`;
}
} else {
return `${days} days`;
}
};

const resolveBadge = (input?: number) => {
const ONE_MONTH = 30;
const ONE_WEEK = 7;

if (!input) {
return null;
}

if (input >= ONE_MONTH) {
return <Badge color='error'>Low</Badge>;
}

if (input <= ONE_MONTH && input >= ONE_WEEK + 1) {
return <Badge>Medium</Badge>;
}

if (input <= ONE_WEEK) {
return <Badge color='success'>High</Badge>;
}
};

export const TimeToProductionTooltip: 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 ExecutiveSummarySchemaProjectFlagTrendsItem,
};
});

const limitedData = data?.slice(0, 5);

return (
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
width: '300px',
})}
>
{limitedData?.map((point, index) => (
<StyledTooltipItemContainer
elevation={3}
key={`${point.title}-${index}`}
>
<StyledItemHeader>
<Typography
variant='body2'
color='textSecondary'
component='span'
>
{point.label}
</Typography>
</StyledItemHeader>
<StyledItemHeader>
<Typography variant='body2' component='span'>
<Typography
sx={{ color: point.color }}
component='span'
>
{'● '}
</Typography>
<strong>{point.title}</strong>
</Typography>
<Typography
variant='body2'
component='span'
sx={{ mr: (theme) => theme.spacing(1), pt: 0.25 }}
>
{getInterval(point.value.timeToProduction)}
</Typography>
{resolveBadge(point.value.timeToProduction)}
</StyledItemHeader>
</StyledTooltipItemContainer>
)) || null}
</Box>
);
};
Expand Up @@ -4,9 +4,12 @@ import { ExecutiveSummarySchema } from 'openapi';
import { LineChart, NotEnoughData } from '../../components/LineChart/LineChart';
import { useProjectChartData } from 'component/executiveDashboard/hooks/useProjectChartData';
import { usePlaceholderData } from 'component/executiveDashboard/hooks/usePlaceholderData';
import { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends';

interface IUsersPerProjectChartProps {
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
projectFlagTrends: GroupedDataByProject<
ExecutiveSummarySchema['projectFlagTrends']
>;
}

export const UsersPerProjectChart: VFC<IUsersPerProjectChartProps> = ({
Expand Down
@@ -0,0 +1,34 @@
import { useAvgTimeToProduction } from './useAvgTimeToProduction';
import { renderHook } from '@testing-library/react-hooks';

describe('useAvgTimeToProduction', () => {
test('returns 0 when projectsData is empty', () => {
const projectsData = {};
const { result } = renderHook(() =>
useAvgTimeToProduction(projectsData),
);
expect(result.current).toBe(0);
});

test('calculates result.current time to production correctly', () => {
const projectsData = {
project1: [{ timeToProduction: 10 }, { timeToProduction: 20 }],
project2: [{ timeToProduction: 15 }, { timeToProduction: 25 }],
} as any;
const { result } = renderHook(() =>
useAvgTimeToProduction(projectsData),
);
expect(result.current).toBe(17.5);
});

test('ignores projects without time to production data', () => {
const projectsData = {
project1: [{ timeToProduction: 10 }, { timeToProduction: 20 }],
project2: [],
} as any;
const { result } = renderHook(() =>
useAvgTimeToProduction(projectsData),
);
expect(result.current).toBe(7.5);
});
});

0 comments on commit 4563468

Please sign in to comment.