diff --git a/plugins/shared-react/.eslintrc.js b/plugins/shared-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/shared-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/shared-react/README.md b/plugins/shared-react/README.md new file mode 100644 index 0000000000..59006c5f73 --- /dev/null +++ b/plugins/shared-react/README.md @@ -0,0 +1,3 @@ +# Janus React Common + +Shared code for utils, types, and React components for the Janus frontend plugins. diff --git a/plugins/shared-react/package.json b/plugins/shared-react/package.json new file mode 100644 index 0000000000..0899990710 --- /dev/null +++ b/plugins/shared-react/package.json @@ -0,0 +1,53 @@ +{ + "name": "@janus-idp/shared-react", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test --passWithNoTests --coverage", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@kubernetes/client-node": "^0.18.1", + "classnames": "2.3.2" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "0.22.7", + "@backstage/core-app-api": "1.8.0", + "@backstage/dev-utils": "1.0.15", + "@backstage/test-utils": "1.3.1", + "@testing-library/jest-dom": "5.10.1", + "@testing-library/react": "12.1.3", + "@testing-library/user-event": "14.0.0", + "@types/node": "18.16.16", + "msw": "1.2.1", + "cross-fetch": "3.1.6" + }, + "files": [ + "dist" + ], + "repository": "github:janus-idp/backstage-plugins", + "keywords": [ + "backstage", + "plugin" + ], + "homepage": "https://janus-idp.io/", + "bugs": "https://github.com/janus-idp/backstage-plugins/issues" +} diff --git a/plugins/shared-react/src/__fixtures__/1-pipelinesData.ts b/plugins/shared-react/src/__fixtures__/1-pipelinesData.ts new file mode 100644 index 0000000000..1149a6f74d --- /dev/null +++ b/plugins/shared-react/src/__fixtures__/1-pipelinesData.ts @@ -0,0 +1,452 @@ +export const mockPLRResponseData = { + pipelineruns: [ + { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + annotations: { + 'pipeline.openshift.io/started-by': 'kube:admin', + }, + creationTimestamp: new Date('2023-03-30T07:03:04Z'), + generation: 1, + labels: { + 'app.kubernetes.io/instance': 'ruby-ex-git', + 'app.kubernetes.io/name': 'ruby-ex-git', + 'backstage.io/kubernetes-id': 'backstage', + 'operator.tekton.dev/operand-name': 'openshift-pipelines-addons', + 'pipeline.openshift.io/runtime': 'ruby', + 'pipeline.openshift.io/runtime-version': '3.0-ubi7', + 'pipeline.openshift.io/type': 'kubernetes', + 'tekton.dev/pipeline': 'ruby-ex-git', + }, + name: 'ruby-ex-git-xf45fo', + namespace: 'jai-test', + resourceVersion: '87613', + uid: 'b7584993-146c-4d4d-ba39-8619237e940b', + }, + spec: { + params: [], + pipelineRef: { + name: 'ruby-ex-git', + }, + serviceAccountName: 'pipeline', + workspaces: [], + }, + status: { + completionTime: '2023-03-30T07:05:13Z', + conditions: [ + { + lastTransitionTime: '2023-03-30T07:05:13Z', + message: 'Tasks Completed: 3 (Failed: 0, Cancelled 0), Skipped: 0', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + pipelineSpec: { + params: [], + tasks: [], + workspaces: [], + }, + startTime: '2023-03-30T07:03:04Z', + }, + }, + { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + annotations: { + 'pipeline.openshift.io/started-by': 'kube-admin', + }, + labels: { + 'backstage.io/kubernetes-id': 'test-backstage', + 'tekton.dev/pipeline': 'pipeline-test', + 'app.kubernetes.io/instance': 'abs', + 'app.kubernetes.io/name': 'ghg', + 'operator.tekton.dev/operand-name': 'ytui', + 'pipeline.openshift.io/runtime-version': 'hjkhk', + 'pipeline.openshift.io/type': 'hhu', + 'pipeline.openshift.io/runtime': 'node', + }, + name: 'pipeline-test-wbvtlk', + namespace: 'deb-test', + resourceVersion: '117337', + uid: '0a091bbf-3813-48d3-a6ce-fc43644a9b24', + creationTimestamp: new Date('2023-04-11T12:31:56Z'), + }, + spec: { + pipelineRef: { + name: 'pipeline-test', + }, + serviceAccountName: 'pipeline', + workspaces: [], + }, + status: { + completionTime: '2023-04-11T06:49:05Z', + conditions: [ + { + lastTransitionTime: '2023-04-11T06:49:05Z', + message: 'Tasks Completed: 4 (Failed: 3, Cancelled 0), Skipped: 0', + reason: 'Failed', + status: 'False', + type: 'Succeeded', + }, + ], + pipelineSpec: { + finally: [], + tasks: [], + workspaces: [], + startTime: '2023-04-11T06:48:50Z', + }, + }, + }, + ], + taskruns: [ + { + apiVersion: 'tekton.dev/v1beta1', + kind: 'TaskRun', + metadata: { + annotations: { + 'operator.tekton.dev/last-applied-hash': + '63911846cb698608618c9a280f25b886ea3ee59f84a4ef6da15738a699e09f0c', + 'pipeline.openshift.io/started-by': 'kube:admin', + 'pipeline.tekton.dev/release': '9ec444e', + 'tekton.dev/displayName': 's2i ruby', + 'tekton.dev/pipelines.minVersion': '0.19', + 'tekton.dev/tags': 's2i, ruby, workspace', + }, + creationTimestamp: new Date('2023-03-30T07:03:20Z'), + generation: 1, + labels: { + 'app.kubernetes.io/instance': 'ruby-ex-git', + 'app.kubernetes.io/managed-by': 'tekton-pipelines', + 'app.kubernetes.io/name': 'ruby-ex-git', + 'app.kubernetes.io/version': '0.1', + 'backstage.io/kubernetes-id': 'backstage', + 'operator.tekton.dev/operand-name': 'openshift-pipelines-addons', + 'operator.tekton.dev/provider-type': 'redhat', + 'pipeline.openshift.io/runtime': 'ruby', + 'pipeline.openshift.io/runtime-version': '3.0-ubi7', + 'pipeline.openshift.io/type': 'kubernetes', + 'tekton.dev/clusterTask': 's2i-ruby', + 'tekton.dev/memberOf': 'tasks', + 'tekton.dev/pipeline': 'ruby-ex-git', + 'tekton.dev/pipelineRun': 'ruby-ex-git-xf45fo', + 'tekton.dev/pipelineTask': 'build', + }, + name: 'ruby-ex-git-xf45fo-build', + namespace: 'jai-test', + ownerReferences: [ + { + apiVersion: 'tekton.dev/v1beta1', + blockOwnerDeletion: true, + controller: true, + kind: 'PipelineRun', + name: 'ruby-ex-git-xf45fo', + uid: 'b7584993-146c-4d4d-ba39-8619237e940b', + }, + ], + resourceVersion: '87287', + uid: 'e8d42c4a-b9c7-4f56-9482-d17f2c861804', + }, + spec: { + params: [], + resources: [], + serviceAccountName: 'pipeline', + taskRef: { + kind: 'ClusterTask', + name: 's2i-ruby', + }, + timeout: '1h0m0s', + workspaces: [], + }, + status: { + completionTime: '2023-03-30T07:04:55Z', + conditions: [ + { + lastTransitionTime: '2023-03-30T07:04:55Z', + message: 'All Steps have completed executing', + reason: 'Succeeded', + status: 'Unknown', + type: 'Succeeded', + }, + ], + podName: 'ruby-ex-git-xf45fo-build-pod', + startTime: '2023-03-30T07:03:20Z', + steps: [ + { + container: 'step-generate', + imageID: + 'registry.redhat.io/ocp-tools-4-tech-preview/source-to-image-rhel8@sha256:98d8cb3a255641ca6a1bce854e5e2460c20de9fb9b28e3cc67eb459f122873dd', + name: 'generate', + terminated: { + containerID: + 'cri-o://3b490fe8f5ed9310fa7b322961e2069b3548a6a8134693ef78c12c8c0760ea0c', + exitCode: 0, + finishedAt: '2023-03-30T07:03:30Z', + reason: 'Completed', + startedAt: '2023-03-30T07:03:30Z', + }, + }, + { + container: 'step-build-and-push', + imageID: + 'registry.redhat.io/rhel8/buildah@sha256:7678ad61e06e442b0093ab73faa73ce536721ae523015dd942f9196c4699a31d', + name: 'build-and-push', + terminated: { + containerID: + 'cri-o://90521ea2114ca3fc6b54216fe8cff26b679788d1c87dee40b98caa90f71e140e', + exitCode: 0, + finishedAt: '2023-03-30T07:04:54Z', + message: + '[{"key":"IMAGE_DIGEST","value":"sha256:14e0715ec241926c081124345cd45d325a44d914261cfd642b3b0969a49ffe02","type":1}]', + reason: 'Completed', + startedAt: '2023-03-30T07:03:30Z', + }, + }, + ], + taskSpec: { + description: + 's2i-ruby task clones a Git repository and builds and pushes a container image using S2I and a Ruby builder image.', + params: [], + results: [], + steps: [ + { + env: [], + image: + 'registry.redhat.io/ocp-tools-4-tech-preview/source-to-image-rhel8@sha256:98d8cb3a255641ca6a1bce854e5e2460c20de9fb9b28e3cc67eb459f122873dd', + name: 'generate', + resources: {}, + script: 'echo', + volumeMounts: [], + workingDir: '/workspace/source', + }, + { + image: + 'registry.redhat.io/rhel8/buildah@sha256:ac0b8714cc260c94435cab46fe41b3de0ccbc3d93e38c395fa9d52ac49e521fe', + name: 'build-and-push', + resources: {}, + script: 'echo', + securityContext: { + capabilities: { + add: ['SETFCAP'], + }, + }, + volumeMounts: [], + workingDir: '/gen-source', + }, + ], + volumes: [], + workspaces: [], + }, + }, + }, + { + apiVersion: 'tekton.dev/v1beta1', + kind: 'TaskRun', + metadata: { + annotations: { + 'operator.tekton.dev/last-applied-hash': 'undefined', + 'pipeline.openshift.io/started-by': 'undefined', + 'pipeline.tekton.dev/release': 'undefined', + 'tekton.dev/displayName': 'undefined', + 'tekton.dev/pipelines.minVersion': 'undefined', + 'tekton.dev/tags': 'undefined', + }, + creationTimestamp: new Date('2023-04-11T06:48:50Z'), + generation: 1, + labels: { + 'app.kubernetes.io/managed-by': 'tekton-pipelines', + 'app.kubernetes.io/version': '0.4', + 'backstage.io/kubernetes-id': 'test-backstage', + 'operator.tekton.dev/operand-name': 'openshift-pipelines-addons', + 'operator.tekton.dev/provider-type': 'redhat', + 'tekton.dev/clusterTask': 'tkn', + 'tekton.dev/memberOf': 'tasks', + 'tekton.dev/pipeline': 'pipeline-test', + 'tekton.dev/pipelineRun': 'pipeline-test-wbvtlk', + 'tekton.dev/pipelineTask': 'tkn', + 'app.kubernetes.io/instance': 'xyz', + 'app.kubernetes.io/name': 'xyz', + 'pipeline.openshift.io/runtime': 'node', + 'pipeline.openshift.io/runtime-version': 'gh', + 'pipeline.openshift.io/type': 'abc', + }, + name: 'pipeline-test-wbvtlk-tkn', + namespace: 'deb-test', + ownerReferences: [ + { + apiVersion: 'tekton.dev/v1beta1', + blockOwnerDeletion: true, + controller: true, + kind: 'PipelineRun', + name: 'pipeline-test-wbvtlk', + uid: '0a091bbf-3813-48d3-a6ce-fc43644a9b24', + }, + ], + resourceVersion: '117189', + uid: 'cb08cb7d-71fc-48a7-888f-4ad14a7277b9', + }, + spec: { + params: [], + resources: [], + serviceAccountName: 'pipeline', + taskRef: { + kind: 'ClusterTask', + name: 'tkn', + }, + timeout: '1h0m0s', + }, + status: { + completionTime: '2023-04-11T06:48:56Z', + conditions: [ + { + lastTransitionTime: '2023-04-11T06:48:56Z', + message: 'All Steps have completed executing', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + podName: 'pipeline-test-wbvtlk-tkn-pod', + startTime: '2023-04-11T06:48:50Z', + steps: [ + { + container: 'step-tkn', + imageID: + 'registry.redhat.io/openshift-pipelines/pipelines-cli-tkn-rhel8@sha256:c73cefdd22522b2309f02dfa9858ed9079f1d5c94a3cd850f3f96dfbeafebc64', + name: 'tkn', + terminated: { + containerID: + 'cri-o://53fbddbb25c08e97d0061a3dd79021e8d411485bbc3f18cfcffd41ae3448c0d2', + exitCode: 0, + finishedAt: '2023-04-11T06:48:56Z', + reason: 'Completed', + startedAt: '2023-04-11T06:48:56Z', + }, + }, + ], + taskSpec: { + description: + 'This task performs operations on Tekton resources using tkn', + params: [], + steps: [ + { + args: ['--help'], + env: [], + image: + 'registry.redhat.io/openshift-pipelines/pipelines-cli-tkn-rhel8@sha256:c73cefdd22522b2309f02dfa9858ed9079f1d5c94a3cd850f3f96dfbeafebc64', + name: 'tkn', + resources: {}, + }, + ], + workspaces: [], + }, + }, + }, + { + apiVersion: 'tekton.dev/v1beta1', + kind: 'TaskRun', + metadata: { + annotations: { + 'operator.tekton.dev/last-applied-hash': 'undefined', + 'pipeline.openshift.io/started-by': 'undefined', + 'pipeline.tekton.dev/release': 'undefined', + 'tekton.dev/displayName': 'undefined', + 'tekton.dev/pipelines.minVersion': 'undefined', + 'tekton.dev/tags': 'undefined', + }, + creationTimestamp: new Date('2023-04-11T06:48:58Z'), + generation: 1, + labels: { + 'app.kubernetes.io/managed-by': 'tekton-pipelines', + 'app.kubernetes.io/version': '0.8', + 'backstage.io/kubernetes-id': 'test-backstage', + 'operator.tekton.dev/operand-name': 'openshift-pipelines-addons', + 'operator.tekton.dev/provider-type': 'redhat', + 'tekton.dev/clusterTask': 'git-clone', + 'tekton.dev/memberOf': 'finally', + 'tekton.dev/pipeline': 'pipeline-test', + 'tekton.dev/pipelineRun': 'pipeline-test-wbvtlk', + 'tekton.dev/pipelineTask': 'git-clone', + 'app.kubernetes.io/instance': 'xyz', + 'app.kubernetes.io/name': 'xyz', + 'pipeline.openshift.io/runtime': 'node', + 'pipeline.openshift.io/runtime-version': 'gh', + 'pipeline.openshift.io/type': 'abc', + }, + name: 'pipeline-test-wbvtlk-git-clone', + namespace: 'deb-test', + ownerReferences: [ + { + apiVersion: 'tekton.dev/v1beta1', + blockOwnerDeletion: true, + controller: true, + kind: 'PipelineRun', + name: 'pipeline-test-wbvtlk', + uid: '0a091bbf-3813-48d3-a6ce-fc43644a9b24', + }, + ], + resourceVersion: '117335', + uid: 'a3e4a1a9-605a-490d-82c1-9042bf6ec86e', + }, + spec: { + params: [], + resources: [], + serviceAccountName: 'pipeline', + taskRef: { + kind: 'ClusterTask', + name: 'git-clone', + }, + timeout: '1h0m0s', + workspaces: [], + }, + status: { + completionTime: '2023-04-11T06:49:05Z', + conditions: [ + { + lastTransitionTime: '2023-04-11T06:49:05Z', + message: + '"step-clone" exited with code 1 (image: "registry.redhat.io/openshift-pipelines/pipelines-git-init-rhel8@sha256:6c3980b3d28c8fb92b17466f5654d5f484ab893f1673ec8f29e49c0d03f8aca9"); for logs run: kubectl -n deb-test logs pipeline-test-wbvtlk-git-clone-pod -c step-clone\n', + reason: 'Failed', + status: 'False', + type: 'Succeeded', + }, + ], + podName: 'pipeline-test-wbvtlk-git-clone-pod', + startTime: '2023-04-11T06:48:58Z', + steps: [ + { + container: 'step-clone', + imageID: + 'registry.redhat.io/openshift-pipelines/pipelines-git-init-rhel8@sha256:6c3980b3d28c8fb92b17466f5654d5f484ab893f1673ec8f29e49c0d03f8aca9', + name: 'clone', + terminated: { + containerID: + 'cri-o://b727febb4b981471a5729cf6002d59d31673d25280192e7dc0ea09de113743dd', + exitCode: 1, + finishedAt: '2023-04-11T06:49:04Z', + reason: 'Error', + startedAt: '2023-04-11T06:49:04Z', + }, + }, + ], + taskSpec: { + description: + "These Tasks are Git tasks to work with repositories used by other tasks in your Pipeline.\nThe git-clone Task will clone a repo from the provided url into the output Workspace. By default the repo will be cloned into the root of your Workspace. You can clone into a subdirectory by setting this Task's subdirectory param. This Task also supports sparse checkouts. To perform a sparse checkout, pass a list of comma separated directory patterns to this Task's sparseCheckoutDirectories param.", + params: [], + steps: [ + { + env: [], + image: + 'registry.redhat.io/openshift-pipelines/pipelines-git-init-rhel8@sha256:6c3980b3d28c8fb92b17466f5654d5f484ab893f1673ec8f29e49c0d03f8aca9', + name: 'clone', + resources: {}, + }, + ], + workspaces: [], + }, + }, + }, + ], +}; diff --git a/plugins/shared-react/src/components/common/CamelCaseWrap.tsx b/plugins/shared-react/src/components/common/CamelCaseWrap.tsx new file mode 100644 index 0000000000..609ccff08e --- /dev/null +++ b/plugins/shared-react/src/components/common/CamelCaseWrap.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +const MEMO: { [key: string]: any } = {}; + +type CamelCaseWrapProps = { + value: string; + dataTest?: string; +}; + +export const CamelCaseWrap = ({ value, dataTest }: CamelCaseWrapProps) => { + if (!value) { + return '-'; + } + + if (MEMO[value]) { + return MEMO[value]; + } + + // Add word break points before capital letters (but keep consecutive capital letters together). + const words = value.match(/[A-Z]+[^A-Z]*|[^A-Z]+/g); + const rendered = ( + + {words?.map((word, i) => ( + + {word} + {i !== words.length - 1 && } + + ))} + + ); + MEMO[value] = rendered; + return rendered; +}; + +export default CamelCaseWrap; diff --git a/plugins/shared-react/src/components/common/StatusIconAndText.css b/plugins/shared-react/src/components/common/StatusIconAndText.css new file mode 100644 index 0000000000..f628d3d57e --- /dev/null +++ b/plugins/shared-react/src/components/common/StatusIconAndText.css @@ -0,0 +1,30 @@ +.bs-shared-icon-and-text { + align-items: baseline; + display: flex; + font-weight: 400; + font-size: 14px; +} + +.bs-shared-icon-and-text__icon { + flex-shrink: 0; + margin-right: 5px; +} + +.bs-shared-icon-and-text--lg { + display: block; +} + +.bs-shared-icon-and-text--lg .bs-shared-icon-and-text__icon { + font-size: 1.2rem; + margin-right: 1rem; +} + +.bs-shared-dashboard-icon { + font-size: 1.2rem; +} + +.bs-shared-icon-flex-child { + flex: 0 0 auto; + position: relative; + top: 0.125em; +} diff --git a/plugins/shared-react/src/components/common/StatusIconAndText.tsx b/plugins/shared-react/src/components/common/StatusIconAndText.tsx new file mode 100644 index 0000000000..433389a987 --- /dev/null +++ b/plugins/shared-react/src/components/common/StatusIconAndText.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import CamelCaseWrap from './CamelCaseWrap'; + +import './StatusIconAndText.css'; + +type StatusIconAndTextProps = { + title?: string; + iconOnly?: boolean; + noTooltip?: boolean; + className?: string; + popoverTitle?: string; + icon?: React.ReactElement; + spin?: boolean; +}; + +const DASH = '-'; + +export const StatusIconAndText = ({ + icon, + title, + spin, + iconOnly, + noTooltip, + className, +}: StatusIconAndTextProps) => { + if (!title) { + return <>{DASH}; + } + + return ( + + {icon && + React.cloneElement(icon, { + className: classNames( + spin && 'fa-spin', + icon.props.className, + !iconOnly && + 'bs-shared-icon-and-text__icon bs-shared-icon-flex-child', + ), + })} + {!iconOnly && } + + ); +}; + +export default StatusIconAndText; diff --git a/plugins/shared-react/src/components/index.ts b/plugins/shared-react/src/components/index.ts new file mode 100644 index 0000000000..51205d74f6 --- /dev/null +++ b/plugins/shared-react/src/components/index.ts @@ -0,0 +1,4 @@ +export { HorizontalStackedBars } from './pipeline/HorizontalStackedBars'; +export { CamelCaseWrap } from './common/CamelCaseWrap'; +export { TaskStatusTooltip } from './pipeline/TaskStatusTooltip'; +export { StatusIconAndText } from './common/StatusIconAndText'; diff --git a/plugins/shared-react/src/components/pipeline/HorizontalStackedBars.css b/plugins/shared-react/src/components/pipeline/HorizontalStackedBars.css new file mode 100644 index 0000000000..b8ace909bc --- /dev/null +++ b/plugins/shared-react/src/components/pipeline/HorizontalStackedBars.css @@ -0,0 +1,23 @@ +.bs-shared-horizontal-stacked-bars { + --bar-gap: 3px; + display: block; + height: 100%; + overflow: hidden; + width: 100%; +} +.bs-shared-horizontal-stacked-bars.is-inline { + display: inline-block; +} +.bs-shared-horizontal-stacked-bars__bars { + display: flex; + flex-direction: row; + height: 100%; + outline: none; + width: calc(100% + var(--bar-gap)); +} +.bs-shared-horizontal-stacked-bars__data-bar { + --bar-gap-neg: calc(var(--bar-gap) * -1); + box-shadow: inset var(--bar-gap-neg) 0 0 #fff; + height: 100%; + transition: flex-grow 300ms linear; +} diff --git a/plugins/shared-react/src/components/pipeline/HorizontalStackedBars.tsx b/plugins/shared-react/src/components/pipeline/HorizontalStackedBars.tsx new file mode 100644 index 0000000000..1bbb8434a5 --- /dev/null +++ b/plugins/shared-react/src/components/pipeline/HorizontalStackedBars.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import './HorizontalStackedBars.css'; + +type StackedValue = { + color: string; + name: string; + size: number; +}; + +type HorizontalStackedBarsProps = { + barGap?: number; + height?: number | string; + inline?: boolean; + values: StackedValue[]; + width?: number | string; +}; + +export const HorizontalStackedBars = ({ + barGap, + height, + inline, + values, + width, +}: HorizontalStackedBarsProps) => { + return ( +
+
+ {values.map(({ color, name, size }) => ( +
+ ))} +
+
+ ); +}; diff --git a/plugins/shared-react/src/components/pipeline/TaskStatusTooltip.css b/plugins/shared-react/src/components/pipeline/TaskStatusTooltip.css new file mode 100644 index 0000000000..847809ddc3 --- /dev/null +++ b/plugins/shared-react/src/components/pipeline/TaskStatusTooltip.css @@ -0,0 +1,11 @@ +.bs-shared-task-status-tooltip { + display: inline-grid; + grid-gap: 0.5rem; + text-align: left; + grid-template-columns: 1rem auto; +} + +.bs-shared-task-status-tooltip__legend { + height: 1rem; + width: 1rem; +} diff --git a/plugins/shared-react/src/components/pipeline/TaskStatusTooltip.tsx b/plugins/shared-react/src/components/pipeline/TaskStatusTooltip.tsx new file mode 100644 index 0000000000..40b3428eb2 --- /dev/null +++ b/plugins/shared-react/src/components/pipeline/TaskStatusTooltip.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { ComputedStatus, TaskStatusTypes } from '../../types'; +import { getRunStatusColor } from '../../utils'; + +import './TaskStatusTooltip.css'; + +interface TaskStatusToolTipProps { + taskStatus: TaskStatusTypes; +} + +export const TaskStatusTooltip = ({ taskStatus }: TaskStatusToolTipProps) => { + return ( +
+ {Object.keys(ComputedStatus).map(status => { + const { message, color } = getRunStatusColor(status); + return taskStatus[status as keyof TaskStatusTypes] ? ( + +
+
+ {status === ComputedStatus.PipelineNotStarted || + status === ComputedStatus.FailedToStart + ? message + : `${taskStatus[status as keyof TaskStatusTypes]} ${message}`} +
+ + ) : null; + })} +
+ ); +}; diff --git a/plugins/shared-react/src/consts/common.ts b/plugins/shared-react/src/consts/common.ts new file mode 100644 index 0000000000..e7f79bc0ff --- /dev/null +++ b/plugins/shared-react/src/consts/common.ts @@ -0,0 +1,6 @@ +export const skippedColor = '#8a8d90'; +export const cancelledColor = '#6a6e73'; +export const pendingColor = '#8bc1f7'; +export const runningColor = '#06c'; +export const successColor = '#38812f'; +export const failureColor = '#c9190b'; diff --git a/plugins/shared-react/src/consts/index.ts b/plugins/shared-react/src/consts/index.ts new file mode 100644 index 0000000000..2ddb6efe74 --- /dev/null +++ b/plugins/shared-react/src/consts/index.ts @@ -0,0 +1,2 @@ +export * from './pipeline'; +export * from './common'; diff --git a/plugins/shared-react/src/consts/pipeline.ts b/plugins/shared-react/src/consts/pipeline.ts new file mode 100644 index 0000000000..edf441c8b9 --- /dev/null +++ b/plugins/shared-react/src/consts/pipeline.ts @@ -0,0 +1 @@ +export const pipelineGroupColor = '#38812f'; diff --git a/plugins/shared-react/src/hooks/index.ts b/plugins/shared-react/src/hooks/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/shared-react/src/index.ts b/plugins/shared-react/src/index.ts new file mode 100644 index 0000000000..73a12f38fb --- /dev/null +++ b/plugins/shared-react/src/index.ts @@ -0,0 +1,4 @@ +export * from './components'; +export * from './consts'; +export * from './types'; +export * from './utils'; diff --git a/plugins/shared-react/src/setupTests.ts b/plugins/shared-react/src/setupTests.ts new file mode 100644 index 0000000000..48c09b5346 --- /dev/null +++ b/plugins/shared-react/src/setupTests.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/shared-react/src/types/index.ts b/plugins/shared-react/src/types/index.ts new file mode 100644 index 0000000000..d2e2cc8017 --- /dev/null +++ b/plugins/shared-react/src/types/index.ts @@ -0,0 +1,4 @@ +export * from './pipeline/pipeline'; +export * from './pipeline/computedStatus'; +export * from './pipeline/pipelineRun'; +export * from './pipeline/taskRun'; diff --git a/plugins/shared-react/src/types/pipeline/computedStatus.ts b/plugins/shared-react/src/types/pipeline/computedStatus.ts new file mode 100644 index 0000000000..da2fb22908 --- /dev/null +++ b/plugins/shared-react/src/types/pipeline/computedStatus.ts @@ -0,0 +1,47 @@ +export enum TerminatedReasons { + Completed = 'Completed', +} + +export enum ComputedStatus { + Cancelling = 'Cancelling', + Succeeded = 'Succeeded', + Failed = 'Failed', + Running = 'Running', + 'In Progress' = 'In Progress', + FailedToStart = 'FailedToStart', + PipelineNotStarted = 'PipelineNotStarted', + Skipped = 'Skipped', + Cancelled = 'Cancelled', + Pending = 'Pending', + Idle = 'Idle', + Other = '-', +} + +export enum SucceedConditionReason { + PipelineRunCancelled = 'StoppedRunFinally', + PipelineRunStopped = 'CancelledRunFinally', + TaskRunCancelled = 'TaskRunCancelled', + Cancelled = 'Cancelled', + PipelineRunStopping = 'PipelineRunStopping', + PipelineRunPending = 'PipelineRunPending', + TaskRunStopping = 'TaskRunStopping', + CreateContainerConfigError = 'CreateContainerConfigError', + ExceededNodeResources = 'ExceededNodeResources', + ExceededResourceQuota = 'ExceededResourceQuota', + ConditionCheckFailed = 'ConditionCheckFailed', +} + +export type StatusMessage = { + message: string; + color: string; +}; + +export type TaskStatusTypes = { + PipelineNotStarted: number; + Pending: number; + Running: number; + Succeeded: number; + Cancelled: number; + Failed: number; + Skipped: number; +}; diff --git a/plugins/shared-react/src/types/pipeline/pipeline.ts b/plugins/shared-react/src/types/pipeline/pipeline.ts new file mode 100644 index 0000000000..ff15cd14a6 --- /dev/null +++ b/plugins/shared-react/src/types/pipeline/pipeline.ts @@ -0,0 +1,129 @@ +import { V1ObjectMeta } from '@kubernetes/client-node'; + +export type TektonParam = { + default?: string | string[]; + description?: string; + name: string; + type?: string; +}; + +export type TektonResource = { + name: string; + optional?: boolean; + type: string; +}; + +export type TektonWorkspace = { + name: string; + description?: string; + mountPath?: string; + readOnly?: boolean; + optional?: boolean; +}; + +export type TektonResourceGroup = { + inputs?: ResourceType[]; + outputs?: ResourceType[]; +}; + +export type TektonTaskSteps = { + name: string; + args?: string[]; + command?: string[]; + image?: string; + resources?: {}[] | {}; + env?: { name: string; value?: string }[]; + script?: string; + workingDir?: string; + volumeMounts?: { name: string; mountPath: string }[]; +}; + +export type TaskResult = { + name: string; + description?: string; +}; + +export type TektonTaskSpec = { + metadata?: {}; + description?: string; + steps: TektonTaskSteps[]; + params?: TektonParam[]; + resources?: TektonResourceGroup; + results?: TaskResult[]; + workspaces?: TektonWorkspace[]; +}; + +export type TektonResultsRun = { + name: string; + value: string; +}; + +export type PipelineTaskRef = { + kind?: string; + name: string; +}; + +export type PipelineTaskWorkspace = { + name: string; + workspace: string; + optional?: boolean; +}; + +export type PipelineTaskResource = { + name: string; + resource?: string; + from?: string[]; +}; + +export type PipelineTaskParam = { + name: string; + value: any; +}; + +export type WhenExpression = { + input: string; + operator: string; + values: string[]; +}; + +export type PipelineResult = { + name: string; + value: string; + description?: string; +}; + +export type PipelineTask = { + name: string; + params?: PipelineTaskParam[]; + resources?: TektonResourceGroup; + runAfter?: string[]; + taskRef?: PipelineTaskRef; + taskSpec?: TektonTaskSpec; + when?: WhenExpression[]; + workspaces?: PipelineTaskWorkspace[]; +}; + +export type PipelineSpec = { + params?: TektonParam[]; + resources?: TektonResource[]; + serviceAccountName?: string; + tasks: PipelineTask[]; + workspaces?: TektonWorkspace[]; + finally?: PipelineTask[]; + results?: PipelineResult[]; +}; + +export type Condition = { + type: string; + status: string; + reason?: string; + message?: string; + lastTransitionTime?: string; +}; + +export type PipelineKind = { + apiVersion?: string; + kind?: string; + metadata?: V1ObjectMeta; + spec: PipelineSpec; +}; diff --git a/plugins/shared-react/src/types/pipeline/pipelineRun.ts b/plugins/shared-react/src/types/pipeline/pipelineRun.ts new file mode 100644 index 0000000000..a69c4c6ed0 --- /dev/null +++ b/plugins/shared-react/src/types/pipeline/pipelineRun.ts @@ -0,0 +1,122 @@ +import { V1ObjectMeta } from '@kubernetes/client-node'; + +import { + Condition, + PipelineSpec, + PipelineTask, + TektonResultsRun, + TektonTaskSpec, +} from './pipeline'; + +export type PLRTaskRunStep = { + container: string; + imageID?: string; + name: string; + waiting?: { + reason: string; + }; + running?: { + startedAt: string; + }; + terminated?: { + containerID: string; + exitCode: number; + finishedAt: string; + reason: string; + startedAt: string; + message?: string; + }; +}; + +export type PipelineRunParam = { + name: string; + value: string | string[]; + input?: string; + output?: string; + resource?: object; +}; + +export type PipelineRunWorkspace = { + name: string; + [volumeType: string]: {}; +}; + +export type PipelineRunEmbeddedResourceParam = { name: string; value: string }; +export type PipelineRunEmbeddedResource = { + name: string; + resourceSpec: { + params: PipelineRunEmbeddedResourceParam[]; + type: string; + }; +}; +export type PipelineRunReferenceResource = { + name: string; + resourceRef: { + name: string; + }; +}; +export type PipelineRunResource = + | PipelineRunReferenceResource + | PipelineRunEmbeddedResource; + +export type PLRTaskRunData = { + pipelineTaskName: string; + status?: { + completionTime?: string; + conditions: Condition[]; + podName: string; + startTime: string; + steps?: PLRTaskRunStep[]; + taskSpec?: TektonTaskSpec; + taskResults?: { name: string; value: string; type?: string }[]; + }; +}; + +export type PLRTaskRuns = { + [taskRunName: string]: PLRTaskRunData; +}; + +export type PipelineRunStatus = { + succeededCondition?: string; + creationTimestamp?: string; + conditions?: Condition[]; + startTime?: string; + completionTime?: string; + taskRuns?: PLRTaskRuns; + pipelineSpec: PipelineSpec; + skippedTasks?: { + name: string; + }[]; + pipelineResults?: TektonResultsRun[]; +}; + +export type PipelineRunKind = { + apiVersion?: string; + kind?: string; + metadata?: V1ObjectMeta; + spec: { + pipelineRef?: { name: string }; + pipelineSpec?: PipelineSpec; + params?: PipelineRunParam[]; + workspaces?: PipelineRunWorkspace[]; + resources?: PipelineRunResource[]; + serviceAccountName?: string; + timeout?: string; + status?: string; + }; + status?: PipelineRunStatus; +}; + +export type PipelineTaskWithStatus = PipelineTask & { + status: { + reason: string; + completionTime?: string; + conditions: Condition[]; + podName?: string; + startTime?: string; + steps?: PLRTaskRunStep[]; + taskSpec?: TektonTaskSpec; + taskResults?: { name: string; value: string }[]; + duration?: string; + }; +}; diff --git a/plugins/shared-react/src/types/pipeline/taskRun.ts b/plugins/shared-react/src/types/pipeline/taskRun.ts new file mode 100644 index 0000000000..62949aec80 --- /dev/null +++ b/plugins/shared-react/src/types/pipeline/taskRun.ts @@ -0,0 +1,55 @@ +import { + V1ConfigMap, + V1ObjectMeta, + V1PersistentVolumeClaimTemplate, + V1Secret, +} from '@kubernetes/client-node'; + +import { + Condition, + PipelineTaskParam, + PipelineTaskRef, + TektonResource, + TektonResultsRun, + TektonTaskSpec, +} from './pipeline'; +import { PLRTaskRunStep } from './pipelineRun'; + +export type VolumeTypePVC = { + claimName: string; +}; + +export type TaskRunWorkspace = { + name: string; + volumeClaimTemplate?: V1PersistentVolumeClaimTemplate; + persistentVolumeClaim?: VolumeTypePVC; + configMap?: V1ConfigMap; + emptyDir?: {}; + secret?: V1Secret; + subPath?: string; +}; + +export type TaskRunStatus = { + completionTime?: string; + conditions?: Condition[]; + podName?: string; + startTime?: string; + steps?: PLRTaskRunStep[]; + taskResults?: TektonResultsRun[]; +}; + +export type TaskRunKind = { + apiVersion?: string; + kind?: string; + metadata?: V1ObjectMeta; + spec: { + taskRef?: PipelineTaskRef; + taskSpec?: TektonTaskSpec; + serviceAccountName?: string; + params?: PipelineTaskParam[]; + resources?: TektonResource[] | {}; + timeout?: string; + workspaces?: TaskRunWorkspace[]; + }; + status?: TaskRunStatus; +}; diff --git a/plugins/shared-react/src/utils/index.ts b/plugins/shared-react/src/utils/index.ts new file mode 100644 index 0000000000..29be82f5f6 --- /dev/null +++ b/plugins/shared-react/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './pipeline-utils/pipeline-utils'; +export * from './pipeline-utils/taskRun-utils'; diff --git a/plugins/shared-react/src/utils/pipeline-utils/pipeline-utils.test.ts b/plugins/shared-react/src/utils/pipeline-utils/pipeline-utils.test.ts new file mode 100644 index 0000000000..5ec0957151 --- /dev/null +++ b/plugins/shared-react/src/utils/pipeline-utils/pipeline-utils.test.ts @@ -0,0 +1,34 @@ +import { ComputedStatus } from '../../types'; +import { getRunStatusColor } from './pipeline-utils'; + +describe('getRunStatusColor should handle ComputedStatus values', () => { + it('should expect all but PipelineNotStarted to produce a non-default result', () => { + // Verify that we cover colour states for all the ComputedStatus values + const failCase = 'PipelineNotStarted'; + const defaultCase = getRunStatusColor(ComputedStatus[failCase]); + const allOtherStatuses = Object.keys(ComputedStatus) + .filter( + status => + status !== failCase && + ComputedStatus[status as keyof typeof ComputedStatus] !== + ComputedStatus.Other, + ) + .map(status => ComputedStatus[status as keyof typeof ComputedStatus]); + + expect(allOtherStatuses).not.toHaveLength(0); + allOtherStatuses.forEach(statusValue => { + const { message } = getRunStatusColor(statusValue); + + expect(defaultCase.message).not.toEqual(message); + }); + }); + + it('should expect all status colors to return visible text to show as a descriptor of the color', () => { + let status = getRunStatusColor(ComputedStatus.Succeeded); + expect(status.message).toBe('Succeeded'); + status = getRunStatusColor(ComputedStatus.FailedToStart); + expect(status.message).toBe('PipelineRun failed to start'); + status = getRunStatusColor('xyz'); + expect(status.message).toBe('PipelineRun not started yet'); + }); +}); diff --git a/plugins/shared-react/src/utils/pipeline-utils/pipeline-utils.ts b/plugins/shared-react/src/utils/pipeline-utils/pipeline-utils.ts new file mode 100644 index 0000000000..0ac3b714d0 --- /dev/null +++ b/plugins/shared-react/src/utils/pipeline-utils/pipeline-utils.ts @@ -0,0 +1,256 @@ +import { + cancelledColor, + failureColor, + pendingColor, + runningColor, + skippedColor, + successColor, +} from '../../consts'; +import { + ComputedStatus, + PipelineRunKind, + StatusMessage, + SucceedConditionReason, + TaskRunKind, + TaskStatusTypes, +} from '../../types'; + +export const getRunStatusColor = (status: string): StatusMessage => { + switch (status) { + case ComputedStatus.Succeeded: + return { message: 'Succeeded', color: successColor }; + case ComputedStatus.Failed: + return { message: 'Failed', color: failureColor }; + case ComputedStatus.FailedToStart: + return { + message: 'PipelineRun failed to start', + color: failureColor, + }; + case ComputedStatus.Running: + case ComputedStatus['In Progress']: + return { message: 'Running', color: runningColor }; + + case ComputedStatus.Skipped: + return { message: 'Skipped', color: skippedColor }; + case ComputedStatus.Cancelled: + return { message: 'Cancelled', color: cancelledColor }; + case ComputedStatus.Cancelling: + return { message: 'Cancelling', color: cancelledColor }; + case ComputedStatus.Idle: + case ComputedStatus.Pending: + return { message: 'Pending', color: pendingColor }; + default: + return { + message: 'PipelineRun not started yet', + color: pendingColor, + }; + } +}; + +const getDate = ( + run: PipelineRunKind, + field: 'completionTime' | 'startTime' | 'creationTimestamp', +) => { + if (field === 'creationTimestamp') { + return run?.metadata?.creationTimestamp ?? ''; + } + if (field === 'startTime' || field === 'completionTime') { + return run?.status?.[field] ?? ''; + } + return ''; +}; + +const getLatestRun = ( + runs: PipelineRunKind[], + field: 'completionTime' | 'startTime' | 'creationTimestamp', +) => { + let latestRun = runs[0]; + for (let i = 1; i < runs.length; i++) { + latestRun = + new Date(getDate(runs?.[i], field)) > new Date(getDate(latestRun, field)) + ? runs[i] + : latestRun; + } + return latestRun; +}; + +export const getLatestPipelineRun = ( + runs: PipelineRunKind[], + field: string, +): PipelineRunKind | null => { + if (runs?.length > 0 && field) { + let latestRun; + if ( + field === 'completionTime' || + field === 'startTime' || + field === 'creationTimestamp' + ) { + latestRun = getLatestRun(runs, field); + } else { + latestRun = runs[runs.length - 1]; + } + return latestRun; + } + return null; +}; + +const getSucceededStatus = (status: string): ComputedStatus => { + if (status === 'True') { + return ComputedStatus.Succeeded; + } else if (status === 'False') { + return ComputedStatus.Failed; + } + return ComputedStatus.Running; +}; + +export const pipelineRunStatus = ( + pipelineRun: PipelineRunKind | TaskRunKind | null, +) => { + const conditions = pipelineRun?.status?.conditions || []; + if (conditions.length === 0) return null; + + const succeedCondition = conditions.find((c: any) => c.type === 'Succeeded'); + const cancelledCondition = conditions.find( + (c: any) => c.reason === 'Cancelled', + ); + const failedCondition = conditions.find((c: any) => c.reason === 'Failed'); + + if ( + [ + SucceedConditionReason.PipelineRunStopped, + SucceedConditionReason.PipelineRunCancelled, + ].includes( + (pipelineRun as PipelineRunKind)?.spec?.status as SucceedConditionReason, + ) && + !cancelledCondition && + !failedCondition + ) { + return ComputedStatus.Cancelling; + } + + if (!succeedCondition?.status) { + return null; + } + + const status = getSucceededStatus(succeedCondition.status); + + if (succeedCondition.reason && succeedCondition.reason !== status) { + switch (succeedCondition.reason) { + case SucceedConditionReason.PipelineRunCancelled: + case SucceedConditionReason.TaskRunCancelled: + case SucceedConditionReason.Cancelled: + case SucceedConditionReason.PipelineRunStopped: + return ComputedStatus.Cancelled; + case SucceedConditionReason.PipelineRunStopping: + case SucceedConditionReason.TaskRunStopping: + return ComputedStatus.Failed; + case SucceedConditionReason.CreateContainerConfigError: + case SucceedConditionReason.ExceededNodeResources: + case SucceedConditionReason.ExceededResourceQuota: + case SucceedConditionReason.PipelineRunPending: + return ComputedStatus.Pending; + case SucceedConditionReason.ConditionCheckFailed: + return ComputedStatus.Skipped; + default: + return status; + } + } + return status; +}; + +export const pipelineRunFilterReducer = ( + pipelineRun: PipelineRunKind | TaskRunKind, +): ComputedStatus => { + const status = pipelineRunStatus(pipelineRun); + return status || ComputedStatus.Other; +}; + +export const updateTaskStatus = ( + pipelinerun: PipelineRunKind | null, + taskRuns: TaskRunKind[], +): TaskStatusTypes => { + const skippedTaskLength = pipelinerun?.status?.skippedTasks?.length || 0; + const taskStatus: TaskStatusTypes = { + PipelineNotStarted: 0, + Pending: 0, + Running: 0, + Succeeded: 0, + Failed: 0, + Cancelled: 0, + Skipped: skippedTaskLength, + }; + + if (taskRuns.length === 0) { + return taskStatus; + } + + taskRuns.forEach((taskRun: TaskRunKind) => { + const status = taskRun && pipelineRunFilterReducer(taskRun); + if (status === 'Succeeded') { + taskStatus[ComputedStatus.Succeeded]++; + } else if (status === 'Running') { + taskStatus[ComputedStatus.Running]++; + } else if (status === 'Failed') { + taskStatus[ComputedStatus.Failed]++; + } else if (status === 'Cancelled') { + taskStatus[ComputedStatus.Cancelled]++; + } else { + taskStatus[ComputedStatus.Pending]++; + } + }); + + return { + ...taskStatus, + }; +}; + +export const totalPipelineRunTasks = ( + pipelinerun: PipelineRunKind | null, +): number => { + if (!pipelinerun?.status?.pipelineSpec) { + return 0; + } + const totalTasks = (pipelinerun.status.pipelineSpec?.tasks || []).length; + const finallyTasks = + (pipelinerun.status.pipelineSpec?.finally || []).length ?? 0; + return totalTasks + finallyTasks; +}; + +export const getTaskStatus = ( + pipelinerun: PipelineRunKind, + taskRuns: TaskRunKind[], +) => { + const totalTasks = totalPipelineRunTasks(pipelinerun); + const plrTaskLength = taskRuns.length; + const skippedTaskLength = pipelinerun?.status?.skippedTasks?.length || 0; + + const taskStatus: TaskStatusTypes = updateTaskStatus(pipelinerun, taskRuns); + + if (taskRuns?.length > 0) { + const pipelineRunHasFailure = taskStatus[ComputedStatus.Failed] > 0; + const pipelineRunIsCancelled = + pipelineRunFilterReducer(pipelinerun) === ComputedStatus.Cancelled; + const unhandledTasks = + totalTasks >= plrTaskLength + ? totalTasks - plrTaskLength - skippedTaskLength + : totalTasks; + + if (pipelineRunHasFailure || pipelineRunIsCancelled) { + taskStatus[ComputedStatus.Cancelled] += unhandledTasks; + } else { + taskStatus[ComputedStatus.Pending] += unhandledTasks; + } + } else if ( + pipelinerun?.status?.conditions?.[0]?.status === 'False' || + pipelinerun?.spec.status === SucceedConditionReason.PipelineRunCancelled + ) { + taskStatus[ComputedStatus.Cancelled] = totalTasks; + } else if ( + pipelinerun?.spec.status === SucceedConditionReason.PipelineRunPending + ) { + taskStatus[ComputedStatus.Pending] += totalTasks; + } else { + taskStatus[ComputedStatus.PipelineNotStarted]++; + } + return taskStatus; +}; diff --git a/plugins/shared-react/src/utils/pipeline-utils/taskRun-utils.test.ts b/plugins/shared-react/src/utils/pipeline-utils/taskRun-utils.test.ts new file mode 100644 index 0000000000..877980656f --- /dev/null +++ b/plugins/shared-react/src/utils/pipeline-utils/taskRun-utils.test.ts @@ -0,0 +1,30 @@ +import { mockPLRResponseData } from '../../__fixtures__/1-pipelinesData'; +import { getTaskRunsForPipelineRun } from './taskRun-utils'; + +describe('getTaskRunsForPipelineRun should return taskruns for a pipelinerun', () => { + it('should return empty array if pipelinerun is null', () => { + const taskRuns = getTaskRunsForPipelineRun(null, []); + expect(taskRuns).toEqual([]); + }); + + it('should return empty array if there are no taskruns for a pipelinerun', () => { + const taskRuns = getTaskRunsForPipelineRun( + mockPLRResponseData.pipelineruns[0], + [], + ); + expect(taskRuns).toEqual([]); + }); + + it('should return taskruns for a pipelinerun', () => { + let taskRuns = getTaskRunsForPipelineRun( + mockPLRResponseData.pipelineruns[0], + mockPLRResponseData.taskruns, + ); + expect(taskRuns.length).toBe(1); + taskRuns = getTaskRunsForPipelineRun( + mockPLRResponseData.pipelineruns[1], + mockPLRResponseData.taskruns, + ); + expect(taskRuns.length).toBe(2); + }); +}); diff --git a/plugins/shared-react/src/utils/pipeline-utils/taskRun-utils.ts b/plugins/shared-react/src/utils/pipeline-utils/taskRun-utils.ts new file mode 100644 index 0000000000..2ccf878383 --- /dev/null +++ b/plugins/shared-react/src/utils/pipeline-utils/taskRun-utils.ts @@ -0,0 +1,21 @@ +import { PipelineRunKind, TaskRunKind } from '../../types'; + +export const getTaskRunsForPipelineRun = ( + pipelinerun: PipelineRunKind | null, + taskRuns: TaskRunKind[], +): TaskRunKind[] => { + const associatedTaskRuns = taskRuns.reduce( + (acc: TaskRunKind[], taskRun: TaskRunKind) => { + if ( + taskRun?.metadata?.ownerReferences?.[0]?.name === + pipelinerun?.metadata?.name + ) { + acc.push(taskRun); + } + return acc; + }, + [], + ); + + return associatedTaskRuns; +}; diff --git a/yarn.lock b/yarn.lock index 37bd341e00..702a6d1fd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5763,6 +5763,16 @@ slash "^3.0.0" write-file-atomic "^4.0.2" +"@jest/types@^25.5.0": + version "25.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" + integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@jest/types@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" @@ -8607,6 +8617,21 @@ lz-string "^1.4.4" pretty-format "^27.0.2" +"@testing-library/jest-dom@5.10.1": + version "5.10.1" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.10.1.tgz#6508a9f007bd74e5d3c0b3135b668027ab663989" + integrity sha512-uv9lLAnEFRzwUTN/y9lVVXVXlEzazDkelJtM5u92PsGkEasmdI+sfzhZHxSDzlhZVTrlLfuMh2safMr8YmzXLg== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + chalk "^3.0.0" + css "^2.2.4" + css.escape "^1.5.1" + jest-diff "^25.1.0" + jest-matcher-utils "^25.1.0" + lodash "^4.17.15" + redent "^3.0.0" + "@testing-library/jest-dom@5.16.5", "@testing-library/jest-dom@^5.10.1": version "5.16.5" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e" @@ -8630,6 +8655,15 @@ "@babel/runtime" "^7.12.5" react-error-boundary "^3.1.0" +"@testing-library/react@12.1.3": + version "12.1.3" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.3.tgz#ef26c5f122661ea9b6f672b23dc6b328cadbbf26" + integrity sha512-oCULRXWRrBtC9m6G/WohPo1GLcLesH7T4fuKzRAKn1CWVu9BzXtqLXDDTA6KhFNNtRwLtfSMr20HFl+Qrdrvmg== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + "@types/react-dom" "*" + "@testing-library/react@12.1.5", "@testing-library/react@^12.1.3": version "12.1.5" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" @@ -8639,6 +8673,11 @@ "@testing-library/dom" "^8.0.0" "@types/react-dom" "<18.0.0" +"@testing-library/user-event@14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.0.0.tgz#3906aa6f0e56fd012d73559f5f05c02e63ba18dd" + integrity sha512-hZhjNrle/DMi1ziHwHy8LS0fYJtf+cID7fuG5+4h+Bk83xQaRDQT/DlqfL4hJYw3mxW6KTIxoODrhGnhqJebdQ== + "@testing-library/user-event@14.4.3", "@testing-library/user-event@^14.0.0": version "14.4.3" resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" @@ -9216,6 +9255,14 @@ dependencies: "@types/istanbul-lib-coverage" "*" +"@types/istanbul-reports@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2" + integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + "@types/istanbul-reports@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" @@ -9444,7 +9491,7 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@17.0.20", "@types/react-dom@<18.0.0", "@types/react-dom@^17.0.2": +"@types/react-dom@*", "@types/react-dom@17.0.20", "@types/react-dom@<18.0.0", "@types/react-dom@^17.0.2": version "17.0.20" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.20.tgz#e0c8901469d732b36d8473b40b679ad899da1b53" integrity sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA== @@ -9677,6 +9724,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== +"@types/yargs@^15.0.0": + version "15.0.15" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.15.tgz#e609a2b1ef9e05d90489c2f5f45bbfb2be092158" + integrity sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^16.0.0": version "16.0.5" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" @@ -10170,7 +10224,7 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== -ansi-regex@^5.0.1: +ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -10463,6 +10517,11 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" @@ -12146,6 +12205,16 @@ css.escape@1.5.1, css.escape@^1.5.1: resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== +css@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -12741,7 +12810,7 @@ decode-named-character-reference@^1.0.0: dependencies: character-entities "^2.0.0" -decode-uri-component@^0.2.2: +decode-uri-component@^0.2.0, decode-uri-component@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== @@ -12956,6 +13025,11 @@ dezalgo@^1.0.0, dezalgo@^1.0.4: asap "^2.0.0" wrappy "1" +diff-sequences@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" + integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diff-sequences@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" @@ -16207,6 +16281,16 @@ jest-css-modules@^2.1.0: dependencies: identity-obj-proxy "3.0.0" +jest-diff@^25.1.0, jest-diff@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" + integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.2.6" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-diff@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63" @@ -16261,6 +16345,11 @@ jest-environment-node@^29.5.0: jest-mock "^29.5.0" jest-util "^29.5.0" +jest-get-type@^25.2.6: + version "25.2.6" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877" + integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig== + jest-get-type@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5" @@ -16293,6 +16382,16 @@ jest-leak-detector@^29.5.0: jest-get-type "^29.4.3" pretty-format "^29.5.0" +jest-matcher-utils@^25.1.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz#fbc98a12d730e5d2453d7f1ed4a4d948e34b7867" + integrity sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw== + dependencies: + chalk "^3.0.0" + jest-diff "^25.5.0" + jest-get-type "^25.2.6" + pretty-format "^25.5.0" + jest-matcher-utils@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5" @@ -20355,6 +20454,16 @@ pretty-error@^4.0.0: lodash "^4.17.20" renderkid "^3.0.0" +pretty-format@^25.5.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" + integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== + dependencies: + "@jest/types" "^25.5.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-format@^27.0.2: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -21513,6 +21622,11 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== + resolve.exports@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" @@ -22179,6 +22293,17 @@ source-map-js@^1.0.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-resolve@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -22195,6 +22320,11 @@ source-map-support@^0.5.21, source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + source-map@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -23734,6 +23864,11 @@ uri-js@^4.2.2, uri-js@^4.4.1: dependencies: punycode "^2.1.0" +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== + url-join@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"