From f31150ea5efdde161f5046a696f2e8192397553d Mon Sep 17 00:00:00 2001 From: Alan Greene Date: Mon, 6 Mar 2023 23:47:27 +0000 Subject: [PATCH] Continue import of graph POC components Import graph component, DAG builder, and convert POC examples to stories. There are a number of TODO items in this code as well as config that needs to be properly tested and cleaned up, but this serves as a starting point for building out the real components, utils, and graph view for the Dashboard. --- packages/graph/src/components/Graph/Graph.js | 126 +++++ .../src/components/Graph/Graph.stories.js | 187 ++++++++ packages/graph/src/components/Graph/index.js | 14 + .../graph/src/components/Node/Node.stories.js | 9 +- .../src/components/StatusIcon/StatusIcon.js | 2 +- .../graph/src/components/examples/example1.js | 79 ++++ .../graph/src/components/examples/example2.js | 97 ++++ .../graph/src/components/examples/example3.js | 132 ++++++ .../examples/example7_pipeline.json | 44 ++ .../graph/src/components/examples/example8.js | 385 ++++++++++++++++ .../components/examples/finally-pipeline.json | 75 +++ .../graph/src/components/examples/index.js | 21 + .../components/examples/release-pipeline.json | 432 ++++++++++++++++++ .../when-expressions-pipelinerun.json | 376 +++++++++++++++ packages/graph/src/constants.js | 16 + packages/graph/src/newGraph.js | 153 +++++++ 16 files changed, 2143 insertions(+), 5 deletions(-) create mode 100644 packages/graph/src/components/Graph/Graph.js create mode 100644 packages/graph/src/components/Graph/Graph.stories.js create mode 100644 packages/graph/src/components/Graph/index.js create mode 100644 packages/graph/src/components/examples/example1.js create mode 100644 packages/graph/src/components/examples/example2.js create mode 100644 packages/graph/src/components/examples/example3.js create mode 100644 packages/graph/src/components/examples/example7_pipeline.json create mode 100644 packages/graph/src/components/examples/example8.js create mode 100644 packages/graph/src/components/examples/finally-pipeline.json create mode 100644 packages/graph/src/components/examples/index.js create mode 100644 packages/graph/src/components/examples/release-pipeline.json create mode 100644 packages/graph/src/components/examples/when-expressions-pipelinerun.json create mode 100644 packages/graph/src/constants.js create mode 100644 packages/graph/src/newGraph.js diff --git a/packages/graph/src/components/Graph/Graph.js b/packages/graph/src/components/Graph/Graph.js new file mode 100644 index 000000000..23ecb4455 --- /dev/null +++ b/packages/graph/src/components/Graph/Graph.js @@ -0,0 +1,126 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* istanbul ignore file */ + +import React, { useEffect, useState } from 'react'; +import ELK from 'elkjs/lib/elk.bundled'; + +import { ArrowRightMarker } from '@carbon/charts-react/diagrams/Marker'; + +import Edge from '../Edge'; +import Node from '../Node/'; + +function buildEdges({ direction, edges }) { + return edges.map((edge, i) => { + // eslint-disable-next-line react/no-array-index-key + return ; + }); +} + +function buildNodes(nodes) { + return nodes.map((node, i) => { + return ( + + ); + }); +} + +export default function Graph({ + direction = 'RIGHT', + id, + nodes, + edges, + type = 'detailed' +}) { + const elk = new ELK({ + defaultLayoutOptions: { + 'elk.algorithm': 'layered', + 'elk.direction': direction, + 'elk.edgeRouting': 'ORTHOGONAL', + 'elk.layered.mergeEdges': true, // avoid multiple input / output ports per node + // TODO: test + // 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED', + // 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', + // 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', + // 'crossingMinimization.semiInteractive': true, + // 'elk.layered.spacing.edgeNodeBetweenLayers': '50', + // 'elk.layered.unnecessaryBendpoints': true, + // 'org.eclipse.elk.layered.layering.strategy': 'INTERACTIVE', + // 'elk.padding': '[left=50, top=50, right=50, bottom=50]', + // portConstraints: 'FIXED_ORDER', // this gives correct node order but ignores mergeEdges and has other issues + // 'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES', + // 'elk.layered.considerModelOrder.crossingCounterNodeInfluence': 0.001, + // 'elk.layered.considerModelOrder.crossingCounterPortInfluence': 0.001, + separateConnectedComponents: false, + 'spacing.nodeNode': type === 'detailed' ? 20 : 5, + 'spacing.nodeNodeBetweenLayers': type === 'detailed' ? 50 : 20 + } + }); + + const [positions, setPositions] = useState(null); + + const graph = { + id, + children: nodes, + edges + }; + + useEffect(() => { + elk + .layout(graph) + .then(g => setPositions(g)) + .catch(console.error); // eslint-disable-line no-console + }, [direction]); + + if (!positions) { + return null; + } + + const { + children: graphNodes, + edges: graphEdges, + height: graphHeight, + width: graphWidth + } = positions; + + const edgeElements = buildEdges({ direction, edges: graphEdges }); + const nodeElements = buildNodes(graphNodes); + + return ( +
+ + + + + {edgeElements} + {nodeElements} + +
+ ); +} diff --git a/packages/graph/src/components/Graph/Graph.stories.js b/packages/graph/src/components/Graph/Graph.stories.js new file mode 100644 index 000000000..72d7117bb --- /dev/null +++ b/packages/graph/src/components/Graph/Graph.stories.js @@ -0,0 +1,187 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import Graph from './Graph'; +import { cardHeight, cardWidth, shapeSize } from '../../constants'; +import { getDAG } from '../../newGraph'; + +import { + example1, + example2, + example3, + example7Pipeline, + example8, + finallyPipeline, + releasePipeline, + whenExpressionsPipelineRun +} from '../examples'; + +export default { + component: Graph, + args: { + direction: 'RIGHT', + id: 'id' + }, + argTypes: { + direction: { + control: { type: 'inline-radio' }, + options: ['DOWN', 'RIGHT'] + } + }, + parameters: { + backgrounds: { + default: 'gray10' + } + }, + title: 'Graph' +}; + +const cardConfig = { + height: cardHeight, + type: 'card', + width: cardWidth +}; + +const iconConfig = { + height: shapeSize, + type: 'icon', + width: shapeSize +}; + +export const Detailed1 = { + args: { + ...example1(cardConfig) + } +}; + +export const Detailed2 = { + args: { + ...example2(cardConfig) + } +}; + +export const Detailed3 = { + args: { + ...example3(cardConfig) + } +}; + +export const Condensed1 = { + args: { + ...example1(iconConfig), + type: 'condensed' + } +}; + +export const Condensed2 = { + args: { + ...example2(iconConfig), + type: 'condensed' + } +}; + +export const Condensed3 = { + args: { + ...example3(iconConfig), + type: 'condensed' + } +}; + +export const DAG1 = { + args: { + ...getDAG({ + pipeline: example7Pipeline, + pipelineRun: true, + trigger: { type: 'manual' } + }) + } +}; + +export const DAG2Wide = { + args: { + ...getDAG({ + pipeline: releasePipeline, + pipelineRun: true, + trigger: { type: 'timer' } + }) + } +}; + +export const DAG3Finally = { + args: { + ...getDAG({ + pipeline: finallyPipeline, + pipelineRun: true, + trigger: { type: 'git' } + }) + } +}; + +export const DAG4WhenExpressions = { + args: { + ...getDAG({ + pipeline: { spec: whenExpressionsPipelineRun.spec.pipelineSpec }, + pipelineRun: true, + trigger: { type: 'webhook' } + }) + } +}; + +export const DAG5Trigger = { + args: { + ...getDAG({ + pipeline: example7Pipeline, + pipelineRun: true, + trigger: { type: 'trigger' } + }) + } +}; + +export const DAG6NoTrigger = { + args: { + ...getDAG({ + pipeline: example7Pipeline, + pipelineRun: true + }) + } +}; + +export const Order = { + args: { + direction: 'DOWN', + ...example8({ + ...cardConfig, + width: cardWidth / 2 + }) + }, + decorators: [ + Story => ( + <> +

+ Uses data from{' '} + + reaflow 'many nodes' story + + . See{' '} + + config + {' '} + for comparison. +

+ {Story()} + + ) + ] +}; diff --git a/packages/graph/src/components/Graph/index.js b/packages/graph/src/components/Graph/index.js new file mode 100644 index 000000000..d21d3f2bb --- /dev/null +++ b/packages/graph/src/components/Graph/index.js @@ -0,0 +1,14 @@ +/* +Copyright 2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export { default } from './Graph'; diff --git a/packages/graph/src/components/Node/Node.stories.js b/packages/graph/src/components/Node/Node.stories.js index d6928825f..e66e9651e 100644 --- a/packages/graph/src/components/Node/Node.stories.js +++ b/packages/graph/src/components/Node/Node.stories.js @@ -12,15 +12,16 @@ limitations under the License. */ import Node from './Node'; +import { cardHeight, cardWidth, shapeSize } from '../../constants'; export default { component: Node, args: { - height: 50, + height: cardHeight, status: 'success', title: 'some-task', type: 'card', - width: 250, + width: cardWidth, x: 0, y: 0 }, @@ -48,7 +49,7 @@ export default { }, parameters: { backgrounds: { - default: 'white' + default: 'gray10' } }, title: 'Node' @@ -94,7 +95,7 @@ export const Trigger = { args: { status: 'trigger', type: 'icon', - width: 24 + width: shapeSize } }; diff --git a/packages/graph/src/components/StatusIcon/StatusIcon.js b/packages/graph/src/components/StatusIcon/StatusIcon.js index bd9939ecb..5edb9662d 100644 --- a/packages/graph/src/components/StatusIcon/StatusIcon.js +++ b/packages/graph/src/components/StatusIcon/StatusIcon.js @@ -28,7 +28,7 @@ import { // TODO: need 'skipped' status (e.g. when expressions) const statusIcons = { - // dummy: () => <>, + dummy: () => <>, // eslint-disable-line react/jsx-no-useless-fragment failed: FailedIcon, git: GitIcon, manual: UserIcon, diff --git a/packages/graph/src/components/examples/example1.js b/packages/graph/src/components/examples/example1.js new file mode 100644 index 000000000..4cccf6bc5 --- /dev/null +++ b/packages/graph/src/components/examples/example1.js @@ -0,0 +1,79 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* istanbul ignore file */ + +function getNodeData({ height, type, width }) { + return [ + { + id: 'skaffold-unit-tests', + status: 'success-warning', + title: 'skaffold-unit-tests', + height, + width, + type + }, + { + id: 'build-skaffold-web', + status: 'success', + title: 'build-skaffold-web', + height, + width, + type + }, + { + id: 'build-skaffold-app', + status: 'success', + title: 'build-skaffold-app', + height, + width, + type + }, + { + id: 'deploy-app', + status: 'success', + title: 'deploy-app', + height, + width, + type + }, + { + id: 'deploy-web', + status: 'failed', + title: 'deploy-web', + height, + width, + type + } + // { id: "start", title: "start", height: 50, width: 50 }, + // { id: "end", title: "end", height: 50, width: 50 } + ]; +} + +function getEdgeData() { + return [ + { id: '1', source: 'skaffold-unit-tests', target: 'build-skaffold-app' }, + { id: '2', source: 'skaffold-unit-tests', target: 'build-skaffold-web' }, + { id: '3', source: 'build-skaffold-app', target: 'deploy-app' }, + { id: '4', source: 'build-skaffold-web', target: 'deploy-web' } + // { id: "5", source: "start", target: "skaffold-unit-tests" }, + // { id: "6", source: "deploy-app", target: "end" }, + // { id: "7", source: "deploy-web", target: "end" } + ]; +} + +export default function getExampleData({ height, type = 'card', width }) { + return { + edges: getEdgeData(), + nodes: getNodeData({ height, type, width }) + }; +} diff --git a/packages/graph/src/components/examples/example2.js b/packages/graph/src/components/examples/example2.js new file mode 100644 index 000000000..b8ff3dd9a --- /dev/null +++ b/packages/graph/src/components/examples/example2.js @@ -0,0 +1,97 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* istanbul ignore file */ + +function getNodeData({ height, type, width }) { + return [ + { + id: 'git-clone', + status: 'success', + title: 'git-clone', + height, + width, + type + }, + { + id: 'precheck', + status: 'success', + title: 'precheck', + height, + width, + type + }, + { + id: 'build', + status: 'success', + title: 'build', + height, + width, + type + }, + { + id: 'publish-images', + status: 'success', + title: 'publish-images', + height, + width, + type + }, + { + id: 'publish-to-bucket', + status: 'success', + title: 'publish-to-bucket', + height, + width, + type + }, + { + id: 'publish-to-bucket-latest', + status: 'success', + title: 'publish-to-bucket-latest', + height, + width, + type + }, + { + id: 'report-bucket', + status: 'success', + title: 'report-bucket', + height, + width, + type + } + // { id: "start", title: "start", height: 50, width: 50 }, + // { id: "end", title: "end", height: 50, width: 50 } + ]; +} + +function getEdgeData() { + return [ + { id: '1', source: 'git-clone', target: 'precheck' }, + { id: '2', source: 'precheck', target: 'build' }, + { id: '3', source: 'build', target: 'publish-images' }, + { id: '4', source: 'publish-images', target: 'publish-to-bucket' }, + { id: '5', source: 'publish-images', target: 'publish-to-bucket-latest' }, + { id: '6', source: 'publish-to-bucket', target: 'report-bucket' } + // { id: "5", source: "start", target: "skaffold-unit-tests" }, + // { id: "6", source: "deploy-app", target: "end" }, + // { id: "7", source: "deploy-web", target: "end" } + ]; +} + +export default function getExampleData({ height, type = 'card', width }) { + return { + edges: getEdgeData(), + nodes: getNodeData({ height, type, width }) + }; +} diff --git a/packages/graph/src/components/examples/example3.js b/packages/graph/src/components/examples/example3.js new file mode 100644 index 000000000..a0b71890e --- /dev/null +++ b/packages/graph/src/components/examples/example3.js @@ -0,0 +1,132 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* istanbul ignore file */ + +function getNodeData({ height, type, width }) { + return [ + { + id: 'git-clone', + status: 'success', + title: 'git-clone', + height, + width, + type + }, + { + id: 'npm-install', + status: 'success', + title: 'npm-install', + height, + width, + type + }, + { + id: 'lint', + status: 'success', + title: 'lint', + height, + width, + type + }, + { + id: 'unit-tests', + status: 'success', + title: 'unit-tests', + height, + width, + type + }, + { + id: 'static-scan', + status: 'success', + title: 'static-scan', + height, + width, + type + }, + { + id: 'build', + status: 'success', + title: 'build', + height, + width, + type + }, + { + id: 'integration-tests', + status: 'success', + title: 'integration-tests', + height, + width, + type + }, + { + id: 'publish-images', + status: 'success', + title: 'publish-images', + height, + width, + type + }, + { + id: 'publish-to-bucket', + status: 'success', + title: 'publish-to-bucket', + height, + width, + type + }, + { + id: 'publish-to-bucket-latest', + status: 'success', + title: 'publish-to-bucket-latest', + height, + width, + type + }, + { + id: 'report-bucket', + status: 'success', + title: 'report-bucket', + height, + width, + type + } + // { id: "start", title: "start", height: 50, width: 50 }, + // { id: "end", title: "end", height: 50, width: 50 } + ]; +} + +function getEdgeData() { + return [ + { id: '1', source: 'git-clone', target: 'npm-install' }, + { id: '2', source: 'npm-install', target: 'lint' }, + { id: '3', source: 'npm-install', target: 'unit-tests' }, + { id: '4', source: 'npm-install', target: 'static-scan' }, + { id: '5', source: 'lint', target: 'build' }, + { id: '6', source: 'unit-tests', target: 'build' }, + { id: '7', source: 'static-scan', target: 'build' }, + { id: '8', source: 'build', target: 'integration-tests' }, + { id: '9', source: 'integration-tests', target: 'publish-images' }, + { id: '10', source: 'publish-images', target: 'publish-to-bucket' }, + { id: '11', source: 'publish-images', target: 'publish-to-bucket-latest' }, + { id: '12', source: 'publish-to-bucket', target: 'report-bucket' } + ]; +} + +export default function getExampleData({ height, type = 'card', width }) { + return { + edges: getEdgeData(), + nodes: getNodeData({ height, type, width }) + }; +} diff --git a/packages/graph/src/components/examples/example7_pipeline.json b/packages/graph/src/components/examples/example7_pipeline.json new file mode 100644 index 000000000..c9b5c6a33 --- /dev/null +++ b/packages/graph/src/components/examples/example7_pipeline.json @@ -0,0 +1,44 @@ +{ + "apiVersion": "tekton.dev/v1beta1", + "kind": "Pipeline", + "metadata": { + "name": "dag-example-from-docs" + }, + "spec": { + "tasks": [ + { + "name": "lint-repo", + "taskRef": { + "name": "pylint" + } + }, + { + "name": "test-app", + "taskRef": { + "name": "make-test" + } + }, + { + "name": "build-app", + "taskRef": { + "name": "kaniko-build-app" + }, + "runAfter": ["test-app"] + }, + { + "name": "build-frontend", + "taskRef": { + "name": "kaniko-build-frontend" + }, + "runAfter": ["test-app"] + }, + { + "name": "deploy-all", + "taskRef": { + "name": "deploy-kubectl" + }, + "runAfter": ["build-app", "build-frontend"] + } + ] + } +} \ No newline at end of file diff --git a/packages/graph/src/components/examples/example8.js b/packages/graph/src/components/examples/example8.js new file mode 100644 index 000000000..4fa929315 --- /dev/null +++ b/packages/graph/src/components/examples/example8.js @@ -0,0 +1,385 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* istanbul ignore file */ + +function getNodeData({ height, width }) { + return [ + { + id: '1', + title: '1', + status: 'success', + height, + width + }, + { + id: '2', + title: '2', + status: 'success', + height, + width + }, + { + id: '3', + title: '3', + status: 'success', + height, + width + }, + { + id: '4', + title: '4', + status: 'success', + height, + width + }, + { + id: '5', + title: '5', + status: 'success', + height, + width + }, + { + id: '6', + title: '6', + status: 'success', + height, + width + }, + { + id: '7', + title: '7', + status: 'success', + height, + width + }, + { + id: '8', + title: '8', + status: 'success', + height, + width + }, + { + id: '9', + title: '9', + status: 'success', + height, + width + }, + { + id: '10', + title: '10', + status: 'success', + height, + width + }, + { + id: '11', + title: '11', + status: 'success', + height, + width + }, + { + id: '12', + title: '12', + status: 'success', + height, + width + }, + { + id: '13', + title: '13', + status: 'success', + height, + width + }, + { + id: '14', + title: '14', + status: 'success', + height, + width + }, + { + id: '15', + title: '15', + status: 'success', + height, + width + }, + { + id: '16', + title: '16', + status: 'success', + height, + width + }, + { + id: '17', + title: '17', + status: 'success', + height, + width + }, + { + id: '18', + title: '18', + status: 'success', + height, + width + }, + { + id: '19', + title: '19', + status: 'success', + height, + width + }, + { + id: '20', + title: '20', + status: 'success', + height, + width + }, + { + id: '21', + title: '21', + status: 'success', + height, + width + }, + { + id: '22', + title: '22', + status: 'success', + height, + width + }, + { + id: '23', + title: '23', + status: 'success', + height, + width + }, + { + id: '24', + title: '24', + status: 'success', + height, + width + }, + { + id: '25', + title: '25', + status: 'success', + height, + width + }, + { + id: '26', + title: '26', + status: 'success', + height, + width + }, + { + id: '27', + title: '27', + status: 'success', + height, + width + }, + { + id: '28', + title: '28', + status: 'success', + height, + width + }, + { + id: '29', + title: '29', + status: 'success', + height, + width + }, + { + id: '30', + title: '30', + status: 'success', + height, + width + } + ]; +} + +function getEdgeData() { + return [ + { + id: '1-2', + source: '1', + target: '2' + }, + { + id: '1-3', + source: '1', + target: '3' + }, + { + id: '1-4', + source: '1', + target: '4' + }, + { + id: '1-5', + source: '1', + target: '5' + }, + { + id: '1-6', + source: '1', + target: '6' + }, + { + id: '1-7', + source: '1', + target: '7' + }, + { + id: '2-8', + source: '2', + target: '8' + }, + { + id: '2-9', + source: '2', + target: '9' + }, + { + id: '2-10', + source: '2', + target: '10' + }, + { + id: '2-11', + source: '2', + target: '11' + }, + { + id: '2-12', + source: '2', + target: '12' + }, + { + id: '2-13', + source: '2', + target: '13' + }, + { + id: '3-14', + source: '3', + target: '14' + }, + { + id: '3-15', + source: '3', + target: '15' + }, + { + id: '3-16', + source: '3', + target: '16' + }, + { + id: '3-17', + source: '3', + target: '17' + }, + { + id: '3-18', + source: '3', + target: '18' + }, + { + id: '3-19', + source: '3', + target: '19' + }, + { + id: '3-20', + source: '3', + target: '20' + }, + { + id: '10-21', + source: '10', + target: '21' + }, + { + id: '10-22', + source: '10', + target: '22' + }, + { + id: '10-23', + source: '10', + target: '23' + }, + { + id: '10-24', + source: '10', + target: '24' + }, + { + id: '10-25', + source: '10', + target: '25' + }, + { + id: '17-26', + source: '17', + target: '26' + }, + { + id: '17-27', + source: '17', + target: '27' + }, + { + id: '17-28', + source: '17', + target: '28' + }, + { + id: '17-29', + source: '17', + target: '29' + }, + { + id: '17-30', + source: '17', + target: '30' + } + ]; +} + +export default function getExampleData({ height, width }) { + return { + edges: getEdgeData(), + nodes: getNodeData({ height, width }) + }; +} diff --git a/packages/graph/src/components/examples/finally-pipeline.json b/packages/graph/src/components/examples/finally-pipeline.json new file mode 100644 index 000000000..2a8fd3e6d --- /dev/null +++ b/packages/graph/src/components/examples/finally-pipeline.json @@ -0,0 +1,75 @@ +{ + "apiVersion": "tekton.dev/v1beta1", + "kind": "Pipeline", + "metadata": { + "name": "clone-cleanup-workspace" + }, + "spec": { + "workspaces": [ + { + "name": "git-source" + } + ], + "tasks": [ + { + "name": "clone-app-repo", + "taskRef": { + "name": "git-clone-from-catalog" + }, + "params": [ + { + "name": "url", + "value": "https://github.com/tektoncd/community.git" + }, + { + "name": "subdirectory", + "value": "application" + } + ], + "workspaces": [ + { + "name": "output", + "workspace": "git-source" + } + ] + } + ], + "finally": [ + { + "name": "cleanup", + "taskRef": { + "name": "cleanup-workspace" + }, + "workspaces": [ + { + "name": "source", + "workspace": "git-source" + } + ] + }, + { + "name": "check-git-commit", + "params": [ + { + "name": "commit", + "value": "$(tasks.clone-app-repo.results.commit)" + } + ], + "taskSpec": { + "params": [ + { + "name": "commit" + } + ], + "steps": [ + { + "name": "check-commit-initialized", + "image": "alpine", + "script": "if [[ ! $(params.commit) ]]; then\n exit 1\nfi\n" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/packages/graph/src/components/examples/index.js b/packages/graph/src/components/examples/index.js new file mode 100644 index 000000000..812176d6e --- /dev/null +++ b/packages/graph/src/components/examples/index.js @@ -0,0 +1,21 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export { default as example1 } from './example1'; +export { default as example2 } from './example2'; +export { default as example3 } from './example3'; +export { default as example8 } from './example8'; +export { default as example7Pipeline } from './example7_pipeline.json'; +export { default as releasePipeline } from './release-pipeline.json'; +export { default as finallyPipeline } from './finally-pipeline.json'; +export { default as whenExpressionsPipelineRun } from './when-expressions-pipelinerun.json'; diff --git a/packages/graph/src/components/examples/release-pipeline.json b/packages/graph/src/components/examples/release-pipeline.json new file mode 100644 index 000000000..5c9e8582b --- /dev/null +++ b/packages/graph/src/components/examples/release-pipeline.json @@ -0,0 +1,432 @@ +{ + "apiVersion": "tekton.dev/v1beta1", + "kind": "Pipeline", + "metadata": { + "annotations": { + "managed-by": "Tekton" + }, + "creationTimestamp": "2021-02-09T13:44:00Z", + "generation": 1510, + "name": "pipeline-pipeline-release", + "namespace": "tekton-nightly", + "resourceVersion": "1323145068", + "uid": "e61738cc-a3c6-4eec-9b8c-938e6514e479" + }, + "spec": { + "params": [ + { + "default": "github.com/tektoncd/pipeline", + "description": "package to release", + "name": "package", + "type": "string" + }, + { + "description": "the git revision to release", + "name": "gitRevision", + "type": "string" + }, + { + "default": "gcr.io", + "description": "The target image registry", + "name": "imageRegistry", + "type": "string" + }, + { + "default": "tekton-releases", + "description": "The path (project) in the image registry", + "name": "imageRegistryPath", + "type": "string" + }, + { + "description": "The X.Y.Z version that the artifacts should be tagged with", + "name": "versionTag", + "type": "string" + }, + { + "default": "gs://tekton-releases-nightly/pipeline", + "description": "bucket where the release is stored. The bucket must be project specific.", + "name": "releaseBucket", + "type": "string" + }, + { + "default": "true", + "description": "Whether to tag and publish this release as Pipelines' latest", + "name": "releaseAsLatest", + "type": "string" + }, + { + "default": "linux/amd64,linux/arm,linux/arm64,linux/s390x,linux/ppc64le", + "description": "Platforms to build images for (e.g. linux/amd64,linux/arm64)", + "name": "buildPlatforms", + "type": "string" + }, + { + "default": "linux/amd64,linux/arm,linux/arm64,linux/s390x,linux/ppc64le,windows/amd64", + "description": "Platforms to publish images for (e.g. linux/amd64,linux/arm64,windows/amd64). This\ncan differ from buildPlatforms due to the fact that a windows-compatible base image\nis constructed for the publishing phase.\n", + "name": "publishPlatforms", + "type": "string" + }, + { + "description": "The path to the service account file within the release-secret workspace", + "name": "serviceAccountPath", + "type": "string" + } + ], + "results": [ + { + "description": "the sha of the commit that was released", + "name": "commit-sha", + "value": "$(tasks.git-clone.results.commit)" + }, + { + "description": "the URL of the release file", + "name": "release-file", + "value": "$(tasks.report-bucket.results.release)" + }, + { + "description": "the URL of the release file", + "name": "release-file-no-tag", + "value": "$(tasks.report-bucket.results.release-no-tag)" + } + ], + "tasks": [ + { + "name": "git-clone", + "params": [ + { + "name": "url", + "value": "https://$(params.package)" + }, + { + "name": "revision", + "value": "$(params.gitRevision)" + } + ], + "taskRef": { + "kind": "Task", + "name": "git-clone" + }, + "workspaces": [ + { + "name": "output", + "subPath": "git", + "workspace": "workarea" + } + ] + }, + { + "name": "precheck", + "params": [ + { + "name": "package", + "value": "$(params.package)" + }, + { + "name": "versionTag", + "value": "$(params.versionTag)" + }, + { + "name": "releaseBucket", + "value": "$(params.releaseBucket)" + } + ], + "runAfter": ["git-clone"], + "taskRef": { + "kind": "Task", + "name": "prerelease-checks" + }, + "workspaces": [ + { + "name": "source-to-release", + "subPath": "git", + "workspace": "workarea" + } + ] + }, + { + "name": "unit-tests", + "params": [ + { + "name": "package", + "value": "$(params.package)" + }, + { + "name": "flags", + "value": "-v -mod=vendor" + } + ], + "runAfter": ["precheck"], + "taskRef": { + "kind": "Task", + "name": "golang-test" + }, + "workspaces": [ + { + "name": "source", + "subPath": "git", + "workspace": "workarea" + } + ] + }, + { + "name": "build", + "params": [ + { + "name": "package", + "value": "$(params.package)" + }, + { + "name": "packages", + "value": "./cmd/..." + } + ], + "runAfter": ["precheck"], + "taskRef": { + "kind": "Task", + "name": "golang-build" + }, + "workspaces": [ + { + "name": "source", + "subPath": "git", + "workspace": "workarea" + } + ] + }, + { + "name": "build-base-image", + "params": [ + { + "name": "package", + "value": "$(params.package)" + }, + { + "name": "imageRegistry", + "value": "$(params.imageRegistry)" + }, + { + "name": "imageRegistryPath", + "value": "$(params.imageRegistryPath)" + }, + { + "name": "platforms", + "value": "$(params.buildPlatforms)" + }, + { + "name": "serviceAccountPath", + "value": "$(params.serviceAccountPath)" + } + ], + "runAfter": ["build", "unit-tests"], + "taskRef": { + "kind": "Task", + "name": "pipeline-build-multiarch-base-image" + }, + "workspaces": [ + { + "name": "source", + "subPath": "git", + "workspace": "workarea" + }, + { + "name": "release-secret", + "workspace": "release-secret" + } + ] + }, + { + "name": "publish-images", + "params": [ + { + "name": "package", + "value": "$(params.package)" + }, + { + "name": "versionTag", + "value": "$(params.versionTag)" + }, + { + "name": "imageRegistry", + "value": "$(params.imageRegistry)" + }, + { + "name": "imageRegistryPath", + "value": "$(params.imageRegistryPath)" + }, + { + "name": "releaseAsLatest", + "value": "$(params.releaseAsLatest)" + }, + { + "name": "serviceAccountPath", + "value": "$(params.serviceAccountPath)" + }, + { + "name": "platforms", + "value": "$(params.publishPlatforms)" + } + ], + "runAfter": ["build-base-image"], + "taskRef": { + "kind": "Task", + "name": "pipeline-publish-release" + }, + "workspaces": [ + { + "name": "source", + "subPath": "git", + "workspace": "workarea" + }, + { + "name": "output", + "subPath": "bucket", + "workspace": "workarea" + }, + { + "name": "release-secret", + "workspace": "release-secret" + } + ] + }, + { + "name": "publish-to-bucket", + "params": [ + { + "name": "location", + "value": "$(params.releaseBucket)/previous/$(params.versionTag)" + }, + { + "name": "path", + "value": "$(params.versionTag)" + }, + { + "name": "serviceAccountPath", + "value": "$(params.serviceAccountPath)" + } + ], + "runAfter": ["publish-images"], + "taskRef": { + "kind": "Task", + "name": "gcs-upload" + }, + "workspaces": [ + { + "name": "credentials", + "workspace": "release-secret" + }, + { + "name": "source", + "subPath": "bucket", + "workspace": "workarea" + } + ] + }, + { + "name": "publish-to-bucket-latest", + "params": [ + { + "name": "location", + "value": "$(params.releaseBucket)/latest" + }, + { + "name": "path", + "value": "$(params.versionTag)" + }, + { + "name": "serviceAccountPath", + "value": "$(params.serviceAccountPath)" + } + ], + "runAfter": ["publish-images"], + "taskRef": { + "kind": "Task", + "name": "gcs-upload" + }, + "when": [ + { + "input": "$(params.releaseAsLatest)", + "operator": "in", + "values": ["true"] + } + ], + "workspaces": [ + { + "name": "credentials", + "workspace": "release-secret" + }, + { + "name": "source", + "subPath": "bucket", + "workspace": "workarea" + } + ] + }, + { + "name": "report-bucket", + "params": [ + { + "name": "releaseBucket", + "value": "$(params.releaseBucket)" + }, + { + "name": "versionTag", + "value": "$(params.versionTag)" + } + ], + "runAfter": ["publish-to-bucket"], + "taskSpec": { + "metadata": {}, + "params": [ + { + "name": "releaseBucket", + "type": "string" + }, + { + "name": "versionTag", + "type": "string" + } + ], + "results": [ + { + "description": "The full URL of the release file in the bucket", + "name": "release" + }, + { + "description": "The full URL of the release file (no tag) in the bucket", + "name": "release-no-tag" + } + ], + "spec": null, + "steps": [ + { + "env": [ + { + "name": "RELEASE_BUCKET", + "value": "$(params.releaseBucket)" + }, + { + "name": "VERSION_TAG", + "value": "$(params.versionTag)" + } + ], + "image": "alpine", + "name": "create-results", + "resources": {}, + "script": "BASE_URL=$(echo \"${RELEASE_BUCKET}/previous/${VERSION_TAG}\")\n# If the bucket is in the gs:// return the corresponding public https URL\nBASE_URL=$(echo ${BASE_URL} | sed 's,gs://,https://storage.googleapis.com/,g')\necho \"${BASE_URL}/release.yaml\" > $(results.release.path)\necho \"${BASE_URL}/release.notag.yaml\" > $(results.release-no-tag.path)\n" + } + ] + } + } + ], + "workspaces": [ + { + "description": "The workspace where the repo will be cloned.", + "name": "workarea" + }, + { + "description": "The secret that contains a service account authorized to push to the imageRegistry and to the output bucket", + "name": "release-secret" + } + ] + } +} \ No newline at end of file diff --git a/packages/graph/src/components/examples/when-expressions-pipelinerun.json b/packages/graph/src/components/examples/when-expressions-pipelinerun.json new file mode 100644 index 000000000..907ae02ac --- /dev/null +++ b/packages/graph/src/components/examples/when-expressions-pipelinerun.json @@ -0,0 +1,376 @@ +{ + "apiVersion": "tekton.dev/v1beta1", + "kind": "PipelineRun", + "metadata": { + "generateName": "guarded-pr-" + }, + "spec": { + "serviceAccountName": "default", + "pipelineSpec": { + "params": [ + { + "name": "path", + "type": "string", + "description": "The path of the file to be created" + }, + { + "name": "branches", + "type": "array", + "description": "The list of branch names" + } + ], + "workspaces": [ + { + "name": "source", + "description": "This workspace is shared among all the pipeline tasks to read/write common resources\n" + } + ], + "tasks": [ + { + "name": "create-file", + "when": [ + { + "input": "$(params.path)", + "operator": "in", + "values": ["README.md"] + } + ], + "workspaces": [ + { + "name": "source", + "workspace": "source" + } + ], + "taskSpec": { + "workspaces": [ + { + "name": "source", + "description": "The workspace to create the readme file in" + } + ], + "steps": [ + { + "name": "write-new-stuff", + "image": "ubuntu", + "script": "touch $(workspaces.source.path)/README.md" + } + ] + } + }, + { + "name": "check-file", + "params": [ + { + "name": "path", + "value": "$(params.path)" + } + ], + "workspaces": [ + { + "name": "source", + "workspace": "source" + } + ], + "runAfter": ["create-file"], + "taskSpec": { + "params": [ + { + "name": "path" + } + ], + "workspaces": [ + { + "name": "source", + "description": "The workspace to check for the file" + } + ], + "results": [ + { + "name": "exists", + "description": "indicates whether the file exists or is missing" + } + ], + "steps": [ + { + "name": "check-file", + "image": "alpine", + "script": "if test -f $(workspaces.source.path)/$(params.path); then\n printf yes | tee /tekton/results/exists\nelse\n printf no | tee /tekton/results/exists\nfi\n" + } + ] + } + }, + { + "name": "echo-file-exists", + "when": [ + { + "input": "$(tasks.check-file.results.exists)", + "operator": "in", + "values": ["yes"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "echo file exists" + } + ] + } + }, + { + "name": "sample-task-with-array-values", + "when": [ + { + "input": "main", + "operator": "in", + "values": ["$(params.branches[*])"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "alpine", + "script": "echo hello" + } + ] + } + }, + { + "name": "task-should-be-skipped-1", + "when": [ + { + "input": "$(tasks.check-file.results.exists)", + "operator": "in", + "values": ["missing"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "task-should-be-skipped-2", + "when": [ + { + "input": "$(params.path)", + "operator": "notin", + "values": ["README.md"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "task-should-be-skipped-3", + "runAfter": ["echo-file-exists"], + "when": [ + { + "input": "monday", + "operator": "in", + "values": ["friday"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "task-should-be-skipped-4", + "when": [ + { + "input": "master", + "operator": "in", + "values": ["$(params.branches[*])"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "alpine", + "script": "exit 1" + } + ] + } + } + ], + "finally": [ + { + "name": "finally-task-should-be-skipped-1", + "when": [ + { + "input": "$(tasks.echo-file-exists.status)", + "operator": "in", + "values": ["Failure"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "finally-task-should-be-skipped-2", + "when": [ + { + "input": "$(tasks.check-file.results.exists)", + "operator": "in", + "values": ["missing"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "finally-task-should-be-skipped-3", + "when": [ + { + "input": "$(params.path)", + "operator": "notin", + "values": ["README.md"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "finally-task-should-be-skipped-4", + "when": [ + { + "input": "$(tasks.status)", + "operator": "in", + "values": ["Failure"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "finally-task-should-be-skipped-5", + "when": [ + { + "input": "$(tasks.status)", + "operator": "in", + "values": ["Succeeded"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "exit 1" + } + ] + } + }, + { + "name": "finally-task-should-be-executed", + "when": [ + { + "input": "$(tasks.echo-file-exists.status)", + "operator": "in", + "values": ["Succeeded"] + }, + { + "input": "$(tasks.status)", + "operator": "in", + "values": ["Completed"] + }, + { + "input": "$(tasks.check-file.results.exists)", + "operator": "in", + "values": ["yes"] + }, + { + "input": "$(params.path)", + "operator": "in", + "values": ["README.md"] + } + ], + "taskSpec": { + "steps": [ + { + "name": "echo", + "image": "ubuntu", + "script": "echo finally done" + } + ] + } + } + ] + }, + "params": [ + { + "name": "path", + "value": "README.md" + }, + { + "name": "branches", + "value": ["main", "hotfix"] + } + ], + "workspaces": [ + { + "name": "source", + "volumeClaimTemplate": { + "spec": { + "accessModes": ["ReadWriteOnce"], + "resources": { + "requests": { + "storage": "16Mi" + } + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/packages/graph/src/constants.js b/packages/graph/src/constants.js new file mode 100644 index 000000000..01671fa95 --- /dev/null +++ b/packages/graph/src/constants.js @@ -0,0 +1,16 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export const cardHeight = 50; +export const cardWidth = 250; +export const shapeSize = 24; diff --git a/packages/graph/src/newGraph.js b/packages/graph/src/newGraph.js new file mode 100644 index 000000000..4a7f1db7a --- /dev/null +++ b/packages/graph/src/newGraph.js @@ -0,0 +1,153 @@ +/* +Copyright 2022-2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +/* istanbul ignore file */ + +import { cardHeight, cardWidth, shapeSize } from './constants'; + +function addEdge({ edges, source, target }) { + edges.push({ + id: `${source}::${target}`, + source, + target + }); +} + +// https://tekton.dev/docs/pipelines/pipelines/#configuring-the-task-execution-order +export function getDAG({ pipeline, pipelineRun, trigger }) { + // TODO: handle taskRun + pipelineRun status + const edges = []; + const nodes = []; + const sourceNodes = []; + let sinkNodes = new Set(pipeline.spec.tasks.map(({ name }) => name)); + + pipeline.spec.tasks.forEach((task, _index) => { + nodes.push({ + id: task.name, + status: pipelineRun ? 'success' : 'unknown', + title: task.name, + height: cardHeight, + width: cardWidth + }); + + const taskDependencies = new Set(); + task.runAfter?.forEach(runAfter => { + taskDependencies.add(runAfter); + }); + + /* + TODO: + - params + - results + - when expressions + - results + + see https://github.com/tektoncd/pipeline/blob/a343e755e4ea8f46050f19862fe3a5c788ad604a/pkg/apis/pipeline/v1beta1/pipeline_types.go#L420 + */ + + if (taskDependencies.size === 0) { + sourceNodes.push(task.name); + } else { + Array.from(taskDependencies).forEach(dependency => { + sinkNodes.delete(dependency); + addEdge({ + edges, + source: dependency, + target: task.name + }); + }); + } + }); + + sinkNodes = Array.from(sinkNodes).map(name => ({ name })); + + const triggerNodeID = 'tkn-graph--trigger'; + const pipelineTasksEndNodeID = 'tkn-graph--pipelineTasksEnd'; + const endNodeID = 'tkn-graph--end'; + + nodes.push({ + id: triggerNodeID, + title: 'Trigger info TBD', + height: shapeSize, + width: shapeSize, + status: trigger?.type || 'dummy', + type: 'icon' + }); + + sourceNodes.forEach(sourceNode => { + addEdge({ + edges, + source: triggerNodeID, + target: sourceNode + }); + }); + + const finallyTasks = pipeline.spec.finally || []; + + if (finallyTasks.length) { + nodes.push({ + id: pipelineTasksEndNodeID, + title: '?', + height: shapeSize, + width: shapeSize, + status: 'dummy', + type: 'icon' + }); + + sinkNodes.forEach(({ name }) => { + addEdge({ + edges, + source: name, + target: pipelineTasksEndNodeID + }); + }); + + finallyTasks.forEach(({ name }) => { + nodes.push({ + id: name, + status: pipelineRun ? 'success' : null, + title: name, + height: cardHeight, + width: cardWidth + }); + + addEdge({ + edges, + source: pipelineTasksEndNodeID, + target: name + }); + }); + } + + // TODO: only include end node for runs? + nodes.push({ + id: endNodeID, + title: 'Status TBD', + height: shapeSize, + width: shapeSize, + status: pipelineRun ? 'success' : null, + type: 'icon' + }); + + (finallyTasks.length ? finallyTasks : sinkNodes).forEach(({ name }) => { + addEdge({ + edges, + source: name, + target: endNodeID + }); + }); + + return { + nodes, + edges + }; +}