├── src ├── assets │ ├── styles │ │ ├── scss │ │ │ ├── main.scss │ │ │ ├── variables.scss │ │ │ └── PipelineGraphWidget.scss │ │ └── css │ │ │ └── main.css │ ├── inc.png │ └── logo.png ├── components │ ├── index.js │ ├── PipelineGraphModel.js │ ├── support │ │ ├── StatusIcons.js │ │ ├── SvgSpinner.vue │ │ ├── SvgBlock.vue │ │ ├── SvgStatus.vue │ │ └── TruncatingLabel.vue │ ├── HelloWorld.vue │ ├── PipelineGraphLayout.js │ └── PipelineGraph.vue ├── main.js └── App.vue ├── public ├── favicon.ico └── index.html ├── babel.config.js ├── .editorconfig ├── .postcssrc.js ├── .gitignore ├── intelij.webpack.js ├── .eslintrc.js ├── vue.config.js ├── README.md └── package.json /src/assets/styles/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'PipelineGraphWidget'; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worry127722/jenkins-pipeline-graph-vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/inc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worry127722/jenkins-pipeline-graph-vue/HEAD/src/assets/inc.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worry127722/jenkins-pipeline-graph-vue/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import JenkinsPipelineGraphVue from './PipelineGraph'; 2 | export default JenkinsPipelineGraphVue; 3 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | autoprefixer: {} 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/assets/styles/scss/variables.scss: -------------------------------------------------------------------------------- 1 | // Theme colours 2 | 3 | $selection-highlight: #4a90e2; 4 | $status-success: #8cc04f; 5 | $status-unstable: #f6b44b; 6 | $status-failure: #d54c53; 7 | $status-paused: #24b0d5; 8 | $status-unknown: #d54cc4; 9 | 10 | $graph-connector-grey: #949393; 11 | $progress-bg: #a7c7f2; 12 | $progress-bar-color: #1d7dcf; 13 | -------------------------------------------------------------------------------- /intelij.webpack.js: -------------------------------------------------------------------------------- 1 | // This configuration file is not used anywhere in the code, it's a hack to handle InteliJ relative path imports 2 | // Keep in sync with actual webpack aliases 3 | 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | resolve: { 8 | alias: { 9 | '@': path.resolve(__dirname, 'src') 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['plugin:vue/essential'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'off' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'off' : 'off' 10 | }, 11 | parserOptions: { 12 | parser: 'babel-eslint' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function resolveSrc(_path) { 4 | return path.join(__dirname, _path); 5 | } 6 | 7 | // vue.config.js 8 | module.exports = { 9 | lintOnSave: true, 10 | configureWebpack: { 11 | // Set up all the aliases we use in our app. 12 | resolve: { 13 | alias: { 14 | assets: resolveSrc('src/assets') 15 | } 16 | } 17 | }, 18 | 19 | css: { 20 | // Enable CSS source maps. 21 | sourceMap: process.env.NODE_ENV !== 'production' 22 | }, 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins-pipeline-graph-vue 2 | Jenkins-ci pipeline for vue.js 3 | 4 | 介绍:vue版本的 Jenkins-ci pipeline,reactjs版本地址[(链接)](https://github.com/jenkinsci/ux-widget-framework/tree/master/packages/pipeline-graph) 5 | 6 | ## 效果图: 7 | ![avatar](/src/assets/inc.png) 8 | 9 | ## 安装: 10 | ``` 11 | npm i jenkins-pipeline-graph-vue 12 | ``` 13 | 14 | ## 使用: 15 | ``` 16 | import JenkinsPipelineGraphVue from "jenkins-pipeline-graph-vue"; 17 | 18 | 24 | ``` 25 | 26 | ## 参数: 27 | layout:样式 28 | 29 | stages:节点信息 30 | 31 | onNodeClick:点击节点时调用,返回两个参数:displayName 、 id 32 | 33 | selectedStage:初始化选中节点 34 | 35 | ## 实例: 36 | ``` 37 | src/components/HelloWorld.vue 38 | ``` 39 | -------------------------------------------------------------------------------- /src/components/PipelineGraphModel.js: -------------------------------------------------------------------------------- 1 | export var Result; 2 | (function (Result) { 3 | Result["success"] = "success"; 4 | Result["failure"] = "failure"; 5 | Result["running"] = "running"; 6 | Result["queued"] = "queued"; 7 | Result["paused"] = "paused"; 8 | Result["unstable"] = "unstable"; 9 | Result["aborted"] = "aborted"; 10 | Result["not_built"] = "not_built"; 11 | Result["skipped"] = "skipped"; 12 | Result["unknown"] = "unknown"; 13 | })(Result || (Result = {})); 14 | 15 | export function decodeResultValue(resultMaybe) { 16 | const lcase = String(resultMaybe).toLowerCase(); 17 | for (const enumKey of Object.keys(Result)) { 18 | const enumValue = Result[enumKey]; 19 | if (enumKey.toLowerCase() === lcase || enumValue.toLowerCase() === lcase) { 20 | return enumValue; 21 | } 22 | } 23 | return Result.unknown; 24 | } 25 | 26 | // Dimensions used for layout, px 27 | export const defaultLayout = { 28 | nodeSpacingH: 120, 29 | parallelSpacingH: 120, 30 | nodeSpacingV: 70, 31 | nodeRadius: 12, 32 | terminalRadius: 7, 33 | curveRadius: 12, 34 | connectorStrokeWidth: 3.5, 35 | labelOffsetV: 20, 36 | smallLabelOffsetV: 15, 37 | ypStart: 55, 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/support/StatusIcons.js: -------------------------------------------------------------------------------- 1 | import {Result} from '../PipelineGraphModel'; 2 | import SvgSpinner from './SvgSpinner'; 3 | import SvgStatus from './SvgStatus'; 4 | import SvgBlock from './SvgBlock'; 5 | 6 | export const nodeStrokeWidth = 3.5; // px. 7 | // Returns the correct element for the result / progress percent. 8 | export function getGroupForResult(result, percentage, radius, createElement) { 9 | switch (result) { 10 | case Result.running: 11 | case Result.queued: 12 | return createElement(SvgSpinner, {props: {radius: radius, result: result, percentage: percentage}}); 13 | case Result.not_built: 14 | case Result.skipped: 15 | case Result.success: 16 | case Result.failure: 17 | case Result.paused: 18 | case Result.unstable: 19 | case Result.aborted: 20 | case Result.unknown: 21 | return createElement(SvgStatus, {props: {radius: radius, result: result}}); 22 | default: 23 | badResult(result); 24 | return createElement(SvgStatus, {props: {radius: radius, result: Result.unknown}}); 25 | } 26 | } 27 | 28 | export function getBlockTarget(node, createElement) { 29 | return createElement(SvgBlock, {props: {node: node}}); 30 | } 31 | 32 | function badResult(x) { 33 | console.error('Unexpected Result value', x); 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jenkins-pipeline-graph-vue", 3 | "author": "Wang Rui", 4 | "version": "1.0.8", 5 | "description": "Jenkins-ci pipeline for vue.js", 6 | "main": "src/components/index.js", 7 | "scripts": { 8 | "serve": "vue-cli-service serve", 9 | "build": "vue-cli-service build", 10 | "lint": "vue-cli-service lint", 11 | "dev": "npm run serve" 12 | }, 13 | "dependencies": { 14 | "core-js": "~3.6.4", 15 | "vue": "~2.6.11" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "~4.2.0", 19 | "@vue/cli-plugin-eslint": "~4.2.0", 20 | "@vue/cli-service": "~4.2.0", 21 | "babel-eslint": "~10.0.3", 22 | "eslint": "~6.7.2", 23 | "eslint-plugin-vue": "~6.1.2", 24 | "node-sass": "~4.12.0", 25 | "sass-loader": "~7.1.0", 26 | "vue-template-compiler": "~2.6.11" 27 | }, 28 | "eslintConfig": { 29 | "root": true, 30 | "env": { 31 | "node": true 32 | }, 33 | "extends": [ 34 | "plugin:vue/essential", 35 | "eslint:recommended" 36 | ], 37 | "parserOptions": { 38 | "parser": "babel-eslint" 39 | }, 40 | "rules": {} 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/worry127722/jenkins-pipeline-graph-vue.git" 45 | }, 46 | "browserslist": [ 47 | "> 1%", 48 | "last 2 versions" 49 | ], 50 | "keywords": [ 51 | "jenkins", 52 | "pipeline", 53 | "vue" 54 | ], 55 | "license": "ISC", 56 | "bugs": { 57 | "url": "https://github.com/worry127722/pipeline-graph/issues" 58 | }, 59 | "homepage": "https://github.com/worry127722/pipeline-graph#readme" 60 | } 61 | -------------------------------------------------------------------------------- /src/assets/styles/css/main.css: -------------------------------------------------------------------------------- 1 | .PWGx-PipelineGraph-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | overflow-x: auto; 6 | margin-bottom: 16px; } 7 | .PWGx-PipelineGraph-container * { 8 | box-sizing: border-box; } 9 | .PWGx-PipelineGraph-container .PWGx-PipelineGraph { 10 | margin-left: auto; 11 | margin-right: auto; } 12 | 13 | circle.halo { 14 | stroke: white; 15 | fill: none; } 16 | 17 | .PWGx-svgResultStatusOutline { 18 | stroke: #949393; 19 | fill: none; } 20 | 21 | .PWGx-result-status-glyph { 22 | stroke: none; 23 | fill: #fff; } 24 | 25 | .PWGx-svgResultStatusSolid { 26 | transform: translateZ(0); } 27 | .PWGx-svgResultStatusSolid > circle.statusColor.success { 28 | fill: #8cc04f; } 29 | .PWGx-svgResultStatusSolid > circle.statusColor.failure { 30 | fill: #d54c53; } 31 | .PWGx-svgResultStatusSolid > circle.statusColor.unstable { 32 | fill: #f6b44b; } 33 | .PWGx-svgResultStatusSolid > circle.statusColor.aborted { 34 | fill: #949393; } 35 | .PWGx-svgResultStatusSolid > circle.statusColor.paused { 36 | fill: #24b0d5; } 37 | .PWGx-svgResultStatusSolid > circle.statusColor.unknown { 38 | fill: #d54cc4; } 39 | .pipeline-node-selected .PWGx-svgResultStatusSolid > circle.statusColor { 40 | stroke: none; } 41 | 42 | .PWGx-progress-spinner.running circle.statusColor { 43 | fill: none; 44 | stroke: #a7c7f2; } 45 | 46 | .PWGx-progress-spinner.running path { 47 | fill: none; 48 | stroke: #1d7dcf; } 49 | 50 | .PWGx-progress-spinner.pc-over-100 circle.statusColor { 51 | fill: none; 52 | stroke: #1d7dcf; } 53 | 54 | .PWGx-progress-spinner.pc-over-100 path { 55 | fill: none; 56 | stroke: #f6b44b; } 57 | 58 | .PWGx-progress-spinner.running.spin { 59 | animation: progress-spinner-rotate 4s linear; 60 | animation-iteration-count: infinite; } 61 | 62 | @keyframes progress-spinner-rotate { 63 | 0% { 64 | transform: rotate(0deg); } 65 | 100% { 66 | transform: rotate(360deg); } } 67 | 68 | .PWGx-progress-spinner circle.inner, 69 | .PWGx-progress-spinner.running.spin circle.inner { 70 | display: none; 71 | animation: progress-spinner-pulsate 1.2s ease-out; 72 | animation-iteration-count: infinite; 73 | opacity: 0; } 74 | 75 | .PWGx-progress-spinner.running circle.inner { 76 | display: block; 77 | fill: #1d7dcf; 78 | stroke: #1d7dcf; } 79 | 80 | @keyframes progress-spinner-pulsate { 81 | 0% { 82 | transform: scale(0.1, 0.1); 83 | opacity: 0; } 84 | 50% { 85 | opacity: 1; } 86 | 100% { 87 | transform: scale(1.2, 1.2); 88 | opacity: 0; } } 89 | 90 | .PWGx-progress-spinner.queued circle.statusColor { 91 | fill: none; 92 | stroke: #949393; } 93 | 94 | .PWGx-progress-spinner.queued circle.statusColor.inner { 95 | display: block; 96 | fill: #949393; 97 | stroke: #949393; } 98 | 99 | .PWGx-progress-spinner.queued path { 100 | fill: none; 101 | stroke: none; } 102 | 103 | .PWGx-pipeline-connector { 104 | stroke: #949393; } 105 | 106 | .PWGx-pipeline-node-terminal { 107 | fill: #949393; } 108 | 109 | .PWGx-pipeline-connector-skipped { 110 | stroke: #949393; 111 | stroke-opacity: 0.25; } 112 | 113 | .PWGx-pipeline-small-label { 114 | font-size: 80%; } 115 | 116 | .PWGx-pipeline-big-label.selected { 117 | font-weight: bold; } 118 | 119 | .PWGx-pipeline-small-label.selected { 120 | font-weight: bold; 121 | margin-top: 3px; } 122 | 123 | .PWGx-pipeline-selection-highlight circle { 124 | fill: none; 125 | stroke: #4a90e2; } 126 | -------------------------------------------------------------------------------- /src/assets/styles/scss/PipelineGraphWidget.scss: -------------------------------------------------------------------------------- 1 | // TODO: Remove manual PWGx- namespacing, replace with tooling 2 | 3 | .PWGx-PipelineGraph-container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | overflow-x: auto; 8 | margin-bottom: 16px; 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | .PWGx-PipelineGraph { 15 | margin-left: auto; 16 | margin-right: auto; 17 | } 18 | } 19 | 20 | circle.halo { 21 | stroke: white; 22 | fill: none; 23 | } 24 | 25 | .PWGx-svgResultStatusOutline { 26 | stroke: $graph-connector-grey; 27 | fill: none; 28 | } 29 | 30 | .PWGx-result-status-glyph { 31 | stroke: none; 32 | fill: #fff; 33 | } 34 | 35 | .PWGx-svgResultStatusSolid { 36 | transform: translateZ(0); 37 | 38 | > circle.statusColor { 39 | &.success { 40 | fill: $status-success; 41 | } 42 | &.failure { 43 | fill: $status-failure; 44 | } 45 | &.unstable { 46 | fill: $status-unstable; 47 | } 48 | &.aborted { 49 | fill: $graph-connector-grey; 50 | } 51 | &.paused { 52 | fill: $status-paused; 53 | } 54 | &.unknown { 55 | fill: $status-unknown; 56 | } 57 | 58 | .pipeline-node-selected & { 59 | stroke: none; 60 | } 61 | } 62 | } 63 | 64 | .PWGx-progress-spinner.running circle.statusColor { 65 | fill: none; 66 | stroke: $progress-bg; 67 | } 68 | 69 | .PWGx-progress-spinner.running path { 70 | fill: none; 71 | stroke: $progress-bar-color; 72 | } 73 | 74 | .PWGx-progress-spinner.pc-over-100 circle.statusColor { 75 | fill: none; 76 | stroke: $progress-bar-color; 77 | } 78 | 79 | .PWGx-progress-spinner.pc-over-100 path { 80 | fill: none; 81 | stroke: $status-unstable; 82 | } 83 | 84 | .PWGx-progress-spinner.running.spin { 85 | animation: progress-spinner-rotate 4s linear; 86 | animation-iteration-count: infinite; 87 | } 88 | 89 | @keyframes progress-spinner-rotate { 90 | 0% { 91 | transform: rotate(0deg); 92 | } 93 | 100% { 94 | transform: rotate(360deg); 95 | } 96 | } 97 | 98 | .PWGx-progress-spinner circle.inner, 99 | .PWGx-progress-spinner.running.spin circle.inner { 100 | display: none; 101 | animation: progress-spinner-pulsate 1.2s ease-out; 102 | animation-iteration-count: infinite; 103 | opacity: 0; 104 | } 105 | 106 | .PWGx-progress-spinner.running circle.inner { 107 | display: block; 108 | fill: $progress-bar-color; 109 | stroke: $progress-bar-color; 110 | } 111 | 112 | @keyframes progress-spinner-pulsate { 113 | 0% { 114 | transform: scale(0.1, 0.1); 115 | opacity: 0; 116 | } 117 | 50% { 118 | opacity: 1; 119 | } 120 | 100% { 121 | transform: scale(1.2, 1.2); 122 | opacity: 0; 123 | } 124 | } 125 | 126 | .PWGx-progress-spinner.queued circle.statusColor { 127 | fill: none; 128 | stroke: $graph-connector-grey; 129 | } 130 | 131 | .PWGx-progress-spinner.queued circle.statusColor.inner { 132 | display: block; 133 | fill: $graph-connector-grey; 134 | stroke: $graph-connector-grey; 135 | } 136 | 137 | .PWGx-progress-spinner.queued path { 138 | fill: none; 139 | stroke: none; 140 | } 141 | 142 | .PWGx-pipeline-connector { 143 | stroke: $graph-connector-grey; 144 | } 145 | 146 | .PWGx-pipeline-node-terminal { 147 | fill: $graph-connector-grey; 148 | } 149 | 150 | .PWGx-pipeline-connector-skipped { 151 | stroke: $graph-connector-grey; 152 | stroke-opacity: 0.25; 153 | } 154 | 155 | .PWGx-pipeline-small-label { 156 | font-size: 80%; 157 | } 158 | 159 | .PWGx-pipeline-big-label.selected { 160 | font-weight: bold; 161 | } 162 | 163 | .PWGx-pipeline-small-label.selected { 164 | font-weight: bold; 165 | margin-top: 3px; 166 | } 167 | 168 | .PWGx-pipeline-selection-highlight circle { 169 | fill: none; 170 | stroke: $selection-highlight; 171 | } 172 | -------------------------------------------------------------------------------- /src/components/support/SvgSpinner.vue: -------------------------------------------------------------------------------- 1 | 94 | 97 | -------------------------------------------------------------------------------- /src/components/support/SvgBlock.vue: -------------------------------------------------------------------------------- 1 | 105 | 108 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 151 | 152 | 153 | 173 | -------------------------------------------------------------------------------- /src/components/support/SvgStatus.vue: -------------------------------------------------------------------------------- 1 | 109 | 112 | -------------------------------------------------------------------------------- /src/components/support/TruncatingLabel.vue: -------------------------------------------------------------------------------- 1 | 156 | 159 | -------------------------------------------------------------------------------- /src/components/PipelineGraphLayout.js: -------------------------------------------------------------------------------- 1 | export const sequentialStagesLabelOffset = 70; 2 | 3 | /** 4 | * Main process for laying out the graph. Creates and positions markers for each component, but creates no components. 5 | * 6 | * 1. Creates nodes for each stage in the pipeline 7 | * 2. Position the nodes in columns for each top stage, and in rows within each column based on execution order 8 | * 3. Create all the connections between nodes that need to be rendered 9 | * 4. Create a bigLabel per column, and a smallLabel for any child nodes 10 | * 5. Measure the extents of the graph 11 | */ 12 | export function layoutGraph(newStages, layout) { 13 | const stageNodeColumns = createNodeColumns(newStages); 14 | const {nodeSpacingH, ypStart} = layout; 15 | const startNode = { 16 | x: 0, 17 | y: 0, 18 | name: 'Start', 19 | id: -1, 20 | isPlaceholder: true, 21 | key: 'start-node', 22 | type: 'start', 23 | }; 24 | const endNode = { 25 | x: 0, 26 | y: 0, 27 | name: 'End', 28 | id: -2, 29 | isPlaceholder: true, 30 | key: 'end-node', 31 | type: 'end', 32 | }; 33 | const allNodeColumns = [ 34 | {rows: [[startNode]], centerX: 0, hasBranchLabels: false, startX: 0}, 35 | ...stageNodeColumns, 36 | {rows: [[endNode]], centerX: 0, hasBranchLabels: false, startX: 0}, 37 | ]; 38 | positionNodes(allNodeColumns, layout); 39 | const bigLabels = createBigLabels(allNodeColumns); 40 | const smallLabels = createSmallLabels(allNodeColumns); 41 | const branchLabels = createBranchLabels(allNodeColumns); 42 | const connections = createConnections(allNodeColumns); 43 | // Calculate the size of the graph 44 | let measuredWidth = 0; 45 | let measuredHeight = 200; 46 | for (const column of allNodeColumns) { 47 | for (const row of column.rows) { 48 | for (const node of row) { 49 | measuredWidth = Math.max(measuredWidth, node.x + nodeSpacingH / 2); 50 | measuredHeight = Math.max(measuredHeight, node.y + ypStart); 51 | } 52 | } 53 | } 54 | return { 55 | nodeColumns: allNodeColumns, 56 | connections, 57 | bigLabels, 58 | smallLabels, 59 | branchLabels, 60 | measuredWidth, 61 | measuredHeight, 62 | }; 63 | } 64 | 65 | /** 66 | * Generate an array of columns, based on the top-level stages 67 | */ 68 | function createNodeColumns(topLevelStages = []) { 69 | const nodeColumns = []; 70 | for (const topStage of topLevelStages) { 71 | // If stage has children, we don't draw a node for it, just its children 72 | const stagesForColumn = topStage.children && topStage.children.length ? topStage.children : [topStage]; 73 | const column = { 74 | topStage, 75 | rows: [], 76 | centerX: 0, 77 | startX: 0, 78 | hasBranchLabels: false, 79 | }; 80 | for (const firstStageForRow of stagesForColumn) { 81 | const rowNodes = []; 82 | let nodeStage = firstStageForRow; 83 | while (nodeStage) { 84 | if (nodeStage.seqContainerName) { 85 | column.hasBranchLabels = true; 86 | } 87 | rowNodes.push({ 88 | x: 0, 89 | y: 0, 90 | name: nodeStage.name, 91 | id: nodeStage.id, 92 | stage: nodeStage, 93 | isPlaceholder: false, 94 | key: 'n_' + nodeStage.id, 95 | }); 96 | nodeStage = nodeStage.nextSibling; 97 | } 98 | column.rows.push(rowNodes); 99 | } 100 | nodeColumns.push(column); 101 | } 102 | return nodeColumns; 103 | } 104 | 105 | /** 106 | * Walks the columns of nodes giving them x and y positions. Mutates the node objects in place for now. 107 | */ 108 | function positionNodes(nodeColumns, {nodeSpacingH, parallelSpacingH, nodeSpacingV, ypStart}) { 109 | let xp = nodeSpacingH / 2; 110 | let previousTopNode; 111 | for (const column of nodeColumns) { 112 | const topNode = column.rows[0][0]; 113 | let yp = ypStart; // Reset Y to top for each column 114 | if (previousTopNode) { 115 | // Advance X position 116 | if (previousTopNode.isPlaceholder || topNode.isPlaceholder) { 117 | // Don't space placeholder nodes (start/end) as wide as normal. 118 | xp += Math.floor(nodeSpacingH * 0.7); 119 | } else { 120 | xp += nodeSpacingH; 121 | } 122 | } 123 | let widestRow = 0; 124 | for (const row of column.rows) { 125 | widestRow = Math.max(widestRow, row.length); 126 | } 127 | const xpStart = xp; // Remember the left-most position in this column 128 | // Make room for row labels 129 | if (column.hasBranchLabels) { 130 | xp += sequentialStagesLabelOffset; 131 | } 132 | let maxX = xp; 133 | for (const row of column.rows) { 134 | let nodeX = xp; // Start nodes at current column xp (not xpstart as that includes branch label) 135 | // Offset the beginning of narrower rows towards column center 136 | nodeX += Math.round((widestRow - row.length) * parallelSpacingH * 0.5); 137 | for (const node of row) { 138 | maxX = Math.max(maxX, nodeX); 139 | node.x = nodeX; 140 | node.y = yp; 141 | nodeX += parallelSpacingH; // Space out nodes in each row 142 | } 143 | yp += nodeSpacingV; // LF 144 | } 145 | column.centerX = Math.round((xpStart + maxX) / 2); 146 | column.startX = xpStart; // Record on column for use later to position branch labels 147 | xp = maxX; // Make sure we're at the end of the widest row for this column before next loop 148 | previousTopNode = topNode; 149 | } 150 | } 151 | 152 | /** 153 | * Generate label descriptions for big labels at the top of each column 154 | */ 155 | function createBigLabels(columns) { 156 | const labels = []; 157 | for (const column of columns) { 158 | const node = column.rows[0][0]; 159 | const stage = column.topStage; 160 | const text = stage ? stage.name : node.name; 161 | const key = 'l_b_' + node.key; 162 | // bigLabel is located above center of column, but offset if there's branch labels 163 | let x = column.centerX; 164 | if (column.hasBranchLabels) { 165 | x += Math.floor(sequentialStagesLabelOffset / 2); 166 | } 167 | labels.push({ 168 | x, 169 | y: node.y, 170 | node, 171 | stage, 172 | text, 173 | key, 174 | }); 175 | } 176 | return labels; 177 | } 178 | 179 | /** 180 | * Generate label descriptions for small labels under the nodes 181 | */ 182 | function createSmallLabels(columns) { 183 | const labels = []; 184 | for (const column of columns) { 185 | for (const row of column.rows) { 186 | for (const node of row) { 187 | // We add small labels to parallel nodes only so skip others 188 | if (node.isPlaceholder || node.stage === column.topStage) { 189 | continue; 190 | } 191 | const label = { 192 | x: node.x, 193 | y: node.y, 194 | text: node.name, 195 | key: 'l_s_' + node.key, 196 | node, 197 | }; 198 | if (node.isPlaceholder === false) { 199 | label.stage = node.stage; 200 | } 201 | labels.push(label); 202 | } 203 | } 204 | } 205 | return labels; 206 | } 207 | 208 | /** 209 | * Generate label descriptions for named sequential parallels 210 | */ 211 | function createBranchLabels(columns) { 212 | const labels = []; 213 | let count = 0; 214 | for (const column of columns) { 215 | if (column.hasBranchLabels) { 216 | for (const row of column.rows) { 217 | const firstNode = row[0]; 218 | if (!firstNode.isPlaceholder && firstNode.stage.seqContainerName) { 219 | labels.push({ 220 | x: column.startX, 221 | y: firstNode.y, 222 | key: `branchLabel-${++count}`, 223 | node: firstNode, 224 | text: firstNode.stage.seqContainerName, 225 | }); 226 | } 227 | } 228 | } 229 | } 230 | return labels; 231 | } 232 | 233 | /** 234 | * Generate connection information from column to column 235 | */ 236 | function createConnections(columns) { 237 | const connections = []; 238 | let sourceNodes = []; 239 | let skippedNodes = []; 240 | for (const column of columns) { 241 | if (column.topStage && column.topStage.state === 'skipped') { 242 | skippedNodes.push(column.rows[0][0]); 243 | continue; 244 | } 245 | // Connections to each row in this column 246 | if (sourceNodes.length) { 247 | connections.push({ 248 | sourceNodes, 249 | destinationNodes: column.rows.map(row => row[0]), 250 | skippedNodes: skippedNodes, 251 | hasBranchLabels: column.hasBranchLabels, 252 | }); 253 | } 254 | // Simple horizontal connections between nodes within each row 255 | for (const row of column.rows) { 256 | for (let i = 0; i < row.length - 1; i++) { 257 | connections.push({ 258 | sourceNodes: [row[i]], 259 | destinationNodes: [row[i + 1]], 260 | skippedNodes: [], 261 | hasBranchLabels: false, 262 | }); 263 | } 264 | } 265 | sourceNodes = column.rows.map(row => row[row.length - 1]); // Last node of each row 266 | skippedNodes = []; 267 | } 268 | return connections; 269 | } 270 | -------------------------------------------------------------------------------- /src/components/PipelineGraph.vue: -------------------------------------------------------------------------------- 1 | 599 | 602 | --------------------------------------------------------------------------------