This is an automated email from the ASF dual-hosted git repository.

stigahuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git

commit 58a94268ba2b719d0ca289f9635992a3ca700976
Author: Surya Hebbar <[email protected]>
AuthorDate: Tue Apr 18 12:43:47 2023 +0530

    IMPALA-11915: Support graphical plan and timeline exports in the WebUI
    
    This adds support for exporting the query plan and timeline
    in SVG format for better scaling and text selection.
    
    The plan and timeline export buttons embed the CSS styling for
    foreign objects and the rendered SVG viewport into a blob,
    enclosed within HTML tags.
    
    Timeline overflow's are associated with each SVG component,
    instead of the entire diagram.
    
    To preserve appropriate styling such as SVG component's borders,
    for each SVG component, DOM elements are deep cloned as wrappers
    during the export.
    
    Memory resources consumed from the ObjectURLs are cleared
    after each export, once previous references are out of scope.
    
    SVG viewer implementations for XML namespaces to render
    foreign objects with CSS styling differs, and are not always
    supported. To avoid such problems, the plan export SVGs
    have been enclosed within a HTML wrapper.
    
    Replacement of foreign objects in the graphical plan with
    SVG elements such as <text> would allow SVG exports within
    the same namespace.
    
    Both the query plan and timeline exports contain 'query_id'
    as a header.
    
    Text styling has been preserved in query plan's node text and
    edge text. Also with timeline's SVG components such as
    'fragment_diagram', 'phases_header' and 'timeticks_footer'.
    
    Testing: Manual testing with with TPC-H and TPS-DS queries
    
    Change-Id: I1bd549318e220419ba3ee40be05f3671a9f1d8d9
    Reviewed-on: http://gerrit.cloudera.org:8080/19763
    Tested-by: Impala Public Jenkins <[email protected]>
    Reviewed-by: Kurt Deschler <[email protected]>
    Reviewed-by: Wenzhe Zhou <[email protected]>
---
 www/query_plan.tmpl     | 67 +++++++++++++++++++++++++++++++++----
 www/query_timeline.tmpl | 89 +++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 139 insertions(+), 17 deletions(-)

diff --git a/www/query_plan.tmpl b/www/query_plan.tmpl
index 341437680..8d0569b62 100644
--- a/www/query_plan.tmpl
+++ b/www/query_plan.tmpl
@@ -27,8 +27,8 @@ under the License.
 /* Text style for graph nodes */
 .node {
   color: white;
-  font-size:14px;
-  font-weight:700;
+  font-size: 14px;
+  font-weight: 700;
   text-align: center;
   white-space: nowrap;
   vertical-align: baseline;
@@ -44,6 +44,12 @@ under the License.
   fill: #333;
   stroke-width: 1.5px;
 }
+
+.nodes, .edgeLabel {
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 
"Helvetica Neue",
+      Arial, "Noto Sans", sans-serif;
+}
+
 </style>
 
 {{> www/query_detail_tabs.tmpl }}
@@ -54,17 +60,46 @@ under the License.
 {{/plan_metadata_unavailable}}
 
 {{^plan_metadata_unavailable}}
-<h3>Plan</h3>
-<div>
+<div style="display:flex; justify-content:space-between;">
+  <h3>Plan</h3>
   <label>
-  <input type="checkbox" checked="true" id="colour_scheme" 
onClick="refresh()"/>
-  Shade nodes according to time spent (if unchecked, shade according to plan 
fragment)
+    <h4 style="display:inline;"> Download : </h4>
+    <input type="button" class="btn btn-primary" data-toggle="modal" 
value="HTML"
+        data-target="#export_modal" role="button"/>
   </label>
 </div>
+<label>
+<input type="checkbox" checked="true" id="colour_scheme" onClick="refresh()"/>
+Shade nodes according to time spent (if unchecked, shade according to plan 
fragment)
+</label>
 
 <svg style="border: 1px solid darkgray" width=1200 height=600 
class="panel"><g/></svg>
 {{/plan_metadata_unavailable}}
 
+<div id="export_modal" style="transition-duration: 0.15s;" class="modal fade"
+    role="dialog" data-keyboard="true" tabindex="-1">
+  <div class="modal-dialog modal-dialog-centered">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5> Download Plan </h5>
+        <input class="btn btn-primary" type="button" value="X" 
data-dismiss="modal"/>
+      </div>
+      <div class="modal-body">
+        <h6 class="d-inline"> Filename: </h6>
+        <input id="export_filename" class="form-control-sm" type="text"
+          value="{{query_id}}_plan"/>
+        <select id="export_format" class="form-control-sm btn btn-primary">
+          <option selected>.html</option>
+        </select>
+      </div>
+      <div class="modal-footer">
+        <a id="export_link" class="btn btn-primary" data-dismiss="modal" 
href="#"
+            role="button"> Download </a>
+      </div>
+    </div>
+  </div>
+</div>
+
 {{> www/common-footer.tmpl }}
 
 <script src="{{ __common__.host-url }}/www/d3.v3.min.js" 
charset="utf-8"></script>
@@ -83,6 +118,11 @@ var g = new dagreD3.graphlib.Graph().setGraph({rankDir: 
"BT"});
 var svg = d3.select("svg");
 var inner = svg.select("g");
 
+var export_link = document.getElementById("export_link");
+var export_filename = document.getElementById("export_filename");
+var export_format = document.getElementById("export_format");
+export_filename.value = export_filename.value.replace(/\W/g,'_');
+
 // Set up zoom support
 var zoom = d3.behavior.zoom().on("zoom", function() {
   inner.attr("transform", "translate(" + d3.event.translate + ")" +
@@ -231,6 +271,21 @@ function refresh() {
   req.send();
 }
 
+// Attaches a blob of the current SVG viewport to the associated link
+export_link.addEventListener('click', function(event) {
+  if (export_format.value == ".html") {
+    var svg_viewport = document.querySelector("svg");
+    var export_style = document.getElementById("css");
+    var html_blob = new Blob([`<!DOCTYPE html><body>`,
+        `<h1 style="font-family:monospace;">Query {{query_id}}</h1>`,
+        export_style.outerHTML, svg_viewport.outerHTML, `</body></html>`],
+        {type: "text/html;charset=utf-8"});
+    export_link.href = URL.createObjectURL(html_blob);
+  }
+  export_link.download = `${export_filename.value}${export_format.value}`;
+  export_link.click();
+});
+
 // Force one refresh before starting the timer.
 refresh();
 
diff --git a/www/query_timeline.tmpl b/www/query_timeline.tmpl
index ad4f057cd..9061c7b12 100644
--- a/www/query_timeline.tmpl
+++ b/www/query_timeline.tmpl
@@ -24,6 +24,11 @@ under the License.
 <div class="container">
 
 <style id="css">
+#fragment_diagram, #phases_header, #timeticks_footer {
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 
"Helvetica Neue",
+      Arial, "Noto Sans", sans-serif;
+  vertical-align: middle;
+}
 </style>
 
 {{> www/query_detail_tabs.tmpl }}
@@ -34,27 +39,57 @@ under the License.
 {{/plan_metadata_unavailable}}
 
 {{^plan_metadata_unavailable}}
-<h3>Timeline</h3>
+  <div style="display:flex; justify-content:space-between;">
+    <h3>Timeline</h3>
+    <label>
+      <h4 style="display:inline;"> Download : </h4>
+      <input type="button" class="btn btn-primary" data-toggle="modal" 
value="HTML"
+          data-target="#export_modal" role="button"/>
+    </label>
+  </div>
   <label>
-  <input type="checkbox" id="plan_order" onClick="renderTiming()"/>
-  Print tree in plan order (if unchecked, print in fragment order)
+    <input type="checkbox" id="plan_order" onClick="renderTiming()"/>
+    Print tree in plan order (if unchecked, print in fragment order)
   </label>
 
 </div>
 
-<div id="timing_diagram" style="border:1px solid #c3c3c3; overflow:hidden;">
-  <div style="margin-top:5px; border:1px solid #c3c3c3;">
+<div id="export_modal" style="transition-duration: 0.15s;" class="modal fade"
+    role="dialog" data-keyboard="true" tabindex="-1">
+  <div class="modal-dialog modal-dialog-centered">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5> Download Timeline </h5>
+        <input class="btn btn-primary" type="button" value="X" 
data-dismiss="modal"/>
+      </div>
+      <div class="modal-body">
+        <h6 class="d-inline"> Filename: </h6>
+        <input id="export_filename" class="form-control-sm" type="text"
+          value="{{query_id}}_timeline"/>
+        <select id="export_format" class="form-control-sm btn btn-primary">
+          <option selected>.html</option>
+        </select>
+      </div>
+      <div class="modal-footer">
+        <a id="export_link" class="btn btn-primary" data-dismiss="modal" 
href="#"
+            role="button"> Download </a>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div id="timing_diagram" style="border:1px solid #c3c3c3;">
+  <div style="margin-top:5px; border:1px solid #c3c3c3; overflow:hidden;">
     <svg id="phases_header" height="15px"></svg>
   </div>
-  <div style="margin-top:5px; border:1px solid #c3c3c3; overflow-y:auto;">
+  <div style="margin-top:5px; border:1px solid #c3c3c3; overflow-y:auto; 
overflow-x:hidden;">
     <svg id="fragment_diagram"></svg>
   </div>
-  <div style="margin-top:5px; border:1px solid #c3c3c3;">
+  <div style="margin-top:5px; border:1px solid #c3c3c3; overflow:hidden;">
     <svg id="timeticks_footer" height="15px"></svg>
   </div>
 </div>
 
-
 {{/plan_metadata_unavailable}}
 
 {{> www/common-footer.tmpl }}
@@ -83,7 +118,11 @@ var receiver_nodes = [];
 var profile_available = false;
 var maxts = 0;
 
-function get_svg_rect(fill_color, x, y, width, height, dash, stroke_color){
+var export_filename = document.getElementById("export_filename");
+var export_format = document.getElementById("export_format");
+export_filename.value = export_filename.value.replace(/\W/g,'_');
+
+function get_svg_rect(fill_color, x, y, width, height, dash, stroke_color) {
   var rect = document.createElementNS("http://www.w3.org/2000/svg";, "rect");
   rect.setAttribute("x", x + "px");
   rect.setAttribute("y", y + "px");
@@ -97,7 +136,7 @@ function get_svg_rect(fill_color, x, y, width, height, dash, 
stroke_color){
   return rect;
 }
 
-function get_svg_text(text, fill_color, x, y, height, container_center, 
max_width = 0){
+function get_svg_text(text, fill_color, x, y, height, container_center, 
max_width = 0) {
   var text_el = document.createElementNS("http://www.w3.org/2000/svg";, "text");
   text_el.appendChild(document.createTextNode(text));
   text_el.setAttribute("x", x + "px");
@@ -115,7 +154,7 @@ function get_svg_text(text, fill_color, x, y, height, 
container_center, max_widt
   return text_el;
 }
 
-function get_svg_line(stroke_color, x1, y1, x2, y2, dash){
+function get_svg_line(stroke_color, x1, y1, x2, y2, dash) {
   var line = document.createElementNS("http://www.w3.org/2000/svg";, "line");
   line.setAttribute("x1", x1 + "px");
   line.setAttribute("y1", y1 + "px");
@@ -519,6 +558,34 @@ function refresh() {
   req.send();
 }
 
+// Attaches a SVG blob of the complete timeline to the associated link
+export_link.addEventListener('click', function(event) {
+  if (export_format.value == ".html") {
+    var phases_header = document.getElementById('phases_header');
+    var fragment_diagram = document.getElementById('fragment_diagram');
+    var timeticks_footer = document.getElementById('timeticks_footer');
+    var export_style = document.getElementById("css");
+
+    // Deep clone 'parentNode's as wrappers to SVG components
+    var phases_header_wrapper = phases_header.parentNode.cloneNode(true);
+    var fragment_diagram_wrapper = fragment_diagram.parentNode.cloneNode(true);
+    var timeticks_footer_wrapper = timeticks_footer.parentNode.cloneNode(true);
+
+    // Set dimensions for fragment diagram's wrapper
+    fragment_diagram_wrapper.style.height = fragment_diagram.style.height;
+    fragment_diagram_wrapper.style.width = fragment_diagram.style.width;
+
+    var html_blob = new Blob([`<!DOCTYPE html><body>`,
+        `<h1 style="font-family:monospace;">Query {{query_id}}</h1>`,
+        export_style.outerHTML, phases_header_wrapper.outerHTML,
+        fragment_diagram_wrapper.outerHTML, timeticks_footer_wrapper.outerHTML,
+        `</body></html>`], {type: "text/html;charset=utf-8"});
+    export_link.href = URL.createObjectURL(html_blob);
+  }
+  export_link.download = `${export_filename.value}${export_format.value}`;
+  export_link.click();
+});
+
 window.addEventListener('resize', function(event) {
   if (profile_available) {
     renderTiming();

Reply via email to