浏览代码

HUE-8674 [jb] Revamp UX for Impala query plan.

jdesjean 7 年之前
父节点
当前提交
fb3eb25b08

+ 32 - 0
apps/jobbrowser/src/jobbrowser/apis/query_api.py

@@ -171,8 +171,40 @@ class QueryApi(Api):
     query = self.api.get_query(query_id=appid)
     query['summary'] = query.get('summary').strip() if query.get('summary') else ''
     query['plan'] = query.get('plan').strip() if query.get('plan') else ''
+    if query['plan_json']:
+      def get_exchange_icon (o):
+        if re.search(r'broadcast', o['label_detail'], re.IGNORECASE):
+          return { 'svg': 'hi-broadcast' }
+        elif re.search(r'hash', o['label_detail'], re.IGNORECASE):
+          return { 'font': 'fa-random' }
+        else:
+          return { 'font': 'fa-exchange' }
+      mapping = {
+        'TOP-N': { 'type': 'SORT', 'icon': { 'font': 'fa-sort' } },
+        'MERGING-EXCHANGE': {'type': 'EXCHANGE', 'icon': { 'fn': get_exchange_icon } },
+        'EXCHANGE': { 'type': 'EXCHANGE', 'icon': { 'fn': get_exchange_icon } },
+        'SCAN HDFS': { 'type': 'SCAN_HDFS', 'icon': { 'font': 'fa-files-o' } },
+        'SCAN KUDU': { 'type': 'SCAN_KUDU', 'icon': { 'font': 'fa-table' } },
+        'HASH JOIN': { 'type': 'HASH_JOIN', 'icon': { 'svg': 'hi-join' } }
+      }
+      def process(node, mapping=mapping):
+        node['id'], node['name'] = node['label'].split(':')
+        details = mapping.get(node['name'])
+        if details:
+          icon = details['icon']
+          if icon and icon.get('fn'):
+            icon = icon['fn'](node)
+          node['icon'] = icon
+
+      for node in query['plan_json']['plan_nodes']:
+        self._for_each_node(node, process)
     return query
 
+  def _for_each_node(self, node, fn):
+    fn(node)
+    for child in node['children']:
+      self._for_each_node(child, fn)
+
   def _query_profile(self, appid):
     return self.api.get_query_profile(query_id=appid)
 

文件差异内容过多而无法显示
+ 0 - 1
apps/jobbrowser/src/jobbrowser/static/jobbrowser/css/jobbrowser-embeddable.css


+ 78 - 16
apps/jobbrowser/src/jobbrowser/static/jobbrowser/js/impala_dagre.js

@@ -15,22 +15,64 @@ function impalaDagre(id) {
   var _impalaDagree = {
     update: function(plan) {
       renderGraph(plan);
+      _impalaDagree._width = $(svg[0]).width();
     },
     height: function(value) {
       var scale = _impalaDagree.scale || 1;
       var height = value || 600;
+      _impalaDagree._height = height;
       svg.attr('height', Math.min(g.graph().height * scale + 40, height) || height);
     },
+    action: function(type) {
+      if (type == 'plus') {
+        zoom.scale(zoom.scale() + 0.25);
+        inner.attr("transform", "translate(" + zoom.translate() + ")" +
+            "scale(" + zoom.scale() + ")");
+      } else if (type == 'minus') {
+        zoom.scale(zoom.scale() - 0.25);
+        inner.attr("transform", "translate(" + zoom.translate() + ")" +
+            "scale(" + zoom.scale() + ")");
+      }
+    },
     moveTo: function(id) {
       zoomToNode(id);
-    }
+    },
+    select: function(id) {
+      select(id);
+    },
   };
+  createActions();
+
+  function createActions () {
+    d3.select("#"+id)
+      .style('position', 'relative')
+    .append('div')
+      .style('position', 'absolute')
+      .style('right', '5px')
+      .style('bottom', '5px')
+      .classed('button', true)
+    .selectAll('button').data([{ type: 'plus', icon: 'fa-plus' }, { type: 'minus', icon: 'fa-minus' }])
+    .enter()
+    .append(function (data) {
+       var button = $("<div class='fa fa-fw valign-middle " + data.icon + "'></div>")[0];
+       $(button).on('click', function () {
+         _impalaDagree.action(data.type);
+       });
+       return button;
+    });
+  }
 
   // Set up zoom support
   var zoom = d3.behavior.zoom().on("zoom", function() {
-    _impalaDagree.scale = d3.event.scale;
-    inner.attr("transform", "translate(" + d3.event.translate + ")" +
-               "scale(" + d3.event.scale + ")");
+    var e = d3.event,
+        scale = Math.min(Math.max(e.scale, Math.min(_impalaDagree._width / g.graph().width, _impalaDagree._height / g.graph().height)), 2),
+        tx = Math.min(40, Math.max(e.translate[0], _impalaDagree._width - 40 - g.graph().width * scale)),
+        ty = Math.min(40, Math.max(e.translate[1], _impalaDagree._height - 40 - g.graph().height * scale));
+    _impalaDagree.scale = scale;
+    zoom.translate([tx, ty]);
+    zoom.scale(scale);
+    inner.attr("transform", "translate(" + [tx, ty] + ")" +
+               "scale(" + scale + ")");
   });
   svg.call(zoom);
 
@@ -47,14 +89,16 @@ function impalaDagre(id) {
   function build(node, parent, edges, states, colour_idx, max_node_time) {
     if (!node["output_card"]) return;
     states.push({ "name": node["label"],
+                  "type": node["type"],
+                  "label": node["name"],
                   "detail": node["label_detail"],
                   "num_instances": node["num_instances"],
                   "num_active": node["num_active"],
                   "max_time": node["max_time"],
                   "avg_time": node["avg_time"],
+                  "icon": node["icon"],
                   "is_broadcast": node["is_broadcast"],
-                  "max_time_val": node["max_time_val"],
-                  "style": "fill: " + colours[colour_idx]});
+                  "max_time_val": node["max_time_val"]});
     if (parent) {
       var label_val = "" + node["output_card"].toLocaleString();
       edges.push({ start: node["label"], end: parent,
@@ -66,7 +110,7 @@ function impalaDagre(id) {
       edges.push({ "start": node["label"],
                    "end": node["data_stream_target"],
                    "style": { label: "" + node["output_card"].toLocaleString(),
-                              style: "stroke: #f66; stroke-dasharray: 5, 5;"}});
+                              style: "stroke-dasharray: 5, 5;"}});
     }
     max_node_time = Math.max(node["max_time_val"], max_node_time)
     for (var i = 0; i < node["children"].length; ++i) {
@@ -78,7 +122,16 @@ function impalaDagre(id) {
 
   var is_first = true;
 
-  function zoomToNode(node) {
+  function select(node) {
+    var key = getKey(node);
+    if (!key) {
+      return;
+    }
+    $("g.node").attr('class', 'node') // addClass doesn't work in svg on our version of jQuery
+    $("g.node:contains('" + key + "')").attr('class', 'node active');
+  }
+
+  function getKey(node) {
     var nodes = g.nodes();
     var key;
     var nNode = parseInt(node, 10);
@@ -89,6 +142,11 @@ function impalaDagre(id) {
         break;
       }
     }
+    return key;
+  }
+
+  function zoomToNode(node) {
+    var key = getKey(node);
     if (!key) {
       return;
     }
@@ -122,14 +180,18 @@ function impalaDagre(id) {
     var states_by_name = { };
     states.forEach(function(state) {
       // Build the label for the node from the name and the detail
-      var html = "<span>" + state.name + "</span><br/>";
-      html += "<span>" + state.detail + "</span><br/>";
-      html += "<span>" + state.num_instances + " instance";
-      if (state.num_instances > 1) {
-        html += "s";
+      var html = "";
+      if (state.icon && state.icon.svg) {
+        html += '<svg class="hi"><use xlink:href="#'+ state.icon.svg +'"></use></svg>'
+        //html += "<img src=\"" + icon.svg + "\"></img>";
+      } else if (state.icon && state.icon.font){
+        html += "<span class='fa fa-fw valign-middle " + state.icon.font + "'></span>";
       }
-      html += "</span><br/>";
-      html += "<span>Max: " + state.max_time + ", avg: " + state.avg_time + "</span>";
+      html += "<span class='name'>" + state.label + "</span><br/>";
+      html += "<span class='metric'>" + state.max_time + "</span>";
+      html += "<span class='detail'>" + state.detail + "</span><br/>";
+      html += "<span class='metric'>" + state.max_time + "</span>"
+      html += "<span class='id'>" + state.name + "</span>";;
 
       var style = state.style;
 
@@ -150,7 +212,7 @@ function impalaDagre(id) {
       // Impala marks 'broadcast' as a property of the receiver, not the sender. We use
       // '(BCAST)' to denote that a node is duplicating its output to all receivers.
       if (states_by_name[edge.end].is_broadcast) {
-        edge.style.label += " \n(BCAST * " + states_by_name[edge.end].num_instances + ")";
+        edge.style.label += " * " + states_by_name[edge.end].num_instances;
       }
       g.setEdge(edge.start, edge.end, edge.style);
     });

+ 83 - 12
apps/jobbrowser/src/jobbrowser/static/jobbrowser/less/jobbrowser-embeddable.less

@@ -90,18 +90,89 @@
   }
 
   .query-plan {
-    border: 1px solid @cui-gray-300
-  }
-
-  .node rect {
-    stroke: @cui-gray-300;
-    fill: @cui-white;
-  }
-
-  .edgePath path {
-    stroke: @cui-gray-800;
-    fill: @cui-gray-800;
-    stroke-width: 1.5px;
+    border: 1px solid @cui-gray-300;
+    .label,
+    .badge {
+      color: @cui-gray-800;
+      text-shadow: none;
+    }
+    .metric {
+      position: absolute;
+      top: 0px;
+      right: 0px;
+      font-weight: normal;
+    }
+    .name {
+      padding-right: 80px;
+    }
+    .detail {
+      font-weight: normal;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      width: calc(~"100% - 32px");
+      display: inline-block;
+    }
+    span.fa {
+      color: @cui-gray-600;
+      float: left;
+      font-size: 21px;
+      padding-top: 3px;
+      padding-right: 2px;
+    }
+    .hi {
+      color: @cui-gray-600 !important;
+      float: left;
+      width: 2.2em !important;
+      height: 2.2em !important;
+      padding-right: 5px;
+    }
+    .id {
+      display: none;
+    }
+    .node.active {
+      rect {
+        filter: url(#dropshadow);
+        stroke: @hue-primary-color-dark;
+        fill: @hue-primary-color-light;
+      }
+    }
+    .output {
+      path,
+      rect {
+        stroke: @cui-gray-600;
+      }
+    }
+    .button {
+      border: 1px solid @cui-gray-300;
+      color: @cui-gray-600;
+      div {
+        line-height: 32px !important;
+        display: block;
+        width: 32px;
+        height: 32px;
+      }
+    }
+    .button div:hover {
+      color: @hue-primary-color-dark
+    }
+    .node rect {
+      fill: @cui-white;
+      stroke-width: 1px
+    }
+    .edgePath > path {
+      fill: none;
+      stroke-width: 1.5px;
+    }
+    .edgePath marker path {
+      fill: @cui-gray-600;
+      stroke-width: 1.5px;
+    }
+    .edgeLabel text {
+      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    }
+    foreignObject > div {
+      position: relative;
+    }
   }
 
   div[data-jobType="queries"] pre {

+ 15 - 2
apps/jobbrowser/src/jobbrowser/templates/job_browser.mako

@@ -1508,8 +1508,21 @@ ${ commonheader("Job Browser", "jobbrowser", user, request) | n,unicode }
       <div class="tab-content">
         <div class="tab-pane" id="queries-page-plan${ SUFFIX }" data-profile="plan">
           <div data-bind="visible:properties.plan && properties.plan().plan_json && properties.plan().plan_json.plan_nodes.length">
-            <div id="queries-page-plan-graph${ SUFFIX }" data-bind="impalaDagre: {value: properties.plan && properties.plan().plan_json, height:$root.isMini() ? 550 : 600 }">
-              <svg class="query-plan" style="width:100%;height:100%;" id="queries-page-plan-svg${ SUFFIX }">
+            <div class="query-plan" id="queries-page-plan-graph${ SUFFIX }" data-bind="impalaDagre: {value: properties.plan && properties.plan().plan_json, height:$root.isMini() ? 535 : 600 }">
+              <svg style="width:100%;height:100%;" id="queries-page-plan-svg${ SUFFIX }">
+                <defs>
+                  <filter id="dropshadow" height="130%">
+                    <feGaussianBlur in="SourceAlpha" stdDeviation="3"/> <!-- stdDeviation is how much to blur -->
+                    <feOffset dx="0" dy="0" result="offsetBlur"/> <!-- how much to offset -->
+                    <feComponentTransfer>
+                      <feFuncA type="linear" slope="0.5"/> <!-- slope is the opacity of the shadow -->
+                    </feComponentTransfer>
+                    <feMerge>
+                      <feMergeNode/> <!-- this contains the offset blurred image -->
+                      <feMergeNode in="SourceGraphic"/> <!-- this contains the element that the filter is applied to -->
+                    </feMerge>
+                  </filter>
+                </defs>
                 <g/>
               </svg>
             </div>

+ 4 - 0
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -7500,8 +7500,12 @@
         var clickSubscription = huePubSub.subscribe('impala.node.moveto', function(value) {
           self._impalaDagre.moveTo(value);
         });
+        var selectSubscription = huePubSub.subscribe('impala.node.select', function(value) {
+          self._impalaDagre.select(value);
+        });
         ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
           clickSubscription.remove();
+          selectSubscription.remove();
         });
       },
       update: function (element, valueAccessor) {

+ 19 - 0
desktop/core/src/desktop/templates/hue_icons.mako

@@ -296,6 +296,25 @@
         </g>
       </g>
     </symbol>
+
+    <symbol id="hi-join" width="32px" height="32px" viewBox="0 0 480 480">
+      <path d="m174.99999,177l-80.80644,-37l0,22.2l-86.19354,0l0,29.6l86.19354,0l0,22.2l80.80644,-37z"/>
+      <path d="m400,164.1875l0,25.625c0,4.2435 -10.92,7.6875 -24.375,7.6875l-81.25,0c-13.455,0 -24.375,-3.444 -24.375,-7.6875l0,-25.625c0,-4.2435 10.92,-7.6875 24.375,-7.6875l81.25,0c13.455,0 24.375,3.444 24.375,7.6875z"/>
+      <path d="m437.375,13l-201.74999,0c-18.49375,0 -33.625,25.36875 -33.625,56.375l0,338.24999c0,31.00625 15.13125,56.375 33.625,56.375l201.74999,0c18.49375,0 33.625,-25.36875 33.625,-56.375l0,-338.24999c0,-31.00625 -15.13125,-56.375 -33.625,-56.375zm0,394.62499l-201.74999,0l0,-338.24999l201.74999,0l0,338.24999z"/>
+      <path d="m456.5,45.8125l0,54.37499c0,9.0045 -20.16,16.3125 -45,16.3125l-150,0c-24.84,0 -45,-7.308 -45,-16.3125l0,-54.37499c0,-9.0045 20.16,-16.3125 45,-16.3125l150,0c24.84,0 45,7.308 45,16.3125z"/>
+      <path d="m400,251.6875l0,25.625c0,4.2435 -10.92,7.6875 -24.375,7.6875l-81.25,0c-13.455,0 -24.375,-3.444 -24.375,-7.6875l0,-25.625c0,-4.2435 10.92,-7.6875 24.375,-7.6875l81.25,0c13.455,0 24.375,3.444 24.375,7.6875z"/>
+      <path d="m400,334.6875l0,25.625c0,4.2435 -10.92,7.6875 -24.375,7.6875l-81.25,0c-13.455,0 -24.375,-3.444 -24.375,-7.6875l0,-25.625c0,-4.2435 10.92,-7.6875 24.375,-7.6875l81.25,0c13.455,0 24.375,3.444 24.375,7.6875z"/>
+      <path d="m174.99999,264l-80.80644,-37l0,22.2l-86.19354,0l0,29.6l86.19354,0l0,22.2l80.80644,-37z"/>
+      <path d="m174.99999,348.5l-80.80644,-37l0,22.2l-86.19354,0l0,29.6l86.19354,0l0,22.2l80.80644,-37z"/>
+    </symbol>
+
+    <symbol id="hi-broadcast" viewBox="0 0 32 32">
+      <path d="M12 16c0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.209-1.791 4-4 4s-4-1.791-4-4zM20.761 7.204c3.12 1.692 5.239 4.997 5.239 8.796s-2.119 7.104-5.239 8.796c1.377-2.191 2.239-5.321 2.239-8.796s-0.862-6.605-2.239-8.796zM9 16c0 3.475 0.862 6.605 2.239 8.796-3.12-1.692-5.239-4.997-5.239-8.796s2.119-7.104 5.239-8.796c-1.377 2.191-2.239 5.321-2.239 8.796zM3 16c0 5.372 1.7 10.193 4.395 13.491-4.447-2.842-7.395-7.822-7.395-13.491s2.948-10.649 7.395-13.491c-2.695 3.298-4.395 8.119-4.395 13.491zM24.605 2.509c4.447 2.842 7.395 7.822 7.395 13.491s-2.948 10.649-7.395 13.491c2.695-3.298 4.395-8.119 4.395-13.491s-1.7-10.193-4.395-13.491z"></path>
+    </symbol>
+
+    <symbol id="hi-sigma" viewBox="0 0 32 32">
+      <path d="M29.425 22.96l1.387-2.96h1.188l-2 12h-30v-2.32l10.361-12.225-10.361-10.361v-7.094h30.625l1.375 8h-1.074l-0.585-1.215c-1.104-2.293-1.934-2.785-4.341-2.785h-20.688l11.033 11.033-9.294 10.967h16.949c3.625 0 4.583-1.299 5.425-3.040z"></path>
+    </symbol>
   </svg>
 
   <script type="text/html" id="app-switcher-icon-template">

+ 2 - 1
desktop/core/src/desktop/templates/ko_components/ko_execution_analysis.mako

@@ -231,18 +231,19 @@ from desktop.views import _ko
 
       ExecutionAnalysis.prototype.handleNodePress = function (contributor) {
         var self = this;
-        debugger;
         //TODO: Loading
         if (!$('[href*="' + self.details().name + '"]')[0]) {
           huePubSub.publish('show.jobs.panel', { id: self.details().name, interface: 'queries' });
           setTimeout(function() {
             huePubSub.publish('impala.node.moveto', contributor.result_id);
+            huePubSub.publish('impala.node.select', contributor.result_id);
           }, 500);
         } else {
           if (!$('#jobsPanel').is(":visible")) {
             $('#jobsPanel').show();
           }
           huePubSub.publish('impala.node.moveto', contributor.result_id);
+          huePubSub.publish('impala.node.select', contributor.result_id);
         }
       };
 

+ 16 - 1
desktop/libs/libanalyze/src/libanalyze/utils.py

@@ -43,7 +43,22 @@ def parse_exec_summary(summary_string):
     for c in cleaned:
         # Key 0 is id and type
         fid, ftype = c[0].split(":")
-        result[int(fid)] = {
+        if len(c) < 9:
+          index_bytes = False
+          for i in range(len(c) - 1, 0, -1):
+            if re.search('\d*.?b', c[i], re.IGNORECASE):
+              index_bytes = i
+          if index_bytes:
+            c.insert(index_bytes, '')
+            c.insert(index_bytes, '')
+          else:
+            c.append('')
+            c.append('')
+        if re.search('F\d*', fid):
+          cleaned_fid = fid
+        else:
+          cleaned_fid = int(fid)
+        result[cleaned_fid] = {
             "type": ftype,
             "hosts": int(c[1]),
             "avg": c[2],

部分文件因为文件数量过多而无法显示