diff --git a/pom.xml b/pom.xml index 4c2180d..2cb36ca 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.jenkins-ci.plugins plugin - 3.54 + 4.15 @@ -58,7 +58,7 @@ - 2.73.3 + 2.176.4 8 2.38 maven-release-plugin: @@ -95,7 +95,7 @@ com.axis.system.jenkins.plugins.downstream downstream-build-cache - 1.5.2 + 1.6 diff --git a/scripts/codenarc_rules.txt b/scripts/codenarc_rules.txt index bcad1bc..84a5ee5 100644 --- a/scripts/codenarc_rules.txt +++ b/scripts/codenarc_rules.txt @@ -131,7 +131,7 @@ ruleset { // rulesets/dry.xml DuplicateListLiteral - DuplicateMapLiteral + // DuplicateMapLiteral Axis: Does not work for HTML-attributes in groovy DuplicateNumberLiteral // DuplicateStringLiteral Axis: Not very useful to us @@ -230,7 +230,7 @@ ruleset { ExplicitArrayListInstantiation ExplicitCallToAndMethod ExplicitCallToCompareToMethod - ExplicitCallToDivMethod + // ExplicitCallToDivMethod Axis: Because div() is used for building html pages. ExplicitCallToEqualsMethod ExplicitCallToGetAtMethod ExplicitCallToLeftShiftMethod diff --git a/src/main/java/com/axis/system/jenkins/plugins/downstream/tree/Matrix.java b/src/main/java/com/axis/system/jenkins/plugins/downstream/tree/Matrix.java index 3e8d642..32d9d28 100644 --- a/src/main/java/com/axis/system/jenkins/plugins/downstream/tree/Matrix.java +++ b/src/main/java/com/axis/system/jenkins/plugins/downstream/tree/Matrix.java @@ -113,6 +113,14 @@ public Entry(@Nullable Arrow arrow) { this.data = null; } + public Arrow getArrow() { + return arrow; + } + + public T getData() { + return data; + } + @Override public String toString() { return "Entry{arrow=" + arrow + ", data=" + data + '}'; diff --git a/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction.java b/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction.java index 4445aca..c7be75d 100644 --- a/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction.java +++ b/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction.java @@ -1,7 +1,5 @@ package com.axis.system.jenkins.plugins.downstream.yabv; -import static com.axis.system.jenkins.plugins.downstream.tree.TreeLaminator.layoutTree; - import com.axis.system.jenkins.plugins.downstream.cache.BuildCache; import com.axis.system.jenkins.plugins.downstream.tree.Matrix; import com.axis.system.jenkins.plugins.downstream.tree.TreeLaminator.ChildrenFunction; @@ -13,14 +11,6 @@ import hudson.model.Job; import hudson.model.Queue; import hudson.model.Run; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import javax.annotation.Nonnull; -import javax.servlet.ServletException; import jenkins.model.Jenkins; import jenkins.model.TransientActionFactory; import org.kohsuke.stapler.StaplerRequest; @@ -28,6 +18,19 @@ import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import static com.axis.system.jenkins.plugins.downstream.tree.TreeLaminator.layoutTree; + /** * Produces Transient Actions for visualizing the flow of downstream builds. * @@ -56,6 +59,13 @@ private static final ChildrenFunction getChildrenFunc() { }; } + public Run getRootUpstreamBuild() { + if (target == null) { + return null; + } + return buildFlowOptions.isShowUpstreamBuilds() ? getRootUpstreamBuild(target) : target; + } + private static Run getRootUpstreamBuild(@Nonnull Run build) { Run parentBuild; while ((parentBuild = getUpstreamBuild(build)) != null) { @@ -92,7 +102,7 @@ public BuildFlowOptions getBuildFlowOptions() { @Exported(visibility = 1) public boolean isAnyBuildOngoing() { return target != null - && isChildrenStillBuilding(getRootUpstreamBuild(target), getChildrenFunc()); + && isChildrenStillBuilding(getRootUpstreamBuild(), getChildrenFunc()); } private static boolean isChildrenStillBuilding(Object current, ChildrenFunction children) { @@ -136,13 +146,54 @@ public Run getTarget() { } public Matrix buildMatrix() { - if (target == null) { + Run root = getRootUpstreamBuild(); + if (root == null) { return new Matrix(); } - Run root = buildFlowOptions.isShowUpstreamBuilds() ? getRootUpstreamBuild(target) : target; return layoutTree(root, getChildrenFunc()); } + /** + * Returns all items in the build flow, populated from the root. Which target is root depends on + * {@link BuildFlowOptions#isShowUpstreamBuilds()}. + * + * @param lookBack number of historic build flows to fetch, based on the root target's previous + * builds. + * @return A list of sets of Objects. Each set represents all items in a flow, starting from the + * root. The first set is calculated from the root, the next set is from the root target's + * previous build and so on. + */ + public List> getAllItemsInFlow(int lookBack) { + Run root = getRootUpstreamBuild(); + if (root == null) { + return Collections.emptyList(); + } + List> result = new ArrayList<>(); + for (; lookBack > 0 && root != null; lookBack--) { + Set itemsInFlow = getAllDownstreamItems(root); + itemsInFlow.add(root); + result.add(itemsInFlow); + root = root.getPreviousBuild(); + } + return result; + } + + private static Set getAllDownstreamItems(Run current) { + Set resultSet = new HashSet<>(); + addAllDownstreamItems(resultSet, current, getChildrenFunc()); + return resultSet; + } + + private static void addAllDownstreamItems( + Set resultSet, Object current, ChildrenFunction children) { + Iterator childIter = children.children(current).iterator(); + while (childIter.hasNext()) { + Object child = childIter.next(); + resultSet.add(child); + addAllDownstreamItems(resultSet, child, children); + } + } + @Override public String getDisplayName() { return null; @@ -166,6 +217,7 @@ public void doBuildFlow(StaplerRequest req, StaplerResponse rsp) Boolean.parseBoolean(req.getParameter("showBuildHistory"))); buildFlowOptions.setShowUpstreamBuilds( Boolean.parseBoolean(req.getParameter("showUpstreamBuilds"))); + buildFlowOptions.setFlattenView(Boolean.parseBoolean(req.getParameter("flattenView"))); rsp.setContentType("text/html;charset=UTF-8"); req.getView(this, "buildFlow.groovy").forward(req, rsp); } diff --git a/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowOptions.java b/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowOptions.java index def68be..a454493 100644 --- a/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowOptions.java +++ b/src/main/java/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowOptions.java @@ -4,18 +4,24 @@ public class BuildFlowOptions { private boolean showBuildHistory; private boolean showDurationInfo; private boolean showUpstreamBuilds; + private boolean flattenView; public BuildFlowOptions() { showBuildHistory = false; showDurationInfo = false; showUpstreamBuilds = false; + flattenView = false; } public BuildFlowOptions( - boolean showBuildHistory, boolean showDurationInfo, boolean showUpstreamBuilds) { + boolean showBuildHistory, + boolean showDurationInfo, + boolean showUpstreamBuilds, + boolean flattenView) { this.showBuildHistory = showBuildHistory; this.showDurationInfo = showDurationInfo; this.showUpstreamBuilds = showUpstreamBuilds; + this.flattenView = flattenView; } public boolean isShowBuildHistory() { @@ -42,12 +48,21 @@ public void setShowUpstreamBuilds(boolean showUpstreamBuilds) { this.showUpstreamBuilds = showUpstreamBuilds; } + public boolean isFlattenView() { + return flattenView; + } + + public void setFlattenView(boolean flattenView) { + this.flattenView = flattenView; + } + @Override public String toString() { final StringBuffer sb = new StringBuffer("BuildFlowOptions{"); sb.append("showBuildHistory=").append(showBuildHistory); sb.append(", showDurationInfo=").append(showDurationInfo); sb.append(", showUpstreamBuilds=").append(showUpstreamBuilds); + sb.append(", flattenView=").append(flattenView); sb.append('}'); return sb.toString(); } diff --git a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlow.groovy b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlow.groovy index dc59082..11640e0 100644 --- a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlow.groovy +++ b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlow.groovy @@ -14,74 +14,124 @@ import static com.axis.system.jenkins.plugins.downstream.tree.Matrix.Arrow div(id: 'build-flow-cache-refreshing-warning') { if (BuildCache.cache.isCacheRefreshing()) { - span("⚠ Cache is still refreshing, the Build Flow graph may not be complete!") + span('⚠ Cache is still refreshing, the Build Flow graph may not be complete!') } } -Matrix matrix = my.buildMatrix() -div(id: 'build-flow-grid', - style: "grid-template-columns: repeat(${matrix.getMaxRowWidth() * 2}, auto);") { - if (matrix.isEmpty()) { - return +// To get rid of warnings where argument types could not be inferred +BuildFlowOptions options = my.buildFlowOptions + +if (options.flattenView) { + List> itemsInFlow = my.getAllItemsInFlow(options.showBuildHistory ? 10 : 1) + Job rootJob = my.rootUpstreamBuild?.parent + List allJobs = itemsInFlow.flatten().collect { data -> + getJob(data) + }.unique().sort { a, b -> + if (a == rootJob) { return -1 } + if (b == rootJob) { return 1 } + return a.fullDisplayName <=> b.fullDisplayName } - Set jobs = matrix.cellDataAsSet.collect { data -> - if (data instanceof Run) { - data.parent - } else if (data instanceof Queue.Item) { - data.task + div(id: 'build-flow-grid', + style: "grid-auto-flow: column; grid-template-rows: repeat(${allJobs.size()}, auto);") { + if (itemsInFlow.isEmpty()) { + return + } + + NameNormalizer nameNormalizer = getNameNormalizer(allJobs.toSet()) + + allJobs.each { job -> + drawJobInfo(job, nameNormalizer) } - }.toSet() - - NameNormalizer nameNormalizer = new NameNormalizer(jobs, - { it.displayName }, - { it instanceof Item ? it.parent : null } - ) - - CssGridCoordinates gridCoords = new CssGridCoordinates() - matrix.get().each { row -> - gridCoords.row++ - gridCoords.col = 1 - row.each { cell -> - if (cell?.arrow) { - drawArrow(gridCoords, cell.arrow) + itemsInFlow.each { items -> + allJobs.each { job -> + div(style: 'display: flex; flex-direction: column; margin: 0.2em 0;') { + items.findAll { item -> + job == getJob(item) + }.each { item -> + drawCellData(item, nameNormalizer, options) + } + } } - gridCoords.col++ - if (cell?.data) { - drawCellData(gridCoords, cell.data, nameNormalizer, my.getBuildFlowOptions()) + } + } +} else { + Matrix matrix = my.buildMatrix() + div(id: 'build-flow-grid', + style: "grid-template-columns: repeat(${matrix.maxRowWidth * 2}, auto);") { + if (matrix.isEmpty()) { + return + } + + Set jobs = matrix.cellDataAsSet.collect { data -> + getJob(data) + }.toSet() + + NameNormalizer nameNormalizer = getNameNormalizer(jobs) + + matrix.get().each { row -> + row.each { cell -> + drawArrow(cell?.arrow) + drawCellData(cell?.data, nameNormalizer, options) } - gridCoords.col++ } } } -private void drawCellData(CssGridCoordinates gridCoords, Object data, NameNormalizer - nameNormalizer, BuildFlowOptions options) { +private static Job getJob(Object data) { + if (data instanceof Run) { + return data.parent + } else if (data instanceof Queue.Item && data.task instanceof Job) { + return (Job) data.task + } + return null +} + +private static NameNormalizer getNameNormalizer(Set jobs) { + return new NameNormalizer(jobs, { + it.displayName + }, { + it instanceof Item ? it.parent : null + }) +} + +private void drawCellData(Object data, NameNormalizer nameNormalizer, BuildFlowOptions options) { if (data instanceof Run) { - drawBuildInfo(gridCoords, data, nameNormalizer, options) + drawBuildInfo(data, nameNormalizer, options) } else if (data instanceof Queue.Item) { - drawQueueItemInfo(gridCoords, data, nameNormalizer) + drawQueueItemInfo(data, nameNormalizer, options) + } else { + div { } } } -private void drawBuildInfo(CssGridCoordinates gridCoords, Run build, NameNormalizer - nameNormalizer, BuildFlowOptions options) { - def color = build.iconColor - def colorClasses = color.name().replace('_', ' ') + ' ' + (build == my.target ? 'SELECTED' : '') - div(class: "build-info ${colorClasses}", - style: gridCoords.cssStyleString) { +private static String getCssColorFromBuild(Run build) { + return build.iconColor.name().replace('_', ' ') +} + +private void drawBuildInfo(Run build, NameNormalizer nameNormalizer, BuildFlowOptions options) { + def colorClasses = getCssColorFromBuild(build) + + (build == my.target ? ' SELECTED' : '') + + (options.flattenView ? ' FLAT' : '') + + div(class: "build-info ${colorClasses}") { a(class: 'model-link inside', href: "${rootURL}/${build.url}") { - span("${nameNormalizer.getNormalizedName(build.parent)} ${build.displayName}") + if (options.flattenView) { + span("${build.displayName}") + } else { + span("${nameNormalizer.getNormalizedName(build.parent)} ${build.displayName}") + } } if (options.showDurationInfo) { span(class: 'duration-info', build.durationString) } - if (options.showBuildHistory) { - div(class: "build-flow-build-history") { + if (options.showBuildHistory && !my.buildFlowOptions.flattenView) { + div(class: 'build-flow-build-history') { currentBuild = build.previousBuild for (int i = 0; i < 5 && currentBuild != null; i++) { a(href: "${rootURL}/${currentBuild.url}") { def currentColor = currentBuild.iconColor - div(class: "build-flow-build-history-dot build-info ${currentColor.name().replace('_', ' ')}", + div(class: 'build-flow-build-history-dot build-info ' + + currentColor.name().replace('_', ' '), tooltip: Util.xmlEscape(currentBuild.displayName)) } currentBuild = currentBuild.previousBuild @@ -91,20 +141,37 @@ private void drawBuildInfo(CssGridCoordinates gridCoords, Run build, NameNormali } } -private void drawQueueItemInfo(CssGridCoordinates gridCoords, - Queue.Item item, - NameNormalizer nameNormalizer) { - div(class: 'build-info NOTBUILT ANIME', - style: gridCoords.cssStyleString) { +private void drawQueueItemInfo(Queue.Item item, NameNormalizer nameNormalizer, BuildFlowOptions + options) { + div(class: "build-info NOTBUILT ANIME ${options.flattenView ? 'FLAT' : ''}") { a(class: 'model-link inside', href: "${rootURL}/${item.task.url}") { - span("${nameNormalizer.getNormalizedName(item.task)} (Queued)") + if (options.flattenView) { + span('Queued') + } else { + span("${nameNormalizer.getNormalizedName(item.task)} (Queued)") + } + } + if (options.showDurationInfo) { + span(class: 'duration-info', item.inQueueForString) + } + } +} + +private void drawJobInfo(Job job, NameNormalizer nameNormalizer) { + def colorClasses = getCssColorFromBuild(job.lastBuild) + div(class: "job-info ${colorClasses}") { + a(class: 'model-link inside', href: "${rootURL}/${job.url}") { + span("${nameNormalizer.getNormalizedName(job)}") } } } -private void drawArrow(CssGridCoordinates gridCoords, Arrow arrow) { - div(class: 'arrow-wrapper', - style: gridCoords.cssStyleString) { +private void drawArrow(Arrow arrow) { + if (arrow == null) { + div { } + return + } + div(class: 'arrow-wrapper') { svg(viewBox: '0 0 100 100', preserveAspectRatio: 'none', width: '100%', @@ -132,18 +199,9 @@ private void drawArrow(CssGridCoordinates gridCoords, Arrow arrow) { } path(d: pathDefinition, 'vector-effect': 'non-scaling-stroke', - 'stroke-width': 2, + 'stroke-width': '2', stroke: '#333', fill: 'transparent') } } } - -class CssGridCoordinates { - int col = 1 - int row = 1 - - String getCssStyleString() { - "grid-row-start: ${row}; grid-column-start: ${col}" - } -} diff --git a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlowJsCss.groovy b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlowJsCss.groovy index 8c389e0..65d347b 100644 --- a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlowJsCss.groovy +++ b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/buildFlowJsCss.groovy @@ -7,11 +7,11 @@ link(rel: 'stylesheet', div(id: 'build-flow-root') { div(id: 'build-flow-switches') div(id: 'build-flow-grid-holder') { - noscript() { + noscript { include(my, 'buildFlow.groovy') } } - script("buildFlowRefreshInterval='${System.getProperty('yabv.buildFlowRefreshInterval', '10000')}'") + script('buildFlowRefreshInterval=' + System.getProperty('yabv.buildFlowRefreshInterval', '10000')) script(src: "${rootURL}/plugin/yet-another-build-visualizer/scripts/render.js", type: 'text/javascript') } diff --git a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/index.groovy b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/index.groovy index f538274..17539a6 100644 --- a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/index.groovy +++ b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/index.groovy @@ -3,6 +3,6 @@ package com.axis.system.jenkins.plugins.downstream.yabv.BuildFlowAction base(href: '..') def st = namespace('jelly:stapler') -st.contentType(value:"text/html;charset=UTF-8") +st.contentType(value:'text/html;charset=UTF-8') include(my, 'buildFlowJsCss.groovy') diff --git a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/jobMain.groovy b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/jobMain.groovy index 294b696..92d0702 100644 --- a/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/jobMain.groovy +++ b/src/main/resources/com/axis/system/jenkins/plugins/downstream/yabv/BuildFlowAction/jobMain.groovy @@ -3,4 +3,4 @@ package com.axis.system.jenkins.plugins.downstream.yabv.BuildFlowAction if (my.shouldDisplayBuildFlow()) { h2('Build Flow') include(my, 'buildFlowJsCss.groovy') -} \ No newline at end of file +} diff --git a/src/main/webapp/css/layout.css b/src/main/webapp/css/layout.css index a7dc0ed..dcc6a26 100644 --- a/src/main/webapp/css/layout.css +++ b/src/main/webapp/css/layout.css @@ -108,26 +108,49 @@ position: relative; } +.job-info { + border-radius: 4px; + display: flex; + flex-direction: column; + font-size: 1em; + font-weight: bold; + justify-content: center; + line-height: 1.2em; + margin: 0.3em 0.2em; + padding: 0.3em; + position: relative; +} + +.build-info.FLAT { + margin: 0.1em 0.1em !important; +} + .build-info.SELECTED { border: 2px dashed rgba(0, 0, 0, 0.8) !important; } +.job-info.RED, .build-info.RED { background: #ffdbdb; border: 1px solid rgba(255, 50, 0, 0.8); box-shadow: 0 0 3px 0px rgba(255, 50, 0, 0.8); } +.job-info.BLUE, .build-info.BLUE { background: #dbfbc9; border: 1px solid rgba(80, 120, 50, 0.8); } +.job-info.YELLOW, .build-info.YELLOW { background: #ffdeb0; border: 1px solid rgba(255, 150, 0, 0.8); } +.job-info.ABORTED, +.job-info.DISABLED, +.job-info.NOTBUILT, .build-info.ABORTED, .build-info.DISABLED, .build-info.NOTBUILT { @@ -135,6 +158,7 @@ border: 1px solid rgba(0, 0, 0, 0.6); } +.job-info.ANIME:after, .build-info.ANIME:after { background: url("../images/in_progress.gif"); border-radius: 4px; diff --git a/src/main/webapp/scripts/render.js b/src/main/webapp/scripts/render.js index 0a6c2b1..8144466 100644 --- a/src/main/webapp/scripts/render.js +++ b/src/main/webapp/scripts/render.js @@ -83,6 +83,10 @@ var buildFlowOptions = { "showUpstreamBuilds": { title: "Toggle Upstream Builds", defaultValue: true + }, + "flattenView": { + title: "Flatten Graph", + defaultValue: false } };