From c0636776013fb1a70020faebb1772e70d472833e Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 12 Sep 2016 23:15:55 -0400 Subject: [PATCH 1/6] [JENKINS-27394] Basic implementation of collapsible sections. --- .../plugins/workflow/job/WorkflowRun.java | 62 ++++++++++++++++-- .../workflow/job/console/NestingNote.java | 60 +++++++++++++++++ .../workflow/job/console/ShowHideNote.java | 65 +++++++++++++++++++ 3 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/workflow/job/console/NestingNote.java create mode 100644 src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java index b913f4fb..3ab78ee7 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java @@ -61,6 +61,7 @@ import java.io.PrintStream; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; @@ -94,8 +95,11 @@ import org.jenkinsci.plugins.workflow.flow.GraphListener; import org.jenkinsci.plugins.workflow.flow.StashManager; import org.jenkinsci.plugins.workflow.graph.BlockEndNode; +import org.jenkinsci.plugins.workflow.graph.BlockStartNode; import org.jenkinsci.plugins.workflow.graph.FlowEndNode; import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.job.console.NestingNote; +import org.jenkinsci.plugins.workflow.job.console.ShowHideNote; import org.jenkinsci.plugins.workflow.job.console.WorkflowConsoleLogger; import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException; import org.jenkinsci.plugins.workflow.steps.StepContext; @@ -370,14 +374,17 @@ private void copyLogs() { AnnotatedLargeText logText = la.getLogText(); try { long old = entry.getValue(); - OutputStream logger; String prefix = getLogPrefix(node); + List nesting = getNesting(node); + String encodedNesting = new NestingNote(nesting).encode(); + String linePrefix; if (prefix != null) { - logger = new LogLinePrefixOutputFilter(listener.getLogger(), "[" + prefix + "] "); + linePrefix = encodedNesting + "[" + prefix + "] "; } else { - logger = listener.getLogger(); + linePrefix = encodedNesting; } + OutputStream logger = new LogLinePrefixOutputFilter(listener.getLogger(), linePrefix); try { long revised = writeRawLogTo(logText, old, logger); @@ -454,6 +461,37 @@ private long writeRawLogTo(AnnotatedLargeText text, long start, OutputStream } } + @GuardedBy("completed") + private transient LoadingCache> nestingCache; + private @Nonnull List getNesting(FlowNode node) { + // TODO could also use FlowScanningUtils.fetchEnclosingBlocks(node) but this would not let us cache intermediate results + synchronized (completed) { + if (nestingCache == null) { + nestingCache = CacheBuilder.newBuilder().weakKeys().build(new CacheLoader>() { + @Override public @Nonnull List load(FlowNode node) { + if (node instanceof BlockEndNode) { + return getNesting(((BlockEndNode) node).getStartNode()); + } else { + List parents = node.getParents(); + if (parents.isEmpty()) { // FlowStartNode + return Collections.emptyList(); + } + List parent = getNesting(parents.get(0)); // multiple parents is only for BlockEndNode after parallel + if (node instanceof BlockStartNode) { + List appended = new ArrayList<>(parent); + appended.add(node.getId()); + return appended; + } else { // AtomNode + return parent; + } + } + } + }); + } + return nestingCache.getUnchecked(node); + } + } + private static final class LogLinePrefixOutputFilter extends LineTransformationOutputStream { private final PrintStream logger; @@ -833,12 +871,26 @@ private final class GraphL implements GraphListener { } private void logNodeMessage(FlowNode node) { + List nesting = getNesting(node); + if (!nesting.isEmpty() && nesting.get(nesting.size() - 1).equals(node.getId())) { + // For a BlockStartNode, we do not want to hide itself. + nesting = new ArrayList<>(nesting.subList(0, nesting.size() - 1)); + } + try { + listener.annotate(new NestingNote(nesting)); + } catch (IOException x) { + LOGGER.log(Level.WARNING, null, x); + } WorkflowConsoleLogger wfLogger = new WorkflowConsoleLogger(listener); String prefix = getLogPrefix(node); + String text = node.getDisplayFunctionName(); + if (node instanceof BlockStartNode) { + text += " (" + ShowHideNote.encodeTo(node.getId(), true, "show") + "/" + ShowHideNote.encodeTo(node.getId(), false, "hide") + ")"; + } if (prefix != null) { - wfLogger.log(String.format("[%s] %s", prefix, node.getDisplayFunctionName())); + wfLogger.log(String.format("[%s] %s", prefix, text)); } else { - wfLogger.log(node.getDisplayFunctionName()); + wfLogger.log(text); } // Flushing to keep logs printed in order as much as possible. The copyLogs method uses // LargeText and possibly LogLinePrefixOutputFilter. Both of these buffer and flush, causing strange diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/NestingNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/NestingNote.java new file mode 100644 index 00000000..3ac31376 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/NestingNote.java @@ -0,0 +1,60 @@ +/* + * The MIT License + * + * Copyright 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.job.console; + +import hudson.MarkupText; +import hudson.console.ConsoleAnnotator; +import hudson.console.ConsoleNote; +import hudson.model.Run; +import java.util.List; + +/** + * Encodes the block-scoped nesting of a step. + */ +public class NestingNote extends ConsoleNote> { + + private static final long serialVersionUID = 1L; + + private final List nesting; + + public NestingNote(List nesting) { + this.nesting = nesting; + } + + @SuppressWarnings("rawtypes") + @Override public ConsoleAnnotator annotate(Run context, MarkupText text, int charPos) { + StringBuilder b = new StringBuilder(" 0) { + b.append(' '); + } + b.append("pipeline-sect-").append(nesting.get(i)); + } + b.append("\">"); + text.addMarkup(0, text.length(), b.toString(), ""); + return null; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java new file mode 100644 index 00000000..512a2502 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java @@ -0,0 +1,65 @@ +/* + * The MIT License + * + * Copyright 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.job.console; + +import hudson.console.HyperlinkNote; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Shows or hides a block by nesting. + */ +public class ShowHideNote extends HyperlinkNote { + + private static final Logger LOGGER = Logger.getLogger(ShowHideNote.class.getName()); + private static final long serialVersionUID = 1L; + + public static String encodeTo(String id, boolean show, String text) { + try { + return new ShowHideNote(id, show, text.length()).encode() + text; + } catch (IOException e) { + // impossible, but don't make this a fatal problem + LOGGER.log(Level.WARNING, "Failed to serialize " + ShowHideNote.class, e); + return text; + } + } + + private final String id; + // TODO better to have a single link that toggles (requires looking up current state, as below) + private final boolean show; + + private ShowHideNote(String id, boolean show, int length) { + super("#", length); + this.id = id; + this.show = show; + } + + @Override protected String extraAttributes() { + // TODO look up any existing rule via .selectorText and change its .style.display (but how can be package this JS into an adjunct?) + return " onclick=\"var ss = document.styleSheets[0]; ss.insertRule('.pipeline-sect-" + id + " {display: " + (show ? "inline" : "none") + "}', ss.rules.length); return false\""; + } + +} From bbbf9511aa9ea441634e20f1dd02ad559f3aaef0 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 12 Sep 2016 23:39:55 -0400 Subject: [PATCH 2/6] Moving logic into a shared script.js. --- .../workflow/job/console/ShowHideNote.java | 8 +++-- .../job/console/ShowHideNote/script.js | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java index 512a2502..9f388ab8 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java @@ -24,6 +24,8 @@ package org.jenkinsci.plugins.workflow.job.console; +import hudson.Extension; +import hudson.console.ConsoleAnnotationDescriptor; import hudson.console.HyperlinkNote; import java.io.IOException; import java.util.logging.Level; @@ -48,7 +50,6 @@ public static String encodeTo(String id, boolean show, String text) { } private final String id; - // TODO better to have a single link that toggles (requires looking up current state, as below) private final boolean show; private ShowHideNote(String id, boolean show, int length) { @@ -58,8 +59,9 @@ private ShowHideNote(String id, boolean show, int length) { } @Override protected String extraAttributes() { - // TODO look up any existing rule via .selectorText and change its .style.display (but how can be package this JS into an adjunct?) - return " onclick=\"var ss = document.styleSheets[0]; ss.insertRule('.pipeline-sect-" + id + " {display: " + (show ? "inline" : "none") + "}', ss.rules.length); return false\""; + return " onclick=\"showHidePipelineSection('" + id + "', " + show + "); return false\""; } + @Extension public static class DescriptorImpl extends ConsoleAnnotationDescriptor {} + } diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js new file mode 100644 index 00000000..4733e73f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js @@ -0,0 +1,31 @@ +/* + * The MIT License + * + * Copyright 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// TODO better to have a single link that toggles (requires looking up current state, as below) +function showHidePipelineSection(id, show) { + var ss = document.styleSheets[0] + // TODO look up any existing rule via .selectorText and change its .style.display + // TODO order rules, so that hiding and reshowing a high-level section will restore expansion of a lower-level section + ss.insertRule('.pipeline-sect-' + id + ' {display: ' + (show ? 'inline' : 'none') + '}', ss.rules.length) +} From d9872e52b72b44188955140b1687139f9e52d476 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Mon, 12 Sep 2016 23:47:59 -0400 Subject: [PATCH 3/6] Modify an existing rule if we have one. --- .../workflow/job/console/ShowHideNote/script.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js index 4733e73f..76472082 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js @@ -24,8 +24,15 @@ // TODO better to have a single link that toggles (requires looking up current state, as below) function showHidePipelineSection(id, show) { + var sect = '.pipeline-sect-' + id + var display = show ? 'inline' : 'none' var ss = document.styleSheets[0] - // TODO look up any existing rule via .selectorText and change its .style.display + for (var i = 0; i < ss.rules.length; i++) { + if (ss.rules[i].selectorText === sect) { + ss.rules[i].style.display = display + return + } + } // TODO order rules, so that hiding and reshowing a high-level section will restore expansion of a lower-level section - ss.insertRule('.pipeline-sect-' + id + ' {display: ' + (show ? 'inline' : 'none') + '}', ss.rules.length) + ss.insertRule(sect + ' {display: ' + display + '}', ss.rules.length) } From c0a7b090e027809158ea2208c2ead1096e3631b0 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 13 Sep 2016 13:32:36 -0700 Subject: [PATCH 4/6] Switched link to a toggle. --- .../plugins/workflow/job/WorkflowRun.java | 2 +- .../plugins/workflow/job/console/ShowHideNote.java | 11 +++++------ .../workflow/job/console/ShowHideNote/script.js | 14 +++++++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java index 3ab78ee7..f47a4344 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/WorkflowRun.java @@ -885,7 +885,7 @@ private void logNodeMessage(FlowNode node) { String prefix = getLogPrefix(node); String text = node.getDisplayFunctionName(); if (node instanceof BlockStartNode) { - text += " (" + ShowHideNote.encodeTo(node.getId(), true, "show") + "/" + ShowHideNote.encodeTo(node.getId(), false, "hide") + ")"; + text += " (" + ShowHideNote.create(node.getId()) + ")"; } if (prefix != null) { wfLogger.log(String.format("[%s] %s", prefix, text)); diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java index 9f388ab8..055960b5 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java @@ -39,9 +39,10 @@ public class ShowHideNote extends HyperlinkNote { private static final Logger LOGGER = Logger.getLogger(ShowHideNote.class.getName()); private static final long serialVersionUID = 1L; - public static String encodeTo(String id, boolean show, String text) { + public static String create(String id) { + String text = "hide"; try { - return new ShowHideNote(id, show, text.length()).encode() + text; + return new ShowHideNote(id, text.length()).encode() + text; } catch (IOException e) { // impossible, but don't make this a fatal problem LOGGER.log(Level.WARNING, "Failed to serialize " + ShowHideNote.class, e); @@ -50,16 +51,14 @@ public static String encodeTo(String id, boolean show, String text) { } private final String id; - private final boolean show; - private ShowHideNote(String id, boolean show, int length) { + private ShowHideNote(String id, int length) { super("#", length); this.id = id; - this.show = show; } @Override protected String extraAttributes() { - return " onclick=\"showHidePipelineSection('" + id + "', " + show + "); return false\""; + return " id=\"show-hide-" + id + "\" onclick=\"showHidePipelineSection('" + id + "'); return false\""; } @Extension public static class DescriptorImpl extends ConsoleAnnotationDescriptor {} diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js index 76472082..0f3bac8d 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js @@ -22,10 +22,18 @@ * THE SOFTWARE. */ -// TODO better to have a single link that toggles (requires looking up current state, as below) -function showHidePipelineSection(id, show) { +// TODO infer section from the event source perhaps? +function showHidePipelineSection(id) { + var link = document.getElementById('show-hide-' + id) + var display + if (link.textContent === 'hide') { + display = 'none' + link.textContent = 'show' + } else { + display = 'inline' + link.textContent = 'hide' + } var sect = '.pipeline-sect-' + id - var display = show ? 'inline' : 'none' var ss = document.styleSheets[0] for (var i = 0; i < ss.rules.length; i++) { if (ss.rules[i].selectorText === sect) { From 1a656d61e17852069bfb0ccc1aa4140ff5493b7c Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 13 Sep 2016 13:40:32 -0700 Subject: [PATCH 5/6] Removed duplication information. --- .../jenkinsci/plugins/workflow/job/console/ShowHideNote.java | 2 +- .../plugins/workflow/job/console/ShowHideNote/script.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java index 055960b5..7bd8fc58 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/job/console/ShowHideNote.java @@ -58,7 +58,7 @@ private ShowHideNote(String id, int length) { } @Override protected String extraAttributes() { - return " id=\"show-hide-" + id + "\" onclick=\"showHidePipelineSection('" + id + "'); return false\""; + return " show-hide-id=\"" + id + "\" onclick=\"showHidePipelineSection(this); return false\""; } @Extension public static class DescriptorImpl extends ConsoleAnnotationDescriptor {} diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js index 0f3bac8d..34b44bb9 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js @@ -23,8 +23,8 @@ */ // TODO infer section from the event source perhaps? -function showHidePipelineSection(id) { - var link = document.getElementById('show-hide-' + id) +function showHidePipelineSection(link) { + var id = link.getAttribute('show-hide-id') var display if (link.textContent === 'hide') { display = 'none' From 457e6f8c0a020e1784b52c36d574c4a69aee3bd2 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Tue, 13 Sep 2016 13:42:46 -0700 Subject: [PATCH 6/6] Obsolete comment. --- .../plugins/workflow/job/console/ShowHideNote/script.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js index 34b44bb9..dd8d26cf 100644 --- a/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js +++ b/src/main/resources/org/jenkinsci/plugins/workflow/job/console/ShowHideNote/script.js @@ -22,7 +22,6 @@ * THE SOFTWARE. */ -// TODO infer section from the event source perhaps? function showHidePipelineSection(link) { var id = link.getAttribute('show-hide-id') var display