From 770c195f34ebdc79c0bee0b6f8c6b8a52bb6b58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Thu, 13 Jan 2022 11:15:32 +0100 Subject: [PATCH 01/25] feat(cicd-statistics): Added CI/CD statistics plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .changeset/strong-ties-exist.md | 5 + plugins/cicd-statistics/.eslintrc.js | 3 + plugins/cicd-statistics/README.md | 7 + plugins/cicd-statistics/api-report.md | 182 +++++++++ plugins/cicd-statistics/package.json | 53 +++ .../src/apis/cicd-statistics.ts | 23 ++ plugins/cicd-statistics/src/apis/index.ts | 18 + plugins/cicd-statistics/src/apis/types.ts | 225 +++++++++++ plugins/cicd-statistics/src/charts/colors.ts | 39 ++ .../cicd-statistics/src/charts/conversions.ts | 238 +++++++++++ .../src/charts/stage-chart.tsx | 212 ++++++++++ .../src/charts/status-chart.tsx | 133 ++++++ plugins/cicd-statistics/src/charts/types.ts | 48 +++ plugins/cicd-statistics/src/charts/utils.tsx | 87 ++++ .../src/components/button-switch.tsx | 117 ++++++ .../src/components/chart-filters.tsx | 382 ++++++++++++++++++ .../src/components/progress.tsx | 147 +++++++ .../cicd-statistics/src/components/toggle.tsx | 40 ++ plugins/cicd-statistics/src/entity-page.tsx | 175 ++++++++ .../src/hooks/use-cicd-configuration.ts | 59 +++ .../src/hooks/use-cicd-statistics-api.ts | 27 ++ .../src/hooks/use-cicd-statistics.ts | 120 ++++++ plugins/cicd-statistics/src/index.ts | 18 + plugins/cicd-statistics/src/plugin.ts | 40 ++ .../cicd-statistics/src/utils/stage-names.ts | 73 ++++ 25 files changed, 2471 insertions(+) create mode 100644 .changeset/strong-ties-exist.md create mode 100644 plugins/cicd-statistics/.eslintrc.js create mode 100644 plugins/cicd-statistics/README.md create mode 100644 plugins/cicd-statistics/api-report.md create mode 100644 plugins/cicd-statistics/package.json create mode 100644 plugins/cicd-statistics/src/apis/cicd-statistics.ts create mode 100644 plugins/cicd-statistics/src/apis/index.ts create mode 100644 plugins/cicd-statistics/src/apis/types.ts create mode 100644 plugins/cicd-statistics/src/charts/colors.ts create mode 100644 plugins/cicd-statistics/src/charts/conversions.ts create mode 100644 plugins/cicd-statistics/src/charts/stage-chart.tsx create mode 100644 plugins/cicd-statistics/src/charts/status-chart.tsx create mode 100644 plugins/cicd-statistics/src/charts/types.ts create mode 100644 plugins/cicd-statistics/src/charts/utils.tsx create mode 100644 plugins/cicd-statistics/src/components/button-switch.tsx create mode 100644 plugins/cicd-statistics/src/components/chart-filters.tsx create mode 100644 plugins/cicd-statistics/src/components/progress.tsx create mode 100644 plugins/cicd-statistics/src/components/toggle.tsx create mode 100644 plugins/cicd-statistics/src/entity-page.tsx create mode 100644 plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts create mode 100644 plugins/cicd-statistics/src/hooks/use-cicd-statistics-api.ts create mode 100644 plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts create mode 100644 plugins/cicd-statistics/src/index.ts create mode 100644 plugins/cicd-statistics/src/plugin.ts create mode 100644 plugins/cicd-statistics/src/utils/stage-names.ts diff --git a/.changeset/strong-ties-exist.md b/.changeset/strong-ties-exist.md new file mode 100644 index 0000000000..dae77c2805 --- /dev/null +++ b/.changeset/strong-ties-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cicd-statistics': minor +--- + +Added new plugin "CI/CD Statistics" which charts pipeline build durations over time diff --git a/plugins/cicd-statistics/.eslintrc.js b/plugins/cicd-statistics/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/cicd-statistics/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/cicd-statistics/README.md b/plugins/cicd-statistics/README.md new file mode 100644 index 0000000000..e4db2166a6 --- /dev/null +++ b/plugins/cicd-statistics/README.md @@ -0,0 +1,7 @@ +# CI/CD Statistics Plugin + +This plugin shows charts of CI/CD pipeline durations over time. It expects to be used on the Software Catalog entity page, as it uses `useEntity` to figure out what component to get the build information for. + +To use this plugin, you need to implement an API `CicdStatisticsApi` and bind it to the `cicdStatisticsApiRef`. This API is defined in `src/apis/types.ts` and is an interface with two functions, `getConfiguration()` and `fetchBuilds(options)`. This plugin will call `getConfiguration` to allow the implementation to specify defaults an settings for the UI. First time the UI shows, and each time the user changes filters and clicks `Update` to refresh the data, `fetchBuilds` is invoked with the filter options. The API implementation is the expected to fetch build information from somewhere, format it into a generic and rather simpe type `Build` (also defined in `types.ts`). The API can optionally signal completion for a progress bar in the UI. + +When this plugin has fetched the builds, it will transpose the list of builds (and build stages) into a tree of build stages. As build pipelines sometimes change, certain stages might end or begin within the timerange of the view. diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md new file mode 100644 index 0000000000..aef376c9b8 --- /dev/null +++ b/plugins/cicd-statistics/api-report.md @@ -0,0 +1,182 @@ +## API Report File for "@backstage/plugin-cicd-statistics" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "AbortError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class AbortError extends Error {} + +// Warning: (ae-missing-release-tag) "branchTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const branchTypes: Array; + +// Warning: (ae-missing-release-tag) "Build" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface Build { + buildType: FilterBranchType; + duration: number; + id: string; + // (undocumented) + raw?: unknown; + requestedAt: Date; + stages: Array; + status: FilterStatusType; +} + +// Warning: (ae-missing-release-tag) "BuildWithRaw" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type BuildWithRaw = Build & { + raw: T; +}; + +// Warning: (ae-missing-release-tag) "CicdConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdConfiguration { + availableStatuses: ReadonlyArray; + defaults: Partial; + formatStageName: (parentNames: Array, stageName: string) => string; +} + +// Warning: (ae-missing-release-tag) "CicdDefaults" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdDefaults { + collapsedLimit: number; + // (undocumented) + filterStatus: Array; + // (undocumented) + filterType: FilterBranchType<'all'>; + lowercaseNames: boolean; + normalizeTimeRange: boolean; + // (undocumented) + timeFrom: Date; + // (undocumented) + timeTo: Date; +} + +// Warning: (ae-missing-release-tag) "CicdState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdState { + // (undocumented) + builds: Array; +} + +// Warning: (ae-missing-release-tag) "CicdStatisticsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdStatisticsApi { + // (undocumented) + fetchBuilds(options: FetchBuildsOptions): Promise; + // (undocumented) + getConfiguration(): Promise>; +} + +// Warning: (ae-missing-release-tag) "cicdStatisticsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const cicdStatisticsApiRef: ApiRef; + +// Warning: (ae-missing-release-tag) "cicdStatisticsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const cicdStatisticsPlugin: BackstagePlugin< + { + entityContent: RouteRef; + }, + {} +>; + +// Warning: (ae-forgotten-export) The symbol "EntityPageCicdCharts" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "EntityCicdStatisticsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityCicdStatisticsContent: EntityPageCicdCharts; + +// Warning: (ae-missing-release-tag) "FetchBuildsOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface FetchBuildsOptions { + // (undocumented) + abortSignal: AbortSignal; + // (undocumented) + entity: Entity; + // (undocumented) + filterStatus: Array>; + // (undocumented) + filterType: FilterBranchType<'all'>; + // (undocumented) + timeFrom: Date; + // (undocumented) + timeTo: Date; + // (undocumented) + updateProgress: UpdateProgress; +} + +// Warning: (ae-missing-release-tag) "FilterBranchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type FilterBranchType = + | Extra + | 'master' + | 'branch'; + +// Warning: (ae-missing-release-tag) "FilterStatusType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type FilterStatusType = + | Extra + | 'unknown' + | 'enqueued' + | 'scheduled' + | 'running' + | 'aborted' + | 'succeeded' + | 'failed' + | 'stalled' + | 'expired'; + +// Warning: (ae-missing-release-tag) "rootCatalogCicdStatsRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const rootCatalogCicdStatsRouteRef: RouteRef; + +// Warning: (ae-missing-release-tag) "Stage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface Stage { + duration: number; + // (undocumented) + name: string; + stages?: Array; +} + +// Warning: (ae-missing-release-tag) "statusTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const statusTypes: Array; + +// Warning: (ae-missing-release-tag) "UpdateProgress" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type UpdateProgress = ( + completed: number, + total: number, + started?: number, +) => void; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json new file mode 100644 index 0000000000..8fad2b25f3 --- /dev/null +++ b/plugins/cicd-statistics/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-cicd-statistics", + "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/cicd-statistics" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.8", + "@backstage/core-plugin-api": "^0.4.1", + "@backstage/plugin-catalog-react": "^0.6.9", + "@date-io/date-fns": "^1.3.13", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "4.0.0-alpha.57", + "@material-ui/pickers": "^3.3.10", + "already": "^3.2.0", + "date-fns": "^2.27.0", + "lodash": "^4.17.21", + "react-use": "^17.3.1", + "recharts": "^2.1.5" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/cicd-statistics/src/apis/cicd-statistics.ts b/plugins/cicd-statistics/src/apis/cicd-statistics.ts new file mode 100644 index 0000000000..714900a1bd --- /dev/null +++ b/plugins/cicd-statistics/src/apis/cicd-statistics.ts @@ -0,0 +1,23 @@ +/* + * 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 { createApiRef } from '@backstage/core-plugin-api'; + +import { CicdStatisticsApi } from './types'; + +export const cicdStatisticsApiRef = createApiRef({ + id: 'cicd-statistics-api', +}); diff --git a/plugins/cicd-statistics/src/apis/index.ts b/plugins/cicd-statistics/src/apis/index.ts new file mode 100644 index 0000000000..cb2fff3700 --- /dev/null +++ b/plugins/cicd-statistics/src/apis/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './types'; +export * from './cicd-statistics'; diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts new file mode 100644 index 0000000000..1f2fa8a5b9 --- /dev/null +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -0,0 +1,225 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; + +/** + * This is a generic enum of build statuses. + * + * If all of these aren't applicable to the underlying CI/CD, these can be + * configured to be hidden, using the `availableStatuses` in `CicdConfiguration`. + */ +export type FilterStatusType = + | Extra + | 'unknown' + | 'enqueued' + | 'scheduled' + | 'running' + | 'aborted' + | 'succeeded' + | 'failed' + | 'stalled' + | 'expired'; +export const statusTypes: Array = [ + 'succeeded', + 'failed', + 'enqueued', + 'scheduled', + 'running', + 'aborted', + 'stalled', + 'expired', + 'unknown', +]; + +/** + * The branch enum of either 'master' or 'branch' (or possibly the meta 'all'). + * + * The concept of what constitues a master branch is generic. It might be called + * something like 'release' or 'main' or 'trunk' in the underlying CI/CD system, + * which is then up to the Api to map accordingly. + */ +export type FilterBranchType = + | Extra + | 'master' + | 'branch'; +export const branchTypes: Array = ['master', 'branch']; + +/** + * A Stage is a part of either a Build or a parent Stage. + * + * This may be called things like Stage or Step or Task in CI/CD systems, but is + * generic here. There's also no concept of parallelism which might exist within + * some stages. + */ +export interface Stage { + name: string; + + /** Stage duration in milliseconds */ + duration: number; + + /** Sub stages within this stage */ + stages?: Array; +} + +/** + * Generic Build type. + * + * A build has e.g. a build type (master/branch), a status and (possibly) sub stages. + */ +export interface Build { + raw?: unknown; + + /** Build id */ + id: string; + + /** The status of the build */ + status: FilterStatusType; + + /** Branch type */ + buildType: FilterBranchType; + + /** Time when the build started */ + requestedAt: Date; + + /** The overall duration of the build */ + duration: number; + + /** Top-level build stages */ + stages: Array; +} + +/** + * Helper type which is a Build with a certain typed 'raw' field. + * + * This can be useful in an Api to use while mapping internal data structures + * (raw) into generic builds. + */ +export type BuildWithRaw = Build & { + raw: T; +}; + +/** + * Default settings for the fetching options and view options. + * + * These are all optional, but can be overridden from the Api to whatever makes + * most sense for that implementation. + */ +export interface CicdDefaults { + timeFrom: Date; + timeTo: Date; + filterStatus: Array; + filterType: FilterBranchType<'all'>; + + /** 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; +} + +/** + * A configuration interface which the Api must implement. + * + * When the UI for the CI/CD Statistics is loaded, it begins with fetching the + * configuration before anything else. + * + * All of these fields are optional though, and will fallback to hard-coded defaults. + */ +export interface CicdConfiguration { + /** + * This field can be used to override what statuses are available + */ + availableStatuses: ReadonlyArray; + + /** + * When transposing the list of builds into a tree of stages, the stage names + * will be transformed through this function. + * + * Override this for a custom implementation. The default will try to remove + * parent names off of child names, if they are prepended by them. + * + * For example; if a stage has the name 'Install' and a child stage has the + * name 'Install - Fetch dependencies', the child name will be replaced with + * 'Fetch dependencies'. + */ + formatStageName: (parentNames: Array, stageName: string) => string; + + /** + * Default options for the UI + */ + defaults: Partial; +} + +/** + * If the Api implements support for aborting the fetching of builds, throw an + * AbortError of this type + */ +export class AbortError extends Error {} + +/** + * The result type for `fetchBuilds`. + */ +export interface CicdState { + builds: Array; +} + +/** + * When fetching, if applicable, the Api can feedback progress back to the UI. + * + * Use the `updateProgress(completed, total, started?)` to signal that + * `completed` builds out of a `total` has finished. Optionally use the + * `started` to signal how many builds have been started in total (i.e. at least + * the amount of `completed`). + * + * This can be called at any rate. Rate limiting (debouncing) is implemented in + * the UI. + */ +export type UpdateProgress = ( + completed: number, + total: number, + started?: number, +) => void; + +/** + * When fetching, the Api should fetch build information about the `entity` and + * respect the `timeFrom`, `timeTo`, `filterStatus` and `filterType`. + * + * Optionally implement support for `updateProgress` and `abortSignal` if + * preferred. + * + * When the UI re-fetches, it will abort any previous fetching, so polling + * `abortSignal.aborted`, and possibly throwing an `AbortError`, can be useful. + */ +export interface FetchBuildsOptions { + entity: Entity; + updateProgress: UpdateProgress; + abortSignal: AbortSignal; + timeFrom: Date; + timeTo: Date; + filterStatus: Array>; + filterType: FilterBranchType<'all'>; +} + +/** + * The interface which is mapped to the `cicdStatisticsApiRef` which is used by + * the UI. + */ +export interface CicdStatisticsApi { + getConfiguration(): Promise>; + fetchBuilds(options: FetchBuildsOptions): Promise; +} diff --git a/plugins/cicd-statistics/src/charts/colors.ts b/plugins/cicd-statistics/src/charts/colors.ts new file mode 100644 index 0000000000..e1ccb67fea --- /dev/null +++ b/plugins/cicd-statistics/src/charts/colors.ts @@ -0,0 +1,39 @@ +/* + * 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 { FilterStatusType } from '../apis/types'; + +export const statusColorMap: Record = { + unknown: '#3d01a4', + enqueued: '#7ad1b9', + scheduled: '#0391ce', + running: '#f3f318', + aborted: '#8600af', + succeeded: '#66b032', + failed: '#fe2712', + stalled: '#fb9904', + expired: '#a7194b', +}; + +export const fireColors: Array<[percent: string, color: string]> = [ + ['5%', '#e19678'], + ['30%', '#dfe178'], + ['50%', '#82ca9d'], + ['95%', '#82ca9d'], +]; + +export const colorStroke = '#c0c0c0'; +export const colorStrokeAvg = '#788ee1'; diff --git a/plugins/cicd-statistics/src/charts/conversions.ts b/plugins/cicd-statistics/src/charts/conversions.ts new file mode 100644 index 0000000000..0f2b024d1f --- /dev/null +++ b/plugins/cicd-statistics/src/charts/conversions.ts @@ -0,0 +1,238 @@ +/* + * 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 { map } from 'already'; + +import { Build, Stage, FilterStatusType, statusTypes } from '../apis/types'; +import { + Averagify, + ChartableStage, + ChartableStageAnalysis, + ChartableStageDatapoints, + ChartableStagesAnalysis, +} from './types'; + +function makeStage(name: string): ChartableStage { + return { + analysis: { + unknown: { avg: 0, max: 0, min: 0 }, + enqueued: { avg: 0, max: 0, min: 0 }, + scheduled: { avg: 0, max: 0, min: 0 }, + running: { avg: 0, max: 0, min: 0 }, + aborted: { avg: 0, max: 0, min: 0 }, + succeeded: { avg: 0, max: 0, min: 0 }, + failed: { avg: 0, max: 0, min: 0 }, + stalled: { avg: 0, max: 0, min: 0 }, + expired: { avg: 0, max: 0, min: 0 }, + }, + combinedAnalysis: { avg: 0, max: 0, min: 0 }, + statusSet: new Set(), + name, + values: [], + stages: new Map(), + }; +} + +export interface ChartableStagesOptions { + normalizeTimeRange: boolean; +} + +/** + * Converts a list of builds, each with a tree of stages (and durations) into a + * merged tree of stages, and calculates {avg, min, max} of each stage. + */ +export async function buildsToChartableStages( + builds: Array, + options: ChartableStagesOptions, +): Promise { + const { normalizeTimeRange } = options; + + const total: ChartableStage = makeStage('Total'); + + const recurseDown = ( + status: FilterStatusType, + stageMap: Map, + stage: Stage, + __epoch: number, + ) => { + const { name, duration } = stage; + + const subChartableStage = getOrSetStage(stageMap, name); + + subChartableStage.statusSet.add(status); + subChartableStage.values.push({ + __epoch, + [status]: duration, + [`${status} avg`]: duration, + }); + + stage.stages?.forEach(subStage => { + recurseDown(status, subChartableStage.stages, subStage, __epoch); + }); + }; + + const stages = new Map(); + + await map(builds, { chunk: 'idle' }, build => { + const { duration, requestedAt, status } = build; + const __epoch = requestedAt.getTime(); + + total.statusSet.add(status); + total.values.push({ + __epoch, + [status]: duration, + [`${status} avg`]: duration, + }); + + build.stages?.forEach(subStage => { + recurseDown(status, stages, subStage, __epoch); + }); + }); + + const allEpochs = normalizeTimeRange + ? builds.map(build => build.requestedAt.getTime()) + : []; + + // Recurse down again and calculate averages + await map([...stages.values()], { chunk: 'idle' }, stage => + finalizeStage(stage, { allEpochs, averageWidth: 10 }), + ); + finalizeStage(total, { allEpochs, averageWidth: 10 }); + + return { total, stages }; +} + +function getAnalysis( + values: Array, + status: FilterStatusType, +): ChartableStageAnalysis { + const analysis: ChartableStageAnalysis = { + max: 0, + min: 0, + avg: 0, + }; + + const definedValues = values.filter( + value => typeof value[status] !== 'undefined', + ); + + analysis.max = definedValues.reduce( + (prev, cur) => Math.max(prev, cur[status]!), + 0, + ); + analysis.min = definedValues.reduce( + (prev, cur) => Math.min(prev, cur[status]!), + analysis.max, + ); + analysis.avg = + definedValues.length === 0 + ? 0 + : definedValues.reduce((prev, cur) => prev + cur[status]!, 0) / + values.length; + + return analysis; +} + +interface FinalizeStageOptions { + averageWidth: number; + allEpochs: Array; +} + +/** + * Calculate {avg, min, max} of a stage and its sub stages, recursively. + * This is calculated per status (successful, failed, etc). + */ +function finalizeStage(stage: ChartableStage, options: FinalizeStageOptions) { + const { averageWidth, allEpochs } = options; + const { values, analysis, combinedAnalysis } = stage; + + if (allEpochs.length > 0) { + const valueEpochs = new Set(values.map(value => value.__epoch)); + + allEpochs.forEach(epoch => { + if (!valueEpochs.has(epoch)) { + values.push({ __epoch: epoch }); + } + }); + } + + values.sort((a, b) => a.__epoch - b.__epoch); + + const avgDuration: [duration: number, count: number] = [0, 0]; + + statusTypes.forEach(status => { + analysis[status] = getAnalysis(values, status); + + const durationsIndexes = values + .map(value => value[status]) + .map((duration, index) => ({ index, duration })) + .filter(({ duration }) => typeof duration !== 'undefined') + .map(({ index }) => index); + const durationsDense = values + .map(value => value[status]) + .filter( + (duration): duration is number => typeof duration !== 'undefined', + ); + + avgDuration[0] += durationsDense.reduce((prev, cur) => prev + cur, 0); + avgDuration[1] += durationsDense.length; + + const averages = durationsDense.map((_, i) => + average( + durationsDense.slice( + Math.max(i - averageWidth, 0), + Math.min(i + averageWidth, durationsDense.length), + ), + ), + ); + + averages.forEach((avg, index) => { + const key: Averagify = `${status} avg`; + values[durationsIndexes[index]][key] = avg; + }); + }); + + const analysisValues = Object.values(analysis); + combinedAnalysis.max = analysisValues.reduce( + (prev, cur) => Math.max(prev, cur.max), + 0, + ); + combinedAnalysis.min = analysisValues.reduce( + (prev, cur) => Math.min(prev, cur.min), + combinedAnalysis.max, + ); + combinedAnalysis.avg = !avgDuration[1] ? 0 : avgDuration[0] / avgDuration[1]; + + stage.stages.forEach(subStage => finalizeStage(subStage, options)); +} + +function average(values: number[]): number { + return !values.length + ? 0 + : Math.round(values.reduce((prev, cur) => prev + cur, 0) / values.length); +} + +function getOrSetStage( + stages: Map, + name: string, +): ChartableStage { + const stage = stages.get(name); + if (stage) return stage; + + const newStage: ChartableStage = makeStage(name); + stages.set(name, newStage); + return newStage; +} diff --git a/plugins/cicd-statistics/src/charts/stage-chart.tsx b/plugins/cicd-statistics/src/charts/stage-chart.tsx new file mode 100644 index 0000000000..f407a274b1 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/stage-chart.tsx @@ -0,0 +1,212 @@ +/* + * 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, { Fragment, useMemo } from 'react'; +import { + Area, + ComposedChart, + XAxis, + YAxis, + YAxisProps, + CartesianGrid, + Legend, + LegendProps, + Line, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import Alert from '@material-ui/lab/Alert'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Grid, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; + +import { statusTypes } from '../apis/types'; +import { ChartableStage } from './types'; +import { + pickElements, + labelFormatter, + tickFormatterX, + tickFormatterY, + tooltipValueFormatter, + formatDuration, +} from './utils'; +import { + statusColorMap, + fireColors, + colorStroke, + colorStrokeAvg, +} from './colors'; + +const fullWidth = { + width: '100%', +}; + +const transitionProps = { unmountOnExit: true }; + +export interface StageChartProps { + stage: ChartableStage; + + defaultCollapsed?: number; + zeroYAxis?: boolean; +} + +export function StageChart(props: StageChartProps) { + const { stage, ...chartOptions } = props; + const { defaultCollapsed = 0, zeroYAxis = false } = chartOptions; + + const ticks = useMemo( + () => pickElements(stage.values, 8).map(val => val.__epoch), + [stage.values], + ); + const domainY = useMemo( + () => [zeroYAxis ? 0 : 'auto', 'auto'] as YAxisProps['domain'], + [zeroYAxis], + ); + const statuses = useMemo( + () => statusTypes.filter(status => stage.statusSet.has(status)), + [stage.statusSet], + ); + const legendPayload = useMemo( + (): LegendProps['payload'] => + statuses.map(status => ({ + value: status, + type: 'line', + id: status, + color: statusColorMap[status], + })), + [statuses], + ); + + return ( + defaultCollapsed} + TransitionProps={transitionProps} + > + }> + + {stage.name} (avg {formatDuration(stage.combinedAnalysis.avg)}) + + + + {stage.values.length === 0 ? ( + No data + ) : ( + + + + + + + {fireColors.map(([percent, color]) => ( + + ))} + + + {statuses.length > 1 && } + + + + + {statuses.map(status => ( + + 1 + ? statusColorMap[status] + : colorStroke + } + fillOpacity={statuses.length > 1 ? 0.5 : 1} + fill={ + statuses.length > 1 + ? statusColorMap[status] + : 'url(#colorDur)' + } + connectNulls + /> + 1 + ? statusColorMap[status] + : colorStrokeAvg + } + opacity={0.8} + strokeWidth={2} + dot={false} + connectNulls + /> + + ))} + + + + {stage.stages.size === 0 ? null : ( + + + }> + Sub stages ({stage.stages.size}) + + +
+ {[...stage.stages.values()].map(subStage => ( + + ))} +
+
+
+
+ )} +
+ )} +
+
+ ); +} diff --git a/plugins/cicd-statistics/src/charts/status-chart.tsx b/plugins/cicd-statistics/src/charts/status-chart.tsx new file mode 100644 index 0000000000..c3153c1700 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/status-chart.tsx @@ -0,0 +1,133 @@ +/* + * 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, { Fragment, useMemo } from 'react'; +import { + Bar, + ComposedChart, + XAxis, + YAxis, + CartesianGrid, + Legend, + LegendProps, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import Alert from '@material-ui/lab/Alert'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { formatISO9075, parseISO } from 'date-fns'; +import { countBy } from 'lodash'; + +import { Build, FilterStatusType, statusTypes } from '../apis/types'; +import { labelFormatterWithoutTime, tickFormatterX } from './utils'; +import { statusColorMap } from './colors'; + +export interface StatusChartProps { + builds: ReadonlyArray; +} + +export function StatusChart(props: StatusChartProps) { + const { builds } = props; + + const { statuses, values } = useMemo(() => { + const buildsByDay = new Map>(); + + const foundStatuses = new Set(); + + builds.forEach(build => { + foundStatuses.add(build.status); + + const dayString = formatISO9075(build.requestedAt, { + representation: 'date', + }); + const dayList = buildsByDay.get(dayString); + if (dayList) { + dayList.push(build); + } else { + buildsByDay.set(dayString, [build]); + } + }); + + return { + statuses: [ + ...statusTypes.filter(status => foundStatuses.has(status)), + ...[...foundStatuses].filter( + status => !(statusTypes as Array).includes(status), + ), + ], + values: [...buildsByDay.entries()].map(([dayString, buildThisDay]) => ({ + __epoch: parseISO(dayString).getTime(), + ...countBy(buildThisDay, 'status'), + })), + }; + }, [builds]); + + const legendPayload = useMemo( + (): LegendProps['payload'] => + statuses.map(status => ({ + value: status, + type: 'line', + id: status, + color: statusColorMap[status as FilterStatusType] ?? '', + })), + [statuses], + ); + + return ( + 1}> + }> + Build count per status + + + {values.length === 0 ? ( + No data + ) : ( + + + + + + + + {statuses.map(status => ( + + + + ))} + + + )} + + + ); +} diff --git a/plugins/cicd-statistics/src/charts/types.ts b/plugins/cicd-statistics/src/charts/types.ts new file mode 100644 index 0000000000..6355343ee0 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/types.ts @@ -0,0 +1,48 @@ +/* + * 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 { FilterStatusType } from '../apis/types'; + +export type Averagify = `${T} avg`; + +export type ChartableStageDatapoints = { + __epoch: number; +} & { + [status in FilterStatusType]?: number; +} & { + [status in Averagify]?: number; +}; + +export interface ChartableStageAnalysis { + max: number; + min: number; + avg: number; +} + +export interface ChartableStage { + analysis: Record; + combinedAnalysis: ChartableStageAnalysis; + name: string; + values: Array; + statusSet: Set; + + stages: Map; +} + +export interface ChartableStagesAnalysis { + total: ChartableStage; + stages: Map; +} diff --git a/plugins/cicd-statistics/src/charts/utils.tsx b/plugins/cicd-statistics/src/charts/utils.tsx new file mode 100644 index 0000000000..8fcd5e08d6 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/utils.tsx @@ -0,0 +1,87 @@ +/* + * 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, { CSSProperties } from 'react'; +import { formatISO9075, formatDistanceStrict } from 'date-fns'; + +const infoText: CSSProperties = { color: 'InfoText' }; + +/** + * Picks {num} elements from {arr} evenly, from the first to the last + */ +export function pickElements(arr: ReadonlyArray, num: number): Array { + if (arr.length <= num) { + return [...arr]; + } + + if (num < 2) { + return [arr[arr.length / 2]]; + } + + const step = arr.length / (num - 1); + return [ + ...Array.from(Array(num - 1)).map( + (_, index) => arr[Math.round(index * step)], + ), + arr[arr.length - 1], + ]; +} + +export function labelFormatter(epoch: number) { + return {formatISO9075(new Date(epoch))}; +} + +export function labelFormatterWithoutTime(epoch: number) { + return ( + + {formatISO9075(new Date(epoch), { representation: 'date' })} + + ); +} + +export function tickFormatterX(epoch: number) { + return formatISO9075(new Date(epoch), { representation: 'date' }); +} + +export function tickFormatterY(duration: number) { + if (duration === 0) { + return '0'; + } else if (duration < 500) { + return `${duration} ms`; + } + return formatDuration(duration) + .replace(/second.*/, 'sec') + .replace(/minute.*/, 'min') + .replace(/hour.*/, 'h') + .replace(/day.*/, 'd') + .replace(/month.*/, 'm') + .replace(/year.*/, 'y'); +} + +export function tooltipValueFormatter(duration: number, name: string) { + return [ + + {name}: {formatDuration(duration)} + , + null, + ]; +} + +const baseDate = new Date(); +const baseEpoch = baseDate.getTime(); +export function formatDuration(milliseconds: number) { + return formatDistanceStrict(baseDate, new Date(baseEpoch + milliseconds)); +} diff --git a/plugins/cicd-statistics/src/components/button-switch.tsx b/plugins/cicd-statistics/src/components/button-switch.tsx new file mode 100644 index 0000000000..03c7066171 --- /dev/null +++ b/plugins/cicd-statistics/src/components/button-switch.tsx @@ -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, MouseEvent } from 'react'; +import { ButtonGroup, Button, Tooltip, Zoom } from '@material-ui/core'; + +export interface SwitchValueDetails { + value: T; + tooltip?: string; +} + +export type SwitchValue = T | SwitchValueDetails; + +export interface ButtonSwitchPropsBase { + values: ReadonlyArray>; + vertical?: boolean; +} +export interface ButtonSwitchPropsSingle + extends ButtonSwitchPropsBase { + multi?: false; + selection: T; + onChange: (selected: T) => void; +} +export interface ButtonSwitchPropsMulti + extends ButtonSwitchPropsBase { + multi: true; + selection: ReadonlyArray; + onChange: (selected: Array) => void; +} + +export type ButtonSwitchProps = + | ButtonSwitchPropsSingle + | ButtonSwitchPropsMulti; + +function switchValue(value: SwitchValue): T { + return typeof value === 'object' ? value.value : value; +} + +export function ButtonSwitch(props: ButtonSwitchProps) { + const { values, vertical = false } = props; + + const onClick = useCallback( + (ev: MouseEvent) => { + const value = (ev.target as HTMLSpanElement).textContent!; + if (props.multi) { + props.onChange( + props.selection.includes(value as T) + ? props.selection.filter(val => val !== value) + : [...props.selection, value as T], + ); + } else { + props.onChange(value as T); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [values, props.selection, props.multi, props.onChange], + ); + + const hasSelection = (value: T) => { + if (props.multi) { + return props.selection.includes(value); + } + return props.selection === value; + }; + + const tooltipify = (value: SwitchValue, elem: JSX.Element) => + typeof value === 'object' && value.tooltip ? ( + + {elem} + + ) : ( + elem + ); + + return ( + + {values.map(value => + tooltipify( + value, + , + ), + )} + + ); +} diff --git a/plugins/cicd-statistics/src/components/chart-filters.tsx b/plugins/cicd-statistics/src/components/chart-filters.tsx new file mode 100644 index 0000000000..a6e2b9ede3 --- /dev/null +++ b/plugins/cicd-statistics/src/components/chart-filters.tsx @@ -0,0 +1,382 @@ +/* + * 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, useState, useEffect, useMemo } from 'react'; +import { + Button, + Card, + CardHeader, + CardContent, + FormControl, + FormGroup, + FormControlLabel, + Switch, + Theme, + Tooltip, + Typography, + makeStyles, +} from '@material-ui/core'; +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from '@material-ui/pickers'; +import { subMonths, isSameDay } from 'date-fns'; +import DateFnsUtils from '@date-io/date-fns'; + +import { + CicdConfiguration, + FilterBranchType, + FilterStatusType, +} from '../apis/types'; +import { ButtonSwitch, SwitchValue } from './button-switch'; +import { Toggle } from './toggle'; + +const useStyles = makeStyles( + theme => ({ + rootCard: { + padding: theme.spacing(0, 0, 0, 0), + margin: theme.spacing(0, 0, 2, 0), + }, + updateButton: { + margin: theme.spacing(1, 0, 0, 0), + }, + header: { + margin: theme.spacing(0, 0, 0, 0), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + title: { + margin: theme.spacing(3, 0, 1, 0), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + '&:first-child': { + margin: theme.spacing(1, 0, 1, 0), + }, + }, + }), + { + name: 'CicdStatisticsChartFilters', + }, +); + +export type BranchSelection = FilterBranchType<'all'>; +export type StatusSelection = FilterStatusType; + +export interface ChartFilter { + fromDate: Date; + toDate: Date; + branch: BranchSelection; + status: Array; +} + +export function getDefaultChartFilter( + cicdConfiguration: CicdConfiguration, +): ChartFilter { + const toDate = cicdConfiguration.defaults?.timeTo ?? new Date(); + return { + fromDate: cicdConfiguration.defaults?.timeFrom ?? subMonths(toDate, 1), + toDate, + branch: cicdConfiguration.defaults?.filterType ?? 'branch', + status: + cicdConfiguration.defaults?.filterStatus ?? + cicdConfiguration.availableStatuses.filter( + status => status === 'succeeded', + ), + }; +} + +function isSameChartFilter(a: ChartFilter, b: ChartFilter): boolean { + return ( + a.branch === b.branch && + [...a.status].sort().join(' ') === [...b.status].sort().join(' ') && + isSameDay(a.fromDate, b.fromDate) && + isSameDay(a.toDate, b.toDate) + ); +} + +export interface ViewOptions { + lowercaseNames: boolean; + normalizeTimeRange: boolean; +} + +export function getDefaultViewOptions( + cicdConfiguration: CicdConfiguration, +): ViewOptions { + return { + lowercaseNames: cicdConfiguration.defaults?.lowercaseNames ?? false, + normalizeTimeRange: cicdConfiguration.defaults?.normalizeTimeRange ?? true, + }; +} + +export interface ChartFiltersProps { + cicdConfiguration: CicdConfiguration; + initialFetchFilter: ChartFilter; + currentFetchFilter?: ChartFilter; + onChangeFetchFilter(filter: ChartFilter): void; + updateFetchFilter(filter: ChartFilter): void; + + initialViewOptions: ViewOptions; + onChangeViewOptions(filter: ViewOptions): void; +} + +interface InternalRef { + first: boolean; +} + +export function ChartFilters(props: ChartFiltersProps) { + const { + cicdConfiguration, + initialFetchFilter, + currentFetchFilter, + onChangeFetchFilter, + updateFetchFilter, + initialViewOptions, + onChangeViewOptions, + } = props; + + const classes = useStyles(); + + const [internalRef] = useState({ first: true }); + + const [useNowAsToDate, setUseNowAsToDate] = useState(true); + const [toDate, setToDate] = useState(initialFetchFilter.toDate); + const [fromDate, setFromDate] = useState(initialFetchFilter.fromDate); + + const branchValues: Array> = [ + 'master', + 'branch', + { + value: 'all', + tooltip: + 'NOTE; If the build pipelines are very different between master and branch ' + + 'builds, viewing them combined might not result in a very useful chart', + }, + ]; + const [branch, setBranch] = useState(initialFetchFilter.branch); + + const statusValues: ReadonlyArray = + cicdConfiguration.availableStatuses; + const [selectedStatus, setSelectedStatus] = useState( + initialFetchFilter.status, + ); + + const [viewOptions, setViewOptions] = useState(initialViewOptions); + + const setLowercaseNames = useCallback( + (lowercaseNames: boolean) => { + setViewOptions(old => ({ ...old, lowercaseNames })); + }, + [setViewOptions], + ); + + const setNormalizeTimeRange = useCallback( + (normalizeTimeRange: boolean) => { + setViewOptions(old => ({ ...old, normalizeTimeRange })); + }, + [setViewOptions], + ); + + useEffect(() => { + onChangeViewOptions(viewOptions); + }, [onChangeViewOptions, viewOptions]); + + useEffect(() => { + if (internalRef.first) { + // Skip calling onChangeFetchFilter first time + internalRef.first = false; + return; + } + onChangeFetchFilter({ + toDate, + fromDate, + branch, + status: selectedStatus, + }); + }, [ + internalRef, + toDate, + fromDate, + branch, + selectedStatus, + onChangeFetchFilter, + ]); + + const toggleUseNowAsDate = useCallback(() => { + setUseNowAsToDate(!useNowAsToDate); + if (!isSameDay(toDate, new Date())) { + setToDate(new Date()); + } + }, [useNowAsToDate, toDate]); + + const hasFetchFilterChanges = useMemo( + () => + !currentFetchFilter || + !isSameChartFilter( + { + toDate, + fromDate, + branch, + status: selectedStatus, + }, + currentFetchFilter, + ), + [toDate, fromDate, branch, selectedStatus, currentFetchFilter], + ); + + const updateFilter = useCallback(() => { + updateFetchFilter({ + toDate, + fromDate, + branch, + status: selectedStatus, + }); + }, [toDate, fromDate, branch, selectedStatus, updateFetchFilter]); + + return ( + + + + Update + + } + title={ + + Fetching options + + } + /> + + + Date range + + setFromDate(date as any as Date)} + /> +
+ + + + } + label="To today" + /> + {useNowAsToDate ? null : ( + setToDate(date as any as Date)} + /> + )} + + + + Branch + + + values={branchValues} + selection={branch} + onChange={setBranch} + /> + + Status + + + values={statusValues} + multi + vertical + selection={selectedStatus} + onChange={setSelectedStatus} + /> +
+
+ + + View options + + } + /> + + + + Lowercase names + + + + + Normalize time range + + + + +
+ ); +} diff --git a/plugins/cicd-statistics/src/components/progress.tsx b/plugins/cicd-statistics/src/components/progress.tsx new file mode 100644 index 0000000000..1b707573c0 --- /dev/null +++ b/plugins/cicd-statistics/src/components/progress.tsx @@ -0,0 +1,147 @@ +/* + * 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, { DependencyList } from 'react'; +import { useAsync } from 'react-use'; +import { Box, LinearProgress } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import { useApp } from '@backstage/core-plugin-api'; + +// Matching react-use, only has loading/error/value +type AsyncState = + | { + loading: boolean; + error?: undefined; + value?: undefined; + } + | { + loading: true; + error?: Error | undefined; + value?: T; + } + | { + loading: false; + error: Error; + value?: undefined; + } + | { + loading: false; + error?: undefined; + value: T; + }; + +export type ProgressAsLoading = { + loading: true; + progress?: number; + progressBuffer?: number; + error?: undefined; + value?: undefined; +}; +export type ProgressAsError = { + loading?: false | undefined; + progress?: undefined; + progressBuffer?: undefined; + error: Error; + value?: undefined; +}; +export type ProgressAsValue = { + loading?: false | undefined; + progress?: undefined; + progressBuffer?: undefined; + error?: undefined; + value: T; +}; + +/** + * An AsyncState but with the addition of progress (decimal 0-1) to allow + * rendering a progress bar while waiting. + */ +export type Progress = + | ProgressAsLoading + | ProgressAsError + | ProgressAsValue; + +const sentry = Symbol(); + +/** + * Casts an AsyncState or Progress into its non-succeeded sub types + */ +type Unsuccessful | AsyncState> = + S extends Progress + ? ProgressAsLoading | ProgressAsError + : Omit, 'value'>; + +/** + * Similar to useAsync except it "waits" for a dependent (upstream) async state + * to finish first, otherwise it forwards the dependent pending state. + * + * When/if the dependent state has settled successfully, the callback will be + * invoked for a new layer of async state with the dependent (upstream) success + * result as argument. + */ +export function useAsyncChain | AsyncState, R>( + parentState: S, + fn: (value: NonNullable) => Promise, + deps: DependencyList, +): Unsuccessful | AsyncState { + const childState = useAsync( + async () => (!parentState.value ? sentry : fn(parentState.value)), + [!parentState.error, !parentState.loading, parentState.value, ...deps], + ); + + if (!parentState.value) { + return parentState as Unsuccessful; + } else if (childState.value === sentry) { + return { loading: true }; + } + return childState as AsyncState; +} + +export function renderFallbacks( + state: Progress | AsyncState, + success: (value: T) => JSX.Element, +): JSX.Element { + if (state.loading) { + return ; + } else if (state.error) { + return {state.error.stack}; + } + + return success(state.value!); +} + +export function ViewProgress({ + state, +}: { + state: ProgressAsLoading | { loading: boolean }; +}) { + const { Progress } = useApp().getComponents(); + + const stateAsProgress = state as ProgressAsLoading; + + if (!stateAsProgress.progress && !stateAsProgress.progressBuffer) { + return ; + } + return ( + + + + ); +} diff --git a/plugins/cicd-statistics/src/components/toggle.tsx b/plugins/cicd-statistics/src/components/toggle.tsx new file mode 100644 index 0000000000..02f1941d85 --- /dev/null +++ b/plugins/cicd-statistics/src/components/toggle.tsx @@ -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, { useCallback, PropsWithChildren } from 'react'; +import { FormControlLabel, Switch } from '@material-ui/core'; + +export interface ToggleProps { + checked: boolean; + setChecked: (checked: boolean) => void; +} + +export function Toggle({ + checked, + setChecked, + children, +}: PropsWithChildren) { + const toggler = useCallback(() => { + setChecked(!checked); + }, [checked, setChecked]); + + return ( + } + label={children} + /> + ); +} diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx new file mode 100644 index 0000000000..d339f52fde --- /dev/null +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -0,0 +1,175 @@ +/* + * 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, useState, useMemo, useEffect } from 'react'; +import { Grid, makeStyles, Theme } from '@material-ui/core'; +import { startOfDay, endOfDay } from 'date-fns'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useApi, errorApiRef } from '@backstage/core-plugin-api'; + +import { + useCicdStatistics, + UseCicdStatisticsOptions, +} from './hooks/use-cicd-statistics'; +import { useCicdConfiguration } from './hooks/use-cicd-configuration'; +import { buildsToChartableStages } from './charts/conversions'; +import { StageChart } from './charts/stage-chart'; +import { StatusChart } from './charts/status-chart'; +import { + ChartFilter, + ChartFilters, + getDefaultChartFilter, + getDefaultViewOptions, + ViewOptions, +} from './components/chart-filters'; +import { AbortError, CicdConfiguration } from './apis'; +import { cleanupBuildTree } from './utils/stage-names'; +import { renderFallbacks, useAsyncChain } from './components/progress'; + +export function EntityPageCicdCharts() { + const state = useCicdConfiguration(); + + return renderFallbacks(state, value => ( + + )); +} + +const useStyles = makeStyles( + theme => ({ + pane: { + padding: theme.spacing(1, 1, 1, 1), + }, + }), + { + name: 'CicdStatisticsView', + }, +); + +interface CicdChartsProps { + cicdConfiguration: CicdConfiguration; +} + +function CicdCharts(props: CicdChartsProps) { + const { cicdConfiguration } = props; + + const errorApi = useApi(errorApiRef); + const { entity } = useEntity(); + + const classes = useStyles(); + + const [chartFilter, setChartFilter] = useState( + getDefaultChartFilter(cicdConfiguration), + ); + const [fetchedChartData, setFetchedChartData] = useState({ + abortController: null as null | AbortController, + chartFilter, + }); + + const [viewOptions, setViewOptions] = useState( + getDefaultViewOptions(cicdConfiguration), + ); + + const fetchStatisticsOptions = useMemo((): UseCicdStatisticsOptions => { + const abortController = new AbortController(); + fetchedChartData.abortController = abortController; + return { + abortController, + entity, + timeFrom: startOfDay(fetchedChartData.chartFilter.fromDate), + timeTo: endOfDay(fetchedChartData.chartFilter.toDate), + filterStatus: fetchedChartData.chartFilter.status, + filterType: fetchedChartData.chartFilter.branch, + }; + }, [entity, fetchedChartData]); + + const statisticsState = useCicdStatistics(fetchStatisticsOptions); + + const updateFilter = useCallback(() => { + // Abort previous fetch + fetchedChartData.abortController?.abort(); + + setFetchedChartData({ abortController: null, chartFilter }); + }, [fetchedChartData, setFetchedChartData, chartFilter]); + + const chartableStagesState = useAsyncChain( + statisticsState, + async value => + buildsToChartableStages( + await cleanupBuildTree(value.builds, { + formatStageName: cicdConfiguration.formatStageName, + lowerCase: viewOptions.lowercaseNames, + }), + { normalizeTimeRange: viewOptions.normalizeTimeRange }, + ), + [statisticsState, cicdConfiguration, viewOptions], + ); + + const onFilterChange = useCallback((filter: ChartFilter) => { + setChartFilter(filter); + }, []); + + const onViewOptionsChange = useCallback( + (options: ViewOptions) => { + setViewOptions(options); + }, + [setViewOptions], + ); + + useEffect(() => { + if ( + !chartableStagesState.error || + chartableStagesState.error instanceof AbortError + ) { + return; + } + errorApi.post(chartableStagesState.error); + }, [errorApi, chartableStagesState.error]); + + const collapsedLimit = cicdConfiguration.defaults.collapsedLimit ?? 60 * 1000; // 1m + + return ( + + + + + + {renderFallbacks(chartableStagesState, chartableStages => ( + <> + {!statisticsState.value?.builds.length ? null : ( + + )} + + {[...chartableStages.stages.entries()].map(([name, stage]) => ( + + ))} + + ))} + + + ); +} diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts new file mode 100644 index 0000000000..89f2c9f666 --- /dev/null +++ b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts @@ -0,0 +1,59 @@ +/* + * 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 { useState, useEffect } from 'react'; + +import { CicdConfiguration, statusTypes } from '../apis'; +import { Progress } from '../components/progress'; +import { defaultFormatStageName } from '../utils/stage-names'; +import { useCicdStatisticsApi } from './use-cicd-statistics-api'; + +export function useCicdConfiguration(): Progress { + const cicdStatisticsApi = useCicdStatisticsApi(); + + const [state, setState] = useState>({ + loading: true, + }); + + useEffect(() => { + if (!cicdStatisticsApi) { + setState({ error: new Error('No CI/CD Statistics API installed') }); + return; + } + + cicdStatisticsApi + .getConfiguration() + .then(configuration => { + const { + availableStatuses = statusTypes, + formatStageName = defaultFormatStageName, + defaults = {}, + } = configuration; + setState({ + value: { + availableStatuses, + formatStageName, + defaults, + }, + }); + }) + .catch(error => { + setState({ error }); + }); + }, [cicdStatisticsApi]); + + return state; +} diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-statistics-api.ts b/plugins/cicd-statistics/src/hooks/use-cicd-statistics-api.ts new file mode 100644 index 0000000000..02773b20ef --- /dev/null +++ b/plugins/cicd-statistics/src/hooks/use-cicd-statistics-api.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 { useApi } from '@backstage/core-plugin-api'; + +import { cicdStatisticsApiRef } from '../apis'; + +export function useCicdStatisticsApi() { + try { + return useApi(cicdStatisticsApiRef); + } catch (err) { + return undefined; + } +} diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts new file mode 100644 index 0000000000..645af9900b --- /dev/null +++ b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts @@ -0,0 +1,120 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { debounce } from 'lodash'; +import { Entity } from '@backstage/catalog-model'; + +import { + CicdState, + FetchBuildsOptions, + AbortError, + FilterStatusType, + FilterBranchType, +} from '../apis'; +import { Progress } from '../components/progress'; +import { useCicdStatisticsApi } from './use-cicd-statistics-api'; + +export interface UseCicdStatisticsOptions { + entity: Entity; + abortController: AbortController; + timeFrom: Date; + timeTo: Date; + filterStatus: Array>; + filterType: FilterBranchType<'all'>; +} + +export function useCicdStatistics( + options: UseCicdStatisticsOptions, +): Progress { + const { + entity, + abortController, + timeFrom, + timeTo, + filterStatus, + filterType, + } = options; + + const [state, setState] = useState>({ loading: true }); + + const cicdStatisticsApi = useCicdStatisticsApi(); + + useEffect(() => { + if (!cicdStatisticsApi) { + setState({ error: new Error('No CI/CD Statistics API installed') }); + return () => {}; + } + + let mounted = true; + let completed = false; // successfully or failed + + const updateProgress = debounce((count, total, started = 0) => { + if (mounted && !completed) { + setState({ + loading: true, + progress: !total ? 0 : count / total, + progressBuffer: !total ? 0 : started / total, + }); + } + }, 200); + + const fetchOptions: FetchBuildsOptions = { + entity, + updateProgress, + abortSignal: abortController.signal, + timeFrom, + timeTo, + filterStatus, + filterType, + }; + + (async () => { + return cicdStatisticsApi.fetchBuilds(fetchOptions); + })() + .then(builds => { + completed = true; + if (mounted) { + setState({ + value: builds, + }); + } + }) + .catch(err => { + completed = true; + if (mounted) { + setState({ + error: abortController.signal.aborted ? new AbortError() : err, + }); + } + }); + + return () => { + mounted = false; + abortController.abort(); + }; + }, [ + abortController, + entity, + timeFrom, + timeTo, + filterStatus, + filterType, + cicdStatisticsApi, + ]); + + return state; +} diff --git a/plugins/cicd-statistics/src/index.ts b/plugins/cicd-statistics/src/index.ts new file mode 100644 index 0000000000..0190f37f20 --- /dev/null +++ b/plugins/cicd-statistics/src/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './plugin'; +export * from './apis'; diff --git a/plugins/cicd-statistics/src/plugin.ts b/plugins/cicd-statistics/src/plugin.ts new file mode 100644 index 0000000000..76f6ce674f --- /dev/null +++ b/plugins/cicd-statistics/src/plugin.ts @@ -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 { + createPlugin, + createRoutableExtension, + createRouteRef, +} from '@backstage/core-plugin-api'; + +export const rootCatalogCicdStatsRouteRef = createRouteRef({ + id: 'cicd-statistics', +}); + +export const cicdStatisticsPlugin = createPlugin({ + id: 'cicd-statistics', + routes: { + entityContent: rootCatalogCicdStatsRouteRef, + }, +}); + +export const EntityCicdStatisticsContent = cicdStatisticsPlugin.provide( + createRoutableExtension({ + component: () => import('./entity-page').then(m => m.EntityPageCicdCharts), + mountPoint: rootCatalogCicdStatsRouteRef, + name: 'cicd-statistics-page', + }), +); diff --git a/plugins/cicd-statistics/src/utils/stage-names.ts b/plugins/cicd-statistics/src/utils/stage-names.ts new file mode 100644 index 0000000000..69c8cdcf25 --- /dev/null +++ b/plugins/cicd-statistics/src/utils/stage-names.ts @@ -0,0 +1,73 @@ +/* + * 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 { map } from 'already'; + +import { Build, Stage } from '../apis/types'; + +export function defaultFormatStageName( + parentNames: Array, + stageName: string, +): string { + let name = stageName; + + // Cut off parent names (if they are prefixed to the stage name) + parentNames.forEach(parentName => { + if (name.startsWith(parentName)) { + const newName = name + .slice(parentName.length) + // Remove things like ' - ' + .replace(/^[^\w\d]+/g, ''); + if (newName) { + name = newName; + } + } + }); + + // Cut off anything after colon in what looks like pulling docker images + return name.replace(/((pulling|(running)) [^:/]*\/.*?):.*/g, '$1'); +} + +export interface CleanupBuildTreeOptions { + formatStageName: typeof defaultFormatStageName; + lowerCase: boolean; +} + +export async function cleanupBuildTree( + builds: Build[], + opts: CleanupBuildTreeOptions, +): Promise { + const { formatStageName, lowerCase } = opts; + + const recurseStage = (stage: Stage, parentNames: Array): Stage => { + const name = formatStageName( + parentNames, + lowerCase ? stage.name.toLocaleLowerCase('en-US') : stage.name, + ); + const ancestry = [...parentNames, name]; + + return { + ...stage, + name, + stages: stage.stages?.map(subStage => recurseStage(subStage, ancestry)), + }; + }; + + return map(builds, { chunk: 'idle' }, build => ({ + ...build, + stages: build.stages.map(stage => recurseStage(stage, [])), + })); +} From d82fe02ecfb16273ee322d4fd045283d54390265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Thu, 13 Jan 2022 14:22:16 +0100 Subject: [PATCH 02/25] docs(cicd-statistics): Fixed spellings in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cicd-statistics/README.md b/plugins/cicd-statistics/README.md index e4db2166a6..78733735d7 100644 --- a/plugins/cicd-statistics/README.md +++ b/plugins/cicd-statistics/README.md @@ -2,6 +2,6 @@ This plugin shows charts of CI/CD pipeline durations over time. It expects to be used on the Software Catalog entity page, as it uses `useEntity` to figure out what component to get the build information for. -To use this plugin, you need to implement an API `CicdStatisticsApi` and bind it to the `cicdStatisticsApiRef`. This API is defined in `src/apis/types.ts` and is an interface with two functions, `getConfiguration()` and `fetchBuilds(options)`. This plugin will call `getConfiguration` to allow the implementation to specify defaults an settings for the UI. First time the UI shows, and each time the user changes filters and clicks `Update` to refresh the data, `fetchBuilds` is invoked with the filter options. The API implementation is the expected to fetch build information from somewhere, format it into a generic and rather simpe type `Build` (also defined in `types.ts`). The API can optionally signal completion for a progress bar in the UI. +To use this plugin, you need to implement an API `CicdStatisticsApi` and bind it to the `cicdStatisticsApiRef`. This API is defined in `src/apis/types.ts` and is an interface with two functions, `getConfiguration()` and `fetchBuilds(options)`. This plugin will call `getConfiguration` to allow the implementation to specify defaults and settings for the UI. First time the UI shows, and each time the user changes filters and clicks `Update` to refresh the data, `fetchBuilds` is invoked with the filter options. The API implementation is the expected to fetch build information from somewhere, format it into a generic and rather simple type `Build` (also defined in `types.ts`). The API can optionally signal completion for a progress bar in the UI. -When this plugin has fetched the builds, it will transpose the list of builds (and build stages) into a tree of build stages. As build pipelines sometimes change, certain stages might end or begin within the timerange of the view. +When this plugin has fetched the builds, it will transpose the list of builds (and build stages) into a tree of build stages. As build pipelines sometimes change, certain stages might end or begin within the date range of the view. From cf0e970bc2788bd567682faa94caa366ac4fa2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 14:14:37 +0100 Subject: [PATCH 03/25] fix(cicd-statistics): Added entity option to getConfiguration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/api-report.md | 10 +++++++++- plugins/cicd-statistics/src/apis/types.ts | 12 +++++++++++- .../src/hooks/use-cicd-configuration.ts | 6 ++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md index aef376c9b8..1af39b6f0f 100644 --- a/plugins/cicd-statistics/api-report.md +++ b/plugins/cicd-statistics/api-report.md @@ -82,7 +82,9 @@ export interface CicdStatisticsApi { // (undocumented) fetchBuilds(options: FetchBuildsOptions): Promise; // (undocumented) - getConfiguration(): Promise>; + getConfiguration( + options: GetConfigurationOptions, + ): Promise>; } // Warning: (ae-missing-release-tag) "cicdStatisticsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -150,9 +152,15 @@ export type FilterStatusType = | 'expired'; // Warning: (ae-missing-release-tag) "rootCatalogCicdStatsRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "GetConfigurationOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const rootCatalogCicdStatsRouteRef: RouteRef; +// @public +export interface GetConfigurationOptions { + // (undocumented) + entity: Entity; +} // Warning: (ae-missing-release-tag) "Stage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 1f2fa8a5b9..6bfc21008d 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -195,6 +195,14 @@ export type UpdateProgress = ( started?: number, ) => void; +/** + * When reading configuration, the Api can return a custom settings depending on + * the entity being viewed. + */ +export interface GetConfigurationOptions { + entity: Entity; +} + /** * When fetching, the Api should fetch build information about the `entity` and * respect the `timeFrom`, `timeTo`, `filterStatus` and `filterType`. @@ -220,6 +228,8 @@ export interface FetchBuildsOptions { * the UI. */ export interface CicdStatisticsApi { - getConfiguration(): Promise>; + getConfiguration( + options: GetConfigurationOptions, + ): Promise>; fetchBuilds(options: FetchBuildsOptions): Promise; } diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts index 89f2c9f666..d435cb21aa 100644 --- a/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts +++ b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts @@ -15,6 +15,7 @@ */ import { useState, useEffect } from 'react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { CicdConfiguration, statusTypes } from '../apis'; import { Progress } from '../components/progress'; @@ -23,6 +24,7 @@ import { useCicdStatisticsApi } from './use-cicd-statistics-api'; export function useCicdConfiguration(): Progress { const cicdStatisticsApi = useCicdStatisticsApi(); + const { entity } = useEntity(); const [state, setState] = useState>({ loading: true, @@ -35,7 +37,7 @@ export function useCicdConfiguration(): Progress { } cicdStatisticsApi - .getConfiguration() + .getConfiguration({ entity }) .then(configuration => { const { availableStatuses = statusTypes, @@ -53,7 +55,7 @@ export function useCicdConfiguration(): Progress { .catch(error => { setState({ error }); }); - }, [cicdStatisticsApi]); + }, [cicdStatisticsApi, entity]); return state; } From 706817149ba88e0742f03e668d0ed7d9cc4f837c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 14:18:15 +0100 Subject: [PATCH 04/25] fix(cicd-statistics): Made FilterBranchType and FilterStatusType not be parameterized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/api-report.md | 14 +++++--------- plugins/cicd-statistics/src/apis/types.ts | 14 +++++--------- .../src/components/chart-filters.tsx | 2 +- .../src/hooks/use-cicd-statistics.ts | 4 ++-- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md index 1af39b6f0f..c3976084e5 100644 --- a/plugins/cicd-statistics/api-report.md +++ b/plugins/cicd-statistics/api-report.md @@ -58,7 +58,7 @@ export interface CicdDefaults { // (undocumented) filterStatus: Array; // (undocumented) - filterType: FilterBranchType<'all'>; + filterType: FilterBranchType | 'all'; lowercaseNames: boolean; normalizeTimeRange: boolean; // (undocumented) @@ -117,9 +117,9 @@ export interface FetchBuildsOptions { // (undocumented) entity: Entity; // (undocumented) - filterStatus: Array>; + filterStatus: Array; // (undocumented) - filterType: FilterBranchType<'all'>; + filterType: FilterBranchType | 'all'; // (undocumented) timeFrom: Date; // (undocumented) @@ -131,16 +131,12 @@ export interface FetchBuildsOptions { // Warning: (ae-missing-release-tag) "FilterBranchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type FilterBranchType = - | Extra - | 'master' - | 'branch'; +export type FilterBranchType = 'master' | 'branch'; // Warning: (ae-missing-release-tag) "FilterStatusType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type FilterStatusType = - | Extra +export type FilterStatusType = | 'unknown' | 'enqueued' | 'scheduled' diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 6bfc21008d..5392657a64 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -22,8 +22,7 @@ import { Entity } from '@backstage/catalog-model'; * If all of these aren't applicable to the underlying CI/CD, these can be * configured to be hidden, using the `availableStatuses` in `CicdConfiguration`. */ -export type FilterStatusType = - | Extra +export type FilterStatusType = | 'unknown' | 'enqueued' | 'scheduled' @@ -52,10 +51,7 @@ export const statusTypes: Array = [ * something like 'release' or 'main' or 'trunk' in the underlying CI/CD system, * which is then up to the Api to map accordingly. */ -export type FilterBranchType = - | Extra - | 'master' - | 'branch'; +export type FilterBranchType = 'master' | 'branch'; export const branchTypes: Array = ['master', 'branch']; /** @@ -122,7 +118,7 @@ export interface CicdDefaults { timeFrom: Date; timeTo: Date; filterStatus: Array; - filterType: FilterBranchType<'all'>; + filterType: FilterBranchType | 'all'; /** Lower-case all stage names (to potentially merge stages with different cases) */ lowercaseNames: boolean; @@ -219,8 +215,8 @@ export interface FetchBuildsOptions { abortSignal: AbortSignal; timeFrom: Date; timeTo: Date; - filterStatus: Array>; - filterType: FilterBranchType<'all'>; + filterStatus: Array; + filterType: FilterBranchType | 'all'; } /** diff --git a/plugins/cicd-statistics/src/components/chart-filters.tsx b/plugins/cicd-statistics/src/components/chart-filters.tsx index a6e2b9ede3..a27fb58cf8 100644 --- a/plugins/cicd-statistics/src/components/chart-filters.tsx +++ b/plugins/cicd-statistics/src/components/chart-filters.tsx @@ -74,7 +74,7 @@ const useStyles = makeStyles( }, ); -export type BranchSelection = FilterBranchType<'all'>; +export type BranchSelection = FilterBranchType | 'all'; export type StatusSelection = FilterStatusType; export interface ChartFilter { diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts index 645af9900b..bf3558b7dc 100644 --- a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts +++ b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts @@ -33,8 +33,8 @@ export interface UseCicdStatisticsOptions { abortController: AbortController; timeFrom: Date; timeTo: Date; - filterStatus: Array>; - filterType: FilterBranchType<'all'>; + filterStatus: Array; + filterType: FilterBranchType | 'all'; } export function useCicdStatistics( From 37218e495ea8a30caf8df161ceb4bb5ebe11ed56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 14:21:19 +0100 Subject: [PATCH 05/25] fix(cicd-statistics): Renamed the exported component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cicd-statistics/src/plugin.ts b/plugins/cicd-statistics/src/plugin.ts index 76f6ce674f..b83764a1a3 100644 --- a/plugins/cicd-statistics/src/plugin.ts +++ b/plugins/cicd-statistics/src/plugin.ts @@ -35,6 +35,6 @@ export const EntityCicdStatisticsContent = cicdStatisticsPlugin.provide( createRoutableExtension({ component: () => import('./entity-page').then(m => m.EntityPageCicdCharts), mountPoint: rootCatalogCicdStatsRouteRef, - name: 'cicd-statistics-page', + name: 'EntityCicdStatisticsContent', }), ); From 721dca4a1446ec7bbd1c947bd8ffc0c0490af485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 14:25:32 +0100 Subject: [PATCH 06/25] fix(cicd-statistics): Don't export the routeRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/api-report.md | 3 --- plugins/cicd-statistics/src/plugin.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md index c3976084e5..400d8de1a6 100644 --- a/plugins/cicd-statistics/api-report.md +++ b/plugins/cicd-statistics/api-report.md @@ -147,11 +147,8 @@ export type FilterStatusType = | 'stalled' | 'expired'; -// Warning: (ae-missing-release-tag) "rootCatalogCicdStatsRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "GetConfigurationOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export const rootCatalogCicdStatsRouteRef: RouteRef; // @public export interface GetConfigurationOptions { // (undocumented) diff --git a/plugins/cicd-statistics/src/plugin.ts b/plugins/cicd-statistics/src/plugin.ts index b83764a1a3..cd53ce53e5 100644 --- a/plugins/cicd-statistics/src/plugin.ts +++ b/plugins/cicd-statistics/src/plugin.ts @@ -20,7 +20,7 @@ import { createRouteRef, } from '@backstage/core-plugin-api'; -export const rootCatalogCicdStatsRouteRef = createRouteRef({ +const rootCatalogCicdStatsRouteRef = createRouteRef({ id: 'cicd-statistics', }); From 6c4744725623fb2007ad8d3a5bb859950b8aaf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 14:43:19 +0100 Subject: [PATCH 07/25] fix(cicd-statistics): Export missing component type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/api-report.md | 6 +++++- plugins/cicd-statistics/src/plugin.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md index 400d8de1a6..f12ab166ab 100644 --- a/plugins/cicd-statistics/api-report.md +++ b/plugins/cicd-statistics/api-report.md @@ -102,12 +102,16 @@ export const cicdStatisticsPlugin: BackstagePlugin< {} >; -// Warning: (ae-forgotten-export) The symbol "EntityPageCicdCharts" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EntityCicdStatisticsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const EntityCicdStatisticsContent: EntityPageCicdCharts; +// Warning: (ae-missing-release-tag) "EntityPageCicdCharts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function EntityPageCicdCharts(): JSX.Element; + // Warning: (ae-missing-release-tag) "FetchBuildsOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/cicd-statistics/src/plugin.ts b/plugins/cicd-statistics/src/plugin.ts index cd53ce53e5..ca9506475a 100644 --- a/plugins/cicd-statistics/src/plugin.ts +++ b/plugins/cicd-statistics/src/plugin.ts @@ -20,6 +20,8 @@ import { createRouteRef, } from '@backstage/core-plugin-api'; +export type { EntityPageCicdCharts } from './entity-page'; + const rootCatalogCicdStatsRouteRef = createRouteRef({ id: 'cicd-statistics', }); From d3d16a2442d536443eb8185b6f82078e05ba3369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 16:28:28 +0100 Subject: [PATCH 08/25] fix(cicd-statistics): Don't instanceof, do fuzzy matching for better compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/apis/types.ts | 2 +- plugins/cicd-statistics/src/entity-page.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 5392657a64..48cb58e350 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -163,7 +163,7 @@ export interface CicdConfiguration { /** * If the Api implements support for aborting the fetching of builds, throw an - * AbortError of this type + * AbortError of this type (or any other error with name === 'AbortError'). */ export class AbortError extends Error {} diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index d339f52fde..28c08f2371 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -35,7 +35,7 @@ import { getDefaultViewOptions, ViewOptions, } from './components/chart-filters'; -import { AbortError, CicdConfiguration } from './apis'; +import { CicdConfiguration } from './apis'; import { cleanupBuildTree } from './utils/stage-names'; import { renderFallbacks, useAsyncChain } from './components/progress'; @@ -131,7 +131,7 @@ function CicdCharts(props: CicdChartsProps) { useEffect(() => { if ( !chartableStagesState.error || - chartableStagesState.error instanceof AbortError + chartableStagesState.error?.name === 'AbortError' ) { return; } From b8492f3b14829051f0913e7c490ed5cd1f075d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 16:32:49 +0100 Subject: [PATCH 09/25] fix(cicd-statistics): buildType -> branchType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/api-report.md | 2 +- plugins/cicd-statistics/src/apis/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md index f12ab166ab..09e0948753 100644 --- a/plugins/cicd-statistics/api-report.md +++ b/plugins/cicd-statistics/api-report.md @@ -24,7 +24,7 @@ export const branchTypes: Array; // // @public export interface Build { - buildType: FilterBranchType; + branchType: FilterBranchType; duration: number; id: string; // (undocumented) diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 48cb58e350..6ff0b67741 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -86,7 +86,7 @@ export interface Build { status: FilterStatusType; /** Branch type */ - buildType: FilterBranchType; + branchType: FilterBranchType; /** Time when the build started */ requestedAt: Date; From 0a7179538a73e71a0e497ec2c8578a50c3abfdf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 14 Jan 2022 16:35:33 +0100 Subject: [PATCH 10/25] fix(cicd-statistics): Don't export branchTypes, it's not very useful MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/api-report.md | 5 ----- plugins/cicd-statistics/src/apis/types.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md index 09e0948753..c2e7e0934d 100644 --- a/plugins/cicd-statistics/api-report.md +++ b/plugins/cicd-statistics/api-report.md @@ -15,11 +15,6 @@ import { RouteRef } from '@backstage/core-plugin-api'; // @public export class AbortError extends Error {} -// Warning: (ae-missing-release-tag) "branchTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const branchTypes: Array; - // Warning: (ae-missing-release-tag) "Build" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 6ff0b67741..d89b0e1f0c 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -52,7 +52,6 @@ export const statusTypes: Array = [ * which is then up to the Api to map accordingly. */ export type FilterBranchType = 'master' | 'branch'; -export const branchTypes: Array = ['master', 'branch']; /** * A Stage is a part of either a Build or a parent Stage. From ecaa8e7a151bb142c122ced25d2a445566dd2cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Thu, 20 Jan 2022 15:25:52 +0100 Subject: [PATCH 11/25] fix(cicd-statistics): date-fns -> luxon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/package.json | 7 +- .../src/charts/status-chart.tsx | 8 +- plugins/cicd-statistics/src/charts/utils.tsx | 57 +++-- .../src/components/chart-filters.tsx | 23 +- plugins/cicd-statistics/src/entity-page.tsx | 9 +- yarn.lock | 221 +++++++++++++++++- 6 files changed, 286 insertions(+), 39 deletions(-) diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index 8fad2b25f3..3e93d2adf6 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -28,18 +28,21 @@ "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" }, + "devDependencies": { + "@types/luxon": "^2.0.9" + }, "dependencies": { "@backstage/catalog-model": "^0.9.8", "@backstage/core-plugin-api": "^0.4.1", "@backstage/plugin-catalog-react": "^0.6.9", - "@date-io/date-fns": "^1.3.13", + "@date-io/luxon": "^1.3.13", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/pickers": "^3.3.10", "already": "^3.2.0", - "date-fns": "^2.27.0", "lodash": "^4.17.21", + "luxon": "^2.3.0", "react-use": "^17.3.1", "recharts": "^2.1.5" }, diff --git a/plugins/cicd-statistics/src/charts/status-chart.tsx b/plugins/cicd-statistics/src/charts/status-chart.tsx index c3153c1700..5b541961fa 100644 --- a/plugins/cicd-statistics/src/charts/status-chart.tsx +++ b/plugins/cicd-statistics/src/charts/status-chart.tsx @@ -34,8 +34,8 @@ import { Typography, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { formatISO9075, parseISO } from 'date-fns'; import { countBy } from 'lodash'; +import { DateTime } from 'luxon'; import { Build, FilterStatusType, statusTypes } from '../apis/types'; import { labelFormatterWithoutTime, tickFormatterX } from './utils'; @@ -56,9 +56,7 @@ export function StatusChart(props: StatusChartProps) { builds.forEach(build => { foundStatuses.add(build.status); - const dayString = formatISO9075(build.requestedAt, { - representation: 'date', - }); + const dayString = DateTime.fromJSDate(build.requestedAt).toISODate(); const dayList = buildsByDay.get(dayString); if (dayList) { dayList.push(build); @@ -75,7 +73,7 @@ export function StatusChart(props: StatusChartProps) { ), ], values: [...buildsByDay.entries()].map(([dayString, buildThisDay]) => ({ - __epoch: parseISO(dayString).getTime(), + __epoch: DateTime.fromISO(dayString).toMillis(), ...countBy(buildThisDay, 'status'), })), }; diff --git a/plugins/cicd-statistics/src/charts/utils.tsx b/plugins/cicd-statistics/src/charts/utils.tsx index 8fcd5e08d6..4a7131b7c9 100644 --- a/plugins/cicd-statistics/src/charts/utils.tsx +++ b/plugins/cicd-statistics/src/charts/utils.tsx @@ -15,7 +15,7 @@ */ import React, { CSSProperties } from 'react'; -import { formatISO9075, formatDistanceStrict } from 'date-fns'; +import { DateTime, Duration } from 'luxon'; const infoText: CSSProperties = { color: 'InfoText' }; @@ -40,20 +40,25 @@ export function pickElements(arr: ReadonlyArray, num: number): Array { ]; } -export function labelFormatter(epoch: number) { - return {formatISO9075(new Date(epoch))}; +function formatDateShort(milliseconds: number) { + return DateTime.fromMillis(milliseconds).toLocaleString(DateTime.DATE_SHORT); } - -export function labelFormatterWithoutTime(epoch: number) { - return ( - - {formatISO9075(new Date(epoch), { representation: 'date' })} - +function formatDateTimeShort(milliseconds: number) { + return DateTime.fromMillis(milliseconds).toLocaleString( + DateTime.DATETIME_SHORT, ); } +export function labelFormatter(epoch: number) { + return {formatDateTimeShort(epoch)}; +} + +export function labelFormatterWithoutTime(epoch: number) { + return {formatDateShort(epoch)}; +} + export function tickFormatterX(epoch: number) { - return formatISO9075(new Date(epoch), { representation: 'date' }); + return formatDateShort(epoch); } export function tickFormatterY(duration: number) { @@ -80,8 +85,32 @@ export function tooltipValueFormatter(duration: number, name: string) { ]; } -const baseDate = new Date(); -const baseEpoch = baseDate.getTime(); -export function formatDuration(milliseconds: number) { - return formatDistanceStrict(baseDate, new Date(baseEpoch + milliseconds)); +export function formatDuration(millis: number) { + let rest = Math.round(millis); + const days = Math.floor(rest / (1000 * 60 * 60 * 24)); + rest -= days * (1000 * 60 * 60 * 24); + const hours = Math.floor(rest / (1000 * 60 * 60)); + rest -= hours * (1000 * 60 * 60); + const minutes = Math.floor(rest / (1000 * 60)); + rest -= minutes * (1000 * 60); + const seconds = Math.floor(rest / 1000); + rest -= seconds * 1000; + const milliseconds = rest; + + if (!days && !hours && !minutes) { + if (seconds < 1) { + return `${milliseconds}ms`; + } else if (seconds < 2) { + return `${((milliseconds + seconds * 1000) / 1000).toFixed(1)}s`; + } + } + + const dur = Duration.fromObject({ + ...(days && { days }), + ...(hours && { hours }), + ...(minutes && !days && { minutes }), + ...(seconds && !days && !hours && { seconds }), + }); + + return dur.toHuman({ unitDisplay: 'narrow' }).replace(/, /g, ''); } diff --git a/plugins/cicd-statistics/src/components/chart-filters.tsx b/plugins/cicd-statistics/src/components/chart-filters.tsx index a27fb58cf8..e14ff366d5 100644 --- a/plugins/cicd-statistics/src/components/chart-filters.tsx +++ b/plugins/cicd-statistics/src/components/chart-filters.tsx @@ -33,8 +33,8 @@ import { MuiPickersUtilsProvider, KeyboardDatePicker, } from '@material-ui/pickers'; -import { subMonths, isSameDay } from 'date-fns'; -import DateFnsUtils from '@date-io/date-fns'; +import { DateTime } from 'luxon'; +import LuxonUtils from '@date-io/luxon'; import { CicdConfiguration, @@ -89,7 +89,9 @@ export function getDefaultChartFilter( ): ChartFilter { const toDate = cicdConfiguration.defaults?.timeTo ?? new Date(); return { - fromDate: cicdConfiguration.defaults?.timeFrom ?? subMonths(toDate, 1), + fromDate: + cicdConfiguration.defaults?.timeFrom ?? + DateTime.fromJSDate(toDate).minus({ months: 1 }).toJSDate(), toDate, branch: cicdConfiguration.defaults?.filterType ?? 'branch', status: @@ -104,8 +106,11 @@ function isSameChartFilter(a: ChartFilter, b: ChartFilter): boolean { return ( a.branch === b.branch && [...a.status].sort().join(' ') === [...b.status].sort().join(' ') && - isSameDay(a.fromDate, b.fromDate) && - isSameDay(a.toDate, b.toDate) + DateTime.fromJSDate(a.fromDate).hasSame( + DateTime.fromJSDate(b.fromDate), + 'day', + ) && + DateTime.fromJSDate(a.toDate).hasSame(DateTime.fromJSDate(b.toDate), 'day') ); } @@ -218,7 +223,7 @@ export function ChartFilters(props: ChartFiltersProps) { const toggleUseNowAsDate = useCallback(() => { setUseNowAsToDate(!useNowAsToDate); - if (!isSameDay(toDate, new Date())) { + if (!DateTime.fromJSDate(toDate).hasSame(DateTime.now(), 'day')) { setToDate(new Date()); } }, [useNowAsToDate, toDate]); @@ -248,7 +253,7 @@ export function ChartFilters(props: ChartFiltersProps) { }, [toDate, fromDate, branch, selectedStatus, updateFetchFilter]); return ( - + setFromDate(date as any as Date)} + onChange={date => setFromDate(date?.toJSDate() ?? new Date())} />
@@ -306,7 +311,7 @@ export function ChartFilters(props: ChartFiltersProps) { format="yyyy-MM-dd" value={toDate} InputAdornmentProps={{ position: 'start' }} - onChange={date => setToDate(date as any as Date)} + onChange={date => setToDate(date?.toJSDate() ?? new Date())} /> )} diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index 28c08f2371..e12b75fe65 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -16,9 +16,9 @@ import React, { useCallback, useState, useMemo, useEffect } from 'react'; import { Grid, makeStyles, Theme } from '@material-ui/core'; -import { startOfDay, endOfDay } from 'date-fns'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useApi, errorApiRef } from '@backstage/core-plugin-api'; +import { DateTime } from 'luxon'; import { useCicdStatistics, @@ -58,6 +58,13 @@ const useStyles = makeStyles( }, ); +function startOfDay(date: Date) { + return DateTime.fromJSDate(date).startOf('day').toJSDate(); +} +function endOfDay(date: Date) { + return DateTime.fromJSDate(date).endOf('day').toJSDate(); +} + interface CicdChartsProps { cicdConfiguration: CicdConfiguration; } diff --git a/yarn.lock b/yarn.lock index ffc877abc6..2245504748 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1409,7 +1409,7 @@ zen-observable "^0.8.15" zod "^3.11.6" -"@backstage/core-plugin-api@^0.4.0": +"@backstage/core-plugin-api@^0.4.0", "@backstage/core-plugin-api@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.4.1.tgz#c0a13504bdfa61ae3d0db96934cd6c32a7574446" integrity sha512-IIb7XTcquTPaSIlamMKUgeTs5uLqkKN0Nw32QdTZhKgFkFFVzWC0AwN+henkaMNBZFdGb0ttPzrvNXGj5E6dGg== @@ -1440,7 +1440,7 @@ "@material-ui/lab" "4.0.0-alpha.57" react-use "^17.2.4" -"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5": +"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5", "@backstage/plugin-catalog-react@^0.6.9": version "0.6.13" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.13.tgz#b325eae501d3edeb8b7caef5d9615f2e632f5430" integrity sha512-XBwop7PwAZqfongx3KP6jAJar+MEscLSp8nLuHYX5XxA+suQNiBgi96uO3SEQmvtae+hvsRM7c0WHSxbYiXsDA== @@ -1835,7 +1835,7 @@ dependencies: "@date-io/core" "^1.3.13" -"@date-io/luxon@1.x": +"@date-io/luxon@1.x", "@date-io/luxon@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== @@ -3622,7 +3622,7 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2": +"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.9.13": version "4.12.3" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== @@ -5309,6 +5309,11 @@ resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== +"@types/d3-color@^2": + version "2.0.3" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82" + integrity sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w== + "@types/d3-force@^2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-2.1.1.tgz#a18b6f029d056eb0f8f84a09471e6228e4469b14" @@ -5321,6 +5326,13 @@ dependencies: "@types/d3-color" "*" +"@types/d3-interpolate@^2.0.0": + version "2.0.2" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz#78eddf7278b19e48e8652603045528d46897aba0" + integrity sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw== + dependencies: + "@types/d3-color" "^2" + "@types/d3-path@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" @@ -5331,6 +5343,18 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== +"@types/d3-path@^2": + version "2.0.1" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.1.tgz#ca03dfa8b94d8add97ad0cd97e96e2006b4763cb" + integrity sha512-6K8LaFlztlhZO7mwsZg7ClRsdLg3FJRzIIi6SZXDWmmSJc2x8dd2VkESbLXdk3p8cuvz71f36S0y8Zv2AxqvQw== + +"@types/d3-scale@^3.0.0": + version "3.3.2" + resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06" + integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ== + dependencies: + "@types/d3-time" "^2" + "@types/d3-selection@*", "@types/d3-selection@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59" @@ -5343,6 +5367,13 @@ dependencies: "@types/d3-path" "^1" +"@types/d3-shape@^2.0.0": + version "2.1.3" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3" + integrity sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ== + dependencies: + "@types/d3-path" "^2" + "@types/d3-shape@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" @@ -5350,6 +5381,11 @@ dependencies: "@types/d3-path" "*" +"@types/d3-time@^2": + version "2.1.1" + resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" + integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== + "@types/d3-zoom@^3.0.1": version "3.0.1" resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" @@ -5756,6 +5792,11 @@ resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.5.tgz#29d3b095d55ee50df8f4cf109b16009334d9828e" integrity sha512-GKrG5v16BOs9XGpouu33hOkAFaiSDi3ZaDXG9F2yAoyzHRBtksZnI60VWY5aM/yAENCccBejrxw8jDY+9OVlxw== +"@types/luxon@^2.0.9": + version "2.0.9" + resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6" + integrity sha1-eCoO36bWmRkSksExaL1JbNZrh8Y= + "@types/mdast@^3.0.0", "@types/mdast@^3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" @@ -6095,6 +6136,11 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/resize-observer-browser@^0.1.6": + version "0.1.7" + resolved "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" + integrity sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -6897,6 +6943,11 @@ alphanum-sort@^1.0.2: resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= +already@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/already/-/already-3.3.0.tgz#a5e5becd167cf537b45f8f1c23d331488ed77003" + integrity sha512-ADGyKddqEp8t/Wu4ITc0y9GGsgZDgyMeMk38AM5qrPK7VEjNAYD87QGTGGgNhSQahmjw76V3mi+3fJRwPJXcTw== + anafanafo@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/anafanafo/-/anafanafo-2.0.0.tgz#43f56274680bc553dd67a9625a920f88d0057b5c" @@ -9635,7 +9686,7 @@ css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-unit-converter@^1.1.2: +css-unit-converter@^1.1.1, css-unit-converter@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== @@ -9850,6 +9901,13 @@ cypress@^7.3.0: url "^0.11.0" yauzl "^2.10.0" +d3-array@2, d3-array@^2.3.0: + version "2.12.1" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + d3-array@^1.2.0: version "1.2.4" resolved "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" @@ -9865,6 +9923,11 @@ d3-color@1: resolved "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + "d3-color@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" @@ -9907,6 +9970,11 @@ d3-format@1: resolved "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + d3-interpolate@1, d3-interpolate@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" @@ -9921,11 +9989,23 @@ d3-interpolate@1, d3-interpolate@^1.3.0: dependencies: d3-color "1 - 3" +"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + d3-path@1: version "1.0.9" resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + "d3-path@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" @@ -9948,6 +10028,17 @@ d3-scale@^2.1.0: d3-time "1" d3-time-format "2" +d3-scale@^3.0.0: + version "3.3.0" + resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" @@ -9960,6 +10051,13 @@ d3-shape@^1.2.0: dependencies: d3-path "1" +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + d3-shape@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" @@ -9974,11 +10072,25 @@ d3-time-format@2: dependencies: d3-time "1" +"d3-time-format@2 - 3": + version "3.0.0" + resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + d3-time@1: version "1.1.0" resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + "d3-timer@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" @@ -11503,7 +11615,7 @@ eventemitter3@^3.1.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -eventemitter3@^4.0.0, eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -11885,6 +11997,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^2.0.0: + version "2.0.4" + resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" + integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== + fast-glob@^3.1.1: version "3.2.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" @@ -13857,6 +13974,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + interpret@^1.0.0: version "1.4.0" resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -16446,6 +16568,11 @@ luxon@^1.27.0: resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== +luxon@^2.3.0: + version "2.3.0" + resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045" + integrity sha1-vxan5kJRPCogpiMKakGwq0RtAEU= + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -19569,6 +19696,11 @@ postcss-unique-selectors@^5.0.2: alphanum-sort "^1.0.2" postcss-selector-parser "^6.0.5" +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" @@ -20260,7 +20392,7 @@ react-inspector@^5.1.1: is-dom "^1.0.0" prop-types "^15.0.0" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -20317,6 +20449,15 @@ react-resize-detector@^2.3.0: prop-types "^15.6.0" resize-observer-polyfill "^1.5.0" +react-resize-detector@^6.6.3: + version "6.7.8" + resolved "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz#318c85d1335e50f99d4fb8eb9ec34e066db597d0" + integrity sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw== + dependencies: + "@types/resize-observer-browser" "^0.1.6" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + react-router-dom@6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0-beta.0.tgz#9dcc8555365f22f7fbd09f26b6b82543f3eb97d6" @@ -20354,6 +20495,15 @@ react-smooth@^1.0.5: raf "^3.4.0" react-transition-group "^2.5.0" +react-smooth@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.0.tgz#561647b33e498b2e25f449b3c6689b2e9111bf91" + integrity sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw== + dependencies: + fast-equals "^2.0.0" + raf "^3.4.0" + react-transition-group "2.9.0" + react-sparklines@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" @@ -20389,7 +20539,7 @@ react-text-truncate@^0.17.0: dependencies: prop-types "^15.5.7" -react-transition-group@^2.5.0: +react-transition-group@2.9.0, react-transition-group@^2.5.0: version "2.9.0" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== @@ -20434,6 +20584,26 @@ react-use@^17.2.4: ts-easing "^0.2.0" tslib "^2.1.0" +react-use@^17.3.1: + version "17.3.2" + resolved "https://registry.npmjs.org/react-use/-/react-use-17.3.2.tgz#448abf515f47c41c32455024db28167cb6e53be8" + integrity sha512-bj7OD0/1wL03KyWmzFXAFe425zziuTf7q8olwCYBfOeFHY1qfO1FAMjROQLsLZYwG4Rx63xAfb7XAbBrJsZmEw== + dependencies: + "@types/js-cookie" "^2.2.6" + "@xobotyi/scrollbar-width" "^1.9.5" + copy-to-clipboard "^3.3.1" + fast-deep-equal "^3.1.3" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.3.1" + react-universal-interface "^0.6.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.1.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^3.0.1" + ts-easing "^0.2.0" + tslib "^2.1.0" + react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" @@ -20647,6 +20817,13 @@ recharts-scale@^0.4.2: dependencies: decimal.js-light "^2.4.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + recharts@^1.8.5: version "1.8.5" resolved "https://registry.npmjs.org/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12" @@ -20664,6 +20841,26 @@ recharts@^1.8.5: recharts-scale "^0.4.2" reduce-css-calc "^1.3.0" +recharts@^2.1.5: + version "2.1.8" + resolved "https://registry.npmjs.org/recharts/-/recharts-2.1.8.tgz#ca8774fcec5f5d7ec15dedd638db9ee12faf1c09" + integrity sha512-Wi7ufdDGyvy/BPf1za1Ok7VeWB2KtEejaewO9ulmlUhvn5l5RPS4AOkrUfhtMRTTjgJ4K6AbWMDpwtDjczUHJA== + dependencies: + "@types/d3-interpolate" "^2.0.0" + "@types/d3-scale" "^3.0.0" + "@types/d3-shape" "^2.0.0" + classnames "^2.2.5" + d3-interpolate "^2.0.0" + d3-scale "^3.0.0" + d3-shape "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-resize-detector "^6.6.3" + react-smooth "^2.0.0" + recharts-scale "^0.4.4" + reduce-css-calc "^2.1.8" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -20710,6 +20907,14 @@ reduce-css-calc@^1.3.0: math-expression-evaluator "^1.2.14" reduce-function-call "^1.0.1" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + reduce-function-call@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f" From cbbab88d9f58ee76d4381088086c2354955bbd11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 21 Jan 2022 16:07:37 +0100 Subject: [PATCH 12/25] feat(cicd-statistics): Allow stages to individual status, and added bar charts counts, and some refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/apis/types.ts | 20 +- .../cicd-statistics/src/charts/conversions.ts | 238 ------------------ .../src/charts/logic/conversions.ts | 90 +++++++ .../src/charts/logic/count-builds-per-day.ts | 51 ++++ .../src/charts/logic/finalize-stage.ts | 103 ++++++++ .../src/charts/logic/get-analysis.ts | 49 ++++ .../cicd-statistics/src/charts/logic/utils.ts | 57 +++++ .../src/charts/stage-chart.tsx | 97 ++++--- plugins/cicd-statistics/src/charts/types.ts | 3 + plugins/cicd-statistics/src/charts/utils.tsx | 7 +- .../src/components/button-switch.tsx | 27 +- .../src/components/chart-filters.tsx | 102 ++++++-- plugins/cicd-statistics/src/entity-page.tsx | 18 +- plugins/cicd-statistics/src/utils/api.ts | 33 +++ 14 files changed, 598 insertions(+), 297 deletions(-) delete mode 100644 plugins/cicd-statistics/src/charts/conversions.ts create mode 100644 plugins/cicd-statistics/src/charts/logic/conversions.ts create mode 100644 plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts create mode 100644 plugins/cicd-statistics/src/charts/logic/finalize-stage.ts create mode 100644 plugins/cicd-statistics/src/charts/logic/get-analysis.ts create mode 100644 plugins/cicd-statistics/src/charts/logic/utils.ts create mode 100644 plugins/cicd-statistics/src/utils/api.ts diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index d89b0e1f0c..b185a4a50e 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -63,6 +63,9 @@ export type FilterBranchType = 'master' | 'branch'; export interface Stage { name: string; + /** The status of the stage */ + status: FilterStatusType; + /** Stage duration in milliseconds */ duration: number; @@ -107,6 +110,16 @@ export type BuildWithRaw = Build & { raw: T; }; +/** + * Chart type. + * + * Values are: + * * `duration`: shows an area chart of the duration over time + * * `count`: shows a bar chart of the number of build per day + */ +export type ChartType = 'duration' | 'count'; +export type ChartTypes = Array; + /** * Default settings for the fetching options and view options. * @@ -119,12 +132,15 @@ export interface CicdDefaults { filterStatus: Array; 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; + /** Chart types per status */ + chartTypes: Record; } /** diff --git a/plugins/cicd-statistics/src/charts/conversions.ts b/plugins/cicd-statistics/src/charts/conversions.ts deleted file mode 100644 index 0f2b024d1f..0000000000 --- a/plugins/cicd-statistics/src/charts/conversions.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * 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 { map } from 'already'; - -import { Build, Stage, FilterStatusType, statusTypes } from '../apis/types'; -import { - Averagify, - ChartableStage, - ChartableStageAnalysis, - ChartableStageDatapoints, - ChartableStagesAnalysis, -} from './types'; - -function makeStage(name: string): ChartableStage { - return { - analysis: { - unknown: { avg: 0, max: 0, min: 0 }, - enqueued: { avg: 0, max: 0, min: 0 }, - scheduled: { avg: 0, max: 0, min: 0 }, - running: { avg: 0, max: 0, min: 0 }, - aborted: { avg: 0, max: 0, min: 0 }, - succeeded: { avg: 0, max: 0, min: 0 }, - failed: { avg: 0, max: 0, min: 0 }, - stalled: { avg: 0, max: 0, min: 0 }, - expired: { avg: 0, max: 0, min: 0 }, - }, - combinedAnalysis: { avg: 0, max: 0, min: 0 }, - statusSet: new Set(), - name, - values: [], - stages: new Map(), - }; -} - -export interface ChartableStagesOptions { - normalizeTimeRange: boolean; -} - -/** - * Converts a list of builds, each with a tree of stages (and durations) into a - * merged tree of stages, and calculates {avg, min, max} of each stage. - */ -export async function buildsToChartableStages( - builds: Array, - options: ChartableStagesOptions, -): Promise { - const { normalizeTimeRange } = options; - - const total: ChartableStage = makeStage('Total'); - - const recurseDown = ( - status: FilterStatusType, - stageMap: Map, - stage: Stage, - __epoch: number, - ) => { - const { name, duration } = stage; - - const subChartableStage = getOrSetStage(stageMap, name); - - subChartableStage.statusSet.add(status); - subChartableStage.values.push({ - __epoch, - [status]: duration, - [`${status} avg`]: duration, - }); - - stage.stages?.forEach(subStage => { - recurseDown(status, subChartableStage.stages, subStage, __epoch); - }); - }; - - const stages = new Map(); - - await map(builds, { chunk: 'idle' }, build => { - const { duration, requestedAt, status } = build; - const __epoch = requestedAt.getTime(); - - total.statusSet.add(status); - total.values.push({ - __epoch, - [status]: duration, - [`${status} avg`]: duration, - }); - - build.stages?.forEach(subStage => { - recurseDown(status, stages, subStage, __epoch); - }); - }); - - const allEpochs = normalizeTimeRange - ? builds.map(build => build.requestedAt.getTime()) - : []; - - // Recurse down again and calculate averages - await map([...stages.values()], { chunk: 'idle' }, stage => - finalizeStage(stage, { allEpochs, averageWidth: 10 }), - ); - finalizeStage(total, { allEpochs, averageWidth: 10 }); - - return { total, stages }; -} - -function getAnalysis( - values: Array, - status: FilterStatusType, -): ChartableStageAnalysis { - const analysis: ChartableStageAnalysis = { - max: 0, - min: 0, - avg: 0, - }; - - const definedValues = values.filter( - value => typeof value[status] !== 'undefined', - ); - - analysis.max = definedValues.reduce( - (prev, cur) => Math.max(prev, cur[status]!), - 0, - ); - analysis.min = definedValues.reduce( - (prev, cur) => Math.min(prev, cur[status]!), - analysis.max, - ); - analysis.avg = - definedValues.length === 0 - ? 0 - : definedValues.reduce((prev, cur) => prev + cur[status]!, 0) / - values.length; - - return analysis; -} - -interface FinalizeStageOptions { - averageWidth: number; - allEpochs: Array; -} - -/** - * Calculate {avg, min, max} of a stage and its sub stages, recursively. - * This is calculated per status (successful, failed, etc). - */ -function finalizeStage(stage: ChartableStage, options: FinalizeStageOptions) { - const { averageWidth, allEpochs } = options; - const { values, analysis, combinedAnalysis } = stage; - - if (allEpochs.length > 0) { - const valueEpochs = new Set(values.map(value => value.__epoch)); - - allEpochs.forEach(epoch => { - if (!valueEpochs.has(epoch)) { - values.push({ __epoch: epoch }); - } - }); - } - - values.sort((a, b) => a.__epoch - b.__epoch); - - const avgDuration: [duration: number, count: number] = [0, 0]; - - statusTypes.forEach(status => { - analysis[status] = getAnalysis(values, status); - - const durationsIndexes = values - .map(value => value[status]) - .map((duration, index) => ({ index, duration })) - .filter(({ duration }) => typeof duration !== 'undefined') - .map(({ index }) => index); - const durationsDense = values - .map(value => value[status]) - .filter( - (duration): duration is number => typeof duration !== 'undefined', - ); - - avgDuration[0] += durationsDense.reduce((prev, cur) => prev + cur, 0); - avgDuration[1] += durationsDense.length; - - const averages = durationsDense.map((_, i) => - average( - durationsDense.slice( - Math.max(i - averageWidth, 0), - Math.min(i + averageWidth, durationsDense.length), - ), - ), - ); - - averages.forEach((avg, index) => { - const key: Averagify = `${status} avg`; - values[durationsIndexes[index]][key] = avg; - }); - }); - - const analysisValues = Object.values(analysis); - combinedAnalysis.max = analysisValues.reduce( - (prev, cur) => Math.max(prev, cur.max), - 0, - ); - combinedAnalysis.min = analysisValues.reduce( - (prev, cur) => Math.min(prev, cur.min), - combinedAnalysis.max, - ); - combinedAnalysis.avg = !avgDuration[1] ? 0 : avgDuration[0] / avgDuration[1]; - - stage.stages.forEach(subStage => finalizeStage(subStage, options)); -} - -function average(values: number[]): number { - return !values.length - ? 0 - : Math.round(values.reduce((prev, cur) => prev + cur, 0) / values.length); -} - -function getOrSetStage( - stages: Map, - name: string, -): ChartableStage { - const stage = stages.get(name); - if (stage) return stage; - - const newStage: ChartableStage = makeStage(name); - stages.set(name, newStage); - return newStage; -} diff --git a/plugins/cicd-statistics/src/charts/logic/conversions.ts b/plugins/cicd-statistics/src/charts/logic/conversions.ts new file mode 100644 index 0000000000..b2e990d9e4 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/conversions.ts @@ -0,0 +1,90 @@ +/* + * 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 { map } from 'already'; + +import { Build, Stage } from '../../apis/types'; +import { ChartableStage, ChartableStagesAnalysis } from '../types'; +import { getOrSetStage, makeStage } from './utils'; +import { finalizeStage } from './finalize-stage'; + +export interface ChartableStagesOptions { + normalizeTimeRange: boolean; +} + +/** + * Converts a list of builds, each with a tree of stages (and durations) into a + * merged tree of stages, and calculates {avg, min, max} of each stage. + */ +export async function buildsToChartableStages( + builds: Array, + options: ChartableStagesOptions, +): Promise { + const { normalizeTimeRange } = options; + + const total: ChartableStage = makeStage('Total'); + + const recurseDown = ( + stageMap: Map, + stage: Stage, + __epoch: number, + ) => { + const { name, status, duration } = stage; + + const subChartableStage = getOrSetStage(stageMap, name); + + subChartableStage.statusSet.add(status); + subChartableStage.values.push({ + __epoch, + [status]: duration, + [`${status} avg`]: duration, + }); + + stage.stages?.forEach(subStage => { + recurseDown(subChartableStage.stages, subStage, __epoch); + }); + }; + + const stages = new Map(); + + await map(builds, { chunk: 'idle' }, build => { + const { duration, requestedAt, status } = build; + const __epoch = requestedAt.getTime(); + + total.statusSet.add(status); + total.values.push({ + __epoch, + [status]: duration, + [`${status} avg`]: duration, + }); + + build.stages?.forEach(subStage => { + recurseDown(stages, subStage, __epoch); + }); + }); + + const allEpochs = normalizeTimeRange + ? builds.map(build => build.requestedAt.getTime()) + : []; + + // Recurse down again and calculate averages + await map([...stages.values()], { chunk: 'idle' }, stage => + finalizeStage(stage, { allEpochs, averageWidth: 10 }), + ); + finalizeStage(total, { allEpochs, averageWidth: 10 }); + + return { total, stages }; +} 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 new file mode 100644 index 0000000000..c7c7ce1c21 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts @@ -0,0 +1,51 @@ +/* + * 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 { 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(); +} + +export function countBuildsPerDay( + values: ReadonlyArray, +) { + const days = groupBy(values, value => startOfDayEpoch(value.__epoch)); + Object.entries(days).forEach(([_startOfDay, valuesThisDay]) => { + const counts = Object.fromEntries( + statusTypes + .map( + type => + [ + type, + valuesThisDay.map(value => value[type] !== undefined).length, + ] as const, + ) + .filter(([_type, count]) => count > 0) + .map(([type, count]): [Countify, number] => [ + `${type} count`, + count, + ]), + ); + + // Assign the count for this day to the first value this day + Object.assign(valuesThisDay[0], counts); + }); +} diff --git a/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts b/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts new file mode 100644 index 0000000000..e56c5649ff --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts @@ -0,0 +1,103 @@ +/* + * 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 { FilterStatusType, statusTypes } from '../../apis/types'; +import { Averagify, ChartableStage } from '../types'; +import { countBuildsPerDay } from './count-builds-per-day'; +import { getAnalysis } from './get-analysis'; +import { average } from './utils'; + +interface FinalizeStageOptions { + averageWidth: number; + allEpochs: Array; +} + +/** + * Calculate: + * * {avg, min, max} + * * count per day + * of a stage and its sub stages, recursively. + * + * This is calculated per status (successful, failed, etc). + */ +export function finalizeStage( + stage: ChartableStage, + options: FinalizeStageOptions, +) { + const { averageWidth, allEpochs } = options; + const { values, analysis, combinedAnalysis } = stage; + + if (allEpochs.length > 0) { + const valueEpochs = new Set(values.map(value => value.__epoch)); + + allEpochs.forEach(epoch => { + if (!valueEpochs.has(epoch)) { + values.push({ __epoch: epoch }); + } + }); + } + + values.sort((a, b) => a.__epoch - b.__epoch); + + countBuildsPerDay(values); + + const avgDuration: [duration: number, count: number] = [0, 0]; + + statusTypes.forEach(status => { + analysis[status] = getAnalysis(values, status); + + const durationsIndexes = values + .map(value => value[status]) + .map((duration, index) => ({ index, duration })) + .filter(({ duration }) => typeof duration !== 'undefined') + .map(({ index }) => index); + const durationsDense = values + .map(value => value[status]) + .filter( + (duration): duration is number => typeof duration !== 'undefined', + ); + + avgDuration[0] += durationsDense.reduce((prev, cur) => prev + cur, 0); + avgDuration[1] += durationsDense.length; + + const averages = durationsDense.map((_, i) => + average( + durationsDense.slice( + Math.max(i - averageWidth, 0), + Math.min(i + averageWidth, durationsDense.length), + ), + ), + ); + + averages.forEach((avg, index) => { + const key: Averagify = `${status} avg`; + values[durationsIndexes[index]][key] = avg; + }); + }); + + const analysisValues = Object.values(analysis); + combinedAnalysis.max = analysisValues.reduce( + (prev, cur) => Math.max(prev, cur.max), + 0, + ); + combinedAnalysis.min = analysisValues.reduce( + (prev, cur) => Math.min(prev, cur.min), + combinedAnalysis.max, + ); + combinedAnalysis.avg = !avgDuration[1] ? 0 : avgDuration[0] / avgDuration[1]; + + stage.stages.forEach(subStage => finalizeStage(subStage, options)); +} diff --git a/plugins/cicd-statistics/src/charts/logic/get-analysis.ts b/plugins/cicd-statistics/src/charts/logic/get-analysis.ts new file mode 100644 index 0000000000..493355991d --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/get-analysis.ts @@ -0,0 +1,49 @@ +/* + * 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 { FilterStatusType } from '../../apis/types'; +import { ChartableStageAnalysis, ChartableStageDatapoints } from '../types'; + +export function getAnalysis( + values: Array, + status: FilterStatusType, +): ChartableStageAnalysis { + const analysis: ChartableStageAnalysis = { + max: 0, + min: 0, + avg: 0, + }; + + const definedValues = values.filter( + value => typeof value[status] !== 'undefined', + ); + + analysis.max = definedValues.reduce( + (prev, cur) => Math.max(prev, cur[status]!), + 0, + ); + analysis.min = definedValues.reduce( + (prev, cur) => Math.min(prev, cur[status]!), + analysis.max, + ); + analysis.avg = + definedValues.length === 0 + ? 0 + : definedValues.reduce((prev, cur) => prev + cur[status]!, 0) / + values.length; + + return analysis; +} diff --git a/plugins/cicd-statistics/src/charts/logic/utils.ts b/plugins/cicd-statistics/src/charts/logic/utils.ts new file mode 100644 index 0000000000..eaed81ac89 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/utils.ts @@ -0,0 +1,57 @@ +/* + * 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 { FilterStatusType } from '../../apis/types'; +import { ChartableStage } from '../types'; + +export function average(values: number[]): number { + return !values.length + ? 0 + : Math.round(values.reduce((prev, cur) => prev + cur, 0) / values.length); +} + +export function getOrSetStage( + stages: Map, + name: string, +): ChartableStage { + const stage = stages.get(name); + if (stage) return stage; + + const newStage: ChartableStage = makeStage(name); + stages.set(name, newStage); + return newStage; +} + +export function makeStage(name: string): ChartableStage { + return { + analysis: { + unknown: { avg: 0, max: 0, min: 0 }, + enqueued: { avg: 0, max: 0, min: 0 }, + scheduled: { avg: 0, max: 0, min: 0 }, + running: { avg: 0, max: 0, min: 0 }, + aborted: { avg: 0, max: 0, min: 0 }, + succeeded: { avg: 0, max: 0, min: 0 }, + failed: { avg: 0, max: 0, min: 0 }, + stalled: { avg: 0, max: 0, min: 0 }, + expired: { avg: 0, max: 0, min: 0 }, + }, + combinedAnalysis: { avg: 0, max: 0, min: 0 }, + statusSet: new Set(), + name, + values: [], + stages: new Map(), + }; +} diff --git a/plugins/cicd-statistics/src/charts/stage-chart.tsx b/plugins/cicd-statistics/src/charts/stage-chart.tsx index f407a274b1..eceb37a35c 100644 --- a/plugins/cicd-statistics/src/charts/stage-chart.tsx +++ b/plugins/cicd-statistics/src/charts/stage-chart.tsx @@ -17,6 +17,7 @@ import React, { Fragment, useMemo } from 'react'; import { Area, + Bar, ComposedChart, XAxis, YAxis, @@ -38,7 +39,7 @@ import { } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { statusTypes } from '../apis/types'; +import { CicdDefaults, statusTypes } from '../apis/types'; import { ChartableStage } from './types'; import { pickElements, @@ -64,13 +65,14 @@ const transitionProps = { unmountOnExit: true }; export interface StageChartProps { stage: ChartableStage; + chartTypes: CicdDefaults['chartTypes']; defaultCollapsed?: number; zeroYAxis?: boolean; } export function StageChart(props: StageChartProps) { const { stage, ...chartOptions } = props; - const { defaultCollapsed = 0, zeroYAxis = false } = chartOptions; + const { chartTypes, defaultCollapsed = 0, zeroYAxis = false } = chartOptions; const ticks = useMemo( () => pickElements(stage.values, 8).map(val => val.__epoch), @@ -134,49 +136,74 @@ export function StageChart(props: StageChartProps) { tickFormatter={tickFormatterX} /> + - {statuses.map(status => ( - - 1 - ? statusColorMap[status] - : colorStroke - } - fillOpacity={statuses.length > 1 ? 0.5 : 1} - fill={ - statuses.length > 1 - ? statusColorMap[status] - : 'url(#colorDur)' - } - connectNulls - /> - 1 - ? statusColorMap[status] - : colorStrokeAvg - } - opacity={0.8} - strokeWidth={2} - dot={false} - connectNulls - /> - + {statuses.reverse().map(status => ( + <> + {!chartTypes[status].includes('duration') ? null : ( + + 1 + ? statusColorMap[status] + : colorStroke + } + fillOpacity={statuses.length > 1 ? 0.5 : 1} + fill={ + statuses.length > 1 + ? statusColorMap[status] + : 'url(#colorDur)' + } + connectNulls + /> + 1 + ? statusColorMap[status] + : colorStrokeAvg + } + opacity={0.8} + strokeWidth={2} + dot={false} + connectNulls + /> + + )} + {!chartTypes[status].includes('count') ? null : ( + + )} + ))} diff --git a/plugins/cicd-statistics/src/charts/types.ts b/plugins/cicd-statistics/src/charts/types.ts index 6355343ee0..42b3cf4f04 100644 --- a/plugins/cicd-statistics/src/charts/types.ts +++ b/plugins/cicd-statistics/src/charts/types.ts @@ -17,6 +17,7 @@ import { FilterStatusType } from '../apis/types'; export type Averagify = `${T} avg`; +export type Countify = `${T} count`; export type ChartableStageDatapoints = { __epoch: number; @@ -24,6 +25,8 @@ export type ChartableStageDatapoints = { [status in FilterStatusType]?: number; } & { [status in Averagify]?: number; +} & { + [status in Countify]?: number; }; export interface ChartableStageAnalysis { diff --git a/plugins/cicd-statistics/src/charts/utils.tsx b/plugins/cicd-statistics/src/charts/utils.tsx index 4a7131b7c9..6086e84ade 100644 --- a/plugins/cicd-statistics/src/charts/utils.tsx +++ b/plugins/cicd-statistics/src/charts/utils.tsx @@ -76,10 +76,13 @@ export function tickFormatterY(duration: number) { .replace(/year.*/, 'y'); } -export function tooltipValueFormatter(duration: number, name: string) { +export function tooltipValueFormatter(durationOrCount: number, name: string) { return [ - {name}: {formatDuration(duration)} + {name}:{' '} + {name.endsWith(' count') + ? durationOrCount + : formatDuration(durationOrCount)} , null, ]; diff --git a/plugins/cicd-statistics/src/components/button-switch.tsx b/plugins/cicd-statistics/src/components/button-switch.tsx index 03c7066171..65f55eee41 100644 --- a/plugins/cicd-statistics/src/components/button-switch.tsx +++ b/plugins/cicd-statistics/src/components/button-switch.tsx @@ -20,6 +20,7 @@ import { ButtonGroup, Button, Tooltip, Zoom } from '@material-ui/core'; export interface SwitchValueDetails { value: T; tooltip?: string; + text?: string | JSX.Element; } export type SwitchValue = T | SwitchValueDetails; @@ -49,12 +50,34 @@ function switchValue(value: SwitchValue): T { return typeof value === 'object' ? value.value : value; } +function switchText( + value: SwitchValue, +): string | JSX.Element { + return typeof value === 'object' ? value.text ?? value.value : value; +} + +function findParent(tagName: string, elem: HTMLElement): HTMLElement { + let node: HTMLElement | null = elem; + while (node.tagName !== tagName) { + node = node.parentElement; + if (!node) { + throw new Error(`Couldn't find ${tagName} parent`); + } + } + return node; +} + export function ButtonSwitch(props: ButtonSwitchProps) { const { values, vertical = false } = props; const onClick = useCallback( (ev: MouseEvent) => { - const value = (ev.target as HTMLSpanElement).textContent!; + const btn = findParent('BUTTON', ev.target as HTMLElement); + const index = [...btn.parentElement!.children].findIndex( + child => child === btn, + ); + const value = switchValue(values[index]); + if (props.multi) { props.onChange( props.selection.includes(value as T) @@ -108,7 +131,7 @@ export function ButtonSwitch(props: ButtonSwitchProps) { } onClick={onClick} > - {switchValue(value)} + {switchText(value)} , ), )} diff --git a/plugins/cicd-statistics/src/components/chart-filters.tsx b/plugins/cicd-statistics/src/components/chart-filters.tsx index e14ff366d5..3c5f10d7e9 100644 --- a/plugins/cicd-statistics/src/components/chart-filters.tsx +++ b/plugins/cicd-statistics/src/components/chart-filters.tsx @@ -23,12 +23,15 @@ import { FormControl, FormGroup, FormControlLabel, + Grid, Switch, Theme, Tooltip, Typography, makeStyles, } from '@material-ui/core'; +import ShowChartIcon from '@material-ui/icons/ShowChart'; +import BarChartIcon from '@material-ui/icons/BarChart'; import { MuiPickersUtilsProvider, KeyboardDatePicker, @@ -37,9 +40,13 @@ import { DateTime } from 'luxon'; import LuxonUtils from '@date-io/luxon'; import { + ChartType, + ChartTypes, CicdConfiguration, + CicdDefaults, FilterBranchType, FilterStatusType, + statusTypes, } from '../apis/types'; import { ButtonSwitch, SwitchValue } from './button-switch'; import { Toggle } from './toggle'; @@ -68,6 +75,10 @@ const useStyles = makeStyles( margin: theme.spacing(1, 0, 1, 0), }, }, + buttonDescription: { + textTransform: 'uppercase', + margin: theme.spacing(1, 0, 0, 1), + }, }), { name: 'CicdStatisticsChartFilters', @@ -97,7 +108,7 @@ export function getDefaultChartFilter( status: cicdConfiguration.defaults?.filterStatus ?? cicdConfiguration.availableStatuses.filter( - status => status === 'succeeded', + status => status === 'succeeded' || status === 'failed', ), }; } @@ -114,10 +125,10 @@ function isSameChartFilter(a: ChartFilter, b: ChartFilter): boolean { ); } -export interface ViewOptions { - lowercaseNames: boolean; - normalizeTimeRange: boolean; -} +export type ViewOptions = Pick< + CicdDefaults, + 'lowercaseNames' | 'normalizeTimeRange' | 'chartTypes' +>; export function getDefaultViewOptions( cicdConfiguration: CicdConfiguration, @@ -125,9 +136,36 @@ export function getDefaultViewOptions( return { lowercaseNames: cicdConfiguration.defaults?.lowercaseNames ?? false, normalizeTimeRange: cicdConfiguration.defaults?.normalizeTimeRange ?? true, + chartTypes: { + succeeded: ['duration'], + failed: ['count'], + enqueued: ['count'], + scheduled: ['count'], + running: ['count'], + aborted: ['count'], + stalled: ['count'], + expired: ['count'], + unknown: ['count'], + }, }; } +const branchValues: Array> = [ + 'master', + 'branch', + { + value: 'all', + tooltip: + 'NOTE; If the build pipelines are very different between master and branch ' + + 'builds, viewing them combined might not result in a very useful chart', + }, +]; + +const chartTypeValues: Array> = [ + { value: 'duration', text: , tooltip: 'Duration' }, + { value: 'count', text: , tooltip: 'Count per day' }, +]; + export interface ChartFiltersProps { cicdConfiguration: CicdConfiguration; initialFetchFilter: ChartFilter; @@ -162,16 +200,6 @@ export function ChartFilters(props: ChartFiltersProps) { const [toDate, setToDate] = useState(initialFetchFilter.toDate); const [fromDate, setFromDate] = useState(initialFetchFilter.fromDate); - const branchValues: Array> = [ - 'master', - 'branch', - { - value: 'all', - tooltip: - 'NOTE; If the build pipelines are very different between master and branch ' + - 'builds, viewing them combined might not result in a very useful chart', - }, - ]; const [branch, setBranch] = useState(initialFetchFilter.branch); const statusValues: ReadonlyArray = @@ -196,6 +224,29 @@ export function ChartFilters(props: ChartFiltersProps) { [setViewOptions], ); + const setChartType = useCallback( + (statusType: FilterStatusType, chartTypes: ChartTypes) => { + setViewOptions(old => ({ + ...old, + chartTypes: { ...old.chartTypes, [statusType]: chartTypes }, + })); + }, + [setViewOptions], + ); + const setChartTypeSpecific = useMemo( + () => + Object.fromEntries( + statusTypes.map( + status => + [ + status, + (chartTypes: ChartTypes) => setChartType(status, chartTypes), + ] as const, + ), + ), + [setChartType], + ); + useEffect(() => { onChangeViewOptions(viewOptions); }, [onChangeViewOptions, viewOptions]); @@ -380,6 +431,27 @@ export function ChartFilters(props: ChartFiltersProps) { Normalize time range + + Chart styles + + {currentFetchFilter?.status.map(status => ( + + + + values={chartTypeValues} + selection={viewOptions.chartTypes[status]} + onChange={setChartTypeSpecific[status]} + multi + /> + + +
{status}
+
+
+ ))}
diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index e12b75fe65..c007e355a9 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -25,7 +25,7 @@ import { UseCicdStatisticsOptions, } from './hooks/use-cicd-statistics'; import { useCicdConfiguration } from './hooks/use-cicd-configuration'; -import { buildsToChartableStages } from './charts/conversions'; +import { buildsToChartableStages } from './charts/logic/conversions'; import { StageChart } from './charts/stage-chart'; import { StatusChart } from './charts/status-chart'; import { @@ -38,6 +38,7 @@ import { import { CicdConfiguration } from './apis'; import { cleanupBuildTree } from './utils/stage-names'; import { renderFallbacks, useAsyncChain } from './components/progress'; +import { sortFilterStatusType } from './utils/api'; export function EntityPageCicdCharts() { const state = useCicdConfiguration(); @@ -64,6 +65,12 @@ function startOfDay(date: Date) { function endOfDay(date: Date) { return DateTime.fromJSDate(date).endOf('day').toJSDate(); } +function cleanChartFilter(filter: ChartFilter): ChartFilter { + return { + ...filter, + status: sortFilterStatusType(filter.status), + }; +} interface CicdChartsProps { cicdConfiguration: CicdConfiguration; @@ -125,7 +132,7 @@ function CicdCharts(props: CicdChartsProps) { ); const onFilterChange = useCallback((filter: ChartFilter) => { - setChartFilter(filter); + setChartFilter(cleanChartFilter(filter)); }, []); const onViewOptionsChange = useCallback( @@ -166,12 +173,17 @@ function CicdCharts(props: CicdChartsProps) { {!statisticsState.value?.builds.length ? null : ( )} - + {[...chartableStages.stages.entries()].map(([name, stage]) => ( ))} diff --git a/plugins/cicd-statistics/src/utils/api.ts b/plugins/cicd-statistics/src/utils/api.ts new file mode 100644 index 0000000000..8af21b3507 --- /dev/null +++ b/plugins/cicd-statistics/src/utils/api.ts @@ -0,0 +1,33 @@ +/* + * 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 { FilterStatusType, statusTypes } from '../apis/types'; + +export function sortFilterStatusType( + statuses: ReadonlyArray, +): Array { + const statusSet = new Set(statuses); + + const sorted = (['all', ...statusTypes] as Array).filter((status: T) => { + if (statusSet.has(status)) { + statusSet.delete(status); + return true; + } + return false; + }); + + return [...sorted, ...statusSet]; +} From 2d7f173e0721f759012cbc4667628f13587cb811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 21 Jan 2022 16:31:00 +0100 Subject: [PATCH 13/25] feat(cicd-statistics): Added median analysis (alongside average) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- .../src/charts/logic/analysis.ts | 79 +++++++++++++++++++ .../src/charts/logic/finalize-stage.ts | 18 +---- .../src/charts/logic/get-analysis.ts | 49 ------------ .../cicd-statistics/src/charts/logic/utils.ts | 20 ++--- .../src/charts/stage-chart.tsx | 3 +- plugins/cicd-statistics/src/charts/types.ts | 5 ++ 6 files changed, 100 insertions(+), 74 deletions(-) create mode 100644 plugins/cicd-statistics/src/charts/logic/analysis.ts delete mode 100644 plugins/cicd-statistics/src/charts/logic/get-analysis.ts diff --git a/plugins/cicd-statistics/src/charts/logic/analysis.ts b/plugins/cicd-statistics/src/charts/logic/analysis.ts new file mode 100644 index 0000000000..4333ae3a6f --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/analysis.ts @@ -0,0 +1,79 @@ +/* + * 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 { FilterStatusType } from '../../apis/types'; +import { ChartableStageAnalysis, ChartableStageDatapoints } from '../types'; + +export function getAnalysis( + values: Array, + status: FilterStatusType, +): ChartableStageAnalysis { + const analysis: ChartableStageAnalysis = { + max: 0, + min: 0, + avg: 0, + med: 0, + }; + + const definedValues = values + .filter(value => typeof value[status] !== 'undefined') + .map(value => value[status]!) + .sort((a, b) => a - b); + + analysis.max = definedValues[definedValues.length - 1] ?? 0; + + analysis.min = definedValues[0] ?? 0; + + analysis.avg = + definedValues.length === 0 + ? 0 + : definedValues.reduce((prev, cur) => prev + cur, 0) / values.length; + + analysis.med = definedValues[Math.ceil(definedValues.length / 2)] ?? 0; + + return analysis; +} + +export function makeCombinedAnalysis( + analysis: Record, + allDurations: Array, +): ChartableStageAnalysis { + if (analysis.succeeded) { + // If succeeded is a viewed status, it's probably what's expected to see + // overall. Otherwise combine all other. + return analysis.succeeded; + } + + const analysisValues = Object.values(analysis); + + const max = analysisValues.reduce((prev, cur) => Math.max(prev, cur.max), 0); + const min = analysisValues.reduce( + (prev, cur) => Math.min(prev, cur.min), + max, + ); + const avg = !allDurations.length + ? 0 + : allDurations.reduce((prev, cur) => prev + cur, 0) / allDurations.length; + allDurations.sort((a, b) => a - b); + const med = allDurations[Math.ceil(allDurations.length / 2)] ?? 0; + + return { + max, + min, + avg, + med, + }; +} diff --git a/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts b/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts index e56c5649ff..bb54fbf41e 100644 --- a/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts +++ b/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts @@ -17,7 +17,7 @@ import { FilterStatusType, statusTypes } from '../../apis/types'; import { Averagify, ChartableStage } from '../types'; import { countBuildsPerDay } from './count-builds-per-day'; -import { getAnalysis } from './get-analysis'; +import { getAnalysis, makeCombinedAnalysis } from './analysis'; import { average } from './utils'; interface FinalizeStageOptions { @@ -54,7 +54,7 @@ export function finalizeStage( countBuildsPerDay(values); - const avgDuration: [duration: number, count: number] = [0, 0]; + const allDurations: Array = []; statusTypes.forEach(status => { analysis[status] = getAnalysis(values, status); @@ -70,8 +70,7 @@ export function finalizeStage( (duration): duration is number => typeof duration !== 'undefined', ); - avgDuration[0] += durationsDense.reduce((prev, cur) => prev + cur, 0); - avgDuration[1] += durationsDense.length; + durationsDense.forEach(dur => allDurations.push(dur)); const averages = durationsDense.map((_, i) => average( @@ -88,16 +87,7 @@ export function finalizeStage( }); }); - const analysisValues = Object.values(analysis); - combinedAnalysis.max = analysisValues.reduce( - (prev, cur) => Math.max(prev, cur.max), - 0, - ); - combinedAnalysis.min = analysisValues.reduce( - (prev, cur) => Math.min(prev, cur.min), - combinedAnalysis.max, - ); - combinedAnalysis.avg = !avgDuration[1] ? 0 : avgDuration[0] / avgDuration[1]; + Object.assign(combinedAnalysis, makeCombinedAnalysis(analysis, allDurations)); stage.stages.forEach(subStage => finalizeStage(subStage, options)); } diff --git a/plugins/cicd-statistics/src/charts/logic/get-analysis.ts b/plugins/cicd-statistics/src/charts/logic/get-analysis.ts deleted file mode 100644 index 493355991d..0000000000 --- a/plugins/cicd-statistics/src/charts/logic/get-analysis.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { FilterStatusType } from '../../apis/types'; -import { ChartableStageAnalysis, ChartableStageDatapoints } from '../types'; - -export function getAnalysis( - values: Array, - status: FilterStatusType, -): ChartableStageAnalysis { - const analysis: ChartableStageAnalysis = { - max: 0, - min: 0, - avg: 0, - }; - - const definedValues = values.filter( - value => typeof value[status] !== 'undefined', - ); - - analysis.max = definedValues.reduce( - (prev, cur) => Math.max(prev, cur[status]!), - 0, - ); - analysis.min = definedValues.reduce( - (prev, cur) => Math.min(prev, cur[status]!), - analysis.max, - ); - analysis.avg = - definedValues.length === 0 - ? 0 - : definedValues.reduce((prev, cur) => prev + cur[status]!, 0) / - values.length; - - return analysis; -} diff --git a/plugins/cicd-statistics/src/charts/logic/utils.ts b/plugins/cicd-statistics/src/charts/logic/utils.ts index eaed81ac89..0c3900d025 100644 --- a/plugins/cicd-statistics/src/charts/logic/utils.ts +++ b/plugins/cicd-statistics/src/charts/logic/utils.ts @@ -38,17 +38,17 @@ export function getOrSetStage( export function makeStage(name: string): ChartableStage { return { analysis: { - unknown: { avg: 0, max: 0, min: 0 }, - enqueued: { avg: 0, max: 0, min: 0 }, - scheduled: { avg: 0, max: 0, min: 0 }, - running: { avg: 0, max: 0, min: 0 }, - aborted: { avg: 0, max: 0, min: 0 }, - succeeded: { avg: 0, max: 0, min: 0 }, - failed: { avg: 0, max: 0, min: 0 }, - stalled: { avg: 0, max: 0, min: 0 }, - expired: { avg: 0, max: 0, min: 0 }, + unknown: { avg: 0, med: 0, max: 0, min: 0 }, + enqueued: { avg: 0, med: 0, max: 0, min: 0 }, + scheduled: { avg: 0, med: 0, max: 0, min: 0 }, + running: { avg: 0, med: 0, max: 0, min: 0 }, + aborted: { avg: 0, med: 0, max: 0, min: 0 }, + succeeded: { avg: 0, med: 0, max: 0, min: 0 }, + failed: { avg: 0, med: 0, max: 0, min: 0 }, + stalled: { avg: 0, med: 0, max: 0, min: 0 }, + expired: { avg: 0, med: 0, max: 0, min: 0 }, }, - combinedAnalysis: { avg: 0, max: 0, min: 0 }, + combinedAnalysis: { avg: 0, med: 0, max: 0, min: 0 }, statusSet: new Set(), name, values: [], diff --git a/plugins/cicd-statistics/src/charts/stage-chart.tsx b/plugins/cicd-statistics/src/charts/stage-chart.tsx index eceb37a35c..3e18bd17cd 100644 --- a/plugins/cicd-statistics/src/charts/stage-chart.tsx +++ b/plugins/cicd-statistics/src/charts/stage-chart.tsx @@ -104,7 +104,8 @@ export function StageChart(props: StageChartProps) { > }> - {stage.name} (avg {formatDuration(stage.combinedAnalysis.avg)}) + {stage.name} (med {formatDuration(stage.combinedAnalysis.med)}, avg{' '} + {formatDuration(stage.combinedAnalysis.avg)}) diff --git a/plugins/cicd-statistics/src/charts/types.ts b/plugins/cicd-statistics/src/charts/types.ts index 42b3cf4f04..75a72a8a4f 100644 --- a/plugins/cicd-statistics/src/charts/types.ts +++ b/plugins/cicd-statistics/src/charts/types.ts @@ -30,9 +30,14 @@ export type ChartableStageDatapoints = { }; export interface ChartableStageAnalysis { + /** Maximum duration */ max: number; + /** Minimum duration */ min: number; + /** Average duration */ avg: number; + /** Median duration */ + med: number; } export interface ChartableStage { From 52756327cfc9b02f57db1f40c6db74bd78fd8c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Fri, 21 Jan 2022 18:00:30 +0100 Subject: [PATCH 14/25] feat(cicd-statistics): Allow custom hiding/collapsing under certain thresholds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/apis/types.ts | 7 +- .../src/charts/stage-chart.tsx | 28 ++++- .../src/charts/status-chart.tsx | 2 +- .../src/components/chart-filters.tsx | 46 +++++-- .../src/components/duration-slider.tsx | 117 ++++++++++++++++++ .../cicd-statistics/src/components/label.tsx | 40 ++++++ .../src/{charts => components}/utils.tsx | 4 + plugins/cicd-statistics/src/entity-page.tsx | 6 +- 8 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 plugins/cicd-statistics/src/components/duration-slider.tsx create mode 100644 plugins/cicd-statistics/src/components/label.tsx rename plugins/cicd-statistics/src/{charts => components}/utils.tsx (96%) diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index b185a4a50e..19b4ac84b2 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -132,13 +132,14 @@ export interface CicdDefaults { filterStatus: Array; 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; } diff --git a/plugins/cicd-statistics/src/charts/stage-chart.tsx b/plugins/cicd-statistics/src/charts/stage-chart.tsx index 3e18bd17cd..f9b0312595 100644 --- a/plugins/cicd-statistics/src/charts/stage-chart.tsx +++ b/plugins/cicd-statistics/src/charts/stage-chart.tsx @@ -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( + [...stage.stages.entries()].filter( + ([_name, subStage]) => subStage.combinedAnalysis.max > defaultHidden, + ), + ), + [stage.stages, defaultHidden], + ); + + return stage.combinedAnalysis.max < defaultHidden ? null : ( defaultCollapsed} TransitionProps={transitionProps} @@ -209,18 +225,18 @@ export function StageChart(props: StageChartProps) { - {stage.stages.size === 0 ? null : ( + {subStages.size === 0 ? null : ( }> - Sub stages ({stage.stages.size}) + Sub stages ({subStages.size})
- {[...stage.stages.values()].map(subStage => ( + {[...subStages.values()].map(subStage => ( ( +export const useStyles = makeStyles( theme => ({ rootCard: { padding: theme.spacing(0, 0, 0, 0), @@ -81,7 +83,7 @@ const useStyles = makeStyles( }, }), { - 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={} /> {useNowAsToDate ? null : ( - Lowercase names + - Normalize time range + + + ({ + 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 ( + <> + + + + ); +} diff --git a/plugins/cicd-statistics/src/components/label.tsx b/plugins/cicd-statistics/src/components/label.tsx new file mode 100644 index 0000000000..fe064d30a6 --- /dev/null +++ b/plugins/cicd-statistics/src/components/label.tsx @@ -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 => ({ + label: { + fontWeight: 'normal', + margin: theme.spacing(0), + }, + }), + { + name: 'CicdStatisticsLabel', + }, +); + +export function Label({ children }: PropsWithChildren<{}>) { + const classes = useStyles(); + + return ( + + {children} + + ); +} diff --git a/plugins/cicd-statistics/src/charts/utils.tsx b/plugins/cicd-statistics/src/components/utils.tsx similarity index 96% rename from plugins/cicd-statistics/src/charts/utils.tsx rename to plugins/cicd-statistics/src/components/utils.tsx index 6086e84ade..6eeb81df23 100644 --- a/plugins/cicd-statistics/src/charts/utils.tsx +++ b/plugins/cicd-statistics/src/components/utils.tsx @@ -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); +} diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index c007e355a9..87f5a9e8d4 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -152,8 +152,6 @@ function CicdCharts(props: CicdChartsProps) { errorApi.post(chartableStagesState.error); }, [errorApi, chartableStagesState.error]); - const collapsedLimit = cicdConfiguration.defaults.collapsedLimit ?? 60 * 1000; // 1m - return ( @@ -176,13 +174,15 @@ function CicdCharts(props: CicdChartsProps) { {[...chartableStages.stages.entries()].map(([name, stage]) => ( ))} From ec1ddbd8121dc1aa1559a330d65167a72f189680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Mon, 24 Jan 2022 10:16:43 +0100 Subject: [PATCH 15/25] docs(cicd-statistics): Made it a bit more clear this plugin requires a custom API implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/cicd-statistics/README.md b/plugins/cicd-statistics/README.md index 78733735d7..8d91d74794 100644 --- a/plugins/cicd-statistics/README.md +++ b/plugins/cicd-statistics/README.md @@ -2,6 +2,12 @@ This plugin shows charts of CI/CD pipeline durations over time. It expects to be used on the Software Catalog entity page, as it uses `useEntity` to figure out what component to get the build information for. -To use this plugin, you need to implement an API `CicdStatisticsApi` and bind it to the `cicdStatisticsApiRef`. This API is defined in `src/apis/types.ts` and is an interface with two functions, `getConfiguration()` and `fetchBuilds(options)`. This plugin will call `getConfiguration` to allow the implementation to specify defaults and settings for the UI. First time the UI shows, and each time the user changes filters and clicks `Update` to refresh the data, `fetchBuilds` is invoked with the filter options. The API implementation is the expected to fetch build information from somewhere, format it into a generic and rather simple type `Build` (also defined in `types.ts`). The API can optionally signal completion for a progress bar in the UI. +## Usage -When this plugin has fetched the builds, it will transpose the list of builds (and build stages) into a tree of build stages. As build pipelines sometimes change, certain stages might end or begin within the date range of the view. +> This plugin cannot be used as-is; it requires a custom implementation to fetch build information + +To use this plugin, you need to implement an API `CicdStatisticsApi` and bind it to the `cicdStatisticsApiRef`. This API is defined in `src/apis/types.ts` and is an interface with two functions, `getConfiguration(options)` and `fetchBuilds(options)`. This plugin will call `getConfiguration` to allow the implementation to specify defaults and settings for the UI. + +First time the UI shows, and each time the user changes filters and clicks `Update` to refresh the data, `fetchBuilds` is invoked with the filter options. The API implementation is the expected to fetch build information from somewhere, format it into a generic and rather simple type `Build` (also defined in `types.ts`). The API can optionally signal completion for a progress bar in the UI. + +When this plugin has fetched the builds, it will transpose the list of builds (and build stages) into a tree of build stages. As build pipelines sometimes change, certain stages might end or begin within the date range of the view (when _Normalize time range_ is enabled, which is the default). 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 16/25] 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 : ( + )} Date: Wed, 2 Feb 2022 16:23:34 +0100 Subject: [PATCH 17/25] fix(cicd-statistics): Fixed progress bar animation, should be throttle, not debounce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts index bf3558b7dc..5c1fe1bfe2 100644 --- a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts +++ b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts @@ -15,7 +15,7 @@ */ import { useState, useEffect } from 'react'; -import { debounce } from 'lodash'; +import { throttle } from 'lodash'; import { Entity } from '@backstage/catalog-model'; import { @@ -62,7 +62,7 @@ export function useCicdStatistics( let mounted = true; let completed = false; // successfully or failed - const updateProgress = debounce((count, total, started = 0) => { + const updateProgress = throttle((count, total, started = 0) => { if (mounted && !completed) { setState({ loading: true, From d5acd9f03554d19c5d15a84e233c976fabe481b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Wed, 2 Feb 2022 16:27:02 +0100 Subject: [PATCH 18/25] feat(cicd-statistics): Added zoom capability to the charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since loading of build information can take a long time, being able to zoom "client-side", using the fetched data, is very useful. This is built using a provider (ZoomProvider) which keeps zoom state, simplifies rendering a gray area while zooming and has a filter-function which can filter only values within the zoomed time range. Signed-off-by: Gustaf Räntilä --- .../src/charts/stage-chart.tsx | 28 ++- .../src/charts/status-chart.tsx | 14 +- plugins/cicd-statistics/src/charts/zoom.tsx | 221 ++++++++++++++++++ .../cicd-statistics/src/components/utils.tsx | 8 + plugins/cicd-statistics/src/entity-page.tsx | 11 +- 5 files changed, 272 insertions(+), 10 deletions(-) create mode 100644 plugins/cicd-statistics/src/charts/zoom.tsx diff --git a/plugins/cicd-statistics/src/charts/stage-chart.tsx b/plugins/cicd-statistics/src/charts/stage-chart.tsx index 4141a6755a..ba3474153c 100644 --- a/plugins/cicd-statistics/src/charts/stage-chart.tsx +++ b/plugins/cicd-statistics/src/charts/stage-chart.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { Fragment, useMemo } from 'react'; +import React, { CSSProperties, Fragment, useMemo } from 'react'; import { Area, Bar, @@ -40,6 +40,7 @@ import { import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { capitalize } from 'lodash'; +import { useZoom, useZoomArea } from './zoom'; import { CicdDefaults, statusTypes } from '../apis/types'; import { ChartableStage } from './types'; import { @@ -57,9 +58,8 @@ import { colorStrokeAvg, } from './colors'; -const fullWidth = { - width: '100%', -}; +const fullWidth: CSSProperties = { width: '100%' }; +const noUserSelect: CSSProperties = { userSelect: 'none' }; const transitionProps = { unmountOnExit: true }; @@ -81,6 +81,9 @@ export function StageChart(props: StageChartProps) { zeroYAxis = false, } = chartOptions; + const { zoomFilterValues } = useZoom(); + const { zoomProps, getZoomArea } = useZoomArea(); + const ticks = useMemo( () => pickElements(stage.values, 8).map(val => val.__epoch), [stage.values], @@ -114,6 +117,11 @@ export function StageChart(props: StageChartProps) { [stage.stages, defaultHidden], ); + const zoomFilteredValues = useMemo( + () => zoomFilterValues(stage.values), + [stage.values, zoomFilterValues], + ); + return stage.combinedAnalysis.max < defaultHidden ? null : ( defaultCollapsed} @@ -130,9 +138,9 @@ export function StageChart(props: StageChartProps) { No data ) : ( - + - + {fireColors.map(([percent, color]) => ( @@ -175,8 +183,9 @@ export function StageChart(props: StageChartProps) { {statuses.reverse().map(status => ( {!chartTypes[status].includes('duration') ? null : ( - + <> - + )} {!chartTypes[status].includes('count') ? null : ( ))} + {getZoomArea({ yAxisId: 1 })} diff --git a/plugins/cicd-statistics/src/charts/status-chart.tsx b/plugins/cicd-statistics/src/charts/status-chart.tsx index 5a4cc953ca..7b07885ca5 100644 --- a/plugins/cicd-statistics/src/charts/status-chart.tsx +++ b/plugins/cicd-statistics/src/charts/status-chart.tsx @@ -37,6 +37,7 @@ import { import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { capitalize } from 'lodash'; +import { useZoom, useZoomArea } from './zoom'; import { FilterStatusType, TriggerReason } from '../apis/types'; import { labelFormatterWithoutTime, tickFormatterX } from '../components/utils'; import { statusColorMap, triggerColorMap } from './colors'; @@ -49,6 +50,9 @@ export interface StatusChartProps { export function StatusChart(props: StatusChartProps) { const { analysis } = props; + const { zoomFilterValues } = useZoom(); + const { zoomProps, getZoomArea } = useZoomArea(); + const values = useMemo(() => { return analysis.daily.values.map(value => { const totTriggers = analysis.daily.triggerReasons.reduce( @@ -122,6 +126,11 @@ export function StatusChart(props: StatusChartProps) { }; }, [analysis.daily.triggerReasons]); + const zoomFilteredValues = useMemo( + () => zoomFilterValues(values), + [values, zoomFilterValues], + ); + const barSize = getBarSize(analysis.daily.values.length); return ( @@ -136,7 +145,7 @@ export function StatusChart(props: StatusChartProps) { No data ) : ( - + ( ( ))} + {getZoomArea({ yAxisId: 1 })} )} diff --git a/plugins/cicd-statistics/src/charts/zoom.tsx b/plugins/cicd-statistics/src/charts/zoom.tsx new file mode 100644 index 0000000000..57e2b8678f --- /dev/null +++ b/plugins/cicd-statistics/src/charts/zoom.tsx @@ -0,0 +1,221 @@ +/* + * 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 { throttle } from 'lodash'; +import React, { + PropsWithChildren, + Dispatch, + SetStateAction, + Fragment, + useContext, + useState, + useCallback, + useMemo, + useEffect, +} from 'react'; +import { ReferenceArea } from 'recharts'; + +import type { Epoch } from './types'; + +interface ZoomState { + left?: number; + right?: number; +} + +interface ZoomContext { + registerSelection(setter: Dispatch): void; + setSelectState: Dispatch>; + + zoomState: ZoomState; + setZoomState: Dispatch>; + + resetZoom: () => void; +} + +const context = React.createContext(undefined as any); + +export function ZoomProvider({ children }: PropsWithChildren<{}>) { + const [registeredSelectors, setRegisteredSelectors] = useState< + Array> + >([]); + const [selectState, setSelectState] = useState({}); + const [zoomState, setZoomState] = useState({}); + + const registerSelection = useCallback( + (selector: Dispatch) => { + setRegisteredSelectors(old => [...old, selector]); + + return () => { + setRegisteredSelectors(old => old.filter(sel => sel === selector)); + }; + }, + [setRegisteredSelectors], + ); + + const callSelectors = useCallback( + (state: ZoomState) => { + registeredSelectors.forEach(selector => { + selector(state); + }); + }, + [registeredSelectors], + ); + + const throttledCallSelectors = useMemo( + () => throttle(callSelectors, 200), + [callSelectors], + ); + + useEffect(() => { + throttledCallSelectors({ + left: selectState.left, + right: selectState.right, + }); + }, [selectState.left, selectState.right, throttledCallSelectors]); + + const resetZoom = useCallback(() => { + setSelectState({}); + setZoomState({}); + }, [setSelectState, setZoomState]); + + const value = useMemo( + (): ZoomContext => ({ + registerSelection, + setSelectState, + + zoomState, + setZoomState, + + resetZoom, + }), + [registerSelection, setSelectState, zoomState, setZoomState, resetZoom], + ); + + return ; +} + +export function useZoom() { + const { zoomState, resetZoom } = useContext(context); + + const zoomFilterValues = useCallback( + (values: Array): Array => { + const { left, right } = zoomState; + return left === undefined || right === undefined + ? values + : values.filter(({ __epoch }) => __epoch > left && __epoch < right); + }, + [zoomState], + ); + + return useMemo( + () => ({ + resetZoom, + zoomState, + zoomFilterValues, + }), + [resetZoom, zoomState, zoomFilterValues], + ); +} + +export interface ZoomAreaProps { + yAxisId?: number | string | undefined; +} + +export function useZoomArea() { + const [showSelection, setShowSelection] = useState(false); + const [state, setState] = useState({}); + const { setSelectState, setZoomState, registerSelection } = + useContext(context); + + const onMouseDown = useCallback( + (e: any) => { + if (!e?.activeLabel) return; + + setSelectState({ left: e.activeLabel }); + setShowSelection(true); + }, + [setSelectState, setShowSelection], + ); + + const onMouseMove = useCallback( + (e: any) => { + if (!e?.activeLabel) return; + + setSelectState(area => { + if (!area.left) { + return area; + } + return { ...area, right: e.activeLabel }; + }); + }, + [setSelectState], + ); + + const doZoom = useCallback(() => { + setSelectState(old => { + const { left, right } = old; + + if (left === undefined || right === undefined || left === right) { + // Either is undefined or both are same - zoom out + setZoomState({}); + } else if (left < right) { + setZoomState({ left, right }); + } else if (left > right) { + setZoomState({ left: right, right: left }); + } + + return {}; + }); + setShowSelection(false); + }, [setSelectState, setZoomState, setShowSelection]); + + const zoomProps = useMemo( + () => ({ + onMouseDown, + onMouseMove, + onMouseUp: doZoom, + }), + [onMouseDown, onMouseMove, doZoom], + ); + + useEffect(() => { + if (!showSelection) { + return undefined; + } + return registerSelection(setState); + }, [registerSelection, setState, showSelection]); + + const getZoomArea = useCallback( + (props?: ZoomAreaProps) => ( + + {showSelection && state.left && state.right ? ( + + ) : null} + + ), + [showSelection, state.left, state.right], + ); + + return { + zoomProps, + getZoomArea, + }; +} diff --git a/plugins/cicd-statistics/src/components/utils.tsx b/plugins/cicd-statistics/src/components/utils.tsx index 2b6d1ad029..e4663c8cde 100644 --- a/plugins/cicd-statistics/src/components/utils.tsx +++ b/plugins/cicd-statistics/src/components/utils.tsx @@ -42,9 +42,17 @@ export function pickElements(arr: ReadonlyArray, num: number): Array { } function formatDateShort(milliseconds: number) { + if ((milliseconds as any) === 'auto') { + // When recharts gets confused (empty data) + return ''; + } return DateTime.fromMillis(milliseconds).toLocaleString(DateTime.DATE_SHORT); } function formatDateTimeShort(milliseconds: number) { + if ((milliseconds as any) === 'auto') { + // When recharts gets confused (empty data) + return ''; + } return DateTime.fromMillis(milliseconds).toLocaleString( DateTime.DATETIME_SHORT, ); diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index 532900c1da..74a0add0b3 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -26,6 +26,7 @@ import { } from './hooks/use-cicd-statistics'; import { useCicdConfiguration } from './hooks/use-cicd-configuration'; import { buildsToChartableStages } from './charts/logic/conversions'; +import { ZoomProvider, useZoom } from './charts/zoom'; import { StageChart } from './charts/stage-chart'; import { StatusChart } from './charts/status-chart'; import { @@ -48,7 +49,9 @@ export function EntityPageCicdCharts() { const state = useCicdConfiguration(); return renderFallbacks(state, value => ( - + + + )); } @@ -88,6 +91,8 @@ function CicdCharts(props: CicdChartsProps) { const classes = useStyles(); + const { resetZoom } = useZoom(); + const [chartFilter, setChartFilter] = useState( getDefaultChartFilter(cicdConfiguration), ); @@ -135,6 +140,10 @@ function CicdCharts(props: CicdChartsProps) { [statisticsState, cicdConfiguration, viewOptions], ); + useEffect(() => { + resetZoom(); + }, [resetZoom, statisticsState.value]); + const onFilterChange = useCallback((filter: ChartFilter) => { setChartFilter(cleanChartFilter(filter)); }, []); From 365e3f87ad0d593205d56bcdeb839f396f8bfc2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Mon, 7 Feb 2022 12:56:21 +0100 Subject: [PATCH 19/25] feat(cicd-statistics): Allow multi-step progress when loading statistics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loading can take a pretty long time, and often in several steps, such as "loading builds" followed by "loading pipelines per build". Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/apis/types.ts | 18 ++- .../src/components/progress.tsx | 113 ++++++++++++++---- plugins/cicd-statistics/src/entity-page.tsx | 4 + .../src/hooks/use-cicd-configuration.ts | 6 +- .../src/hooks/use-cicd-statistics.ts | 39 +++++- 5 files changed, 144 insertions(+), 36 deletions(-) diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 0726c566c7..0a87b40fc2 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -220,12 +220,20 @@ export interface CicdState { * * This can be called at any rate. Rate limiting (debouncing) is implemented in * the UI. + * + * Optionally this can signal multiple progresses in several steps */ -export type UpdateProgress = ( - completed: number, - total: number, - started?: number, -) => void; +export interface UpdateProgress { + (completed: number, total: number, started?: number): void; + ( + steps: Array<{ + title: string; + completed: number; + total: number; + started?: number; + }>, + ): void; +} /** * When reading configuration, the Api can return a custom settings depending on diff --git a/plugins/cicd-statistics/src/components/progress.tsx b/plugins/cicd-statistics/src/components/progress.tsx index 1b707573c0..9973ba170d 100644 --- a/plugins/cicd-statistics/src/components/progress.tsx +++ b/plugins/cicd-statistics/src/components/progress.tsx @@ -14,12 +14,23 @@ * limitations under the License. */ -import React, { DependencyList } from 'react'; +import React, { CSSProperties, DependencyList } from 'react'; import { useAsync } from 'react-use'; import { Box, LinearProgress } from '@material-ui/core'; +import Timeline from '@material-ui/lab/Timeline'; +import TimelineItem from '@material-ui/lab/TimelineItem'; +import TimelineSeparator from '@material-ui/lab/TimelineSeparator'; +import TimelineConnector from '@material-ui/lab/TimelineConnector'; +import TimelineContent from '@material-ui/lab/TimelineContent'; +import TimelineOppositeContent from '@material-ui/lab/TimelineOppositeContent'; +import TimelineDot, { TimelineDotProps } from '@material-ui/lab/TimelineDot'; import Alert from '@material-ui/lab/Alert'; import { useApp } from '@backstage/core-plugin-api'; +const stepProgressStyle: CSSProperties = { + marginTop: 6, +}; + // Matching react-use, only has loading/error/value type AsyncState = | { @@ -43,24 +54,33 @@ type AsyncState = value: T; }; -export type ProgressAsLoading = { +export interface ProgressStep extends ProgessAsSingle { + title: string; +} +export interface ProgessAsSteps { + steps: Array; +} +export interface ProgessAsSingle { + progress?: T; + progressBuffer?: T; +} + +export type ProgessState = + | ProgessAsSingle + | (T extends number ? ProgessAsSteps : { steps?: undefined }); + +export type ProgressAsLoading = ProgessState & { loading: true; - progress?: number; - progressBuffer?: number; error?: undefined; value?: undefined; }; -export type ProgressAsError = { +export type ProgressAsError = ProgessState & { loading?: false | undefined; - progress?: undefined; - progressBuffer?: undefined; error: Error; value?: undefined; }; -export type ProgressAsValue = { +export type ProgressAsValue = ProgessState & { loading?: false | undefined; - progress?: undefined; - progressBuffer?: undefined; error?: undefined; value: T; }; @@ -69,7 +89,7 @@ export type ProgressAsValue = { * An AsyncState but with the addition of progress (decimal 0-1) to allow * rendering a progress bar while waiting. */ -export type Progress = +export type ProgressType = | ProgressAsLoading | ProgressAsError | ProgressAsValue; @@ -79,8 +99,8 @@ const sentry = Symbol(); /** * Casts an AsyncState or Progress into its non-succeeded sub types */ -type Unsuccessful | AsyncState> = - S extends Progress +type Unsuccessful | AsyncState> = + S extends ProgressType ? ProgressAsLoading | ProgressAsError : Omit, 'value'>; @@ -92,7 +112,7 @@ type Unsuccessful | AsyncState> = * invoked for a new layer of async state with the dependent (upstream) success * result as argument. */ -export function useAsyncChain | AsyncState, R>( +export function useAsyncChain | AsyncState, R>( parentState: S, fn: (value: NonNullable) => Promise, deps: DependencyList, @@ -111,7 +131,7 @@ export function useAsyncChain | AsyncState, R>( } export function renderFallbacks( - state: Progress | AsyncState, + state: ProgressType | AsyncState, success: (value: T) => JSX.Element, ): JSX.Element { if (state.loading) { @@ -130,18 +150,67 @@ export function ViewProgress({ }) { const { Progress } = useApp().getComponents(); - const stateAsProgress = state as ProgressAsLoading; + const stateAsSingleProgress = state as ProgessAsSingle; + const stateAsStepProgress = state as ProgessAsSteps; - if (!stateAsProgress.progress && !stateAsProgress.progressBuffer) { + if ( + !stateAsSingleProgress.progress && + !stateAsSingleProgress.progressBuffer && + !stateAsStepProgress.steps + ) { + // Simple spinner return ; + } else if (stateAsSingleProgress.progress !== undefined) { + // Simple _single_ progress + return ( + + + + ); } + + // Multi-step progresses + return ( - + + {stateAsStepProgress.steps.map((step, index) => ( + + {step.title} + + + {index < stateAsStepProgress.steps.length - 1 ? ( + + ) : null} + + + {!step.progress && !step.progressBuffer ? null : ( + + )} + + + ))} + ); } + +function getDotColor(step: ProgressStep): TimelineDotProps['color'] { + const progress = step.progress ?? 0; + + if (progress >= 1) { + return 'primary'; + } else if (progress > 0) { + return 'secondary'; + } + return 'grey'; +} diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index 74a0add0b3..3c51dabf2a 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -16,6 +16,7 @@ import React, { useCallback, useState, useMemo, useEffect } from 'react'; import { Grid, makeStyles, Theme } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; import { useEntity } from '@backstage/plugin-catalog-react'; import { useApi, errorApiRef } from '@backstage/core-plugin-api'; import { DateTime } from 'luxon'; @@ -182,6 +183,9 @@ function CicdCharts(props: CicdChartsProps) { {renderFallbacks(chartableStagesState, chartableStages => ( <> + {chartableStages.stages.size > 0 ? null : ( + No data + )} {!statisticsState.value?.builds?.length || !chartableStagesState.value?.daily?.values?.length ? null : ( diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts index d435cb21aa..d21b978a92 100644 --- a/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts +++ b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts @@ -18,15 +18,15 @@ import { useState, useEffect } from 'react'; import { useEntity } from '@backstage/plugin-catalog-react'; import { CicdConfiguration, statusTypes } from '../apis'; -import { Progress } from '../components/progress'; +import { ProgressType } from '../components/progress'; import { defaultFormatStageName } from '../utils/stage-names'; import { useCicdStatisticsApi } from './use-cicd-statistics-api'; -export function useCicdConfiguration(): Progress { +export function useCicdConfiguration(): ProgressType { const cicdStatisticsApi = useCicdStatisticsApi(); const { entity } = useEntity(); - const [state, setState] = useState>({ + const [state, setState] = useState>({ loading: true, }); diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts index 5c1fe1bfe2..e52673a576 100644 --- a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts +++ b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts @@ -24,8 +24,9 @@ import { AbortError, FilterStatusType, FilterBranchType, + UpdateProgress, } from '../apis'; -import { Progress } from '../components/progress'; +import { ProgressType } from '../components/progress'; import { useCicdStatisticsApi } from './use-cicd-statistics-api'; export interface UseCicdStatisticsOptions { @@ -39,7 +40,7 @@ export interface UseCicdStatisticsOptions { export function useCicdStatistics( options: UseCicdStatisticsOptions, -): Progress { +): ProgressType { const { entity, abortController, @@ -49,7 +50,9 @@ export function useCicdStatistics( filterType, } = options; - const [state, setState] = useState>({ loading: true }); + const [state, setState] = useState>({ + loading: true, + }); const cicdStatisticsApi = useCicdStatisticsApi(); @@ -62,15 +65,39 @@ export function useCicdStatistics( let mounted = true; let completed = false; // successfully or failed - const updateProgress = throttle((count, total, started = 0) => { - if (mounted && !completed) { + const updateProgressImpl: UpdateProgress = (_count, _total?, _started?) => { + if (!mounted || completed) { + return; + } + + if (Array.isArray(_count)) { + // Multi-progress + setState({ + loading: true, + steps: _count.map(step => ({ + title: step.title, + progress: !step.total ? 0 : step.completed / step.total, + progressBuffer: !step.total ? 0 : (step.started ?? 0) / step.total, + })), + }); + } else { + // Single-progress + const count = _count; + const total = _total as number; + const started = (_started as number) ?? 0; setState({ loading: true, progress: !total ? 0 : count / total, progressBuffer: !total ? 0 : started / total, }); } - }, 200); + }; + + const updateProgress = throttle( + updateProgressImpl, + 200, + // throttle doesn't handle types of multi-signature functions + ) as any as UpdateProgress; const fetchOptions: FetchBuildsOptions = { entity, From 07faca1f278b3eaf424d4b96d6768836d7cd95c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Mon, 7 Feb 2022 17:21:17 +0100 Subject: [PATCH 20/25] fix(cicd-statistics): Fixed test issue after refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/src/charts/logic/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cicd-statistics/src/charts/logic/utils.test.ts b/plugins/cicd-statistics/src/charts/logic/utils.test.ts index 420a6bf90c..72e0a22101 100644 --- a/plugins/cicd-statistics/src/charts/logic/utils.test.ts +++ b/plugins/cicd-statistics/src/charts/logic/utils.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { sortTriggerReasons } from './daily-summary'; +import { sortTriggerReasons } from './utils'; describe('daily-summary', () => { it('sortTriggerReasons', () => { From b947238d1dd0b4fde53e85ad452fb26a2d1e847e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Tue, 8 Feb 2022 11:47:11 +0100 Subject: [PATCH 21/25] fix(yarn.lock): Fixed urls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- yarn.lock | 221 ++---------------------------------------------------- 1 file changed, 8 insertions(+), 213 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2245504748..ffc877abc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1409,7 +1409,7 @@ zen-observable "^0.8.15" zod "^3.11.6" -"@backstage/core-plugin-api@^0.4.0", "@backstage/core-plugin-api@^0.4.1": +"@backstage/core-plugin-api@^0.4.0": version "0.4.1" resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.4.1.tgz#c0a13504bdfa61ae3d0db96934cd6c32a7574446" integrity sha512-IIb7XTcquTPaSIlamMKUgeTs5uLqkKN0Nw32QdTZhKgFkFFVzWC0AwN+henkaMNBZFdGb0ttPzrvNXGj5E6dGg== @@ -1440,7 +1440,7 @@ "@material-ui/lab" "4.0.0-alpha.57" react-use "^17.2.4" -"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5", "@backstage/plugin-catalog-react@^0.6.9": +"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5": version "0.6.13" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.13.tgz#b325eae501d3edeb8b7caef5d9615f2e632f5430" integrity sha512-XBwop7PwAZqfongx3KP6jAJar+MEscLSp8nLuHYX5XxA+suQNiBgi96uO3SEQmvtae+hvsRM7c0WHSxbYiXsDA== @@ -1835,7 +1835,7 @@ dependencies: "@date-io/core" "^1.3.13" -"@date-io/luxon@1.x", "@date-io/luxon@^1.3.13": +"@date-io/luxon@1.x": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== @@ -3622,7 +3622,7 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.9.13": +"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2": version "4.12.3" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== @@ -5309,11 +5309,6 @@ resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== -"@types/d3-color@^2": - version "2.0.3" - resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82" - integrity sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w== - "@types/d3-force@^2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-2.1.1.tgz#a18b6f029d056eb0f8f84a09471e6228e4469b14" @@ -5326,13 +5321,6 @@ dependencies: "@types/d3-color" "*" -"@types/d3-interpolate@^2.0.0": - version "2.0.2" - resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz#78eddf7278b19e48e8652603045528d46897aba0" - integrity sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw== - dependencies: - "@types/d3-color" "^2" - "@types/d3-path@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" @@ -5343,18 +5331,6 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== -"@types/d3-path@^2": - version "2.0.1" - resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.1.tgz#ca03dfa8b94d8add97ad0cd97e96e2006b4763cb" - integrity sha512-6K8LaFlztlhZO7mwsZg7ClRsdLg3FJRzIIi6SZXDWmmSJc2x8dd2VkESbLXdk3p8cuvz71f36S0y8Zv2AxqvQw== - -"@types/d3-scale@^3.0.0": - version "3.3.2" - resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06" - integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ== - dependencies: - "@types/d3-time" "^2" - "@types/d3-selection@*", "@types/d3-selection@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59" @@ -5367,13 +5343,6 @@ dependencies: "@types/d3-path" "^1" -"@types/d3-shape@^2.0.0": - version "2.1.3" - resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3" - integrity sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ== - dependencies: - "@types/d3-path" "^2" - "@types/d3-shape@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" @@ -5381,11 +5350,6 @@ dependencies: "@types/d3-path" "*" -"@types/d3-time@^2": - version "2.1.1" - resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" - integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== - "@types/d3-zoom@^3.0.1": version "3.0.1" resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" @@ -5792,11 +5756,6 @@ resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.5.tgz#29d3b095d55ee50df8f4cf109b16009334d9828e" integrity sha512-GKrG5v16BOs9XGpouu33hOkAFaiSDi3ZaDXG9F2yAoyzHRBtksZnI60VWY5aM/yAENCccBejrxw8jDY+9OVlxw== -"@types/luxon@^2.0.9": - version "2.0.9" - resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6" - integrity sha1-eCoO36bWmRkSksExaL1JbNZrh8Y= - "@types/mdast@^3.0.0", "@types/mdast@^3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" @@ -6136,11 +6095,6 @@ "@types/tough-cookie" "*" form-data "^2.5.0" -"@types/resize-observer-browser@^0.1.6": - version "0.1.7" - resolved "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" - integrity sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg== - "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -6943,11 +6897,6 @@ alphanum-sort@^1.0.2: resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -already@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/already/-/already-3.3.0.tgz#a5e5becd167cf537b45f8f1c23d331488ed77003" - integrity sha512-ADGyKddqEp8t/Wu4ITc0y9GGsgZDgyMeMk38AM5qrPK7VEjNAYD87QGTGGgNhSQahmjw76V3mi+3fJRwPJXcTw== - anafanafo@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/anafanafo/-/anafanafo-2.0.0.tgz#43f56274680bc553dd67a9625a920f88d0057b5c" @@ -9686,7 +9635,7 @@ css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-unit-converter@^1.1.1, css-unit-converter@^1.1.2: +css-unit-converter@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== @@ -9901,13 +9850,6 @@ cypress@^7.3.0: url "^0.11.0" yauzl "^2.10.0" -d3-array@2, d3-array@^2.3.0: - version "2.12.1" - resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" - integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== - dependencies: - internmap "^1.0.0" - d3-array@^1.2.0: version "1.2.4" resolved "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" @@ -9923,11 +9865,6 @@ d3-color@1: resolved "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== -"d3-color@1 - 2": - version "2.0.0" - resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" - integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== - "d3-color@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" @@ -9970,11 +9907,6 @@ d3-format@1: resolved "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== -"d3-format@1 - 2": - version "2.0.0" - resolved "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" - integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== - d3-interpolate@1, d3-interpolate@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" @@ -9989,23 +9921,11 @@ d3-interpolate@1, d3-interpolate@^1.3.0: dependencies: d3-color "1 - 3" -"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" - integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== - dependencies: - d3-color "1 - 2" - d3-path@1: version "1.0.9" resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== -"d3-path@1 - 2": - version "2.0.0" - resolved "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" - integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== - "d3-path@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" @@ -10028,17 +9948,6 @@ d3-scale@^2.1.0: d3-time "1" d3-time-format "2" -d3-scale@^3.0.0: - version "3.3.0" - resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" - integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== - dependencies: - d3-array "^2.3.0" - d3-format "1 - 2" - d3-interpolate "1.2.0 - 2" - d3-time "^2.1.1" - d3-time-format "2 - 3" - "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" @@ -10051,13 +9960,6 @@ d3-shape@^1.2.0: dependencies: d3-path "1" -d3-shape@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" - integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== - dependencies: - d3-path "1 - 2" - d3-shape@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" @@ -10072,25 +9974,11 @@ d3-time-format@2: dependencies: d3-time "1" -"d3-time-format@2 - 3": - version "3.0.0" - resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" - integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== - dependencies: - d3-time "1 - 2" - d3-time@1: version "1.1.0" resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== -"d3-time@1 - 2", d3-time@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" - integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== - dependencies: - d3-array "2" - "d3-timer@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" @@ -11615,7 +11503,7 @@ eventemitter3@^3.1.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -11997,11 +11885,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-equals@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" - integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== - fast-glob@^3.1.1: version "3.2.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" @@ -13974,11 +13857,6 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" -internmap@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" - integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== - interpret@^1.0.0: version "1.4.0" resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -16568,11 +16446,6 @@ luxon@^1.27.0: resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== -luxon@^2.3.0: - version "2.3.0" - resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/luxon/-/luxon-2.3.0.tgz#bf16a7e642513c2a20a6230a6a41b0ab446d0045" - integrity sha1-vxan5kJRPCogpiMKakGwq0RtAEU= - lz-string@^1.4.4: version "1.4.4" resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -19696,11 +19569,6 @@ postcss-unique-selectors@^5.0.2: alphanum-sort "^1.0.2" postcss-selector-parser "^6.0.5" -postcss-value-parser@^3.3.0: - version "3.3.1" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" @@ -20392,7 +20260,7 @@ react-inspector@^5.1.1: is-dom "^1.0.0" prop-types "^15.0.0" -react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -20449,15 +20317,6 @@ react-resize-detector@^2.3.0: prop-types "^15.6.0" resize-observer-polyfill "^1.5.0" -react-resize-detector@^6.6.3: - version "6.7.8" - resolved "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz#318c85d1335e50f99d4fb8eb9ec34e066db597d0" - integrity sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw== - dependencies: - "@types/resize-observer-browser" "^0.1.6" - lodash "^4.17.21" - resize-observer-polyfill "^1.5.1" - react-router-dom@6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0-beta.0.tgz#9dcc8555365f22f7fbd09f26b6b82543f3eb97d6" @@ -20495,15 +20354,6 @@ react-smooth@^1.0.5: raf "^3.4.0" react-transition-group "^2.5.0" -react-smooth@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.0.tgz#561647b33e498b2e25f449b3c6689b2e9111bf91" - integrity sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw== - dependencies: - fast-equals "^2.0.0" - raf "^3.4.0" - react-transition-group "2.9.0" - react-sparklines@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" @@ -20539,7 +20389,7 @@ react-text-truncate@^0.17.0: dependencies: prop-types "^15.5.7" -react-transition-group@2.9.0, react-transition-group@^2.5.0: +react-transition-group@^2.5.0: version "2.9.0" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== @@ -20584,26 +20434,6 @@ react-use@^17.2.4: ts-easing "^0.2.0" tslib "^2.1.0" -react-use@^17.3.1: - version "17.3.2" - resolved "https://registry.npmjs.org/react-use/-/react-use-17.3.2.tgz#448abf515f47c41c32455024db28167cb6e53be8" - integrity sha512-bj7OD0/1wL03KyWmzFXAFe425zziuTf7q8olwCYBfOeFHY1qfO1FAMjROQLsLZYwG4Rx63xAfb7XAbBrJsZmEw== - dependencies: - "@types/js-cookie" "^2.2.6" - "@xobotyi/scrollbar-width" "^1.9.5" - copy-to-clipboard "^3.3.1" - fast-deep-equal "^3.1.3" - fast-shallow-equal "^1.0.0" - js-cookie "^2.2.1" - nano-css "^5.3.1" - react-universal-interface "^0.6.2" - resize-observer-polyfill "^1.5.1" - screenfull "^5.1.0" - set-harmonic-interval "^1.0.1" - throttle-debounce "^3.0.1" - ts-easing "^0.2.0" - tslib "^2.1.0" - react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" @@ -20817,13 +20647,6 @@ recharts-scale@^0.4.2: dependencies: decimal.js-light "^2.4.1" -recharts-scale@^0.4.4: - version "0.4.5" - resolved "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" - integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== - dependencies: - decimal.js-light "^2.4.1" - recharts@^1.8.5: version "1.8.5" resolved "https://registry.npmjs.org/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12" @@ -20841,26 +20664,6 @@ recharts@^1.8.5: recharts-scale "^0.4.2" reduce-css-calc "^1.3.0" -recharts@^2.1.5: - version "2.1.8" - resolved "https://registry.npmjs.org/recharts/-/recharts-2.1.8.tgz#ca8774fcec5f5d7ec15dedd638db9ee12faf1c09" - integrity sha512-Wi7ufdDGyvy/BPf1za1Ok7VeWB2KtEejaewO9ulmlUhvn5l5RPS4AOkrUfhtMRTTjgJ4K6AbWMDpwtDjczUHJA== - dependencies: - "@types/d3-interpolate" "^2.0.0" - "@types/d3-scale" "^3.0.0" - "@types/d3-shape" "^2.0.0" - classnames "^2.2.5" - d3-interpolate "^2.0.0" - d3-scale "^3.0.0" - d3-shape "^2.0.0" - eventemitter3 "^4.0.1" - lodash "^4.17.19" - react-is "^16.10.2" - react-resize-detector "^6.6.3" - react-smooth "^2.0.0" - recharts-scale "^0.4.4" - reduce-css-calc "^2.1.8" - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -20907,14 +20710,6 @@ reduce-css-calc@^1.3.0: math-expression-evaluator "^1.2.14" reduce-function-call "^1.0.1" -reduce-css-calc@^2.1.8: - version "2.1.8" - resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" - integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== - dependencies: - css-unit-converter "^1.1.1" - postcss-value-parser "^3.3.0" - reduce-function-call@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f" From c2cbcaf234566c33ad440b784589c89cf4922630 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 9 Feb 2022 11:50:56 +0100 Subject: [PATCH 22/25] =?UTF-8?q?chore:=20de-dedupe=20dependencies=20and?= =?UTF-8?q?=20make=20the=20build=20green=20=F0=9F=A4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: blam --- plugins/cicd-statistics/package.json | 7 +- .../src/components/progress.tsx | 2 +- .../cicd-statistics/src/components/utils.tsx | 3 +- yarn.lock | 209 +++++++++++++++++- 4 files changed, 209 insertions(+), 12 deletions(-) diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index 3e93d2adf6..c7c3900c22 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -29,20 +29,21 @@ "clean": "backstage-cli clean" }, "devDependencies": { - "@types/luxon": "^2.0.9" + "@types/luxon": "^2.0.5" }, "dependencies": { "@backstage/catalog-model": "^0.9.8", "@backstage/core-plugin-api": "^0.4.1", "@backstage/plugin-catalog-react": "^0.6.9", - "@date-io/luxon": "^1.3.13", + "@date-io/luxon": "2.x", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/pickers": "^3.3.10", + "humanize-duration": "^3.27.0", "already": "^3.2.0", "lodash": "^4.17.21", - "luxon": "^2.3.0", + "luxon": "^2.0.2", "react-use": "^17.3.1", "recharts": "^2.1.5" }, diff --git a/plugins/cicd-statistics/src/components/progress.tsx b/plugins/cicd-statistics/src/components/progress.tsx index 9973ba170d..6ea8e6497d 100644 --- a/plugins/cicd-statistics/src/components/progress.tsx +++ b/plugins/cicd-statistics/src/components/progress.tsx @@ -15,7 +15,7 @@ */ import React, { CSSProperties, DependencyList } from 'react'; -import { useAsync } from 'react-use'; +import useAsync from 'react-use/lib/useAsync'; import { Box, LinearProgress } from '@material-ui/core'; import Timeline from '@material-ui/lab/Timeline'; import TimelineItem from '@material-ui/lab/TimelineItem'; diff --git a/plugins/cicd-statistics/src/components/utils.tsx b/plugins/cicd-statistics/src/components/utils.tsx index e4663c8cde..0133cf7ba8 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 humanizeDuration from 'humanize-duration'; import { capitalize } from 'lodash'; const infoText: CSSProperties = { color: 'InfoText' }; @@ -124,7 +125,7 @@ export function formatDuration(millis: number) { ...(seconds && !days && !hours && { seconds }), }); - return dur.toHuman({ unitDisplay: 'narrow' }).replace(/, /g, ''); + return humanizeDuration(dur.toMillis(), { round: true }); } export function formatDurationFromSeconds(seconds: number) { diff --git a/yarn.lock b/yarn.lock index ffc877abc6..a0ba55df6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1409,7 +1409,7 @@ zen-observable "^0.8.15" zod "^3.11.6" -"@backstage/core-plugin-api@^0.4.0": +"@backstage/core-plugin-api@^0.4.0", "@backstage/core-plugin-api@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.4.1.tgz#c0a13504bdfa61ae3d0db96934cd6c32a7574446" integrity sha512-IIb7XTcquTPaSIlamMKUgeTs5uLqkKN0Nw32QdTZhKgFkFFVzWC0AwN+henkaMNBZFdGb0ttPzrvNXGj5E6dGg== @@ -1440,7 +1440,7 @@ "@material-ui/lab" "4.0.0-alpha.57" react-use "^17.2.4" -"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5": +"@backstage/plugin-catalog-react@^0.6.13", "@backstage/plugin-catalog-react@^0.6.5", "@backstage/plugin-catalog-react@^0.6.9": version "0.6.13" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.13.tgz#b325eae501d3edeb8b7caef5d9615f2e632f5430" integrity sha512-XBwop7PwAZqfongx3KP6jAJar+MEscLSp8nLuHYX5XxA+suQNiBgi96uO3SEQmvtae+hvsRM7c0WHSxbYiXsDA== @@ -3622,7 +3622,7 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2": +"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.9.13": version "4.12.3" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== @@ -5309,6 +5309,11 @@ resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== +"@types/d3-color@^2": + version "2.0.3" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82" + integrity sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w== + "@types/d3-force@^2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-2.1.1.tgz#a18b6f029d056eb0f8f84a09471e6228e4469b14" @@ -5321,6 +5326,13 @@ dependencies: "@types/d3-color" "*" +"@types/d3-interpolate@^2.0.0": + version "2.0.2" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz#78eddf7278b19e48e8652603045528d46897aba0" + integrity sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw== + dependencies: + "@types/d3-color" "^2" + "@types/d3-path@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" @@ -5331,6 +5343,18 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== +"@types/d3-path@^2": + version "2.0.1" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.1.tgz#ca03dfa8b94d8add97ad0cd97e96e2006b4763cb" + integrity sha512-6K8LaFlztlhZO7mwsZg7ClRsdLg3FJRzIIi6SZXDWmmSJc2x8dd2VkESbLXdk3p8cuvz71f36S0y8Zv2AxqvQw== + +"@types/d3-scale@^3.0.0": + version "3.3.2" + resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06" + integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ== + dependencies: + "@types/d3-time" "^2" + "@types/d3-selection@*", "@types/d3-selection@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59" @@ -5343,6 +5367,13 @@ dependencies: "@types/d3-path" "^1" +"@types/d3-shape@^2.0.0": + version "2.1.3" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3" + integrity sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ== + dependencies: + "@types/d3-path" "^2" + "@types/d3-shape@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" @@ -5350,6 +5381,11 @@ dependencies: "@types/d3-path" "*" +"@types/d3-time@^2": + version "2.1.1" + resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" + integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== + "@types/d3-zoom@^3.0.1": version "3.0.1" resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" @@ -6095,6 +6131,11 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/resize-observer-browser@^0.1.6": + version "0.1.7" + resolved "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" + integrity sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -6897,6 +6938,11 @@ alphanum-sort@^1.0.2: resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= +already@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/already/-/already-3.3.0.tgz#a5e5becd167cf537b45f8f1c23d331488ed77003" + integrity sha512-ADGyKddqEp8t/Wu4ITc0y9GGsgZDgyMeMk38AM5qrPK7VEjNAYD87QGTGGgNhSQahmjw76V3mi+3fJRwPJXcTw== + anafanafo@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/anafanafo/-/anafanafo-2.0.0.tgz#43f56274680bc553dd67a9625a920f88d0057b5c" @@ -9635,7 +9681,7 @@ css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-unit-converter@^1.1.2: +css-unit-converter@^1.1.1, css-unit-converter@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== @@ -9850,6 +9896,13 @@ cypress@^7.3.0: url "^0.11.0" yauzl "^2.10.0" +d3-array@2, d3-array@^2.3.0: + version "2.12.1" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + d3-array@^1.2.0: version "1.2.4" resolved "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" @@ -9865,6 +9918,11 @@ d3-color@1: resolved "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + "d3-color@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" @@ -9907,6 +9965,11 @@ d3-format@1: resolved "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + d3-interpolate@1, d3-interpolate@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" @@ -9921,11 +9984,23 @@ d3-interpolate@1, d3-interpolate@^1.3.0: dependencies: d3-color "1 - 3" +"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + d3-path@1: version "1.0.9" resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + "d3-path@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" @@ -9948,6 +10023,17 @@ d3-scale@^2.1.0: d3-time "1" d3-time-format "2" +d3-scale@^3.0.0: + version "3.3.0" + resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" @@ -9960,6 +10046,13 @@ d3-shape@^1.2.0: dependencies: d3-path "1" +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + d3-shape@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" @@ -9974,11 +10067,25 @@ d3-time-format@2: dependencies: d3-time "1" +"d3-time-format@2 - 3": + version "3.0.0" + resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + d3-time@1: version "1.1.0" resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + "d3-timer@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" @@ -11503,7 +11610,7 @@ eventemitter3@^3.1.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -eventemitter3@^4.0.0, eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -11885,6 +11992,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^2.0.0: + version "2.0.4" + resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" + integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== + fast-glob@^3.1.1: version "3.2.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" @@ -13857,6 +13969,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + interpret@^1.0.0: version "1.4.0" resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -19569,6 +19686,11 @@ postcss-unique-selectors@^5.0.2: alphanum-sort "^1.0.2" postcss-selector-parser "^6.0.5" +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" @@ -20260,7 +20382,7 @@ react-inspector@^5.1.1: is-dom "^1.0.0" prop-types "^15.0.0" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -20317,6 +20439,15 @@ react-resize-detector@^2.3.0: prop-types "^15.6.0" resize-observer-polyfill "^1.5.0" +react-resize-detector@^6.6.3: + version "6.7.8" + resolved "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz#318c85d1335e50f99d4fb8eb9ec34e066db597d0" + integrity sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw== + dependencies: + "@types/resize-observer-browser" "^0.1.6" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + react-router-dom@6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0-beta.0.tgz#9dcc8555365f22f7fbd09f26b6b82543f3eb97d6" @@ -20354,6 +20485,15 @@ react-smooth@^1.0.5: raf "^3.4.0" react-transition-group "^2.5.0" +react-smooth@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.0.tgz#561647b33e498b2e25f449b3c6689b2e9111bf91" + integrity sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw== + dependencies: + fast-equals "^2.0.0" + raf "^3.4.0" + react-transition-group "2.9.0" + react-sparklines@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" @@ -20389,7 +20529,7 @@ react-text-truncate@^0.17.0: dependencies: prop-types "^15.5.7" -react-transition-group@^2.5.0: +react-transition-group@2.9.0, react-transition-group@^2.5.0: version "2.9.0" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== @@ -20434,6 +20574,26 @@ react-use@^17.2.4: ts-easing "^0.2.0" tslib "^2.1.0" +react-use@^17.3.1: + version "17.3.2" + resolved "https://registry.npmjs.org/react-use/-/react-use-17.3.2.tgz#448abf515f47c41c32455024db28167cb6e53be8" + integrity sha512-bj7OD0/1wL03KyWmzFXAFe425zziuTf7q8olwCYBfOeFHY1qfO1FAMjROQLsLZYwG4Rx63xAfb7XAbBrJsZmEw== + dependencies: + "@types/js-cookie" "^2.2.6" + "@xobotyi/scrollbar-width" "^1.9.5" + copy-to-clipboard "^3.3.1" + fast-deep-equal "^3.1.3" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.3.1" + react-universal-interface "^0.6.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.1.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^3.0.1" + ts-easing "^0.2.0" + tslib "^2.1.0" + react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" @@ -20647,6 +20807,13 @@ recharts-scale@^0.4.2: dependencies: decimal.js-light "^2.4.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + recharts@^1.8.5: version "1.8.5" resolved "https://registry.npmjs.org/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12" @@ -20664,6 +20831,26 @@ recharts@^1.8.5: recharts-scale "^0.4.2" reduce-css-calc "^1.3.0" +recharts@^2.1.5: + version "2.1.8" + resolved "https://registry.npmjs.org/recharts/-/recharts-2.1.8.tgz#ca8774fcec5f5d7ec15dedd638db9ee12faf1c09" + integrity sha512-Wi7ufdDGyvy/BPf1za1Ok7VeWB2KtEejaewO9ulmlUhvn5l5RPS4AOkrUfhtMRTTjgJ4K6AbWMDpwtDjczUHJA== + dependencies: + "@types/d3-interpolate" "^2.0.0" + "@types/d3-scale" "^3.0.0" + "@types/d3-shape" "^2.0.0" + classnames "^2.2.5" + d3-interpolate "^2.0.0" + d3-scale "^3.0.0" + d3-shape "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-resize-detector "^6.6.3" + react-smooth "^2.0.0" + recharts-scale "^0.4.4" + reduce-css-calc "^2.1.8" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -20710,6 +20897,14 @@ reduce-css-calc@^1.3.0: math-expression-evaluator "^1.2.14" reduce-function-call "^1.0.1" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + reduce-function-call@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f" From 476c4c52fc9a3d9f71f3e747f496ffed70368f63 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 9 Feb 2022 13:50:20 +0100 Subject: [PATCH 23/25] =?UTF-8?q?chore:=20api-reports=20=F0=9F=98=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: blam --- plugins/cicd-statistics/api-report.md | 50 ++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md index c2e7e0934d..3c7a52cdad 100644 --- a/plugins/cicd-statistics/api-report.md +++ b/plugins/cicd-statistics/api-report.md @@ -27,6 +27,7 @@ export interface Build { requestedAt: Date; stages: Array; status: FilterStatusType; + triggeredBy?: TriggerReason; } // Warning: (ae-missing-release-tag) "BuildWithRaw" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -36,6 +37,16 @@ export type BuildWithRaw = Build & { raw: T; }; +// Warning: (ae-missing-release-tag) "ChartType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type ChartType = 'duration' | 'count'; + +// Warning: (ae-missing-release-tag) "ChartTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ChartTypes = Array; + // Warning: (ae-missing-release-tag) "CicdConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -49,11 +60,13 @@ export interface CicdConfiguration { // // @public export interface CicdDefaults { + chartTypes: Record; collapsedLimit: number; // (undocumented) filterStatus: Array; // (undocumented) filterType: FilterBranchType | 'all'; + hideLimit: number; lowercaseNames: boolean; normalizeTimeRange: boolean; // (undocumented) @@ -162,6 +175,7 @@ export interface Stage { // (undocumented) name: string; stages?: Array; + status: FilterStatusType; } // Warning: (ae-missing-release-tag) "statusTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -169,14 +183,40 @@ export interface Stage { // @public (undocumented) export const statusTypes: Array; +// Warning: (ae-missing-release-tag) "TriggerReason" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +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'; + +// Warning: (ae-missing-release-tag) "triggerReasons" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const triggerReasons: Array; + // Warning: (ae-missing-release-tag) "UpdateProgress" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type UpdateProgress = ( - completed: number, - total: number, - started?: number, -) => void; +export interface UpdateProgress { + // (undocumented) + (completed: number, total: number, started?: number): void; + // (undocumented) + ( + steps: Array<{ + title: string; + completed: number; + total: number; + started?: number; + }>, + ): void; +} // (No @packageDocumentation comment for this package) ``` From 29302d39db8d1a27e8df1970a1976e95e22b76ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Wed, 9 Feb 2022 16:54:32 +0100 Subject: [PATCH 24/25] fix(cicd-statistics): @types/react -> devDependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index c7c3900c22..696231c529 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -29,6 +29,7 @@ "clean": "backstage-cli clean" }, "devDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "@types/luxon": "^2.0.5" }, "dependencies": { @@ -48,7 +49,6 @@ "recharts": "^2.1.5" }, "peerDependencies": { - "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "files": [ From ad136d1c8732afbada469a134b17b0c0b77cd261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustaf=20R=C3=A4ntil=C3=A4?= Date: Wed, 9 Feb 2022 17:48:43 +0100 Subject: [PATCH 25/25] chore: go back to @date-io/luxon 1 for mui pickers... MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gustaf Räntilä --- plugins/cicd-statistics/package.json | 2 +- yarn.lock | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index 696231c529..1c4d8a455e 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -36,7 +36,7 @@ "@backstage/catalog-model": "^0.9.8", "@backstage/core-plugin-api": "^0.4.1", "@backstage/plugin-catalog-react": "^0.6.9", - "@date-io/luxon": "2.x", + "@date-io/luxon": "^1.3.13", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "4.0.0-alpha.57", diff --git a/yarn.lock b/yarn.lock index a0ba55df6d..6988edda3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1842,6 +1842,13 @@ dependencies: "@date-io/core" "^1.3.13" +"@date-io/luxon@^1.3.13": + version "1.3.13" + resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" + integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== + dependencies: + "@date-io/core" "^1.3.13" + "@elastic/elasticsearch-mock@^0.3.0": version "0.3.0" resolved "https://registry.npmjs.org/@elastic/elasticsearch-mock/-/elasticsearch-mock-0.3.0.tgz#6b1d8448aad3ca20f760fa01c0206b733c9c1e54"