diff --git a/src/main/java/se/diabol/jenkins/pipeline/DeliveryPipelineView.java b/src/main/java/se/diabol/jenkins/pipeline/DeliveryPipelineView.java index d115dd05f..93400ed8f 100644 --- a/src/main/java/se/diabol/jenkins/pipeline/DeliveryPipelineView.java +++ b/src/main/java/se/diabol/jenkins/pipeline/DeliveryPipelineView.java @@ -103,6 +103,7 @@ public class DeliveryPipelineView extends View implements PipelineView { private boolean showTotalBuildTime = false; private boolean allowRebuild = false; private boolean allowPipelineStart = false; + private boolean allowAbort = false; private boolean showDescription = false; private boolean showPromotions = false; private boolean showTestResults = false; @@ -211,6 +212,15 @@ public void setAllowPipelineStart(boolean allowPipelineStart) { this.allowPipelineStart = allowPipelineStart; } + @Exported + public boolean isAllowAbort() { + return allowAbort; + } + + public void setAllowAbort(boolean allowAbort) { + this.allowAbort = allowAbort; + } + @Exported public boolean isAllowManualTriggers() { return allowManualTriggers; @@ -482,6 +492,20 @@ public void triggerRebuild(String projectName, String buildId) { build.getAction(ParametersAction.class)); } + @Override + public void abortBuild(String projectName, String buildId) throws TriggerException { + AbstractProject project = ProjectUtil.getProject(projectName, Jenkins.getInstance()); + if (!project.hasPermission(Item.CANCEL)) { + throw new BadCredentialsException("Not authorized to abort build"); + } + AbstractBuild build = project.getBuildByNumber(Integer.parseInt(buildId)); + try { + build.doStop(); + } catch (IOException | ServletException e) { + throw new TriggerException("Could not abort build"); + } + } + protected static String triggerExceptionMessage(final String projectName, final String upstreamName, final String buildId) { String message = "Could not trigger manual build " + projectName + " for upstream " + upstreamName diff --git a/src/main/java/se/diabol/jenkins/pipeline/PipelineApi.java b/src/main/java/se/diabol/jenkins/pipeline/PipelineApi.java index 131a159f2..95e8758ab 100644 --- a/src/main/java/se/diabol/jenkins/pipeline/PipelineApi.java +++ b/src/main/java/se/diabol/jenkins/pipeline/PipelineApi.java @@ -46,7 +46,7 @@ public void doManualStep(StaplerRequest request, StaplerResponse response, @QueryParameter String project, @QueryParameter String upstream, - @QueryParameter String buildId) throws IOException, ServletException { + @QueryParameter String buildId) { if (project != null && upstream != null && buildId != null) { try { view.triggerManual(project, upstream, buildId); @@ -65,7 +65,7 @@ public void doManualStep(StaplerRequest request, public void doRebuildStep(StaplerRequest request, StaplerResponse response, @QueryParameter String project, - @QueryParameter String buildId) throws IOException, ServletException { + @QueryParameter String buildId) { if (project != null && buildId != null) { try { view.triggerRebuild(project, buildId); @@ -87,4 +87,22 @@ public void doInputStep(StaplerRequest request, doManualStep(request, response, project, upstream, buildId); } + @SuppressWarnings("UnusedDeclaration") + public void doAbortBuild(StaplerRequest request, + StaplerResponse response, + @QueryParameter String project, + @QueryParameter String buildId) { + if (project != null && buildId != null) { + try { + view.abortBuild(project, buildId); + } catch (AuthenticationException e) { + response.setStatus(SC_FORBIDDEN); + } catch (TriggerException e) { + response.setStatus(SC_NOT_ACCEPTABLE); + } + } else { + response.setStatus(SC_NOT_ACCEPTABLE); + } + } + } diff --git a/src/main/java/se/diabol/jenkins/pipeline/PipelineView.java b/src/main/java/se/diabol/jenkins/pipeline/PipelineView.java index 40eed878d..434f12563 100644 --- a/src/main/java/se/diabol/jenkins/pipeline/PipelineView.java +++ b/src/main/java/se/diabol/jenkins/pipeline/PipelineView.java @@ -26,4 +26,6 @@ void triggerManual(String projectName, String upstreamName, String buildId) throws TriggerException, AuthenticationException; void triggerRebuild(String projectName, String buildId); + + void abortBuild(String projectName, String buildId) throws TriggerException, AuthenticationException; } diff --git a/src/main/java/se/diabol/jenkins/workflow/WorkflowPipelineView.java b/src/main/java/se/diabol/jenkins/workflow/WorkflowPipelineView.java index 4f828ce9e..48f816625 100644 --- a/src/main/java/se/diabol/jenkins/workflow/WorkflowPipelineView.java +++ b/src/main/java/se/diabol/jenkins/workflow/WorkflowPipelineView.java @@ -36,6 +36,7 @@ import hudson.util.RunList; import jenkins.model.Jenkins; import org.acegisecurity.AuthenticationException; +import org.acegisecurity.BadCredentialsException; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.support.steps.input.InputAction; @@ -64,6 +65,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -85,6 +87,7 @@ public class WorkflowPipelineView extends View implements PipelineView { private int noOfColumns = 1; private String sorting = NONE_SORTER; private boolean allowPipelineStart = false; + private boolean allowAbort = false; private boolean showChanges = false; private String theme = DEFAULT_THEME; private int maxNumberOfVisiblePipelines = -1; @@ -152,6 +155,15 @@ public void setAllowPipelineStart(boolean allowPipelineStart) { this.allowPipelineStart = allowPipelineStart; } + @Exported + public boolean isAllowAbort() { + return allowAbort; + } + + public void setAllowAbort(boolean allowAbort) { + this.allowAbort = allowAbort; + } + public boolean isShowChanges() { return showChanges; } @@ -303,8 +315,7 @@ public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOExcep } @Override - public void triggerManual(String projectName, String upstreamName, String buildId) - throws TriggerException, AuthenticationException { + public void triggerManual(String projectName, String upstreamName, String buildId) throws AuthenticationException { LOG.fine("Manual/Input step called for project: " + projectName + " and build id: " + buildId); WorkflowJob workflowJob; @@ -329,6 +340,23 @@ public void triggerRebuild(String projectName, String buildId) { LOG.log(Level.SEVERE, "Rebuild not implemented for workflow/pipeline projects"); } + @Override + public void abortBuild(String projectName, String buildId) throws TriggerException { + try { + WorkflowJob workflowJob = ProjectUtil.getWorkflowJob(projectName, getOwnerItemGroup()); + if (!workflowJob.hasAbortPermission()) { + throw new BadCredentialsException("Not authorized to abort build"); + } + RunList builds = workflowJob.getBuilds(); + Optional run = builds.stream() + .filter(r -> Integer.toString(r.getNumber()).equals(buildId)) + .findFirst(); + run.ifPresent(WorkflowRun::doStop); + } catch (PipelineException e) { + throw new TriggerException("Could not abort build"); + } + } + @Override public Collection getItems() { Set jobs = Sets.newHashSet(); diff --git a/src/main/resources/se/diabol/jenkins/pipeline/DeliveryPipelineView/configure-entries.jelly b/src/main/resources/se/diabol/jenkins/pipeline/DeliveryPipelineView/configure-entries.jelly index 67805b379..4be3eee2e 100644 --- a/src/main/resources/se/diabol/jenkins/pipeline/DeliveryPipelineView/configure-entries.jelly +++ b/src/main/resources/se/diabol/jenkins/pipeline/DeliveryPipelineView/configure-entries.jelly @@ -51,6 +51,10 @@ + + + + diff --git a/src/main/resources/se/diabol/jenkins/pipeline/DeliveryPipelineView/help-allowAbort.html b/src/main/resources/se/diabol/jenkins/pipeline/DeliveryPipelineView/help-allowAbort.html new file mode 100644 index 000000000..809d33a59 --- /dev/null +++ b/src/main/resources/se/diabol/jenkins/pipeline/DeliveryPipelineView/help-allowAbort.html @@ -0,0 +1,3 @@ +
+ Allow cancelling a running job from the delivery pipeline view. +
diff --git a/src/main/resources/se/diabol/jenkins/workflow/WorkflowPipelineView/configure-entries.jelly b/src/main/resources/se/diabol/jenkins/workflow/WorkflowPipelineView/configure-entries.jelly index adee9cb0c..6e858f63d 100644 --- a/src/main/resources/se/diabol/jenkins/workflow/WorkflowPipelineView/configure-entries.jelly +++ b/src/main/resources/se/diabol/jenkins/workflow/WorkflowPipelineView/configure-entries.jelly @@ -25,6 +25,9 @@ + + + diff --git a/src/main/resources/se/diabol/jenkins/workflow/WorkflowPipelineView/help-allowAbort.html b/src/main/resources/se/diabol/jenkins/workflow/WorkflowPipelineView/help-allowAbort.html new file mode 100644 index 000000000..809d33a59 --- /dev/null +++ b/src/main/resources/se/diabol/jenkins/workflow/WorkflowPipelineView/help-allowAbort.html @@ -0,0 +1,3 @@ +
+ Allow cancelling a running job from the delivery pipeline view. +
diff --git a/src/main/webapp/pipe.js b/src/main/webapp/pipe.js index 139a86389..e64905dd4 100644 --- a/src/main/webapp/pipe.js +++ b/src/main/webapp/pipe.js @@ -171,6 +171,14 @@ function pipelineUtils() { consoleLogLink = 'console'; } + var showAbortButton = false; + if (data.allowAbort) { + progressClass += ' task-abortable'; + if (progressClass.indexOf('task-progress-running') !== -1) { + showAbortButton = true; + } + } + html.push( '
' + '
' @@ -190,9 +198,18 @@ function pipelineUtils() { html.push('
'); } if (task.requiringInput) { + showAbortButton = true; html.push('
'); html.push('
'); } + if (showAbortButton) { + var projectName = component.fullJobName; + if (typeof projectName === "undefined") { + projectName = task.id; + } + html.push('
'); + html.push('
'); + } } html.push('
'); @@ -660,6 +677,34 @@ function specifyInput(taskId, project, buildId, viewUrl) { }); } +function abortBuild(taskId, project, buildId, viewUrl) { + Q('#abort-' + taskId).hide(); + var formData = {project: project, upstream: 'N/A', buildId: buildId}, before; + + var before; + if (crumb.value !== null && crumb.value !== '') { + console.info('Crumb found and will be added to request header'); + before = function(xhr){xhr.setRequestHeader(crumb.fieldName, crumb.value);} + } else { + console.info('Crumb not needed'); + before = function(xhr){} + } + + Q.ajax({ + url: rootURL + '/' + viewUrl + 'api/abortBuild', + type: 'POST', + data: formData, + beforeSend: before, + timeout: 20000, + success: function (data, textStatus, jqXHR) { + console.info('Successfully aborted build of ' + project + '!') + }, + error: function (jqXHR, textStatus, errorThrown) { + window.alert('Could not abort build! error: ' + errorThrown + ' status: ' + textStatus) + } + }); +} + function triggerParameterizedBuild(url, taskId) { console.info('Job is parameterized'); window.location.href = rootURL + '/' + url + 'build?delay=0sec'; diff --git a/src/main/webapp/themes/contrast/abort.png b/src/main/webapp/themes/contrast/abort.png new file mode 100644 index 000000000..81565d554 Binary files /dev/null and b/src/main/webapp/themes/contrast/abort.png differ diff --git a/src/main/webapp/themes/contrast/pipeline-common.css b/src/main/webapp/themes/contrast/pipeline-common.css index 4f16ad17b..5f843ff7c 100644 --- a/src/main/webapp/themes/contrast/pipeline-common.css +++ b/src/main/webapp/themes/contrast/pipeline-common.css @@ -106,6 +106,7 @@ div.stage a:visited { } div.stage-task { + position: relative; white-space: nowrap; margin: 2px 2px 2px 2px; } @@ -171,7 +172,22 @@ div.task-rebuild { float: right; padding-right: 5px; z-index: 100; +} +div.task-abort { + display: table-cell; + background-color: #808080; + background: url("abort.png") no-repeat; + background-size: 100%; + position: absolute; + bottom: 6px; + right: 3px; + cursor: pointer; + height: 14px; + width: 14px; + float: right; + margin-left: 5px; + z-index: 100; } .timestamp { @@ -183,6 +199,10 @@ div.task-rebuild { padding-left: 20px; } +.task-abortable .duration { + padding-right: 20px; +} + div.task-progress-running { background-color: lightskyblue; animation-duration: 1s; @@ -398,7 +418,7 @@ div.aggregatedChangesPanelInner > ul { .pagination { display:block; text-align:left; - clear:both; + clear:both; font-family:Arial, Helvetica, sans-serif; font-size:14px; font-weight:normal; @@ -420,7 +440,7 @@ div.aggregatedChangesPanelInner > ul { .pagination a:hover { background-color:#DDEEFF; border:1px solid #BBDDFF; - color:#0072BC; + color:#0072BC; } .pagination .active_link a { diff --git a/src/main/webapp/themes/contrast/pipeline-fullscreen.css b/src/main/webapp/themes/contrast/pipeline-fullscreen.css index 783d52d06..73645090a 100644 --- a/src/main/webapp/themes/contrast/pipeline-fullscreen.css +++ b/src/main/webapp/themes/contrast/pipeline-fullscreen.css @@ -45,6 +45,15 @@ div.task-progress { min-height: 36px; } +div.task-abort { + bottom: 4px; +} + +div.task-details { + padding-top: 5px; +} + + .stage-name { padding: 10px 10px 10px 10px; } diff --git a/src/main/webapp/themes/default/abort.png b/src/main/webapp/themes/default/abort.png new file mode 100644 index 000000000..81565d554 Binary files /dev/null and b/src/main/webapp/themes/default/abort.png differ diff --git a/src/main/webapp/themes/default/pipeline-common.css b/src/main/webapp/themes/default/pipeline-common.css index 94a055e3a..673ba10ab 100644 --- a/src/main/webapp/themes/default/pipeline-common.css +++ b/src/main/webapp/themes/default/pipeline-common.css @@ -106,6 +106,7 @@ div.stage a:visited { } div.stage-task { + position: relative; white-space: nowrap; margin: 2px 2px 2px 2px; } @@ -173,6 +174,22 @@ div.task-rebuild { z-index: 100; } +div.task-abort { + display: table-cell; + background-color: #808080; + background: url("abort.png") no-repeat; + background-size: 100%; + position: absolute; + bottom: 6px; + right: 3px; + cursor: pointer; + height: 14px; + width: 14px; + float: right; + margin-left: 5px; + z-index: 100; +} + .timestamp { display: table-cell; } @@ -182,6 +199,10 @@ div.task-rebuild { padding-left: 20px; } +.task-abortable .duration { + padding-right: 20px; +} + div.task-progress-running { background-color: lightskyblue; animation-duration: 1s; diff --git a/src/main/webapp/themes/default/pipeline-fullscreen.css b/src/main/webapp/themes/default/pipeline-fullscreen.css index 13a63a4c0..d50ffcc1c 100644 --- a/src/main/webapp/themes/default/pipeline-fullscreen.css +++ b/src/main/webapp/themes/default/pipeline-fullscreen.css @@ -45,6 +45,14 @@ div.task-progress { min-height: 36px; } +div.task-abort { + bottom: 4px; +} + +div.task-details { + padding-top: 5px; +} + .stage-name { padding: 10px 10px 10px 10px; } diff --git a/src/main/webapp/themes/overview/abort.png b/src/main/webapp/themes/overview/abort.png new file mode 100644 index 000000000..81565d554 Binary files /dev/null and b/src/main/webapp/themes/overview/abort.png differ diff --git a/src/main/webapp/themes/overview/pipe-fullscreen.css b/src/main/webapp/themes/overview/pipe-fullscreen.css deleted file mode 100644 index 43443f02e..000000000 --- a/src/main/webapp/themes/overview/pipe-fullscreen.css +++ /dev/null @@ -1,320 +0,0 @@ -a { - text-decoration: none; - color: inherit; -} - -a:visited { - color: inherit; -} - -a:hover { - text-decoration: underline; -} - -body { - font-family: sans-serif; - background: #000000; - color: #fefefe; -} - -section.component { - display: block; - background: #000000; - border-radius: 20px; - padding: 10px; - margin: 0; -} - -section.component h1 { - font-size: 36px; - font-weight: bold; - font-stretch: expanded; - margin-top: 0; -} - -div.pipeline-row { - display: table-row; - white-space: nowrap; - margin-top: 20px; -} - -div.pipeline-cell { - display: table-cell; - width: auto; - margin: 0 60px 0 0; -} - -div.pipeline-row-spacer { - white-space: nowrap; - height: 20px; -} - -section.pipeline { - display: table; -} - -section.pipeline img { - height: 18px; -} - -section.pipeline h1 { - display: inline-table; - font-size: 18px; - font-weight: bold; - font-stretch: expanded; - margin-top: 0; -} - -div.changes { - padding-top: 0; - padding-left: 20px; - padding-bottom: 10px; - display: table; -} - -div.changes h1 { - font-size: 14px; - line-height: 14px; - padding: 0; - margin-bottom: 5px; -} - -div.change { - clear: both; - padding-left: 5px; - display: table-row; -} - -div.change-author { - display: table-cell; - vertical-align: top; - min-width: 75px; -} - -div.change-message { - display: table-cell; - padding-left: 10px; -} - -div.stage { - display: inline-block; - min-height: 100px; - min-width: 180px; - vertical-align: top; - background: #404040; - margin: 0 60px 0 0; -} - -div.stage div.stage-header { - font-size: medium; - margin: 0; - margin-bottom: 3px; - border: 0; - background: #808080; - height: 20px; - white-space: nowrap; - display: table; - width: 180px; - min-width: 180px; -} - -div.stage div.task { - display: block; - margin: 0 5px 2px 5px; - padding-left: 5px; -} - -div.task-progress { - position: relative; - height: 34px; - width: 0; - background-color: #0197fe; - -webkit-animation-name: bluePulse; - -webkit-animation-duration: 2s; - -webkit-animation-iteration-count: infinite; -} - -@-webkit-keyframes bluePulse { - from { - background-color: #0197fe; - -webkit-box-shadow: 0 0 9px #333; - } - 50% { - background-color: #2daebf; - -webkit-box-shadow: 0 0 18px #2daebf; - } - to { - background-color: #0197fe; - -webkit-box-shadow: 0 0 9px #333; - } -} - -div.task-content { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 30px; - margin-left: 2px; - margin-top: 2px; -} - -div.task-header { - position: relative; - width: 155px; - height: 0; - border: 0; -} - -div.taskname { - position: relative; -} - -div.stage a { - text-decoration: none; - color: inherit; -} - -div.stage a:visited { - color: inherit; -} - -div.stage a:hover { - text-decoration: underline; -} - -.stage-version { - display: table-cell; - text-align: right; - padding: 10px 10px 10px 10px; -} - -.stage-name { - display: table-cell; - padding: 10px 10px 10px 10px; -} - -div.hide { - visibility: hidden; -} - -.IDLE { - border-left: 7px solid #d3d3d3; - color: lightgray; -} - -.FAILED { - border-left: 7px solid #fe0000; - color: #fe0000; - font-weight: 700; -} - -.SUCCESS { - border-left: 7px solid #00AC00; - color: #d3d3d3; -} - -.UNSTABLE { - border-left: 7px solid #ffdb00; - color: #ffdb00; - font-weight: bold; -} - -.CANCELLED { - border-left: 7px solid #a9a9a9; - color: #a9a9a9; - font-weight: bold; -} - -.DISABLED { - border-left: 6px solid transparent; - color: #a9a9a9; -} - -.QUEUED { - border-left: 6px solid darkblue; - color: lightgray; -} - -.RUNNING { - border-left: 6px solid #0197fe; - color: lightgray; -} - -span.timestamp { - display: inline-block; - font-size: 10px; - color: #f5f5f5; -} - -span.duration { - display: inline-block; - font-size: 10px; - margin-left: 15px; - color: #f5f5f5; -} - -section.legend { - display: block; - margin-left: 30px; -} - -section.legend h1 { - display: inline; - font-size: medium; - font-weight: normal; - margin-right: 10px; -} - -section.legend span { - display: inline-block; - border-radius: 3px; - margin: 0 5px; - padding: 1px 5px; - min-width: 75px; -} - -div.pipelineerror { - text-align: center; - background-color: red; - font-size: 22px; - color: #ffffff; - border-radius: 5px; - padding: 5px; - display: none; -} - -div.pipeline-message { - font-size: 20px; - text-align: center; -} - -div.taskname { - float: left; -} - -div.task-details { - clear: both; -} - -div.task-manual { - float: right; - content: url("play.png"); -} - -div.left { - float: left; -} - -div.right { - float: right; -} - -div.clear { - clear: both; -} - -svg.relation path { - stroke: #fefefe; - stroke-width: 3; -} diff --git a/src/main/webapp/themes/overview/pipe.js b/src/main/webapp/themes/overview/pipe.js index bec8a3e9c..75159d8fc 100644 --- a/src/main/webapp/themes/overview/pipe.js +++ b/src/main/webapp/themes/overview/pipe.js @@ -173,6 +173,14 @@ function pipelineUtils() { consoleLogLink = 'console'; } + var showAbortButton = false; + if (data.allowAbort) { + progressClass += ' task-abortable'; + if (progressClass.indexOf('task-progress-running') !== -1) { + showAbortButton = true; + } + } + html.push( '
' + '
' @@ -192,9 +200,18 @@ function pipelineUtils() { html.push('
'); } if (task.requiringInput) { + showAbortButton = true; html.push('
'); html.push('
'); } + if (showAbortButton) { + var projectName = component.fullJobName; + if (typeof projectName === "undefined") { + projectName = task.id; + } + html.push('
'); + html.push('
'); + } } html.push('
'); @@ -684,6 +701,34 @@ function specifyInput(taskId, project, buildId, viewUrl) { }); } +function abortBuild(taskId, project, buildId, viewUrl) { + Q('#abort-' + taskId).hide(); + var formData = {project: project, upstream: 'N/A', buildId: buildId}, before; + + var before; + if (crumb.value !== null && crumb.value !== '') { + console.info('Crumb found and will be added to request header'); + before = function(xhr){xhr.setRequestHeader(crumb.fieldName, crumb.value);} + } else { + console.info('Crumb not needed'); + before = function(xhr){} + } + + Q.ajax({ + url: rootURL + '/' + viewUrl + 'api/abortBuild', + type: 'POST', + data: formData, + beforeSend: before, + timeout: 20000, + success: function (data, textStatus, jqXHR) { + console.info('Successfully aborted build of ' + project + '!') + }, + error: function (jqXHR, textStatus, errorThrown) { + window.alert('Could not abort build! error: ' + errorThrown + ' status: ' + textStatus) + } + }); +} + function triggerParameterizedBuild(url, taskId) { console.info('Job is parameterized'); window.location.href = rootURL + '/' + url + 'build?delay=0sec'; diff --git a/src/main/webapp/themes/overview/pipeline-common.css b/src/main/webapp/themes/overview/pipeline-common.css index 30d28305e..0bdb3b683 100644 --- a/src/main/webapp/themes/overview/pipeline-common.css +++ b/src/main/webapp/themes/overview/pipeline-common.css @@ -53,6 +53,7 @@ div.stage a:visited { } div.stage-task { + position: relative; white-space: nowrap; margin: 2px 2px 2px 2px; } @@ -104,6 +105,7 @@ div.task-manual { float: right; padding-right: 5px; z-index: 100; + margin-top: -18px; } div.task-rebuild { @@ -118,6 +120,22 @@ div.task-rebuild { z-index: 100; } +div.task-abort { + display: table-cell; + background-color: #808080; + background: url("abort.png") no-repeat; + background-size: 100%; + position: absolute; + bottom: 6px; + right: 3px; + cursor: pointer; + height: 14px; + width: 14px; + float: right; + margin-left: 5px; + z-index: 100; +} + .timestamp { display: inline-block; } @@ -127,6 +145,10 @@ div.task-rebuild { display: inline-block; } +.task-abortable .duration { + padding-right: 20px; +} + div.task-progress-running { background-color: lightskyblue; animation-duration: 1s; diff --git a/src/main/webapp/themes/overview/pipeline-fullscreen.css b/src/main/webapp/themes/overview/pipeline-fullscreen.css index 1d86203cd..40fd98bad 100644 --- a/src/main/webapp/themes/overview/pipeline-fullscreen.css +++ b/src/main/webapp/themes/overview/pipeline-fullscreen.css @@ -43,6 +43,14 @@ div.stage-task, div.task-progress { min-height: 42px; } +div.task-abort { + bottom: 4px; +} + +div.task-details { + padding-top: 5px; +} + .stage-name, .stage-version { padding: 10px; }