feat(cicd-statistics): Allow custom hiding/collapsing under certain thresholds

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
Gustaf Räntilä
2022-01-21 18:00:30 +01:00
committed by blam
parent 2d7f173e07
commit 52756327cf
8 changed files with 230 additions and 20 deletions
+4 -3
View File
@@ -132,13 +132,14 @@ export interface CicdDefaults {
filterStatus: Array<FilterStatusType>;
filterType: FilterBranchType | 'all';
/** Default collapse the stages with a max-duration below this value */
collapsedLimit: number;
/** Lower-case all stage names (to potentially merge stages with different cases) */
lowercaseNames: boolean;
/** Normalize the from-to date range in all charts */
normalizeTimeRange: boolean;
/** Default collapse the stages with a max-duration below this value */
collapsedLimit: number;
/** Default hide stages with a max-duration below this value */
hideLimit: number;
/** Chart types per status */
chartTypes: Record<FilterStatusType, ChartTypes>;
}
@@ -48,7 +48,7 @@ import {
tickFormatterY,
tooltipValueFormatter,
formatDuration,
} from './utils';
} from '../components/utils';
import {
statusColorMap,
fireColors,
@@ -67,12 +67,18 @@ export interface StageChartProps {
chartTypes: CicdDefaults['chartTypes'];
defaultCollapsed?: number;
defaultHidden?: number;
zeroYAxis?: boolean;
}
export function StageChart(props: StageChartProps) {
const { stage, ...chartOptions } = props;
const { chartTypes, defaultCollapsed = 0, zeroYAxis = false } = chartOptions;
const {
chartTypes,
defaultCollapsed = 0,
defaultHidden = 0,
zeroYAxis = false,
} = chartOptions;
const ticks = useMemo(
() => pickElements(stage.values, 8).map(val => val.__epoch),
@@ -97,7 +103,17 @@ export function StageChart(props: StageChartProps) {
[statuses],
);
return (
const subStages = useMemo(
() =>
new Map<string, ChartableStage>(
[...stage.stages.entries()].filter(
([_name, subStage]) => subStage.combinedAnalysis.max > defaultHidden,
),
),
[stage.stages, defaultHidden],
);
return stage.combinedAnalysis.max < defaultHidden ? null : (
<Accordion
defaultExpanded={stage.combinedAnalysis.max > defaultCollapsed}
TransitionProps={transitionProps}
@@ -209,18 +225,18 @@ export function StageChart(props: StageChartProps) {
</ComposedChart>
</ResponsiveContainer>
</Grid>
{stage.stages.size === 0 ? null : (
{subStages.size === 0 ? null : (
<Grid item>
<Accordion
defaultExpanded={false}
TransitionProps={transitionProps}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Sub stages ({stage.stages.size})</Typography>
<Typography>Sub stages ({subStages.size})</Typography>
</AccordionSummary>
<AccordionDetails>
<div style={fullWidth}>
{[...stage.stages.values()].map(subStage => (
{[...subStages.values()].map(subStage => (
<StageChart
key={subStage.name}
{...chartOptions}
@@ -38,7 +38,7 @@ import { countBy } from 'lodash';
import { DateTime } from 'luxon';
import { Build, FilterStatusType, statusTypes } from '../apis/types';
import { labelFormatterWithoutTime, tickFormatterX } from './utils';
import { labelFormatterWithoutTime, tickFormatterX } from '../components/utils';
import { statusColorMap } from './colors';
export interface StatusChartProps {
@@ -25,9 +25,9 @@ import {
FormControlLabel,
Grid,
Switch,
Theme,
Tooltip,
Typography,
Theme,
makeStyles,
} from '@material-ui/core';
import ShowChartIcon from '@material-ui/icons/ShowChart';
@@ -50,8 +50,10 @@ import {
} from '../apis/types';
import { ButtonSwitch, SwitchValue } from './button-switch';
import { Toggle } from './toggle';
import { DurationSlider } from './duration-slider';
import { Label } from './label';
const useStyles = makeStyles<Theme>(
export const useStyles = makeStyles<Theme>(
theme => ({
rootCard: {
padding: theme.spacing(0, 0, 0, 0),
@@ -81,7 +83,7 @@ const useStyles = makeStyles<Theme>(
},
}),
{
name: 'CicdStatisticsChartFilters',
name: 'CicdStatistics',
},
);
@@ -127,7 +129,11 @@ function isSameChartFilter(a: ChartFilter, b: ChartFilter): boolean {
export type ViewOptions = Pick<
CicdDefaults,
'lowercaseNames' | 'normalizeTimeRange' | 'chartTypes'
| 'lowercaseNames'
| 'normalizeTimeRange'
| 'collapsedLimit'
| 'hideLimit'
| 'chartTypes'
>;
export function getDefaultViewOptions(
@@ -136,6 +142,8 @@ export function getDefaultViewOptions(
return {
lowercaseNames: cicdConfiguration.defaults?.lowercaseNames ?? false,
normalizeTimeRange: cicdConfiguration.defaults?.normalizeTimeRange ?? true,
collapsedLimit: 60 * 1000, // 1m
hideLimit: 20 * 1000, // 20s
chartTypes: {
succeeded: ['duration'],
failed: ['count'],
@@ -224,6 +232,20 @@ export function ChartFilters(props: ChartFiltersProps) {
[setViewOptions],
);
const setHideLimit = useCallback(
(value: number) => {
setViewOptions(old => ({ ...old, hideLimit: value }));
},
[setViewOptions],
);
const setCollapseLimit = useCallback(
(value: number) => {
setViewOptions(old => ({ ...old, collapsedLimit: value }));
},
[setViewOptions],
);
const setChartType = useCallback(
(statusType: FilterStatusType, chartTypes: ChartTypes) => {
setViewOptions(old => ({
@@ -351,7 +373,7 @@ export function ChartFilters(props: ChartFiltersProps) {
onChange={toggleUseNowAsDate}
/>
}
label="To today"
label={<Label>To today</Label>}
/>
{useNowAsToDate ? null : (
<KeyboardDatePicker
@@ -413,7 +435,7 @@ export function ChartFilters(props: ChartFiltersProps) {
'when stage names have changed casing'
}
>
<span>Lowercase names</span>
<Label>Lowercase names</Label>
</Tooltip>
</Toggle>
<Toggle
@@ -428,9 +450,19 @@ export function ChartFilters(props: ChartFiltersProps) {
'and only appear in a part of the time range.'
}
>
<span>Normalize time range</span>
<Label>Normalize time range</Label>
</Tooltip>
</Toggle>
<DurationSlider
header="Hide under peak"
value={viewOptions.hideLimit}
setValue={setHideLimit}
/>
<DurationSlider
header="Collapse under peak"
value={viewOptions.collapsedLimit}
setValue={setCollapseLimit}
/>
<Typography
variant="subtitle2"
className={`${classes.title} ${classes.title}`}
@@ -0,0 +1,117 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useCallback, useMemo, useState } from 'react';
import { Slider } from '@material-ui/core';
import { debounce } from 'lodash';
import { formatDuration, formatDurationFromSeconds } from './utils';
import { Label } from './label';
const marks = [
0,
1,
2,
3,
4,
5,
10,
20,
30,
60,
2 * 60,
3 * 60,
4 * 60,
5 * 60,
10 * 60,
15 * 60,
30 * 60,
45 * 60,
60 * 60,
].map((value, index) => ({
value: index,
label: formatDurationFromSeconds(value),
seconds: value,
}));
function findMarkIndex(seconds: number): number {
if (marks[0].seconds > seconds) {
return 0;
} else if (marks[marks.length - 1].seconds < seconds) {
return marks.length - 1;
}
for (let i = 0; i < marks.length - 1; ++i) {
const a = marks[i];
const b = marks[i + 1];
if (seconds === a.seconds) {
return i;
} else if (seconds === b.seconds) {
return i + 1;
} else if (a.seconds < seconds && b.seconds > seconds) {
return seconds - a.seconds < b.seconds - seconds ? i : i - 1;
}
}
return 0; // Won't happen
}
function formatDurationFromIndex(index: number) {
return formatDurationFromSeconds(marks[index].seconds);
}
export interface DurationSliderProps {
header: string;
value: number;
setValue: (value: number) => void;
}
export function DurationSlider(props: DurationSliderProps) {
const { header, value, setValue } = props;
const [curValue, setCurValue] = useState(value);
const debouncedSetValue = useMemo(() => debounce(setValue, 1000), [setValue]);
const onChange = useCallback(
(_: any, index: number | number[]) => {
const millis = marks[index as number].seconds * 1000;
setCurValue(millis);
debouncedSetValue(millis);
},
[debouncedSetValue],
);
const indexValue = useMemo(() => findMarkIndex(curValue / 1000), [curValue]);
return (
<>
<Label>
{header} {formatDuration(curValue)}
</Label>
<Slider
value={indexValue}
min={0}
step={1}
max={marks.length - 1}
marks
getAriaValueText={formatDurationFromIndex}
valueLabelFormat={formatDurationFromIndex}
onChange={onChange}
valueLabelDisplay="auto"
aria-labelledby="slider-hide-limit"
/>
</>
);
}
@@ -0,0 +1,40 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { PropsWithChildren } from 'react';
import { Typography, Theme, makeStyles } from '@material-ui/core';
export const useStyles = makeStyles<Theme>(
theme => ({
label: {
fontWeight: 'normal',
margin: theme.spacing(0),
},
}),
{
name: 'CicdStatisticsLabel',
},
);
export function Label({ children }: PropsWithChildren<{}>) {
const classes = useStyles();
return (
<Typography variant="subtitle2" className={classes.label}>
{children}
</Typography>
);
}
@@ -117,3 +117,7 @@ export function formatDuration(millis: number) {
return dur.toHuman({ unitDisplay: 'narrow' }).replace(/, /g, '');
}
export function formatDurationFromSeconds(seconds: number) {
return formatDuration(seconds * 1000);
}
+3 -3
View File
@@ -152,8 +152,6 @@ function CicdCharts(props: CicdChartsProps) {
errorApi.post(chartableStagesState.error);
}, [errorApi, chartableStagesState.error]);
const collapsedLimit = cicdConfiguration.defaults.collapsedLimit ?? 60 * 1000; // 1m
return (
<Grid container>
<Grid item lg={2} className={classes.pane}>
@@ -176,13 +174,15 @@ function CicdCharts(props: CicdChartsProps) {
<StageChart
stage={chartableStages.total}
defaultCollapsed={0}
defaultHidden={viewOptions.hideLimit}
chartTypes={viewOptions.chartTypes}
/>
{[...chartableStages.stages.entries()].map(([name, stage]) => (
<StageChart
key={name}
stage={stage}
defaultCollapsed={collapsedLimit}
defaultCollapsed={viewOptions.collapsedLimit}
defaultHidden={viewOptions.hideLimit}
chartTypes={viewOptions.chartTypes}
/>
))}