From fdabd241d2b42916014ab9cc2d155a67c7fb3458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Wed, 2 Feb 2022 13:18:19 +0100 Subject: [PATCH] feat(cicd-statistics): Added trigger reason MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some refactoring and tweaks. The trigger reason is an overlay to the build counts, and is a percentage of manually triggered builds vs all builds. When this is high, it indicates likelyhood of flaky tests. Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/apis/types.ts | 20 +++ plugins/cicd-statistics/src/charts/colors.ts | 9 +- .../src/charts/logic/conversions.ts | 36 +++- .../src/charts/logic/count-builds-per-day.ts | 10 +- .../src/charts/logic/daily-summary.ts | 115 ++++++++++++ .../src/charts/logic/utils.test.ts | 27 +++ .../cicd-statistics/src/charts/logic/utils.ts | 32 +++- .../src/charts/stage-chart.tsx | 7 +- .../src/charts/status-chart.tsx | 163 +++++++++++++----- plugins/cicd-statistics/src/charts/types.ts | 55 +++++- .../src/components/chart-filters.tsx | 20 ++- .../cicd-statistics/src/components/utils.tsx | 3 +- plugins/cicd-statistics/src/entity-page.tsx | 18 +- 13 files changed, 436 insertions(+), 79 deletions(-) create mode 100644 plugins/cicd-statistics/src/charts/logic/daily-summary.ts create mode 100644 plugins/cicd-statistics/src/charts/logic/utils.test.ts diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 19b4ac84b2..0726c566c7 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -53,6 +53,23 @@ export const statusTypes: Array = [ */ export type FilterBranchType = 'master' | 'branch'; +export type TriggerReason = + /** Triggered by source code management, e.g. a Github hook */ + | 'scm' + /** Triggered manually */ + | 'manual' + /** Triggered internally (non-scm, or perhaps after being delayed/enqueued) */ + | 'internal' + /** Triggered for some other reason */ + | 'other'; + +export const triggerReasons: Array = [ + 'scm', + 'manual', + 'internal', + 'other', +]; + /** * A Stage is a part of either a Build or a parent Stage. * @@ -84,6 +101,9 @@ export interface Build { /** Build id */ id: string; + /** The reason this build was started */ + triggeredBy?: TriggerReason; + /** The status of the build */ status: FilterStatusType; diff --git a/plugins/cicd-statistics/src/charts/colors.ts b/plugins/cicd-statistics/src/charts/colors.ts index e1ccb67fea..670b214665 100644 --- a/plugins/cicd-statistics/src/charts/colors.ts +++ b/plugins/cicd-statistics/src/charts/colors.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FilterStatusType } from '../apis/types'; +import { FilterStatusType, TriggerReason } from '../apis/types'; export const statusColorMap: Record = { unknown: '#3d01a4', @@ -28,6 +28,13 @@ export const statusColorMap: Record = { expired: '#a7194b', }; +export const triggerColorMap: Record = { + scm: '#0391ce', + manual: '#a7194b', + internal: '#82ca9d', + other: '#f3f318', +}; + export const fireColors: Array<[percent: string, color: string]> = [ ['5%', '#e19678'], ['30%', '#dfe178'], diff --git a/plugins/cicd-statistics/src/charts/logic/conversions.ts b/plugins/cicd-statistics/src/charts/logic/conversions.ts index b2e990d9e4..c548b0e2f5 100644 --- a/plugins/cicd-statistics/src/charts/logic/conversions.ts +++ b/plugins/cicd-statistics/src/charts/logic/conversions.ts @@ -16,10 +16,11 @@ import { map } from 'already'; -import { Build, Stage } from '../../apis/types'; +import { Build, Stage, FilterStatusType } from '../../apis/types'; import { ChartableStage, ChartableStagesAnalysis } from '../types'; -import { getOrSetStage, makeStage } from './utils'; +import { getOrSetStage, makeStage, sortStatuses } from './utils'; import { finalizeStage } from './finalize-stage'; +import { dailySummary } from './daily-summary'; export interface ChartableStagesOptions { normalizeTimeRange: boolean; @@ -86,5 +87,34 @@ export async function buildsToChartableStages( ); finalizeStage(total, { allEpochs, averageWidth: 10 }); - return { total, stages }; + const daily = dailySummary(builds); + + const statuses = findStatuses(total, [...stages.values()]); + + return { daily, total, stages, statuses }; +} + +function findStatuses( + total: ChartableStage, + stages: Array, +): Array { + const statuses = new Set(); + + const addStatuses = (set: Set) => { + set.forEach(status => { + statuses.add(status); + }); + }; + + addStatuses(total.statusSet); + + const recurse = (subStages: Array) => { + subStages.forEach(stage => { + addStatuses(stage.statusSet); + recurse([...stage.stages.values()]); + }); + }; + recurse(stages); + + return sortStatuses([...statuses]); } diff --git a/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts b/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts index c7c7ce1c21..54e47c3f5b 100644 --- a/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts +++ b/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts @@ -14,20 +14,16 @@ * limitations under the License. */ -import { DateTime } from 'luxon'; import { groupBy } from 'lodash'; import { FilterStatusType, statusTypes } from '../../apis/types'; import { Countify, ChartableStageDatapoints } from '../types'; - -function startOfDayEpoch(epoch: number) { - return DateTime.fromMillis(epoch).startOf('day').toMillis(); -} +import { startOfDay } from './utils'; export function countBuildsPerDay( values: ReadonlyArray, ) { - const days = groupBy(values, value => startOfDayEpoch(value.__epoch)); + const days = groupBy(values, value => startOfDay(value.__epoch)); Object.entries(days).forEach(([_startOfDay, valuesThisDay]) => { const counts = Object.fromEntries( statusTypes @@ -35,7 +31,7 @@ export function countBuildsPerDay( type => [ type, - valuesThisDay.map(value => value[type] !== undefined).length, + valuesThisDay.filter(value => value[type] !== undefined).length, ] as const, ) .filter(([_type, count]) => count > 0) diff --git a/plugins/cicd-statistics/src/charts/logic/daily-summary.ts b/plugins/cicd-statistics/src/charts/logic/daily-summary.ts new file mode 100644 index 0000000000..93c7b07c34 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/daily-summary.ts @@ -0,0 +1,115 @@ +/* + * 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 { groupBy, countBy } from 'lodash'; + +import { Build } from '../../apis/types'; +import { + Epoch, + TriggerReasonsDatapoint, + StatusesDatapoint, + ChartableDaily, +} from '../types'; +import { sortStatuses, sortTriggerReasons, startOfDay } from './utils'; + +export function dailySummary(builds: ReadonlyArray): ChartableDaily { + const triggersDaily = countTriggersPerDay(builds); + const statusesDaily = countStatusesPerDay(builds); + + const { triggerReasons } = triggersDaily; + const { statuses } = statusesDaily; + + const reasonMap = new Map( + triggersDaily.values.map(value => [value.__epoch, value]), + ); + const statusMap = new Map( + statusesDaily.values.map(value => [value.__epoch, value]), + ); + + const days = Object.keys( + groupBy(builds, value => startOfDay(value.requestedAt)), + ) + .map(epoch => parseInt(epoch, 10)) + .sort(); + + return { + values: days.map(epoch => ({ + __epoch: epoch, + ...reasonMap.get(epoch), + ...statusMap.get(epoch), + })), + triggerReasons, + statuses, + }; +} + +function countTriggersPerDay(builds: ReadonlyArray) { + const days = groupBy(builds, value => startOfDay(value.requestedAt)); + + const triggerReasons = sortTriggerReasons([ + ...new Set( + builds + .map(({ triggeredBy }) => triggeredBy) + .filter((v): v is NonNullable => !!v), + ), + ]); + + const values = Object.entries(days).map(([epoch, buildsThisDay]) => { + const datapoint = Object.fromEntries( + triggerReasons + .map(reason => [ + reason, + buildsThisDay.filter(build => build.triggeredBy === reason).length, + ]) + .filter(([_type, count]) => count > 0), + ) as Omit; + + // Assign the count for this day to the first value this day + const value: Epoch & TriggerReasonsDatapoint = Object.assign(datapoint, { + __epoch: parseInt(epoch, 10), + }); + + return value; + }); + + return { triggerReasons, values }; +} + +function countStatusesPerDay(builds: ReadonlyArray) { + const days = groupBy(builds, value => startOfDay(value.requestedAt)); + + const foundStatuses = new Set(); + + const values = Object.entries(days).map(([epoch, buildsThisDay]) => { + const byStatus = countBy(buildsThisDay, 'status'); + + const value: Epoch & StatusesDatapoint = { + __epoch: parseInt(epoch, 10), + ...byStatus, + }; + + Object.keys(byStatus).forEach(status => { + foundStatuses.add(status); + }); + + return value; + }); + + return { + statuses: sortStatuses([...foundStatuses]), + values, + }; +} diff --git a/plugins/cicd-statistics/src/charts/logic/utils.test.ts b/plugins/cicd-statistics/src/charts/logic/utils.test.ts new file mode 100644 index 0000000000..420a6bf90c --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/utils.test.ts @@ -0,0 +1,27 @@ +/* + * 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 { sortTriggerReasons } from './daily-summary'; + +describe('daily-summary', () => { + it('sortTriggerReasons', () => { + const values = ['a', 'manual', 'b', 'other', 'c', 'scm', 'd']; + const expected = ['manual', 'scm', 'other', 'a', 'b', 'c', 'd']; + + expect(sortTriggerReasons(values)).toStrictEqual(expected); + expect(sortTriggerReasons(values.reverse())).toStrictEqual(expected); + }); +}); diff --git a/plugins/cicd-statistics/src/charts/logic/utils.ts b/plugins/cicd-statistics/src/charts/logic/utils.ts index 0c3900d025..f3a0087a3d 100644 --- a/plugins/cicd-statistics/src/charts/logic/utils.ts +++ b/plugins/cicd-statistics/src/charts/logic/utils.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { FilterStatusType } from '../../apis/types'; +import { DateTime } from 'luxon'; + +import { FilterStatusType, statusTypes } from '../../apis/types'; import { ChartableStage } from '../types'; export function average(values: number[]): number { @@ -55,3 +57,31 @@ export function makeStage(name: string): ChartableStage { stages: new Map(), }; } + +export function startOfDay(date: number | Date) { + if (typeof date === 'number') { + return DateTime.fromMillis(date).startOf('day').toMillis(); + } + return DateTime.fromJSDate(date).startOf('day').toMillis(); +} + +export function sortTriggerReasons(reasons: Array): Array { + return reasons.sort((a, b) => { + if (a === 'manual') return -1; + else if (b === 'manual') return 1; + else if (a === 'scm') return -1; + else if (b === 'scm') return 1; + else if (a === 'other') return -1; + else if (b === 'other') return 1; + return a.localeCompare(b); + }); +} + +export function sortStatuses(statuses: Array): Array { + return [ + ...statusTypes.filter(status => statuses.includes(status)), + ...statuses + .filter(status => !(statusTypes as Array).includes(status)) + .sort((a, b) => a.localeCompare(b)), + ]; +} diff --git a/plugins/cicd-statistics/src/charts/stage-chart.tsx b/plugins/cicd-statistics/src/charts/stage-chart.tsx index f9b0312595..4141a6755a 100644 --- a/plugins/cicd-statistics/src/charts/stage-chart.tsx +++ b/plugins/cicd-statistics/src/charts/stage-chart.tsx @@ -38,6 +38,7 @@ import { Typography, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { capitalize } from 'lodash'; import { CicdDefaults, statusTypes } from '../apis/types'; import { ChartableStage } from './types'; @@ -95,7 +96,7 @@ export function StageChart(props: StageChartProps) { const legendPayload = useMemo( (): LegendProps['payload'] => statuses.map(status => ({ - value: status, + value: capitalize(status), type: 'line', id: status, color: statusColorMap[status], @@ -172,7 +173,7 @@ export function StageChart(props: StageChartProps) { labelFormatter={labelFormatter} /> {statuses.reverse().map(status => ( - <> + {!chartTypes[status].includes('duration') ? null : ( )} - + ))} diff --git a/plugins/cicd-statistics/src/charts/status-chart.tsx b/plugins/cicd-statistics/src/charts/status-chart.tsx index ff4d3fea3a..5a4cc953ca 100644 --- a/plugins/cicd-statistics/src/charts/status-chart.tsx +++ b/plugins/cicd-statistics/src/charts/status-chart.tsx @@ -16,6 +16,7 @@ import React, { Fragment, useMemo } from 'react'; import { + Area, Bar, ComposedChart, XAxis, @@ -34,66 +35,101 @@ import { Typography, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { countBy } from 'lodash'; -import { DateTime } from 'luxon'; +import { capitalize } from 'lodash'; -import { Build, FilterStatusType, statusTypes } from '../apis/types'; +import { FilterStatusType, TriggerReason } from '../apis/types'; import { labelFormatterWithoutTime, tickFormatterX } from '../components/utils'; -import { statusColorMap } from './colors'; +import { statusColorMap, triggerColorMap } from './colors'; +import { ChartableStagesAnalysis } from './types'; export interface StatusChartProps { - builds: ReadonlyArray; + analysis: ChartableStagesAnalysis; } export function StatusChart(props: StatusChartProps) { - const { builds } = props; + const { analysis } = props; - const { statuses, values } = useMemo(() => { - const buildsByDay = new Map>(); + const values = useMemo(() => { + return analysis.daily.values.map(value => { + const totTriggers = analysis.daily.triggerReasons.reduce( + (prev, cur) => prev + (value[cur as TriggerReason] ?? 0), + 0, + ); - const foundStatuses = new Set(); - - builds.forEach(build => { - foundStatuses.add(build.status); - - const dayString = DateTime.fromJSDate(build.requestedAt).toISODate(); - const dayList = buildsByDay.get(dayString); - if (dayList) { - dayList.push(build); - } else { - buildsByDay.set(dayString, [build]); + if (!totTriggers) { + return value; } - }); - return { - statuses: [ - ...statusTypes.filter(status => foundStatuses.has(status)), - ...[...foundStatuses].filter( - status => !(statusTypes as Array).includes(status), + return { + ...value, + ...Object.fromEntries( + analysis.daily.triggerReasons.map(reason => [ + reason, + (value[reason as TriggerReason] ?? 0) / totTriggers, + ]), ), - ], - values: [...buildsByDay.entries()].map(([dayString, buildThisDay]) => ({ - __epoch: DateTime.fromISO(dayString).toMillis(), - ...countBy(buildThisDay, 'status'), - })), - }; - }, [builds]); + }; + }); + }, [analysis.daily]); - const legendPayload = useMemo( - (): LegendProps['payload'] => - statuses.map(status => ({ - value: status, + const triggerReasonLegendPayload = useMemo( + (): NonNullable => + analysis.daily.triggerReasons.map(reason => ({ + value: humanTriggerReason(reason), + type: 'line', + id: reason, + color: triggerColorMap[reason as TriggerReason] ?? '', + })), + [analysis.daily.triggerReasons], + ); + + const statusesLegendPayload = useMemo( + (): NonNullable => + analysis.daily.statuses.map(status => ({ + value: capitalize(status), type: 'line', id: status, color: statusColorMap[status as FilterStatusType] ?? '', })), - [statuses], + [analysis.daily.statuses], ); + const legendPayload = useMemo( + (): NonNullable => [ + ...triggerReasonLegendPayload, + ...statusesLegendPayload, + ], + [statusesLegendPayload, triggerReasonLegendPayload], + ); + + const tooltipFormatter = useMemo(() => { + const reasonSet = new Set(analysis.daily.triggerReasons); + + return (percentOrCount: number, name: string) => { + const label = reasonSet.has(name) + ? humanTriggerReason(name) + : capitalize(name); + const valueText = reasonSet.has(name) + ? `${(percentOrCount * 100).toFixed(0)}%` + : percentOrCount; + + return [ + + {label}: {valueText} + , + null, + ]; + }; + }, [analysis.daily.triggerReasons]); + + const barSize = getBarSize(analysis.daily.values.length); + return ( - 1}> + 1}> }> - Build count per status + + Build count per status over build trigger reason + {values.length === 0 ? ( @@ -108,14 +144,33 @@ export function StatusChart(props: StatusChartProps) { type="category" tickFormatter={tickFormatterX} /> - - - {statuses.map(status => ( + + + + {triggerReasonLegendPayload.map(reason => ( + + + + ))} + {[...analysis.daily.statuses].reverse().map(status => ( ); } + +function humanTriggerReason(reason: string): string { + if ((reason as TriggerReason) === 'manual') { + return 'Triggered manually'; + } else if ((reason as TriggerReason) === 'scm') { + return 'Triggered by SCM'; + } else if ((reason as TriggerReason) === 'internal') { + return 'Triggered internally'; + } else if ((reason as TriggerReason) === 'other') { + return 'Triggered by another reason'; + } + return `Triggered by ${reason}`; +} + +function getBarSize(count: number): number { + if (count < 20) { + return 10; + } else if (count < 40) { + return 8; + } + return 5; +} diff --git a/plugins/cicd-statistics/src/charts/types.ts b/plugins/cicd-statistics/src/charts/types.ts index 75a72a8a4f..88c0085f77 100644 --- a/plugins/cicd-statistics/src/charts/types.ts +++ b/plugins/cicd-statistics/src/charts/types.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { FilterStatusType } from '../apis/types'; +import { FilterStatusType, TriggerReason } from '../apis/types'; export type Averagify = `${T} avg`; export type Countify = `${T} count`; -export type ChartableStageDatapoints = { - __epoch: number; -} & { +export type Epoch = { __epoch: number }; + +export type ChartableStageDatapoints = Epoch & { [status in FilterStatusType]?: number; } & { [status in Averagify]?: number; @@ -50,7 +50,48 @@ export interface ChartableStage { stages: Map; } -export interface ChartableStagesAnalysis { - total: ChartableStage; - stages: Map; +export type TriggerReasonsDatapoint = { + [K in TriggerReason]?: number; +}; +export type StatusesDatapoint = { [status in FilterStatusType]?: number }; + +export type ChartableDailyDatapoint = Epoch & + TriggerReasonsDatapoint & + StatusesDatapoint; + +export interface ChartableDaily { + values: Array; + + /** + * The build trigger reasons + */ + triggerReasons: Array; + + /** + * The top-level (build) statuses + */ + statuses: Array; +} + +export interface ChartableStagesAnalysis { + /** + * Summary of statuses and trigger reasons per day + */ + daily: ChartableDaily; + + /** + * Total aggregates of sub stages + */ + total: ChartableStage; + + /** + * Top-level stages {name -> stage} + */ + stages: Map; + + /** + * All statuses found deeper in the stage tree. A stage might have been + * _aborted_ although the build actually _failed_, e.g. + */ + statuses: Array; } diff --git a/plugins/cicd-statistics/src/components/chart-filters.tsx b/plugins/cicd-statistics/src/components/chart-filters.tsx index 6ff44bc40a..03cbea5dd0 100644 --- a/plugins/cicd-statistics/src/components/chart-filters.tsx +++ b/plugins/cicd-statistics/src/components/chart-filters.tsx @@ -48,6 +48,7 @@ import { FilterStatusType, statusTypes, } from '../apis/types'; +import { ChartableStagesAnalysis } from '../charts/types'; import { ButtonSwitch, SwitchValue } from './button-switch'; import { Toggle } from './toggle'; import { DurationSlider } from './duration-slider'; @@ -93,8 +94,8 @@ export type StatusSelection = FilterStatusType; export interface ChartFilter { fromDate: Date; toDate: Date; - branch: BranchSelection; - status: Array; + branch: string; + status: Array; } export function getDefaultChartFilter( @@ -175,6 +176,8 @@ const chartTypeValues: Array> = [ ]; export interface ChartFiltersProps { + analysis?: ChartableStagesAnalysis; + cicdConfiguration: CicdConfiguration; initialFetchFilter: ChartFilter; currentFetchFilter?: ChartFilter; @@ -191,6 +194,7 @@ interface InternalRef { export function ChartFilters(props: ChartFiltersProps) { const { + analysis, cicdConfiguration, initialFetchFilter, currentFetchFilter, @@ -325,6 +329,8 @@ export function ChartFilters(props: ChartFiltersProps) { }); }, [toDate, fromDate, branch, selectedStatus, updateFetchFilter]); + const inrefferedStatuses = analysis?.statuses ?? selectedStatus; + return ( @@ -395,7 +401,7 @@ export function ChartFilters(props: ChartFiltersProps) { > Branch - + values={branchValues} selection={branch} onChange={setBranch} @@ -406,7 +412,7 @@ export function ChartFilters(props: ChartFiltersProps) { > Status - + values={statusValues} multi vertical @@ -469,12 +475,12 @@ export function ChartFilters(props: ChartFiltersProps) { > Chart styles - {currentFetchFilter?.status.map(status => ( - + {inrefferedStatuses.map(status => ( + values={chartTypeValues} - selection={viewOptions.chartTypes[status]} + selection={viewOptions.chartTypes[status as FilterStatusType]} onChange={setChartTypeSpecific[status]} multi /> diff --git a/plugins/cicd-statistics/src/components/utils.tsx b/plugins/cicd-statistics/src/components/utils.tsx index 6eeb81df23..2b6d1ad029 100644 --- a/plugins/cicd-statistics/src/components/utils.tsx +++ b/plugins/cicd-statistics/src/components/utils.tsx @@ -16,6 +16,7 @@ import React, { CSSProperties } from 'react'; import { DateTime, Duration } from 'luxon'; +import { capitalize } from 'lodash'; const infoText: CSSProperties = { color: 'InfoText' }; @@ -79,7 +80,7 @@ export function tickFormatterY(duration: number) { export function tooltipValueFormatter(durationOrCount: number, name: string) { return [ - {name}:{' '} + {capitalize(name)}:{' '} {name.endsWith(' count') ? durationOrCount : formatDuration(durationOrCount)} diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index 87f5a9e8d4..532900c1da 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -35,7 +35,11 @@ import { getDefaultViewOptions, ViewOptions, } from './components/chart-filters'; -import { CicdConfiguration } from './apis'; +import { + CicdConfiguration, + FilterStatusType, + FilterBranchType, +} from './apis/types'; import { cleanupBuildTree } from './utils/stage-names'; import { renderFallbacks, useAsyncChain } from './components/progress'; import { sortFilterStatusType } from './utils/api'; @@ -68,7 +72,7 @@ function endOfDay(date: Date) { function cleanChartFilter(filter: ChartFilter): ChartFilter { return { ...filter, - status: sortFilterStatusType(filter.status), + status: sortFilterStatusType(filter.status as FilterStatusType[]), }; } @@ -104,8 +108,8 @@ function CicdCharts(props: CicdChartsProps) { entity, timeFrom: startOfDay(fetchedChartData.chartFilter.fromDate), timeTo: endOfDay(fetchedChartData.chartFilter.toDate), - filterStatus: fetchedChartData.chartFilter.status, - filterType: fetchedChartData.chartFilter.branch, + filterStatus: fetchedChartData.chartFilter.status as FilterStatusType[], + filterType: fetchedChartData.chartFilter.branch as FilterBranchType, }; }, [entity, fetchedChartData]); @@ -156,6 +160,7 @@ function CicdCharts(props: CicdChartsProps) { {renderFallbacks(chartableStagesState, chartableStages => ( <> - {!statisticsState.value?.builds.length ? null : ( - + {!statisticsState.value?.builds?.length || + !chartableStagesState.value?.daily?.values?.length ? null : ( + )}