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"