├── .editorconfig ├── .gitignore ├── README.md ├── gulpfile.js ├── images ├── add-step.png ├── collapsed-pipeline.png ├── command.png ├── convert-to-parallel.png ├── expanded-pipeline.png ├── open-editor.png ├── stage.png └── user-approval.png ├── package.json ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── pipelineeditor │ │ └── WorkflowVisualEditor.java ├── js │ ├── editor.js │ ├── model │ │ ├── json.js │ │ ├── stringify.js │ │ └── workflow.js │ ├── pipelineeditor.js │ ├── steps │ │ ├── EXTENDING.md │ │ ├── archive.js │ │ ├── env.js │ │ ├── git.js │ │ ├── index.js │ │ ├── input.js │ │ ├── rick.js │ │ ├── shell.js │ │ ├── sleep.js │ │ ├── stash.js │ │ ├── template.js │ │ ├── unstash.js │ │ └── workflowScript.js │ ├── svg │ │ ├── lines.js │ │ └── svg.js │ └── templates │ │ ├── editor-popover.hbs │ │ ├── normal-stage-block.hbs │ │ ├── parallel-stack.hbs │ │ ├── parallel-stage-block.hbs │ │ ├── stage-block.hbs │ │ ├── stage-button.hbs │ │ ├── stage-config-block.hbs │ │ ├── step-block.hbs │ │ ├── steps-listing.hbs │ │ ├── stream-block.hbs │ │ ├── stream-button.hbs │ │ └── stream-config-block.hbs ├── less │ └── pipelineeditor.less └── resources │ ├── index.jelly │ └── org │ └── jenkinsci │ └── plugins │ └── pipelineeditor │ └── WorkflowVisualEditor │ └── config.jelly └── test └── js ├── editor-spec.js ├── json-spec.js ├── steps-spec.js ├── steps └── shell-spec.js └── workflow-spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | # 4 | # Subline Text: https://github.com/editorconfig/editorconfig-sublime 5 | # Emacs: https://github.com/editorconfig/editorconfig-emacs 6 | # Jetbrains (IntelliJ etc): https://github.com/editorconfig/editorconfig-jetbrains 7 | # Eclipse: ?? 8 | # 9 | 10 | 11 | root = true 12 | 13 | [*] 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [**.jelly] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [**.xml] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | [**.hbs] 28 | indent_style = space 29 | indent_size = 2 30 | 31 | [**.js] 32 | indent_style = space 33 | indent_size = 2 34 | 35 | [**.css] 36 | indent_style = space 37 | indent_size = 2 38 | 39 | [**.less] 40 | indent_style = space 41 | indent_size = 2 42 | 43 | [**.js] 44 | indent_style = space 45 | indent_size = 2 46 | 47 | [*.md] 48 | trim_trailing_whitespace = false~ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | work 3 | node_modules 4 | node 5 | src/main/resources/org/jenkinsci/plugins/pipelineeditor/pipelineeditor.* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This was done as a proof of concept and should not be used. 4 | Any future work on a visual pipeline editor that was inspired by this is being moved to https://github.com/jenkinsci/blueocean-pipeline-editor-plugin 5 | 6 | 7 | # (Experimental) Visual pipeline editor for Jenkins Workflow. 8 | 9 | _NOTE: this plugin is experimental. It is likely to abruptly change course in design or source code. It may set your hair on fire. Consider yourself warned before using._ 10 | 11 | # Visual pipelines 12 | 13 | ![Pipeline Collapsed](images/collapsed-pipeline.png) 14 | 15 | The visual pipeline editor supports a stage centric view of Workflow. You expand stages to see the steps within: 16 | 17 | ![Pipeline Expanded](images/expanded-pipeline.png) 18 | 19 | ## Editing 20 | 21 | Clicking on a step will load the editor specific to that plugin: 22 | 23 | ![Command](images/command.png) 24 | or 25 | ![Ask for approval](images/user-approval.png) 26 | 27 | You can of course add more steps, from a choice of pre-made types: 28 | 29 | ![Add Step](images/add-step.png) 30 | 31 | The steps are implemented in [src/main/js/steps](src/main/js/steps) and the intention is to make it extensible (by this and other plugins). 32 | 33 | Parallelism is supported as first class. As you can guess by the branching and joining lines shown above. Any set of steps can be split up into parallel branches of execution: 34 | 35 | ![Make Parallel](images/convert-to-parallel.png) 36 | 37 | You can then add more steps to each parallel branch, or more branches, as needed (and you can convert back too). 38 | 39 | 40 | 41 | 42 | 43 | 44 | # Try it out 45 | 46 | This is a regular Jenkins plugin (although using various javascript tools) so you can run it using the usual 47 | 48 | `mvn hpi:run` or build it using `mvn install` if you want to install it in an existing Jenkins master (note the experimental status, it may blow up in your face!). 49 | 50 | Create a new job of the Workflow type, and click "edit" next to the drop down that choses the type of editor you want to use for Workflow: 51 | 52 | ![Open Editor](images/open-editor.png) 53 | 54 | Then Click save. It will have saved the workflow and you can execute it (it uses a pre-canned test repo just to show it works end to end). 55 | 56 | ### Using a pre-built hpi 57 | 58 | This plugin should be available on the [experimental update center](http://jenkins-ci.org/content/experimental-plugins-update-center). 59 | 60 | You can try a [preview](https://github.com/jenkinsci/pipeline-editor-plugin/releases/tag/v0.0.1-alpha) release if you are unable to run mvn. To use this, please first install the depndencies mentioned on that url, before installing the hpi into your Jenkins instance. Remember this is experimental/preview status. 61 | 62 | 63 | ## Developing the plugin 64 | 65 | Run `mvn hpi:run` as it is a normal plugin. 66 | 67 | To work with the JavaScript, you run the following (in another window): 68 | 69 | `npm install` 70 | 71 | `npm install -g gulp` 72 | 73 | `gulp bundle && gulp rebundle` 74 | 75 | This will set up the js tooling needed and automatically build the changes to the source 76 | js which is stored in main/js to the plugin directory (just reload the browser after that). 77 | 78 | # How it works 79 | 80 | Right now a json DSL is used (see `json.js`) (and stored in the jenkins config.xml) to keep the state of the UI and pipeline. On change events, a workflow script (via `toWorkflow`) is emitted and stored in the config.xml for actual execution. 81 | 82 | ## How to I see the generated workflow script 83 | 84 | The plugin regularly logs it to the console (in the browser), but it is also stored in the config(xml) of the Workflow Job. 85 | 86 | # It's a JavaScript plugin 87 | 88 | This JavaScript heavy plugin is possible because of https://github.com/tfennelly/jenkins-js-modules and libs made available by: https://github.com/tfennelly/jenkins-js-libs/ 89 | 90 | This means that namespaced, clean js libraries and css can be made available to any Jenkins plugin that needs it. Ideally, you use the common jQuery, or any library, but if you really need your own, this framework can support it. Using modern js tools like gulp and less make for a smooth development workflow (I like to call it "refresh driven development"). 91 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // 2 | // See https://github.com/tfennelly/jenkins-js-builder 3 | // 4 | var builder = require('jenkins-js-builder'); 5 | 6 | builder.bundle('src/main/js/pipelineeditor.js') 7 | .withExternalModuleMapping('bootstrap-detached', 'bootstrap:bootstrap3', {addDefaultCSS: true}) 8 | .withExternalModuleMapping('handlebars', 'handlebars:handlebars3') 9 | .less('src/main/less/pipelineeditor.less') 10 | .inDir('src/main/resources/org/jenkinsci/plugins/pipelineeditor'); 11 | 12 | -------------------------------------------------------------------------------- /images/add-step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/add-step.png -------------------------------------------------------------------------------- /images/collapsed-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/collapsed-pipeline.png -------------------------------------------------------------------------------- /images/command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/command.png -------------------------------------------------------------------------------- /images/convert-to-parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/convert-to-parallel.png -------------------------------------------------------------------------------- /images/expanded-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/expanded-pipeline.png -------------------------------------------------------------------------------- /images/open-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/open-editor.png -------------------------------------------------------------------------------- /images/stage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/stage.png -------------------------------------------------------------------------------- /images/user-approval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/pipeline-editor-plugin/4c5d30fec7851a5751ab46f067d985114e8428e8/images/user-approval.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudbees-pipeline-editor", 3 | "version": "0.0.1", 4 | "devDependencies": { 5 | "gulp": "^3.9.0", 6 | "jenkins-handlebars-rt": "^1.0.1", 7 | "jenkins-js-builder": "0.0.33", 8 | "jenkins-js-test": "0.0.16" 9 | }, 10 | "dependencies": { 11 | "bootstrap-detached": "^3.3.4-v6", 12 | "handlebars": "^3.0.3", 13 | "hbsfy": "^2.4.1", 14 | "jenkins-js-builder": "0.0.36", 15 | "jenkins-js-modules": "^1.4.0", 16 | "moment": "^2.10.6", 17 | "window-handle": "0.0.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 1.639 9 | 10 | 11 | pipeline-editor 12 | 1.0-alpha-2-SNAPSHOT 13 | hpi 14 | 15 | Visual pipeline editor plugin 16 | Edit your Jenkins Workflow pipelines visually 17 | https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Editor+Plugin 18 | 19 | 20 | MIT License 21 | http://opensource.org/licenses/MIT 22 | 23 | 24 | 33 | 34 | 35 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 36 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 37 | http://github.com/jenkinsci/${project.artifactId}-plugin 38 | HEAD 39 | 40 | 41 | 42 | 43 | repo.jenkins-ci.org 44 | http://repo.jenkins-ci.org/public/ 45 | 46 | 47 | 48 | 49 | repo.jenkins-ci.org 50 | http://repo.jenkins-ci.org/public/ 51 | 52 | 53 | 54 | 55 | 56 | org.jenkins-ci.ui 57 | bootstrap 58 | 1.2 59 | hpi 60 | 61 | 62 | org.jenkins-ci.ui 63 | handlebars 64 | 1.0 65 | hpi 66 | 67 | 68 | org.jenkins-ci.plugins.workflow 69 | workflow-aggregator 70 | 1.10 71 | hpi 72 | 73 | 74 | org.jenkins-ci.plugins 75 | git 76 | 2.3 77 | hpi 78 | test 79 | 80 | 81 | 82 | 87 | 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-surefire-plugin 92 | 93 | 94 | InjectedTest.java 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/pipelineeditor/WorkflowVisualEditor.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.pipelineeditor; 2 | 3 | import hudson.Extension; 4 | import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; 5 | import org.kohsuke.stapler.DataBoundConstructor; 6 | 7 | /** 8 | * @author Kohsuke Kawaguchi 9 | */ 10 | public class WorkflowVisualEditor extends CpsFlowDefinition { 11 | /** 12 | * JSON data model that the front end uses. 13 | */ 14 | private final String json; 15 | 16 | @DataBoundConstructor 17 | public WorkflowVisualEditor(String script, String json) { 18 | super(script); 19 | this.json = json; 20 | } 21 | 22 | public String getJson() { 23 | return json; 24 | } 25 | 26 | @Extension 27 | public static class DescriptorImpl extends CpsFlowDefinition.DescriptorImpl { 28 | @Override 29 | public String getDisplayName() { 30 | return "Pipeline Visual Editor"; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/js/editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pipeline editor main module. Dreaming of Alaskan pipelines 4 eva. 3 | * Elements that are to be used when actually editing should have a class of 'edit-mode'. 4 | * Then we can support read only by simply $('.edit-mode').hide(); 5 | * 6 | * I Recommended that you understand the stage/step/parallel/node concepts in workflow well before looking further. 7 | * "Normal stage" means a normal workflow stage. A "parallel stage" is just a stage that 8 | * has a parallel set of streams (sometimes called branches) under it, as is the workflow convention. 9 | */ 10 | 11 | var $ = require('bootstrap-detached').getBootstrap(); 12 | var lines = require('./svg/lines'); 13 | 14 | var stringify = require('./model/stringify'); 15 | var wf = require('./model/workflow'); 16 | 17 | var steps = require('./steps'); 18 | 19 | /** 20 | * Draw the pipeline visualisation based on the pipeline data, including svg. 21 | * Current pipeline is stored in the "pipeline" variable assumed to be in scope. 22 | * Also requires formFields of script and json 23 | */ 24 | exports.drawPipeline = function (pipeline, formFields) { 25 | var pRow = $('#pipeline-row'); 26 | pRow.empty(); 27 | 28 | for (var i=0; i < pipeline.length; i++) { 29 | var stage = pipeline[i]; 30 | var currentId = "stage-" + i; 31 | //append a block if non parallel 32 | if (!wf.isParallelStage(stage)) { 33 | pRow.append(normalStageBlock(currentId, stage)); 34 | } else { 35 | var subStages = ""; 36 | for (var j = 0; j < stage.streams.length; j++) { 37 | var subStage = stage.streams[j]; 38 | var subStageId = currentId + "-" + j; 39 | subStages += parStageBlock(stage.name, subStageId, subStage, currentId); 40 | } 41 | 42 | pRow.append(parallelStack(subStages, currentId)); 43 | } 44 | } 45 | pRow.append(addStageButton()); 46 | 47 | addNewStageListener(pipeline, formFields); 48 | addNewStreamListener(pipeline, formFields); 49 | 50 | lines.autoJoinDelay(pipeline); 51 | addAutoJoinHooks(pipeline); 52 | 53 | addOpenStepListener(pipeline, formFields); 54 | addNewStepListener(pipeline, formFields); 55 | 56 | addConfigStageListener(pipeline, formFields); 57 | addConfigStreamListener(pipeline, formFields); 58 | 59 | }; 60 | 61 | /** redraw the pipeline and apply changes to the formFields in the Jenksin config page */ 62 | function redrawPipeline(pipeline, formFields) { 63 | exports.drawPipeline(pipeline, formFields); 64 | writeOutChanges(pipeline, formFields); 65 | } 66 | 67 | /** a parallel stage is really just a stage, but with a stack of parallel streams (each which have a name) */ 68 | function parallelStack(subStages, currentId) { 69 | return require('./templates/parallel-stack.hbs')({subStages: subStages, addStreamButton : addStreamButton(currentId), currentId : currentId }); 70 | } 71 | 72 | /** This will add a plain stage to the end of the set of stages */ 73 | function addStageButton() { 74 | return require('./templates/stage-button.hbs')(); 75 | } 76 | 77 | /** A stream is a named part of a parallel block in workflow */ 78 | function addStreamButton(stageId) { 79 | return require('./templates/stream-button.hbs')({stageId: stageId}); 80 | } 81 | 82 | /** show a popover for changing stage level settings, or deleting */ 83 | function addConfigStageListener(pipeline, formFields) { 84 | $("#pipeline-visual-editor").on('click', ".open-stage-config", function() { 85 | var stageId = $( this ).attr('data-stage-id'); 86 | var coords = wf.stageIdToCoordinates(stageId); 87 | var currentStage = pipeline[coords[0]]; 88 | 89 | var stageConfigP = $("#edit-stage-popover-" + stageId); 90 | var popContent = require('./templates/stage-config-block.hbs')({stageId: stageId, stageName: currentStage.name}); 91 | stageConfigP.popover({'content' : popContent, 'html' : true}); 92 | stageConfigP.popover('show'); 93 | $('#toggleParallel-' + stageId).off('click').click(function() { 94 | wf.toggleParallel(pipeline, stageId); 95 | redrawPipeline(pipeline, formFields); 96 | stageConfigP.popover('destroy'); 97 | }); 98 | $('#closeStageConfig-' + stageId).off('click').click(function () { 99 | var newName = $('#stageName_' + stageId).val(); 100 | if (newName !== currentStage.name) { 101 | currentStage.name = newName; 102 | redrawPipeline(pipeline, formFields); 103 | } 104 | stageConfigP.popover('destroy'); 105 | }); 106 | $('#deleteStage-' + stageId).off('click').click(function() { 107 | if (window.confirm("Are you sure you want to delete this stage?")) { 108 | wf.removeStage(pipeline, stageId); 109 | redrawPipeline(pipeline, formFields); 110 | stageConfigP.popover('destroy'); 111 | } 112 | }); 113 | 114 | 115 | }); 116 | } 117 | 118 | /** popover for changing stream settings for a parallel stage, including deleting */ 119 | function addConfigStreamListener(pipeline, formFields) { 120 | $("#pipeline-visual-editor").on('click', ".open-stream-config", function() { 121 | var stageId = $( this ).attr('data-stage-id'); 122 | var streamId = $( this ).attr('data-stream-id'); 123 | var coords = wf.stageIdToCoordinates(streamId); 124 | var currentStage = pipeline[coords[0]]; 125 | var currentStream = currentStage.streams[coords[1]]; 126 | 127 | console.log(stageId); 128 | var streamConfigP = $("#edit-stage-popover-" + streamId); 129 | var popContent = require('./templates/stream-config-block.hbs')( 130 | { stageId: stageId, 131 | streamId: streamId, 132 | stageName: currentStage.name, 133 | streamName: currentStage.streams[coords[1]].name 134 | }); 135 | streamConfigP.popover({'content' : popContent, 'html' : true}); 136 | streamConfigP.popover('show'); 137 | 138 | $('#makeSequential-' + stageId).off('click').click(function() { 139 | wf.parallelToNormal(pipeline, stageId); 140 | redrawPipeline(pipeline, formFields); 141 | streamConfigP.popover('destroy'); 142 | }); 143 | 144 | $('#closeStageConfig-' + stageId).off('click').click(function () { 145 | var newName = $('#stageName_' + stageId).val(); 146 | var newStreamName = $('#streamName_' + streamId).val(); 147 | if (newName !== currentStage.name || newStreamName !== currentStream.name) { 148 | currentStage.name = newName; 149 | currentStream.name = newStreamName; 150 | redrawPipeline(pipeline, formFields); 151 | } 152 | streamConfigP.popover('destroy'); 153 | }); 154 | 155 | $('#deleteStage-' + stageId).off('click').click(function() { 156 | if (window.confirm("Are you sure you want to delete the whole stage and all its parallel branches?")) { 157 | wf.removeStage(pipeline, stageId); 158 | redrawPipeline(pipeline, formFields); 159 | streamConfigP.popover('destroy'); 160 | } 161 | }); 162 | 163 | $('#deleteStream-' + streamId).off('click').click(function() { 164 | if (window.confirm("Are you sure you want to delete branch?")) { 165 | wf.removeStage(pipeline, streamId); 166 | redrawPipeline(pipeline, formFields); 167 | streamConfigP.popover('destroy'); 168 | } 169 | }); 170 | 171 | 172 | 173 | }); 174 | } 175 | 176 | 177 | /** add a new stream (sometimes called a branch) to the end of the list of streams in a stage */ 178 | function addNewStreamListener(pipeline) { 179 | $("#pipeline-visual-editor").on('click', ".open-add-stream", function(){ 180 | var stageId = $( this ).attr('data-stage-id'); 181 | var newStreamP = $('#add-stream-popover-' + stageId); 182 | var newStreamBlock = require('./templates/stream-block.hbs')({stageId: stageId}); 183 | newStreamP.popover({'content' : newStreamBlock, 'html' : true}); 184 | newStreamP.popover('show'); 185 | $('#addStreamBtn-' + stageId).off('click').click(function() { 186 | handleAddStream(newStreamP, stageId, pipeline); 187 | }); 188 | $("#newStreamName-" + stageId).off('keydown').keydown(function (e) { 189 | if (e.which === 13) { 190 | handleAddStream(newStreamP, stageId, pipeline); 191 | } 192 | }); 193 | $("#newStreamName-" + stageId).focus(); 194 | }); 195 | 196 | } 197 | 198 | 199 | /** add a new stream to an existing stage and redraw just that section */ 200 | function handleAddStream(newStreamP, stageId, pipeline) { 201 | newStreamP.popover('toggle'); 202 | var newStreamName = $("#newStreamName-" + stageId).val(); 203 | if (newStreamName !== '') { 204 | var coords = wf.stageIdToCoordinates(stageId); 205 | var outerStage = pipeline[coords[0]]; 206 | var newStream = {"name": newStreamName, "steps": []}; 207 | outerStage.streams.push(newStream); 208 | // Insert the new stream stage directly into the DOM... 209 | var subStageId = stageId + "-" + (outerStage.streams.length - 1); 210 | var streamView = parStageBlock(outerStage.name, subStageId, newStream, stageId); 211 | $(streamView).insertAfter( 212 | $(".outer-stage[data-stage-id='" + stageId + "'] ul li").last() 213 | ); 214 | lines.autoJoinDelay(pipeline, 0); // redraw immediately ... no delay 215 | } 216 | } 217 | 218 | 219 | 220 | /** We will want to redraw the joins in some cases */ 221 | function addAutoJoinHooks(pipeline) { 222 | $("#pipeline-visual-editor").on('click', ".autojoin", function() { 223 | lines.autoJoinDelay(pipeline); 224 | }); 225 | } 226 | 227 | /** clicking on a step will open the editor */ 228 | function addOpenStepListener(pipeline, formFields) { 229 | $("#pipeline-visual-editor").on('click', ".open-editor", function(){ 230 | openEditor(pipeline, $( this ).attr('data-action-id'), formFields); 231 | }); 232 | } 233 | 234 | /** clicking on add a step should open a popover with a selection of available steps */ 235 | function addNewStepListener(pipeline, formFields) { // jshint ignore:line 236 | $("#pipeline-visual-editor").on('click', ".open-add-step", function(){ 237 | var stageId = $( this ).attr('data-stage-id'); 238 | var newStepP = $('#add-step-popover-' + stageId); 239 | newStepP.popover({'content' : newStepBlock(stageId, steps), 'html' : true}); 240 | newStepP.popover('show'); 241 | $("#addStepBtn-" + stageId).off('click').click(function() { 242 | handleAddNewStep(newStepP, pipeline, formFields, stageId); 243 | }); 244 | $("#newStepName-" + stageId).off('keydown').keydown(function(e) { 245 | if (e.which === 13) { 246 | handleAddNewStep(newStepP, pipeline, formFields, stageId); 247 | } 248 | }); 249 | }); 250 | } 251 | 252 | /** Add the new step and redraw just the current stage listing of steps */ 253 | function handleAddNewStep(newStepP, pipeline, formFields, stageId) { 254 | var selected = document.querySelector('input[name="newStepType-' + stageId + '"]:checked'); 255 | var name = $('#newStepName-' + stageId).val(); 256 | newStepP.popover('toggle'); 257 | if (selected) { 258 | if (!name) { 259 | name = "New Step"; 260 | } 261 | var newStep = {"type": selected.value, "name": name}; 262 | var insertResult = wf.insertStep(pipeline, stageId, newStep); 263 | writeOutChanges(pipeline, formFields); 264 | refreshStepListing(stageId, insertResult.stepContainer.steps); 265 | lines.autoJoinDelay(pipeline, 0); // redraw immediately ... no delay 266 | openEditor(pipeline, insertResult.actionId, formFields); 267 | } 268 | } 269 | 270 | /** the popover for a new step */ 271 | function newStepBlock(stageId, pipelineEditors) { 272 | return require('./templates/step-block.hbs')({ 273 | stageId: stageId, 274 | steps: pipelineEditors 275 | }); 276 | } 277 | 278 | /** clicking on add a stage will at least ask a user for a name */ 279 | function addNewStageListener(pipeline, formFields) { // jshint ignore:line 280 | $("#pipeline-visual-editor").on('click', ".open-add-stage", function() { 281 | var newStageP = $('#add-stage-popover'); 282 | newStageP.popover({'content': newStageBlock(), 'html': true}); 283 | newStageP.popover('show'); 284 | 285 | function addStage() { 286 | newStageP.popover('toggle'); 287 | var newStageName = $("#newStageName").val(); 288 | if (newStageName !== '') { 289 | pipeline.push({"name": newStageName, "steps": []}); 290 | redrawPipeline(pipeline, formFields); 291 | } 292 | } 293 | 294 | $('#addStageBtn').off('click').click(function() { 295 | addStage(); 296 | }); 297 | $("#newStageName").off('keydown').keydown(function (e) { 298 | if (e.which === 13) { 299 | addStage(); 300 | } 301 | }); 302 | $("#newStageName").focus(); 303 | }); 304 | } 305 | 306 | /** the popover for a new stage */ 307 | function newStageBlock() { 308 | return require('./templates/stage-block.hbs')(); 309 | } 310 | 311 | 312 | /** apply changes to any form-control elements */ 313 | function addApplyChangesHooks(pipeline, formFields) { 314 | $(".currently-editing").change(function() { 315 | var actionId = $( this ).attr('data-action-id'); 316 | handleEditorSave(pipeline, actionId, formFields); 317 | }); 318 | $(".close-editor-popover").click(function() { 319 | var actionId = $( this ).attr('data-action-id'); 320 | var editorP = $("#show-editor-popover-" + actionId); 321 | editorP.popover('destroy'); 322 | }); 323 | $(".delete-current-step").click(function() { 324 | if (window.confirm("Are you sure you want to delete this step?")) { 325 | var actionId = $( this ).attr('data-action-id'); 326 | var editorP = $("#show-editor-popover-" + actionId); 327 | wf.removeActionId(pipeline, actionId); 328 | redrawPipeline(pipeline, formFields); 329 | editorP.popover('destroy'); 330 | } 331 | 332 | }); 333 | 334 | } 335 | 336 | /** 337 | * For the given pipeline, put the values in the script and json form fields. 338 | */ 339 | function writeOutChanges(pipeline, formFields) { 340 | var generatedScript = wf.toWorkflow(pipeline, steps); 341 | formFields.script.val(generatedScript); 342 | formFields.json.val(stringify.writeJSON(pipeline)); 343 | console.log(generatedScript); 344 | } 345 | exports.writeOutChanges = writeOutChanges; 346 | 347 | /** 348 | * parallel stages are an item in an ordered list. 349 | */ 350 | function parStageBlock(stageName, subStageId, subStage, stageId) { 351 | return require('./templates/parallel-stage-block.hbs')({ 352 | stageName: stageName, 353 | subStageId: subStageId, 354 | subStage: subStage, 355 | stageId: stageId, 356 | stepListing: stepListing(subStageId, subStage.steps) 357 | }); 358 | } 359 | exports.parStageBlock = parStageBlock; 360 | 361 | /** 362 | * A non parallel stage. Parallel stages are a pipeline editor construct, not an inherent workflow property. 363 | */ 364 | function normalStageBlock(currentId, stage) { 365 | return require('./templates/normal-stage-block.hbs')({ 366 | currentId: currentId, 367 | stage: stage, 368 | stepListing: stepListing(currentId, stage.steps) 369 | }); 370 | } 371 | exports.normalStageBlock = normalStageBlock; 372 | 373 | /** 374 | * Take a list of steps and return a listing of step buttons 375 | */ 376 | function stepListing(stageId, steps) { 377 | return require('./templates/steps-listing.hbs')({ 378 | stageId: stageId, 379 | steps: steps 380 | }); 381 | } 382 | 383 | function refreshStepListing(stageId, steps) { 384 | var content = stepListing(stageId, steps); 385 | $('#' + stageId + ' .step-listing').replaceWith(content); 386 | } 387 | 388 | /** 389 | * Taking the actionId (co-ordinates), find the step info and load it up. 390 | */ 391 | function openEditor(pipeline, actionId, formFields) { 392 | var editorP = $("#show-editor-popover-" + actionId); 393 | var coordinates = wf.actionIdToStep(actionId); 394 | var stepInfo = wf.fetchStep(coordinates, pipeline); 395 | var editorModule = steps[stepInfo.type]; 396 | var editorHtml = editorModule.renderEditor(stepInfo, actionId); 397 | var content = require('./templates/editor-popover.hbs')({ 398 | actionId : actionId, 399 | editorHtml: editorHtml 400 | }); 401 | 402 | editorP.popover({'content' : content, 'html' : true}); 403 | editorP.popover('show'); 404 | 405 | addApplyChangesHooks(pipeline, formFields); 406 | 407 | $(".open-editor").removeClass('selected'); 408 | $(".open-editor[data-action-id='" + actionId + "']").addClass('selected'); 409 | $('.form-group').first().focus(); 410 | 411 | } 412 | 413 | /** 414 | * When a change is made to a step config, this will be called to apply the changes. 415 | */ 416 | function handleEditorSave(pipeline, actionId, formFields) { 417 | var currentStep = wf.fetchStep(wf.actionIdToStep(actionId), pipeline); 418 | var edModule = steps[currentStep.type]; 419 | if (edModule.readChanges(actionId, currentStep)) { 420 | console.log("applied changes for " + actionId); 421 | writeOutChanges(pipeline, formFields); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/main/js/model/json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The json model of the data: 3 | * pipeline is a [list of stages] 4 | * each stage is an object that is either an ordinary stage, or a parallel stage. 5 | * (this is not a workflow script concept, just a model abstraction here). 6 | * 7 | * An ordinary stage: { "name" : for display purposes and logging, "steps" : [list of steps]} 8 | * a step: { "name" : for display purposes only, "type" : determines what editor is loaded. } 9 | * 10 | * A parallel stage: { "name": ..., "streams" : [list streams] } 11 | * A "stream" is sometimes called a "branch" in workflow examples, but this is confusing with SCM branches, 12 | * so I have called it a stream. 13 | * A stream is similar to a stage, { "name" : ... , "steps" : [list of steps]} containing steps. 14 | * The name of the stream is used in logs, and for display, however it is all under the one 15 | * workflow "stage". 16 | * 17 | * See the samples below to make this more concrete. 18 | */ 19 | 20 | /** 21 | * Load the json from the json field, if its a new job lets apply a default. 22 | */ 23 | exports.loadModelOrUseDefault = function(jsonText) { 24 | if (exports.existingPipeline(jsonText)) { 25 | var pipelineParsed = JSON.parse(jsonText); 26 | return pipelineParsed; 27 | } else { 28 | console.log("No pipeline has been saved, applying a sample template"); 29 | return simpleSample; 30 | } 31 | }; 32 | 33 | /** if not valid json then we need to resort to a sample */ 34 | exports.existingPipeline = function(jsonText) { 35 | return jsonText !== null && jsonText !== ""; 36 | }; 37 | 38 | /* some sample json starting points to default to */ 39 | 40 | var simpleSample = [ 41 | { 42 | "name" : "Checkout and Build", 43 | "steps" : [ 44 | {"type": "git", "name" : "Clone webapp", "url" : "https://github.com/michaelneale/sample-pipeline-project.git"}, 45 | {"type": "sh", "name" : "Build", "command" : "echo 'hello world'"} 46 | ] 47 | }, 48 | 49 | { 50 | "name" : "Test", 51 | "streams" : [ 52 | {"name" : "Unit", "steps" : [ 53 | {"type": "sh", "name" : "Run unit test suite", "command" : "./bin/ci/test"} 54 | ]}, 55 | {"name" : "Integration","steps" : [ 56 | {"type": "sh", "name" : "Run slower tests", "command" : "./bin/ci/integration-tests"} 57 | ]} 58 | ] 59 | }, 60 | 61 | { 62 | "name" : "Deploy", 63 | "steps" : [ 64 | {"type": "sh", "name" : "Deploy to staging", "command" : "./bin/ci/deploy"}, 65 | ] 66 | } 67 | 68 | 69 | ]; 70 | exports.simpleSample = simpleSample; 71 | 72 | 73 | exports.complexSample = 74 | [ 75 | { 76 | "name" : "Checkout", 77 | "steps" : [ 78 | {"type": "git", "name" : "Clone webapp", "url" : "git@github.com/thing/awesome.git"}, 79 | ] 80 | }, 81 | 82 | 83 | { 84 | "name" : "Prepare Test Database", 85 | "steps" : [ 86 | {"type": "sh", "name" : "Install Postgress", "command" : "install_postgres"}, 87 | {"type": "sh", "name" : "Initialise DB", "command" : "pgsql data/init.sql"}, 88 | ] 89 | }, 90 | 91 | 92 | 93 | { 94 | "name" : "Prepare", 95 | "streams" : [ 96 | {"name" : "Ruby", "steps" : [ 97 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"}, 98 | {"type": "stash", "name" : "Stash compiled app", "includes": "/app", "excludes" : ""} 99 | ]}, 100 | {"name" : "Python","steps" : [ 101 | {"type": "sh", "name" : "Yeah", "command" : "exit()"} 102 | 103 | ]} 104 | ] 105 | }, 106 | 107 | { 108 | "name" : "Stage and Test", 109 | "steps" : [] 110 | }, 111 | 112 | { 113 | "name" : "Approve", 114 | "steps" : [], 115 | "type" : "input" 116 | }, 117 | 118 | { 119 | "name" : "Deploy", 120 | "steps" : [], 121 | "node" : "devops-production" 122 | }, 123 | 124 | { 125 | "name" : "Party", 126 | "steps" : [{"type": "rick", "name" : "Awesome" } ] 127 | } 128 | 129 | 130 | ]; 131 | -------------------------------------------------------------------------------- /src/main/js/model/stringify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Awful hack to get around JSONifying things with Prototype taking over wrong. ugh. Prototype is the worst. 3 | * Prototype is bad and you should feel bad. 4 | * Prototype is bad and you should feel bad. 5 | * Prototype is bad and you should feel bad. 6 | * Prototype is bad and you should feel bad. 7 | * Prototype is bad and you should feel bad. 8 | * Prototype is bad and you should feel bad. 9 | * Prototype is bad and you should feel bad. 10 | * Prototype is bad and you should feel bad. 11 | * Prototype is bad and you should feel bad. 12 | * Prototype is bad and you should feel bad. 13 | */ 14 | exports.writeJSON = function(o) { 15 | if(Array.prototype.toJSON) { // Prototype f's this up something bad 16 | var protoJSON = { 17 | a: Array.prototype.toJSON, 18 | o: Object.prototype.toJSON, 19 | h: Hash.prototype.toJSON, 20 | s: String.prototype.toJSON 21 | }; 22 | try { 23 | delete Array.prototype.toJSON; 24 | delete Object.prototype.toJSON; 25 | delete Hash.prototype.toJSON; 26 | delete String.prototype.toJSON; 27 | 28 | return JSON.stringify(o); 29 | } 30 | finally { 31 | if(protoJSON.a) { 32 | Array.prototype.toJSON = protoJSON.a; 33 | } 34 | if(protoJSON.o) { 35 | Object.prototype.toJSON = protoJSON.o; 36 | } 37 | if(protoJSON.h) { 38 | Hash.prototype.toJSON = protoJSON.h; 39 | } 40 | if(protoJSON.s) { 41 | String.prototype.toJSON = protoJSON.s; 42 | } 43 | } 44 | } 45 | else { 46 | return JSON.stringify(o); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/main/js/model/workflow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains magic for rendering out to workflow script from the json model. 3 | * 4 | * In some places we use the convention "stageId" style string to point to what stage we are talking about. This stage id is of the form: 5 | * stage-X for a "normal" stage. and stage-X-Y to specity the stream in a parallel stage. 6 | * In some places we use actionId. This is the coordinates to a step. 7 | * stage-0-0 wll indicate it is the first step of the first stage. 8 | * stage-0-0-0 will indicate it is the first step of the first stream of the first stage. 9 | * ie stage-- if normal. stage--- if it has parallel. 10 | */ 11 | 12 | exports.isParallelStage = isParallelStage; 13 | exports.toWorkflow = toWorkflow; 14 | exports.insertStep = insertStep; 15 | exports.stageIdToCoordinates = stageIdToCoordinates; 16 | exports.actionIdToStep = actionIdToStep; 17 | exports.fetchStep = fetchStep; 18 | exports.parallelToNormal = parallelToNormal; 19 | exports.makeParallel = makeParallel; 20 | exports.toggleParallel = toggleParallel; 21 | exports.removeActionId = removeActionId; 22 | exports.removeStage = removeStage; 23 | 24 | /** 25 | * a parallel stage has to have streams 26 | */ 27 | function isParallelStage(stage) { 28 | if (stage.streams && stage.streams.length > 0) { 29 | return true; 30 | } else { 31 | return false; 32 | } 33 | } 34 | 35 | /** 36 | * take the pipeLineData model and insert the newStep at the end of the stage. 37 | * A stage may be stage-0 or stage-1-1, for example (latter if it is a multi stream stage) 38 | */ 39 | function insertStep(pipelineData, stageId, newStep) { 40 | var coords = stageIdToCoordinates(stageId); 41 | var stepContainer; 42 | if (coords.length === 1) { 43 | stepContainer = pipelineData[coords[0]]; 44 | } else { 45 | stepContainer = pipelineData[coords[0]].streams[coords[1]]; 46 | } 47 | stepContainer.steps.push(newStep); 48 | return { 49 | actionId: stageId + "-" + (stepContainer.steps.length - 1), 50 | stepContainer: stepContainer 51 | }; 52 | } 53 | 54 | 55 | /** 56 | * get the array coordinates of a stage 57 | * ["stage-0"] -> [0] 58 | * ["stage-1-1"] -> [1,1] 59 | */ 60 | function stageIdToCoordinates(stageId) { 61 | var elements = stageId.split('-'); 62 | switch (elements.length) { 63 | case 2: 64 | return [parseInt(elements[1])]; 65 | case 3: 66 | return [parseInt(elements[1]), parseInt(elements[2])]; 67 | default: 68 | console.log("ERROR: not a valid stageId"); 69 | } 70 | } 71 | 72 | /** Toggle the parallel-ness of a stage */ 73 | function toggleParallel(pipeline, stageId) { 74 | var coords = stageIdToCoordinates(stageId); 75 | if (isParallelStage(pipeline[coords[0]])) { 76 | parallelToNormal(pipeline, stageId); 77 | } else { 78 | makeParallel(pipeline, stageId); 79 | } 80 | } 81 | 82 | /** 83 | * Convert a stage that is already parallel, to one that is normal, but combining the streams. 84 | */ 85 | function parallelToNormal(pipeline, stageId) { 86 | var coords = stageIdToCoordinates(stageId); 87 | var stage = pipeline[coords[0]]; 88 | var streams = stage.streams; 89 | var newSteps = streams.reduce(function(acc, val){ 90 | return acc.concat(val.steps); 91 | }, []); 92 | delete stage.streams; 93 | stage.steps = newSteps; 94 | } 95 | 96 | /** 97 | * Take a normal stage which is a list of steps, and split it evenly to parallel streams. 98 | * Use the name of the first steps as the stream name. 99 | */ 100 | function makeParallel(pipeline, stageId) { 101 | var coords = stageIdToCoordinates(stageId); 102 | var stage = pipeline[coords[0]]; 103 | var steps = stage.steps; 104 | 105 | var splitAt = Math.floor(steps.length/2); 106 | var leftSteps = []; 107 | var rightSteps = []; 108 | for (var i=0; i < steps.length; ++i) { 109 | if (i < splitAt) { 110 | leftSteps.push(steps[i]); 111 | } else { 112 | rightSteps.push(steps[i]); 113 | } 114 | } 115 | var leftName = ''; 116 | var rightName = ''; 117 | if (leftSteps[0]) { 118 | leftName = leftSteps[0].name; 119 | } 120 | if (rightSteps[0]) { 121 | rightName = rightSteps[0].name; 122 | } 123 | 124 | delete stage.steps; 125 | stage.streams = [ 126 | {"name" : leftName, "steps" : leftSteps}, 127 | {"name" : rightName, "steps" : rightSteps} 128 | ]; 129 | 130 | } 131 | 132 | /** 133 | * given an action id like stage-1-2 - remove it from the pipeline 134 | */ 135 | function removeActionId(pipeline, actionId) { 136 | var coords = actionIdToStep(actionId); 137 | if (coords.length === 2) { 138 | pipeline[coords[0]].steps.splice(coords[1], 1); 139 | } else { 140 | pipeline[coords[0]].streams[coords[1]].steps.splice(coords[2], 1); 141 | } 142 | } 143 | 144 | /** Remove a whole stage, which may actually be a streamId or a stageId 145 | * stage-0 or stage-0-1 for example. 146 | */ 147 | function removeStage(pipeline, stageId) { 148 | var coords = stageIdToCoordinates(stageId); 149 | if (coords.length === 1) { 150 | pipeline.splice(coords[0], 1); 151 | } else { 152 | pipeline[coords[0]].streams.splice(coords[1], 1); 153 | } 154 | } 155 | 156 | /** 157 | * an actionId is something like stage-1-2 or stage-1-2-3 158 | * This will return an array of the step co-ordinates. 159 | * So stage-1-2 = [1,2] 160 | * stage-1-2-3 = [1,2,3] 161 | * the first number is the stage index, second is the step or stream index. 162 | * the third number is if it is a parallel stage (so it is [stage, stream, step]) 163 | */ 164 | function actionIdToStep(actionId) { 165 | var elements = actionId.split('-'); 166 | switch (elements.length) { 167 | case 3: 168 | return [parseInt(elements[1]), parseInt(elements[2])]; 169 | case 4: 170 | return [parseInt(elements[1]), parseInt(elements[2]), parseInt(elements[3])]; 171 | default: 172 | console.log("ERROR: not a valid actionId"); 173 | } 174 | } 175 | 176 | 177 | /** 178 | * Take 2 or 3 indexes and find the step out of the pipelineData. 179 | */ 180 | function fetchStep(coordinates, pipelineData) { 181 | if (coordinates.length === 2) { 182 | return pipelineData[coordinates[0]].steps[coordinates[1]]; 183 | } else { 184 | return pipelineData[coordinates[0]].streams[coordinates[1]].steps[coordinates[2]]; 185 | } 186 | } 187 | 188 | 189 | 190 | 191 | /** 192 | * Print out the workflow script using the given modules to render the steps. 193 | */ 194 | function toWorkflow(pipelineData, modules) { 195 | function toStreams(streamData, modules) { 196 | var par = "\n parallel ("; 197 | for (var i = 0; i < streamData.length; i++) { 198 | var stream = streamData[i]; 199 | par += '\n "' + stream.name + '" : {'; 200 | par += toSteps(stream.steps, modules); 201 | if (i === (streamData.length - 1)) { 202 | par += "\n }"; 203 | } else { 204 | par += "\n },"; 205 | } 206 | } 207 | return par + "\n )"; 208 | } 209 | 210 | function toSteps(stepData, modules) { 211 | var steps = ""; 212 | for (var i = 0; i < stepData.length; i++) { 213 | var stepInfo = stepData[i]; 214 | var mod = modules[stepInfo.type]; 215 | steps += "\n " + mod.generateScript(stepInfo); 216 | } 217 | return steps; 218 | } 219 | 220 | var inner = ""; 221 | for (var i = 0; i < pipelineData.length; i++) { 222 | var stage = pipelineData[i]; 223 | inner += '\n stage name: "' + stage.name + '"'; 224 | if (stage.streams) { 225 | inner += toStreams(stage.streams, modules); 226 | } else { 227 | inner += toSteps(stage.steps, modules); 228 | } 229 | } 230 | return "node {" + inner + "\n}"; 231 | } 232 | -------------------------------------------------------------------------------- /src/main/js/pipelineeditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Jenkins pipeline editor adjunct and entry point. 3 | */ 4 | 5 | var $ = require('bootstrap-detached').getBootstrap(); 6 | var h = require('./editor'); 7 | var Belay = require('./svg/svg'); 8 | var lines = require('./svg/lines'); 9 | var storage = require("./model/json"); 10 | var editors = require('./steps'); 11 | var win = require('window-handle').getWindow(); 12 | 13 | win.mic = $; //For debugging! - you can use `mic` as jquery. 14 | win.Belay = Belay; 15 | /* print out what editors are registered for use */ 16 | console.log(editors); 17 | 18 | /** 19 | * Hook in to the edit button on the regular jenkins job config screen 20 | * There may need to be changes here to do the equivalent of Behaviour js-Prototype code. 21 | * (scroll down the bottom to see the original code concept). 22 | * 23 | * Get both the script and the json text that is stored in config.xml 24 | */ 25 | $(document).ready(function () { 26 | var script = $("input[name='_.script']"); 27 | var json = $("input[name='_.json']"); 28 | var confEditor = $('#page-body > div'); 29 | var pageBody = $('#page-body'); 30 | if ("#pipeline-editor" === win.location.hash) { 31 | showEditor($, confEditor, pageBody, script, json); 32 | } 33 | $('#edit-pipeline').click(function() { 34 | showEditor($, confEditor, pageBody, script, json); 35 | }); 36 | }); 37 | 38 | /** 39 | * Clear the regular jenkins conf editor, show the pipeline editor 40 | */ 41 | function showEditor($, confEditor, pageBody, script, json) { 42 | confEditor.hide(); 43 | win.location.hash = "#pipeline-editor"; 44 | pageBody.append("
" + 45 | "

" + 46 | fixFlowCSS() + 47 | pipelineEditorArea() + 48 | detailContainer() + 49 | "

"+ 50 | "
"); 51 | 52 | $('#back-to-config').click(function() { 53 | $("#pipeline-visual-editor").remove(); 54 | win.location.hash = ""; 55 | Belay.off(); 56 | confEditor.show(); 57 | $('#main-panel > form').submit(); 58 | 59 | //remove the absolute position from the bottom sticker on return 60 | //otherwise the buttons may not appear until a resize. LOL (probably a better solution) 61 | //TODO: must be a better way. 62 | $('#bottom-sticker').attr('style', function(i, style) { 63 | return style.replace(/position[^;]+;?/g, ''); 64 | }); 65 | }); 66 | 67 | 68 | var pipeline = storage.loadModelOrUseDefault(json.val()); 69 | 70 | lines.initSVG(); 71 | 72 | if (!storage.existingPipeline(json.val())) { 73 | console.log("Brand new pipeline so saving the changes the first time"); 74 | h.writeOutChanges(pipeline, {"script" : script, "json" : json }); 75 | } 76 | 77 | h.drawPipeline(pipeline, {"script" : script, "json" : json }); 78 | reJoinOnResize(pipeline); 79 | 80 | window.onpopstate = function() { 81 | window.location = window.location.pathname; 82 | }; 83 | 84 | 85 | } 86 | 87 | 88 | 89 | /** The bit that holds the pipeline visualisation */ 90 | function pipelineEditorArea() { 91 | return '
' + 92 | '
' + 93 | '
'; 94 | } 95 | 96 | 97 | /** container for holding the specific editors depending on what is clicked */ 98 | function detailContainer() { 99 | return '
' + 100 | '
' + 101 | '

' + 102 | '
' + 103 | '
' + 104 | '
' + 105 | '
' + 106 | 'Click on a Step to view the details. ' + 107 | '
' + 108 | '
' + 109 | '
' + 110 | '
' + 111 | '
' + 112 | '
'; 113 | } 114 | 115 | /** 116 | * This will fix a problem with wrapping elements in bootstrap 117 | * this is documented here: http://stackoverflow.com/questions/25598728/irregular-bootstrap-column-wrapping 118 | * Without this, things won't wrap clearly left to right. Read it. 119 | */ 120 | function fixFlowCSS() { 121 | return ''; 124 | } 125 | 126 | /** 127 | * As svg lines are overlayed based on positions of divs, when the divs move around 128 | * the lines need to be redrawn. 129 | */ 130 | function reJoinOnResize(pipeline) { 131 | $(window).resize(function(){ 132 | lines.autoJoin(pipeline); 133 | }); 134 | } 135 | 136 | 137 | 138 | 139 | /** 140 | * Originally we used this prototype snipped to create the edit button and clear 141 | * doens't play nice with the new stuff, but kept here for reference as 142 | * some of the behaviour stuff may need to be retrofitted to the jquery based code. 143 | * Behaviour.specify("INPUT.pipeline-editor", 'pipeline-editor-button', 0, function(e) { 144 | var script = e.next("input"); 145 | var json = script.next("input"); 146 | 147 | makeButton(e,function(_) { 148 | var pageBody = $('page-body'); 149 | var row = pageBody.down(".row"); 150 | 151 | row.style.display = "none"; 152 | 153 | pageBody.insert({bottom:"
pipeline-editor
"}); 154 | 155 | var canvas = pageBody.down("> .pipeline-editor"); 156 | var accept = canvas.down("> INPUT"); 157 | 158 | makeButton(accept,function(_){ 159 | // update fhe form values 160 | script.value = "..."; 161 | json.value = "..."; 162 | 163 | // kill the dialog 164 | canvas.remove(); 165 | row.style.display = "block"; 166 | }); 167 | }); 168 | }); 169 | */ 170 | -------------------------------------------------------------------------------- /src/main/js/steps/EXTENDING.md: -------------------------------------------------------------------------------- 1 | To extend and add more editors, take a look at an existing one. 2 | 3 | editors are an object that follows a signature like: 4 | 5 | ``` 6 | exports.editor = { 7 | description: "Clone a git repository", 8 | renderEditor : function(stepInfo, actionId) { 9 | // provide a form to edit 10 | var template = '
' + 11 | '' + 12 | '' + 13 | '
' + 14 | '
' + 15 | '' + 16 | '' + 17 | '
' + 18 | ''; 19 | 20 | return renderTemplate(template, stepInfo, { "actionId" : actionId }); 21 | }, 22 | 23 | readChanges : function(actionId, currentStep) { 24 | // read the changes from the form and apply them. 25 | // if it all checks out, return true, otherwise don't apply them, and return false. 26 | currentStep.name = $('#' + actionId + "_stepName").val(); 27 | currentStep.url = $('#' + actionId + "_url").val(); 28 | return true; //this will cause pipeline view to be re-rendered. 29 | }, 30 | 31 | generateScript : function(stepInfo){ 32 | return 'git ' + stepInfo.url; 33 | }, 34 | }; 35 | ``` 36 | 37 | it is up to each editor to render a form - in this case a template is used that reads value out of the stepInfo object. It also has to save changes back to the current step (in this case only name and url are saved or displayed). Finally the results are rendered as workflow script. 38 | 39 | // TODO: Get this info from Jenkins. See CJP-3974 40 | -------------------------------------------------------------------------------- /src/main/js/steps/archive.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | 5 | module.exports = { 6 | description: "Archive an artifact permanently", 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '

Store an artifact permanently as part of a build record.

' + 11 | '' + 12 | '' + 13 | '' + 14 | '' + 15 | '
' + 16 | '' + 17 | '' + 18 | '
' + 19 | '
'; 20 | var info = $.extend({"includes" : ""}, stepInfo); 21 | info = $.extend({"excludes" : ""}, info); 22 | return renderTemplate(template, info, { "actionId" : actionId }); 23 | }, 24 | 25 | readChanges : function(actionId, currentStep) { 26 | // read the changes from the form and apply them. 27 | // if it all checks out, return true, otherwise don't apply them, and return false. 28 | currentStep.name = $('#' + actionId + "_stepName").val(); 29 | currentStep.includes = $('#' + actionId + "_includes").val(); 30 | currentStep.excludes = $('#' + actionId + "_excludes").val(); 31 | return true; //this will cause pipeline view to be re-rendered. 32 | }, 33 | 34 | generateScript : function(stepInfo){ 35 | if (stepInfo.excludes === "") { 36 | return 'archive "' + stepInfo.includes + '"'; 37 | } 38 | return 'archive excludes: "' + stepInfo.excludes + '", includes: "' + stepInfo.includes + '"'; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/main/js/steps/env.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | module.exports = { 5 | description: "Set an environment variable", 6 | 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '' + 11 | '' + 12 | '' + 13 | '' + 14 | '

Environment variable will be available for subsequent steps.

' + 15 | '
' + 16 | '
' + 17 | '' + 18 | '' + 19 | '
' + 20 | ''; 21 | 22 | var info = $.extend({"envValue" : ""}, stepInfo); 23 | info = $.extend({"envName" : ""}, info); 24 | return renderTemplate(template, info, { "actionId" : actionId }); 25 | }, 26 | 27 | readChanges : function(actionId, currentStep) { 28 | // read the changes from the form and apply them. 29 | // if it all checks out, return true, otherwise don't apply them, and return false. 30 | currentStep.name = $('#' + actionId + "_stepName").val(); 31 | currentStep.envValue = $('#' + actionId + "_envValue").val(); 32 | currentStep.envName = $('#' + actionId + "_envName").val(); 33 | return true; //this will cause pipeline view to be re-rendered. 34 | }, 35 | 36 | generateScript : function(stepInfo){ 37 | if (stepInfo.envValue && stepInfo.envName && stepInfo.envName !== '') { 38 | return "env." + stepInfo.envName + " = '" + stepInfo.envValue + "'"; 39 | } else { 40 | return "// no environment name or variable set for step " + stepInfo.name; 41 | } 42 | 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/main/js/steps/git.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Basic git step. 3 | */ 4 | 5 | var renderTemplate = require('./template').renderTemplate; 6 | var $ = require('bootstrap-detached').getBootstrap(); 7 | 8 | 9 | module.exports = { 10 | description: "Clone a git repository", 11 | renderEditor : function(stepInfo, actionId) { 12 | // provide a form to edit 13 | var template = '
' + 14 | '' + 15 | '' + 16 | '
' + 17 | '
' + 18 | '' + 19 | '' + 20 | '
' + 21 | ''; 22 | 23 | return renderTemplate(template, stepInfo, { "actionId" : actionId }); 24 | }, 25 | 26 | readChanges : function(actionId, currentStep) { 27 | // read the changes from the form and apply them. 28 | // if it all checks out, return true, otherwise don't apply them, and return false. 29 | currentStep.name = $('#' + actionId + "_stepName").val(); 30 | currentStep.url = $('#' + actionId + "_url").val(); 31 | return true; //this will cause pipeline view to be re-rendered. 32 | }, 33 | 34 | generateScript : function(stepInfo){ 35 | return 'git \'' + stepInfo.url + '\''; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/main/js/steps/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Editor modules for the built in steps. 3 | * This just wraps up the editor modules in this directory 4 | * 5 | * The installEditors function is called by the main adjunct, but it doesn't really matter 6 | * where it is called from, as long as they are installed before they are needed. 7 | */ 8 | 9 | var editorModules = { 10 | "sh" : require('./shell'), 11 | "git" : require('./git'), 12 | "sleep" : require("./sleep"), 13 | "input" : require("./input"), 14 | "env" : require("./env"), 15 | "archive" : require("./archive"), 16 | "stash" : require('./stash'), 17 | "unstash" : require('./unstash'), 18 | "rick" : require('./rick'), 19 | "workflow" : require('./workflowScript') 20 | }; 21 | 22 | module.exports = editorModules; 23 | 24 | // TODO: Get this info from Jenkins. See CJP-3974 25 | -------------------------------------------------------------------------------- /src/main/js/steps/input.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | module.exports = { 5 | description: "Wait for user approval", 6 | 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '' + 11 | '' + 12 | '

A short question to ask the user to let them to decide if to proceed or not.

' + 13 | '
' + 14 | '
' + 15 | '' + 16 | '' + 17 | '
' + 18 | ''; 19 | 20 | var info = $.extend({"question" : ""}, stepInfo); 21 | return renderTemplate(template, info, { "actionId" : actionId }); 22 | }, 23 | 24 | /** return true if this should happen outside of a node block in the generated script */ 25 | isTopLevel : function(/* stepInfo */) { 26 | return true; 27 | }, 28 | 29 | readChanges : function(actionId, currentStep) { 30 | // read the changes from the form and apply them. 31 | // if it all checks out, return true, otherwise don't apply them, and return false. 32 | currentStep.name = $('#' + actionId + "_stepName").val(); 33 | currentStep.question = $('#' + actionId + "_question").val(); 34 | return true; //this will cause pipeline view to be re-rendered. 35 | }, 36 | 37 | generateScript : function(stepInfo){ 38 | if (stepInfo.question) { 39 | return "input '" + stepInfo.question + "'"; 40 | } else { 41 | return '// ' + stepInfo.name; 42 | } 43 | 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/main/js/steps/rick.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A rick rolling module. 3 | */ 4 | 5 | var renderTemplate = require('./template').renderTemplate; 6 | 7 | module.exports = { 8 | description: "Never going to give you up", 9 | renderEditor : function(stepInfo, actionId) { 10 | // provide a form to edit 11 | var template = '
' + 12 | '' + 14 | '
'; 15 | return renderTemplate(template, stepInfo, { "actionId" : actionId }); 16 | }, 17 | 18 | readChanges : function(actionId, currentStep) { 19 | console.log("current step for rick is" + currentStep + actionId); 20 | // read the changes from the form and apply them. 21 | // if it all checks out, return true, otherwise don't apply them, and return false. 22 | return true; //this will cause pipeline view to be re-rendered. 23 | }, 24 | 25 | generateScript : function(stepInfo){ 26 | console.log(stepInfo); 27 | return '/* you have been rick rolled */'; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/main/js/steps/shell.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | module.exports = { 5 | description: "Run a shell script", 6 | 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '' + 11 | '' + 12 | '

Run a command in the currently allocated build server. All standard shell scripting commands apply.

' + 13 | '
' + 14 | '
' + 15 | '' + 16 | '' + 17 | '
' + 18 | ''; 19 | 20 | var info = $.extend({"command" : ""}, stepInfo); 21 | return renderTemplate(template, info, { "actionId" : actionId }); 22 | }, 23 | 24 | readChanges : function(actionId, currentStep) { 25 | // read the changes from the form and apply them. 26 | // if it all checks out, return true, otherwise don't apply them, and return false. 27 | currentStep.name = $('#' + actionId + "_stepName").val(); 28 | currentStep.command = $('#' + actionId + "_command").val(); 29 | return true; //this will cause pipeline view to be re-rendered. 30 | }, 31 | 32 | generateScript : function(stepInfo){ 33 | if (stepInfo.command) { 34 | return "sh '''" + stepInfo.command.replace(/'/g, "\\'") + "'''"; 35 | } else { 36 | return '// no command set for step'; 37 | } 38 | 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/main/js/steps/sleep.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | module.exports = { 5 | description: "Wait for a specified time", 6 | 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '' + 11 | '' + 12 | '

Wait for the specified number of seconds.

' + 13 | '
' + 14 | '
' + 15 | '' + 16 | '' + 17 | '
' + 18 | ''; 19 | 20 | var info = $.extend({"sleepSeconds" : ""}, stepInfo); 21 | return renderTemplate(template, info, { "actionId" : actionId }); 22 | }, 23 | 24 | readChanges : function(actionId, currentStep) { 25 | // read the changes from the form and apply them. 26 | // if it all checks out, return true, otherwise don't apply them, and return false. 27 | currentStep.name = $('#' + actionId + "_stepName").val(); 28 | currentStep.sleepSeconds = $('#' + actionId + "_seconds").val(); 29 | return true; //this will cause pipeline view to be re-rendered. 30 | }, 31 | 32 | generateScript : function(stepInfo){ 33 | if (stepInfo.sleepSeconds) { 34 | return 'sleep ' + stepInfo.sleepSeconds; 35 | } else { 36 | return '// ' + stepInfo.name; 37 | } 38 | 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/main/js/steps/stash.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | 5 | module.exports = { 6 | description: "Stash artifacts temporarily", 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '' + 11 | '' + 12 | '

This name is used when you need to unstash onto a new node.

' + 13 | '
' + 14 | '
' + 15 | '' + 16 | '' + 17 | '' + 18 | '' + 19 | '
' + 20 | ''; 21 | 22 | return renderTemplate(template, stepInfo, { "actionId" : actionId }); 23 | }, 24 | 25 | readChanges : function(actionId, currentStep) { 26 | // read the changes from the form and apply them. 27 | // if it all checks out, return true, otherwise don't apply them, and return false. 28 | currentStep.name = $('#' + actionId + "_name").val(); 29 | currentStep.includes = $('#' + actionId + "_includes").val(); 30 | currentStep.excludes = $('#' + actionId + "_excludes").val(); 31 | return true; //this will cause pipeline view to be re-rendered. 32 | }, 33 | 34 | generateScript : function(stepInfo){ 35 | return 'stash name: "' + stepInfo.name + '", includes: "' + stepInfo.includes + '", excludes: "' + stepInfo.excludes + '"'; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/main/js/steps/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templating in a few bytes eh. 3 | */ 4 | exports.renderTemplate = function(template, values, moreValues) { 5 | var result = template; 6 | var key; 7 | for (key in values) { 8 | result = result.split("{{" + key + "}}").join(values[key]); 9 | } 10 | if (moreValues) { 11 | for (key in moreValues) { 12 | result = result.split("{{" + key + "}}").join(moreValues[key]); 13 | } 14 | } 15 | return result; 16 | }; 17 | -------------------------------------------------------------------------------- /src/main/js/steps/unstash.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | 5 | module.exports = { 6 | description: "Unstash temporary artifacts", 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '' + 11 | '' + 12 | '

This is the name that was used to temporarily stash artifacts.

' + 13 | '
' + 14 | ''; 15 | 16 | return renderTemplate(template, stepInfo, { "actionId" : actionId }); 17 | }, 18 | 19 | readChanges : function(actionId, currentStep) { 20 | // read the changes from the form and apply them. 21 | // if it all checks out, return true, otherwise don't apply them, and return false. 22 | currentStep.name = $('#' + actionId + "_name").val(); 23 | return true; //this will cause pipeline view to be re-rendered. 24 | }, 25 | 26 | generateScript : function(stepInfo){ 27 | return 'unstash "' + stepInfo.name + '"'; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/main/js/steps/workflowScript.js: -------------------------------------------------------------------------------- 1 | var renderTemplate = require('./template').renderTemplate; 2 | var $ = require('bootstrap-detached').getBootstrap(); 3 | 4 | module.exports = { 5 | description: "Jenkins Workflow script", 6 | 7 | renderEditor : function(stepInfo, actionId) { 8 | // provide a form to edit 9 | var template = '
' + 10 | '' + 11 | '' + 12 | '

Run any Jenkins Workflow script. See some examples.

' + 13 | '
' + 14 | '
' + 15 | '' + 16 | '' + 17 | '
' + 18 | ''; 19 | 20 | var info = $.extend({"scriptSnippet" : ""}, stepInfo); 21 | return renderTemplate(template, info, { "actionId" : actionId }); 22 | }, 23 | 24 | /** return true if this should happen outside of a node block in the generated script */ 25 | isTopLevel : function(/* stepInfo */) { 26 | return true; 27 | }, 28 | 29 | 30 | readChanges : function(actionId, currentStep) { 31 | // read the changes from the form and apply them. 32 | // if it all checks out, return true, otherwise don't apply them, and return false. 33 | currentStep.name = $('#' + actionId + "_stepName").val(); 34 | currentStep.scriptSnippet = $('#' + actionId + "_command").val(); 35 | return true; //this will cause pipeline view to be re-rendered. 36 | }, 37 | 38 | generateScript : function(stepInfo){ 39 | if (stepInfo.scriptSnippet) { 40 | return stepInfo.scriptSnippet; 41 | } else { 42 | return '// no workflow script set for step'; 43 | } 44 | 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/main/js/svg/lines.js: -------------------------------------------------------------------------------- 1 | var Belay = require('./svg'); 2 | var wf = require('../model/workflow'); 3 | 4 | /** 5 | * Join up the pipeline elements visually allowing for parallelism. 6 | * 7 | * from a pipeline that looks logically like: 8 | * ["stage-0", ["stage-1-0", "stage-1-1"], "stage-2"] 9 | * 10 | * Becomes: 11 | * 12 | * /[]\ 13 | * [] -- --[] 14 | * \[]/ 15 | * 16 | */ 17 | function autoJoin(pipeline) { 18 | Belay.off(); 19 | var previousPils = []; 20 | for (var i=0; i < pipeline.length; i++) { 21 | var stage = pipeline[i]; 22 | var currentId = "stage-" + i; 23 | if (!wf.isParallelStage(stage)) { 24 | joinWith(previousPils, currentId); 25 | previousPils = [currentId]; 26 | } else { 27 | var currentPils = []; 28 | for (j = 0; j < stage.streams.length; j++) { 29 | currentPils[j] = currentId + "-" + j; 30 | } 31 | for (var j=0; j < stage.streams.length; ++j) { 32 | if (previousPils.length === 1) { 33 | joinWith(previousPils, currentPils[j]); 34 | } 35 | } 36 | previousPils = currentPils; 37 | } 38 | } 39 | } 40 | 41 | exports.autoJoin = autoJoin; 42 | 43 | 44 | /** 45 | * Draw the connecting lines using SVG and the div ids. 46 | */ 47 | function joinWith(pilList, currentId) { 48 | for (var i = 0; i < pilList.length; i++) { 49 | Belay.on("#" + pilList[i], "#" + currentId); 50 | } 51 | } 52 | 53 | 54 | /** 55 | * Wait until the steps are expanded before joining them together again 56 | */ 57 | exports.autoJoinDelay = function(pipeline, delay) { 58 | if (delay === undefined) { 59 | delay = 500; 60 | } 61 | if (delay > 0) { 62 | Belay.off(); 63 | setTimeout(function() { 64 | autoJoin(pipeline); 65 | }, delay); 66 | } else { 67 | autoJoin(pipeline); 68 | } 69 | }; 70 | 71 | /** 72 | * Before SVG can be used need to set it up. Only needed once per whole page refresh. 73 | */ 74 | exports.initSVG = function() { 75 | Belay.init({strokeWidth: 2}); 76 | Belay.set('strokeColor', '#999'); 77 | }; 78 | -------------------------------------------------------------------------------- /src/main/js/svg/svg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use SVG to draw lines between adjacent divs. 3 | */ 4 | 5 | var $ = require('bootstrap-detached').getBootstrap(); 6 | 7 | var settings = { 8 | strokeColor : '#fff', 9 | strokeWidth : 10, 10 | opacity : 1, 11 | fill : 'none', 12 | animate : true, 13 | animationDirection: 'right', 14 | animationDuration : 0.3 15 | }; 16 | 17 | 18 | exports.init = function(initObj) { 19 | if (initObj) { 20 | $.each(initObj, function(index, value) { 21 | //TODO validation on settings 22 | settings[index] = value; 23 | }); 24 | } 25 | }; 26 | 27 | exports.set = function(prop, val){ 28 | //TODO validate 29 | settings[prop] = val; 30 | }; 31 | 32 | exports.on = function(el1, el2){ 33 | var $el1 = $(el1); 34 | var $el2 = $(el2); 35 | if ($el1.length && $el2.length) { 36 | var svgheight, 37 | p, 38 | svgleft, 39 | svgtop, 40 | svgwidth; 41 | 42 | var el1pos = $(el1).offset(); 43 | var el2pos = $(el2).offset(); 44 | 45 | var el1H = $(el1).outerHeight(); 46 | var el1W = $(el1).outerWidth(); 47 | 48 | var el2H = $(el2).outerHeight(); 49 | 50 | svgleft = Math.round(el1pos.left + el1W); 51 | svgwidth = Math.round(el2pos.left - svgleft); 52 | 53 | var cpt; 54 | 55 | ////Determine which is higher/lower 56 | if( (el2pos.top+(el2H/2)) <= ( el1pos.top+(el1H/2))){ 57 | // console.log("low to high"); 58 | svgheight = Math.round((el1pos.top+el1H/2) - (el2pos.top+el2H/2)); 59 | svgtop = Math.round(el2pos.top + el2H/2) - settings.strokeWidth; 60 | cpt = Math.round(svgwidth*Math.min(svgheight/300, 1)); 61 | p = "M0,"+ (svgheight+settings.strokeWidth) +" C"+cpt+","+(svgheight+settings.strokeWidth)+" "+(svgwidth-cpt)+"," + settings.strokeWidth + " "+svgwidth+"," + settings.strokeWidth; 62 | }else{ 63 | // console.log("high to low"); 64 | svgheight = Math.round((el2pos.top+el2H/2) - (el1pos.top+el1H/2)); 65 | svgtop = Math.round(el1pos.top + el1H/2) - settings.strokeWidth; 66 | cpt = Math.round(svgwidth*Math.min(svgheight/300, 1)); 67 | p = "M0," + settings.strokeWidth + " C"+ cpt +",0 "+ (svgwidth-cpt) +","+(svgheight+settings.strokeWidth)+" "+svgwidth+","+(svgheight+settings.strokeWidth); 68 | } 69 | 70 | //ugly one-liner 71 | var $ropebag = $('#ropebag').length ? $('#ropebag') : $('body').append($( "
" )).find('#ropebag'); 72 | 73 | var svgnode = document.createElementNS('http://www.w3.org/2000/svg','svg'); 74 | var newpath = document.createElementNS('http://www.w3.org/2000/svg',"path"); 75 | newpath.setAttributeNS(null, "d", p); 76 | newpath.setAttributeNS(null, "stroke", settings.strokeColor); 77 | newpath.setAttributeNS(null, "stroke-width", settings.strokeWidth); 78 | newpath.setAttributeNS(null, "opacity", settings.opacity); 79 | newpath.setAttributeNS(null, "fill", settings.fill); 80 | svgnode.appendChild(newpath); 81 | //for some reason, adding a min-height to the svg div makes the lines appear more correctly. 82 | $(svgnode).css({left: svgleft, top: svgtop, position: 'absolute',width: svgwidth, height: svgheight + settings.strokeWidth*2, minHeight: '20px' }); 83 | $ropebag.append(svgnode); 84 | if (settings.animate) { 85 | // THANKS to http://jakearchibald.com/2013/animated-line-drawing-svg/ 86 | var pl = newpath.getTotalLength(); 87 | // Set up the starting positions 88 | newpath.style.strokeDasharray = pl + ' ' + pl; 89 | 90 | if (settings.animationDirection === 'right') { 91 | newpath.style.strokeDashoffset = pl; 92 | } else { 93 | newpath.style.strokeDashoffset = -pl; 94 | } 95 | 96 | // Trigger a layout so styles are calculated & the browser 97 | // picks up the starting position before animating 98 | // WON'T WORK IN IE. If you want that, use requestAnimationFrame to update instead of CSS animation 99 | newpath.getBoundingClientRect(); 100 | newpath.style.transition = newpath.style.WebkitTransition ='stroke-dashoffset ' + settings.animationDuration + 's ease-in-out'; 101 | // Go! 102 | newpath.style.strokeDashoffset = '0'; 103 | } 104 | } 105 | }; 106 | 107 | exports.off = function(){ 108 | $("#ropebag").empty(); 109 | }; 110 | -------------------------------------------------------------------------------- /src/main/js/templates/editor-popover.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{{editorHtml}}} 3 |
4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/js/templates/normal-stage-block.hbs: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 | -------------------------------------------------------------------------------- /src/main/js/templates/parallel-stack.hbs: -------------------------------------------------------------------------------- 1 |
2 |
    {{{subStages}}} {{{addStreamButton}}}
3 |
4 | -------------------------------------------------------------------------------- /src/main/js/templates/parallel-stage-block.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 | 14 |
  • 15 | -------------------------------------------------------------------------------- /src/main/js/templates/stage-block.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    -------------------------------------------------------------------------------- /src/main/js/templates/stage-button.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 5 |
    6 |
    -------------------------------------------------------------------------------- /src/main/js/templates/stage-config-block.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | 6 |
    7 | 8 |
    9 | 10 |
    11 |

    You can convert this sequential block to a set of parallel branches:

    12 | 13 |

    14 |

    Delete whole stage and all its steps:

    15 | 16 | 17 |
    18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/js/templates/step-block.hbs: -------------------------------------------------------------------------------- 1 | {{#each steps}} 2 |
    3 | 6 |
    7 | {{/each}} 8 |
    9 | 10 | 11 |
    -------------------------------------------------------------------------------- /src/main/js/templates/steps-listing.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{#each steps}} 4 | 5 |
    6 | {{/each}} 7 |
    8 | 11 |
    12 |
    13 | -------------------------------------------------------------------------------- /src/main/js/templates/stream-block.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    -------------------------------------------------------------------------------- /src/main/js/templates/stream-button.hbs: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    6 | -------------------------------------------------------------------------------- /src/main/js/templates/stream-config-block.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | 6 |
    7 | 8 | 9 |
    10 | 11 |
    12 | 13 |
    14 | 15 |
    16 |

    You can convert this parallel set of branches to a single stage:

    17 | 18 |

    19 |

    Delete whole stage, or just the current branch:

    20 | 21 | 22 | 23 |
    24 | -------------------------------------------------------------------------------- /src/main/less/pipelineeditor.less: -------------------------------------------------------------------------------- 1 | #pipeline-visual-editor { 2 | .stage-block { 3 | position: relative; 4 | 5 | .open-stage-config { 6 | position: absolute; 7 | top: 10px; 8 | right: 15px; 9 | } 10 | 11 | .open-stream-config { 12 | position: absolute; 13 | top: 10px; 14 | right: 15px; 15 | } 16 | 17 | 18 | } 19 | 20 | .stage-block.collapsed { 21 | cursor: pointer; 22 | } 23 | 24 | button.btn-block { 25 | text-align: left; 26 | } 27 | 28 | .step-listing { 29 | margin-top: 10px; 30 | 31 | button.btn-block { 32 | margin-top: 5px; 33 | } 34 | .selected { 35 | color: #fff; 36 | background-color: #5bc0de; 37 | border-color: #46b8da; 38 | } 39 | } 40 | 41 | .opaque-text { 42 | opacity: 0.4; 43 | } 44 | 45 | .step-list .popover { 46 | max-width: none; 47 | width:500px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 5 |
    6 | Edit pipeline visually. 7 |
    8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/plugins/pipelineeditor/WorkflowVisualEditor/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/test/js/editor-spec.js: -------------------------------------------------------------------------------- 1 | 2 | var jsTest = require('jenkins-js-test'); 3 | 4 | describe('Editor controller basics', function() { 5 | 6 | it('should show the name', function (done) { 7 | jsTest.onPage(function() { 8 | var e = jsTest.requireSrcModule("editor"); 9 | var block = e.normalStageBlock("idvaluehere", {"name":"myname"}); 10 | expect(block.indexOf("myname")).not.toBe(-1); 11 | expect(block.indexOf("idvaluehere")).not.toBe(-1); 12 | done(); 13 | }); 14 | }); 15 | 16 | it('should list steps', function (done) { 17 | jsTest.onPage(function() { 18 | var e = jsTest.requireSrcModule("editor"); 19 | var stage = {"name" : "yeah", "steps" : [ 20 | {"type": "git", "name" : "Clone webapp", "url" : "git@github.com/thing/awesome.git"}, 21 | {"type": "git", "name" : "Clone webapp2", "url" : "git@github.com/thing/awesome.git"} 22 | ] 23 | }; 24 | var block = e.normalStageBlock("ignore", stage); 25 | expect(block.indexOf("Clone webapp")).not.toBe(-1); 26 | expect(block.indexOf("Clone webapp2")).not.toBe(-1); 27 | done(); 28 | }); 29 | }); 30 | 31 | 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /src/test/js/json-spec.js: -------------------------------------------------------------------------------- 1 | 2 | var jsTest = require('jenkins-js-test'); 3 | var storage = jsTest.requireSrcModule("model/json"); 4 | var assert = require("assert"); 5 | 6 | 7 | describe('json storage basics', function() { 8 | it('should default pipeline', function () { 9 | assert.deepEqual(storage.simpleSample, storage.loadModelOrUseDefault("")); 10 | assert.deepEqual(storage.simpleSample, storage.loadModelOrUseDefault(null)); 11 | }); 12 | it('should load the pipeline', function () { 13 | assert.deepEqual([], storage.loadModelOrUseDefault("[]")); 14 | }); 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /src/test/js/steps-spec.js: -------------------------------------------------------------------------------- 1 | var jsTest = require('jenkins-js-test'); 2 | var renderTemplate = jsTest.requireSrcModule("steps/template.js").renderTemplate; 3 | var assert = require("assert"); 4 | 5 | 6 | describe('Super simple template', function() { 7 | 8 | it('should render a template', function () { 9 | assert.equal("yeah 42 yeah", renderTemplate("yeah {{something}} yeah", {"something" : 42})); 10 | assert.equal("yeah {{something}} yeah", renderTemplate("yeah {{something}} yeah", {})); 11 | assert.equal("yeah 2 yeah", renderTemplate("yeah 2 yeah", {"something" : 42})); 12 | assert.equal("yeah 1 2", renderTemplate("yeah {{s1}} {{s2}}", {"s1" : 1, "s2": 2})); 13 | assert.equal("yeah 1 2", renderTemplate("yeah {{s1}} {{s2}}", {"s1" : 1}, {"s2": 2})); 14 | }); 15 | } 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/test/js/steps/shell-spec.js: -------------------------------------------------------------------------------- 1 | 2 | var jsTest = require('jenkins-js-test'); 3 | 4 | var assert = require("assert"); 5 | 6 | 7 | describe('json storage basics', function() { 8 | it('should render shell', function (done) { 9 | jsTest.onPage(function() { 10 | var sh = jsTest.requireSrcModule("steps/shell"); 11 | assert.equal("sh '''yeah'''", sh.generateScript({'command' : "yeah"})); 12 | done(); 13 | }); 14 | 15 | }); 16 | 17 | it('should escape single quotes', function (done) { 18 | jsTest.onPage(function() { 19 | var sh = jsTest.requireSrcModule("steps/shell"); 20 | assert.equal("sh '''ye\\'ah'''", sh.generateScript({'command' : "ye'ah"})); 21 | done(); 22 | }); 23 | }); 24 | 25 | 26 | it('should handle no command', function (done) { 27 | jsTest.onPage(function() { 28 | var sh = jsTest.requireSrcModule("steps/shell"); 29 | assert.equal("// no command set for step", sh.generateScript({})); 30 | done(); 31 | }); 32 | }); 33 | 34 | 35 | it('should have default empty shell command', function (done) { 36 | jsTest.onPage(function() { 37 | var sh = jsTest.requireSrcModule("steps/shell"); 38 | var block = sh.renderEditor({}, "1234"); 39 | expect(block.indexOf("{{command}}")).toBe(-1); 40 | done(); 41 | }); 42 | }); 43 | 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /src/test/js/workflow-spec.js: -------------------------------------------------------------------------------- 1 | var jsTest = require('jenkins-js-test'); 2 | var assert = require("assert"); 3 | 4 | describe('Workflow rendering', function() { 5 | 6 | it('should know if parallel stage', function (done) { 7 | jsTest.onPage(function() { 8 | var wf = jsTest.requireSrcModule("model/workflow"); 9 | var stage = {"name": "yeah", "streams": [ 10 | {"name": "Clone webapp"} 11 | ] 12 | }; 13 | assert.equal(true, wf.isParallelStage(stage)); 14 | stage = {"name": "yeah", "steps": [ 15 | {"name": "Clone webapp"} 16 | ] 17 | }; 18 | assert.equal(false, wf.isParallelStage(stage)); 19 | done(); 20 | }); 21 | }); 22 | 23 | 24 | it('should render script', function(done) { 25 | jsTest.onPage(function() { 26 | var wf = jsTest.requireSrcModule("model/workflow"); 27 | var pipe = [ 28 | { 29 | "name" : "Prepare", 30 | "streams" : [ 31 | {"name" : "Ruby", "steps" : [ 32 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"}, 33 | {"type": "stash", "name" : "Stash compiled app", "includes": "/app", "excludes" : ""} 34 | ]}, 35 | {"name" : "Python","steps" : [ 36 | {"type": "sh", "name" : "Yeah", "command" : "exit()"}, 37 | 38 | ]} 39 | ] 40 | }, 41 | { 42 | "name" : "Do It", 43 | "steps" : [ 44 | {"type": "git", "url" : "git@thing.com/yeah"} 45 | ] 46 | } 47 | ]; 48 | 49 | var editorModules = jsTest.requireSrcModule('steps'); 50 | 51 | var script = wf.toWorkflow(pipe, editorModules); 52 | assert.notEqual(-1, script.indexOf("/app")); 53 | assert.notEqual(-1, script.indexOf("git 'git@thing.com/yeah'")); 54 | done(); 55 | }); 56 | }); 57 | 58 | 59 | }); 60 | 61 | describe('Add steps to stages', function() { 62 | it('should work out coordinates correctly from stageId', function(done) { 63 | jsTest.onPage(function() { 64 | var wf = jsTest.requireSrcModule("model/workflow"); 65 | assert.deepEqual([0], wf.stageIdToCoordinates("stage-0")); 66 | assert.deepEqual([1,0], wf.stageIdToCoordinates("stage-1-0")); 67 | done(); 68 | }); 69 | }); 70 | 71 | it('should insert into non parallel stage', function(done) { 72 | jsTest.onPage(function() { 73 | var wf = jsTest.requireSrcModule("model/workflow"); 74 | var pipe = [ 75 | { 76 | "name" : "Do It", 77 | "steps" : [] 78 | } 79 | ]; 80 | assert.equal(0, pipe[0].steps.length); 81 | var newStep = {"type" : "sh", "command" :"x", "name": "foo"}; 82 | var insertResult = wf.insertStep(pipe, "stage-0", newStep); 83 | assert.equal("stage-0-0", insertResult.actionId); 84 | assert.equal(1, pipe[0].steps.length); 85 | assert.deepEqual(newStep, pipe[0].steps[0]); 86 | 87 | assert.equal("stage-0-1", wf.insertStep(pipe, "stage-0", newStep).actionId); 88 | done(); 89 | }); 90 | }); 91 | 92 | it('should insert into parallel stage', function(done) { 93 | jsTest.onPage(function() { 94 | var wf = jsTest.requireSrcModule("model/workflow"); 95 | var pipe = [ 96 | { 97 | "name" : "first stage" 98 | }, 99 | { 100 | "name" : "Do It", 101 | "streams" : [ 102 | {"name" : "Unit", "steps" : [ 103 | {"type": "sh", "name" : "Run unit test suit", "command" : "/bin/ci/test"}, 104 | ] 105 | } 106 | ] 107 | } 108 | ]; 109 | assert.equal(1, pipe[1].streams[0].steps.length); 110 | var newStep = {"type" : "sh", "command" :"x", "name": "foo"}; 111 | var insertResult = wf.insertStep(pipe, "stage-1-0", newStep); 112 | assert.equal("stage-1-0-1", insertResult.actionId); 113 | assert.equal(2, pipe[1].streams[0].steps.length); 114 | done(); 115 | }); 116 | }); 117 | 118 | 119 | 120 | }); 121 | 122 | describe('Find things in the pipeline array', function() { 123 | it('should resolve actionId', function (done) { 124 | jsTest.onPage(function() { 125 | var wf = jsTest.requireSrcModule("model/workflow"); 126 | assert.deepEqual([0,1,2], wf.actionIdToStep("stage-0-1-2")); 127 | assert.deepEqual([0,1], wf.actionIdToStep("stage-0-1")); 128 | assert.deepEqual([1,1], wf.actionIdToStep("stage-1-1")); 129 | done(); 130 | }); 131 | }); 132 | 133 | 134 | it('should resolve actionId', function (done) { 135 | jsTest.onPage(function() { 136 | var wf = jsTest.requireSrcModule("model/workflow"); 137 | assert.deepEqual([0,1,2], wf.actionIdToStep("stage-0-1-2")); 138 | assert.deepEqual([0,1], wf.actionIdToStep("stage-0-1")); 139 | assert.deepEqual([1,1], wf.actionIdToStep("stage-1-1")); 140 | done(); 141 | }); 142 | }); 143 | 144 | 145 | it('should find the step info', function (done) { 146 | jsTest.onPage(function() { 147 | var wf = jsTest.requireSrcModule("model/workflow"); 148 | var pipeline = [ 149 | { 150 | "name" : "Checkout", 151 | "steps" : [ 152 | {"name" : "Clone webapp"}, 153 | {"name" : "Hair on fire"} 154 | ] 155 | }, 156 | 157 | { 158 | "name" : "Prepare", 159 | "streams" : [ 160 | {"name" : "Ruby", "steps" : [ 161 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"} 162 | ] 163 | } 164 | ] 165 | }]; 166 | var s1 = wf.fetchStep([0,0], pipeline); 167 | var s2 = wf.fetchStep([0,1], pipeline); 168 | var s3 = wf.fetchStep([1,0,0], pipeline); 169 | assert.equal("Clone webapp", s1.name); 170 | assert.equal("Hair on fire", s2.name); 171 | assert.equal("Install Ruby", s3.name); 172 | done(); 173 | }); 174 | }); 175 | 176 | it('should combine parallel to regular', function(done) { 177 | jsTest.onPage(function() { 178 | var wf = jsTest.requireSrcModule("model/workflow"); 179 | var pipeline = [ 180 | { 181 | "name" : "Prepare", 182 | "streams" : [ 183 | {"name" : "Ruby", "steps" : [ 184 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"} 185 | ] 186 | }, 187 | {"name" : "Ruby", "steps" : [ 188 | {"type": "git", "name" : "Another thing", "command" : "/bin/ci/install_ruby version=2.0.1"} 189 | ] 190 | } 191 | ] 192 | } 193 | ]; 194 | 195 | wf.parallelToNormal(pipeline, "stage-0"); 196 | 197 | var stage = pipeline[0]; 198 | assert.equal(2, stage.steps.length); 199 | assert.equal(undefined, stage.streams); 200 | 201 | assert.equal('sh', pipeline[0].steps[0].type); 202 | assert.equal('Install Ruby', pipeline[0].steps[0].name); 203 | 204 | done(); 205 | }); 206 | 207 | }); 208 | 209 | it('should remove a step from the pipeline', function(done) { 210 | jsTest.onPage(function() { 211 | var wf = jsTest.requireSrcModule("model/workflow"); 212 | var pipeline = [ 213 | { 214 | "name" : "Prepare", 215 | "steps" : [ 216 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"}, 217 | {"type": "git", "name" : "Another thing", "command" : "/bin/ci/install_ruby version=2.0.1"} 218 | ] 219 | } 220 | ]; 221 | assert.equal(2, pipeline[0].steps.length); 222 | wf.removeActionId(pipeline, "stage-0-0"); 223 | assert.equal(1, pipeline[0].steps.length); 224 | var expect = {"type": "git", "name" : "Another thing", "command" : "/bin/ci/install_ruby version=2.0.1"}; 225 | assert.deepEqual([expect], pipeline[0].steps); 226 | 227 | pipeline = [ 228 | { 229 | "name" : "Prepare", 230 | "streams" : [ 231 | {"name" : "Ruby", "steps" : [ 232 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"} 233 | ] 234 | }, 235 | {"name" : "Ruby", "steps" : [ 236 | {"type": "git", "name" : "Another thing", "command" : "/bin/ci/install_ruby version=2.0.1"} 237 | ] 238 | } 239 | ] 240 | } 241 | ]; 242 | 243 | assert.equal(1, pipeline[0].streams[0].steps.length); 244 | wf.removeActionId(pipeline, "stage-0-0-0"); 245 | assert.equal(0, pipeline[0].streams[0].steps.length); 246 | 247 | 248 | done(); 249 | }); 250 | }); 251 | 252 | it('should remove a stage from the pipeline', function(done) { 253 | jsTest.onPage(function() { 254 | var wf = jsTest.requireSrcModule("model/workflow"); 255 | var pipeline = [ 256 | { 257 | "name" : "Prepare", 258 | "steps" : [ 259 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"}, 260 | {"type": "git", "name" : "Another thing", "command" : "/bin/ci/install_ruby version=2.0.1"} 261 | ] 262 | } 263 | ]; 264 | wf.removeStage(pipeline, "stage-0"); 265 | assert.equal(0, pipeline.length); 266 | 267 | pipeline = [ 268 | { 269 | "name" : "Prepare", 270 | "streams" : [ 271 | {"name" : "Ruby", "steps" : [ 272 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"} 273 | ] 274 | }, 275 | {"name" : "Ruby", "steps" : [ 276 | {"type": "git", "name" : "Another thing", "command" : "/bin/ci/install_ruby version=2.0.1"} 277 | ] 278 | } 279 | ] 280 | } 281 | ]; 282 | 283 | 284 | assert.equal(2, pipeline[0].streams.length); 285 | wf.removeStage(pipeline, "stage-0-0"); 286 | assert.equal(1, pipeline[0].streams.length); 287 | 288 | done(); 289 | }); 290 | }); 291 | 292 | it('should convert to parallel automatically and toggle', function(done) { 293 | jsTest.onPage(function() { 294 | var wf = jsTest.requireSrcModule("model/workflow"); 295 | var pipeline = [ 296 | { 297 | "name" : "Prepare", 298 | "steps" : [ 299 | {"type": "sh", "name" : "Install Ruby", "command" : "/bin/ci/install_ruby version=2.0.1"}, 300 | {"type": "git", "name" : "Another thing", "command" : "/bin/ci/install_ruby version=2.0.1"} 301 | ] 302 | } 303 | ]; 304 | 305 | wf.makeParallel(pipeline, "stage-0"); 306 | 307 | var stage = pipeline[0]; 308 | assert.equal(undefined, stage.steps); 309 | assert.equal(2, stage.streams.length); 310 | 311 | assert.equal("Install Ruby", stage.streams[0].name); 312 | assert.equal("Install Ruby", stage.streams[0].steps[0].name); 313 | 314 | wf.toggleParallel(pipeline, "stage-0"); 315 | assert.equal(undefined, stage.streams); 316 | assert.equal(2, stage.steps.length); 317 | 318 | wf.toggleParallel(pipeline, "stage-0"); 319 | assert.equal(undefined, stage.steps); 320 | assert.equal(2, stage.streams.length); 321 | 322 | 323 | done(); 324 | }); 325 | 326 | }); 327 | 328 | 329 | 330 | 331 | }); 332 | --------------------------------------------------------------------------------