├── 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 |
2 |
3 |

4 |
5 |
6 |
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 | 
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 |
2 |
3 |
JenkinsPipelineGraphVue
4 |
5 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------