Преглед изворни кода

HUE-4032 [editor] Show table sample and analysis for tables in the editor

Right-click on top of a table to open the sql context popover.
Johan Ahlen пре 9 година
родитељ
комит
dde3211

+ 9 - 0
desktop/core/src/desktop/static/desktop/css/hue3.css

@@ -2827,4 +2827,13 @@ input[type='password']::-ms-reveal {
   position: absolute;
   border-bottom: 1px dotted #FF0000;
   border-radius: 0 !important;
+}
+
+.hue-ace-location {
+  position: absolute;
+  background-color: #DBE8F1;
+  border: 1px solid #DBE8F1;
+  border-radius: 1px;
+  margin-left: -1px;
+  margin-top: -1px;
 }

+ 61 - 238
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -2869,254 +2869,77 @@
         }
       });
 
-      var currentAssistTables = {};
-
-      var refreshTables = function() {
-        currentAssistTables = {};
-        if (snippet.database()) {
-          apiHelper.fetchTables({
-            sourceType: snippet.type(),
-            databaseName: snippet.database(),
-            successCallback: function(data) {
-              $.each(data.tables_meta, function(index, tableMeta) {
-                currentAssistTables[tableMeta.name] = true;
-              });
-            },
-            silenceErrors: true
-          });
+      var lastHoveredToken = null;
+      var activeMarker = null;
+
+      var clearActiveMarker = function () {
+        if (activeMarker !== null) {
+          editor.session.removeMarker(activeMarker);
+          activeMarker = null;
         }
       };
-      snippet.database.subscribe(refreshTables);
-      refreshTables();
-
-      ace.define("huelink", [], function (require, exports, module) {
-        "use strict";
-
-        var Oop = ace.require("ace/lib/oop");
-        var Event = ace.require("ace/lib/event");
-        var Range = ace.require("ace/range").Range;
-        var EventEmitter = ace.require("ace/lib/event_emitter").EventEmitter;
-        var Tooltip = ace.require("ace/tooltip").Tooltip;
-
-        var HueLink = function (editor) {
-          if (editor.hueLink)
-            return;
-          editor.hueLink = this;
-          this.editor = editor;
-          Tooltip.call(this, editor.container);
-
-          this.update = this.update.bind(this);
-          this.onMouseMove = this.onMouseMove.bind(this);
-          this.onMouseOut = this.onMouseOut.bind(this);
-          this.onClick = this.onClick.bind(this);
-          Event.addListener(editor.renderer.scroller, "mousemove", this.onMouseMove);
-          Event.addListener(editor.renderer.content, "mouseout", this.onMouseOut);
-          Event.addListener(editor.renderer.content, "click", this.onClick);
-        };
-
-        Oop.inherits(HueLink, Tooltip);
-
-        (function () {
-          Oop.implement(this, EventEmitter);
-
-          this.token = {};
-          this.marker = null;
-          this.updateThrottle = -1;
-
-          this.update = function () {
-            var self = this;
-            self.$timer = null;
-            window.clearTimeout(self.updateThrottle);
-            self.updateThrottle = window.setTimeout(function () {
-              self.performUpdate();
-            }, 100);
-          };
-
-          this.performUpdate = function () {
-            var editor = this.editor;
-            var renderer = editor.renderer;
-
-            var canvasPos = renderer.scroller.getBoundingClientRect();
-            var offset = (this.x + renderer.scrollLeft - canvasPos.left - renderer.$padding) / renderer.characterWidth;
-            var row = Math.floor((this.y + renderer.scrollTop - canvasPos.top) / renderer.lineHeight);
-            var col = Math.round(offset);
 
-            var screenPos = {row: row, column: col, side: offset - col > 0 ? 1 : -1};
-            var session = editor.session;
-            var docPos = session.screenToDocumentPosition(screenPos.row, screenPos.column);
+      var markLocation = function (parseLocation) {
+        var range;
+        if (parseLocation.type === 'function') {
+          // Todo: Figure out why functions need an extra char at the end
+          range = new AceRange(parseLocation.location.first_line - 1, parseLocation.location.first_column - 1, parseLocation.location.last_line - 1, parseLocation.location.last_column);
+        } else {
+          range = new AceRange(parseLocation.location.first_line - 1, parseLocation.location.first_column - 1, parseLocation.location.last_line - 1, parseLocation.location.last_column - 1);
+        }
+        activeMarker = editor.session.addMarker(range, 'hue-ace-location');
+        return range;
+      };
 
-            var selectionRange = editor.selection.getRange();
-            if (!selectionRange.isEmpty()) {
-              if (selectionRange.start.row <= row && selectionRange.end.row >= row)
-                return this.clear();
+      editor.on("mousemove", function (e) {
+        var selectionRange = editor.selection.getRange();
+        if (selectionRange.isEmpty()) {
+          var pointerPosition = editor.renderer.screenToTextCoordinates(e.clientX+5, e.clientY);
+          var token = editor.session.getTokenAt(pointerPosition.row, pointerPosition.column);
+          if (lastHoveredToken !== token) {
+            clearActiveMarker();
+            if (token.parseLocation) {
+              markLocation(token.parseLocation);
             }
+            lastHoveredToken = token;
+          }
+        }
+      });
 
-            var line = editor.session.getLine(docPos.row);
-            if (docPos.column == line.length) {
-              var clippedPos = editor.session.documentToScreenPosition(docPos.row, docPos.column);
-              if (clippedPos.column != screenPos.column) {
-                return this.clear();
-              }
-            }
+      editor.on("input", function (e) {
+        clearActiveMarker();
+        lastHoveredToken = null;
+      });
 
-            var token = editor.session.getTokenAt(docPos.row, docPos.column);
-
-            if (token) {
-              var self = this;
-              if (token.value === " * ") {
-                var start = token.value == " * " ? token.start + 1 : token.start;
-                var end = token.value == " * " ? token.start + 2 : token.start + token.value.length;
-                var beforeToken = editor.session.doc.getTextRange(new AceRange(0, 0, docPos.row, start));
-                var afterToken = editor.getValue().substring(beforeToken.length + 1);
-
-                var colHiglightCallback = function (cols) {
-                  if (cols.length > 0) {
-                    // add highlight for the clicked token
-                    var range = new AceRange(docPos.row, start, docPos.row, end);
-                    token.range = range;
-                    token.columns = cols;
-                    editor.session.removeMarker(self.marker);
-                    self.marker = editor.session.addMarker(range, 'ace_bracket red');
-                    editor.renderer.setCursorStyle("pointer");
-                    self.setText(options.expandStar);
-                    if ($.totalStorage("hue.ace.showLinkTooltips") == null || $.totalStorage("hue.ace.showLinkTooltips")) {
-                      self.show(null, self.x + 10, self.y + 10);
-                    }
-                    self.link = token;
-                    self.isClearable = true
-                  }
-                };
-
-                if ((typeof token.columns === 'undefined' || token.lastLengthAfter !== afterToken.length) && beforeToken.length < 50000 && afterToken.length < 50000) {
-                  token.lastLengthAfter = afterToken.length;
-                  snippet.autocompleter.autocomplete(beforeToken, afterToken, function (suggestions) {
-                    var cols = [];
-                    $.each(suggestions, function (idx, suggestion) {
-                      if (suggestion.isColumn) {
-                        cols.push(suggestion.value);
-                      }
-                    });
-                    colHiglightCallback(cols);
-                  });
-                } else if (typeof token.columns !== 'undefined') {
-                  colHiglightCallback(token.columns);
-                }
-              } else if (token.parseLocation) {
-                var range = new AceRange(docPos.row, token.start, docPos.row, token.start + token.value.length);
-                editor.session.removeMarker(this.marker);
-                this.marker = editor.session.addMarker(range, 'ace_bracket red');
-                editor.renderer.setCursorStyle("pointer");
-                this.setText(options.openIt);
-                if ($.totalStorage("hue.ace.showLinkTooltips") == null || $.totalStorage("hue.ace.showLinkTooltips")) {
-                  this.show(null, this.x + 10, this.y + 10);
-                }
-                this.link = { token: token, marker: this.marker };
-                this.isClearable = true
-              } else if (token.error) {
-                this.setText(this.createErrorMessage(token.error));
-                this.show(null, this.x + 10, this.y + 10);
-                this.isClearable = true
-              } else {
-                this.clear();
-              }
-            } else {
-              this.clear();
-            }
-          };
+      editor.container.addEventListener("mouseout", function (e) {
+        clearActiveMarker();
+        lastHoveredToken = null;
+      });
 
-          this.createErrorMessage = function (error) {
-            var expectedTokens = {};
-            error.expected.forEach(function (expected) {
-              expected = expected.substring(1, expected.length-1);
-              if (expected.indexOf('<') === 0) {
-                expected = expected.substring(expected.indexOf('>') + 1);
-              }
-              if (/^[A-Z]+$/.test(expected) && !/PARTIAL_CURSOR|CURSOR|EOF/.test(expected)) {
-                expectedTokens[expected] = true;
+      editor.container.addEventListener("contextmenu", function (e) {
+        var selectionRange = editor.selection.getRange();
+        huePubSub.publish('sql.context.popover.hide');
+        if (selectionRange.isEmpty()) {
+          var pointerPosition = editor.renderer.screenToTextCoordinates(e.clientX + 5, e.clientY);
+          var token = editor.session.getTokenAt(pointerPosition.row, pointerPosition.column);
+
+          if (typeof token.parseLocation !== 'undefined') {
+            var range = markLocation(token.parseLocation);
+            var startCoordinates = editor.renderer.textToScreenCoordinates(range.start.row, range.start.column);
+            var endCoordinates = editor.renderer.textToScreenCoordinates(range.end.row, range.end.column);
+            huePubSub.publish('sql.context.popover.show', {
+              data: token.parseLocation,
+              snippet: snippet,
+              source: {
+                left: startCoordinates.pageX - 3,
+                top: startCoordinates.pageY,
+                right: endCoordinates.pageX - 3,
+                bottom: endCoordinates.pageY + editor.renderer.lineHeight
               }
             });
-            var errorString = 'Expected: ';
-            var count = 1;
-            Object.keys(expectedTokens).forEach(function (expected) {
-              if (count > 1) {
-                errorString += ', ';
-              }
-              if (count % 5 === 0) {
-                errorString += '\n';
-              }
-              errorString += expected;
-              count++;
-            });
-            return errorString;
-          };
-
-          this.clear = function () {
-            if (this.isClearable) {
-              this.hide(); // hides the tooltip
-              this.editor.session.removeMarker(this.marker);
-              this.editor.renderer.setCursorStyle("");
-              this.isClearable = false;
-              this.link = null;
-            }
-          };
-
-          this.onClick = function (e) {
-            if (this.link && e.shiftKey) {
-              this.link.editor = this.editor;
-              this.editor.session.selection.clearSelection();
-              this._signal("open", this.link);
-              this.clear();
-            }
-          };
-
-          this.onMouseMove = function (e) {
-            if (this.editor.$mouseHandler.isMousePressed) {
-              if (!this.editor.selection.isEmpty())
-                this.clear();
-              return;
-            }
-            this.x = e.clientX;
-            this.y = e.clientY;
-            this.update();
-          };
-
-          this.onMouseOut = function (e) {
-            Tooltip.prototype.hide();
-            this.clear();
-          };
-
-          this.destroy = function () {
-            this.onMouseOut();
-            Event.removeListener(this.editor.renderer.scroller, "mousemove", this.onMouseMove);
-            Event.removeListener(this.editor.renderer.content, "mouseout", this.onMouseOut);
-            delete this.editor.hueLink;
-          };
-
-        }).call(HueLink.prototype);
-
-        exports.HueLink = HueLink;
-      });
-
-      HueLink = ace.require("huelink").HueLink;
-      editor.hueLink = new HueLink(editor);
-      editor.hueLink.on("open", function (link) {
-        var marker = editor.session.getMarkers()[link.marker];
-        var startCoordinates = editor.renderer.textToScreenCoordinates(marker.range.start.row, marker.range.start.column);
-        var endCoordinates = editor.renderer.textToScreenCoordinates(marker.range.end.row, marker.range.end.column);
-        var token = link.token;
-        if (token.parseLocation) {
-          huePubSub.publish('sql.context.popover.show', { data: token.parseLocation, source: {
-            left: startCoordinates.pageX,
-            top: startCoordinates.pageY,
-            right: endCoordinates.pageX,
-            bottom: endCoordinates.pageY + editor.renderer.lineHeight
-          }});
-        } else if (token.value === " * " && token.columns.length > 0) {
-          editor.session.replace(token.range, token.columns.join(", "))
-        } else if (token.value.indexOf("'/") == 0 && token.value.lastIndexOf("'") == token.value.length - 1) {
-          window.open("/filebrowser/#" + token.value.replace(/'/gi, ""), '_blank');
-        } else if (token.value.indexOf("\"/") == 0 && token.value.lastIndexOf("\"") == token.value.length - 1) {
-          window.open("/filebrowser/#" + token.value.replace(/\"/gi, ""), '_blank');
+            e.preventDefault();
+            return false;
+          }
         }
       });
 

+ 260 - 9
desktop/core/src/desktop/templates/sql_context_popover.mako

@@ -31,7 +31,7 @@ from metadata.conf import has_navigator
       z-index: 1060;
       display: block;
       width: 450px;
-      height: 300px;
+      height: 425px;
       padding: 1px;
       text-align: left;
       text-align: start;
@@ -126,7 +126,6 @@ from metadata.conf import has_navigator
       margin: 0;
       font-size: 0.9rem;
       background-color: #fff;
-      border-bottom: 1px solid #ebebeb;
     }
 
     .sql-context-popover-title:empty {
@@ -134,7 +133,8 @@ from metadata.conf import has_navigator
     }
 
     .sql-context-popover-content {
-      padding: 9px 14px;
+      padding: 0 9px 14px 9px;
+      overflow: hidden;
     }
 
     .sql-context-popover-arrow, .sql-context-popover-arrow::after {
@@ -146,6 +146,19 @@ from metadata.conf import has_navigator
       border-style: solid;
     }
 
+    .sql-context-tabs {
+      border-bottom: 1px solid #ebebeb;
+      margin-left: -10px;
+      margin-right: -10px;
+      padding-left: 15px;
+    }
+
+    .sql-context-tab {
+      padding-top: 0 !important;
+      padding-bottom: 5px !important;
+      margin-bottom: -1px !important
+    }
+
     .sql-context-popover-arrow {
       border-width: 6px;
     }
@@ -156,24 +169,102 @@ from metadata.conf import has_navigator
     }
   </style>
 
+  <script type="text/html" id="sql-context-table-sample">
+    <!-- ko with: tableStats -->
+    <!-- ko hueSpinner: { spin: loadingSamples, center: true, size: 'large' } --><!-- /ko -->
+    <!-- ko ifnot: loadingSamples -->
+    <div class="sample-scroll" style="text-align: left; padding: 3px; overflow: hidden">
+      <!-- ko with: samples -->
+      <!-- ko if: rows.length == 0 -->
+      <div class="alert">${ _('The selected table has no data.') }</div>
+      <!-- /ko -->
+      <!-- ko if: rows.length > 0 -->
+      <table id="samples-table" class="samples-table table table-striped table-condensed">
+        <thead>
+        <tr>
+          <th style="width: 10px">&nbsp;</th>
+          <!-- ko foreach: headers -->
+          <th data-bind="text: $data"></th>
+          <!-- /ko -->
+        </tr>
+        </thead>
+        <tbody>
+        </tbody>
+      </table>
+      <!-- /ko -->
+      <!-- /ko -->
+    </div>
+    <!-- /ko -->
+  </script>
+
+  <script type="text/html" id="sql-context-table-analysis">
+    <!-- ko with: tableStats -->
+      <!-- ko hueSpinner: { spin: loadingStats, center: true, size: 'large' } --><!-- /ko -->
+      <!-- ko ifnot: loadingStats -->
+        <div class="alert" style="text-align: left; display:none" data-bind="visible: statsHasError">${ _('There is no table analysis available') }</div>
+        <!-- ko ifnot: statsHasError -->
+          <div class="content" data-bind="niceScroll">
+            <!-- ko if: statRows().length -->
+              <table class="table table-striped">
+                <tbody data-bind="foreach: statRows">
+                <tr><th data-bind="text: data_type, style:{'border-top-color': $index() == 0 ? '#ffffff' : '#e5e5e5'}" style="background-color: #FFF"></th><td data-bind="text: $parents[1].formatAnalysisValue(data_type, comment), style:{'border-top-color': $index() == 0 ? '#ffffff' : '#e5e5e5'}" style="background-color: #FFF"></td></tr>
+                </tbody>
+              </table>
+            <!-- /ko -->
+          </div>
+        <!-- /ko -->
+      <!-- /ko -->
+    <!-- /ko -->
+  </script>
+
+  <script type="text/html" id="sql-context-column-sample">
+    Column sample
+  </script>
+
+  <script type="text/html" id="sql-context-function-details">
+    Function details
+  </script>
+
   <script type="text/html" id="sql-context-popover-template">
     <div class="sql-context-popover sql-context-popover-bottom" data-bind="css: orientationClass, style: { left: left() + 'px', top: top() + 'px' }">
       <div class="sql-context-popover-arrow"></div>
       <div class="sql-context-popover-title">
         <i class="pull-left fa muted" data-bind="css: iconClass" style="margin-top: 3px"></i> <span data-bind="text: title"></span>
+        <a class="pull-right pointer inactive-action" data-bind="click: close"><i class="fa fa-fw fa-times"></i></a>
+        <a class="pull-right pointer inactive-action" data-bind="visible: isTable, click: openInMetastore"><i style="margin-top: 3px;" title="${ _('Open in metastore...') }" class="fa fa-fw fa-external-link"></i></a>
       </div>
       <div class="sql-context-popover-content">
-        <pre data-bind="text: ko.mapping.toJSON(data)"></pre>
+        <!-- ko with: contents -->
+        <ul class="nav nav-pills sql-context-tabs" data-bind="foreach: tabs">
+          <li data-bind="click: function () { $parent.activeTab(id); }, css: { 'active' : $parent.activeTab() === id }">
+            <a class="sql-context-tab" data-toggle="tab" data-bind="text: label, attr: { href: '#' + id }"></a>
+          </li>
+        </ul>
+        <div class="tab-content" style="border: none; overflow: auto; height: 365px;" data-bind="foreach: tabs">
+          <div class="tab-pane" id="sampleTab" data-bind="attr: { id: id }, css: { 'active' : $parent.activeTab() === id }" style="overflow: hidden">
+            <!-- ko template: { name: template, data: $parent } --><!-- /ko -->
+          </div>
+        </div>
+        <!-- /ko -->
       </div>
     </div>
   </script>
 
   <script type="text/javascript" charset="utf-8">
-    require(['knockout'], function (ko) {
+    require([
+      'knockout',
+      'desktop/js/apiHelper',
+      'desktop/js/assist/tableStats'
+    ], function (ko, ApiHelper, TableStats) {
+
+      var intervals = [];
 
       var hidePopover = function () {
         $('#sqlContextPopover').remove();
         $(document).off('click', hideOnClickOutside);
+        intervals.forEach(function (interval) {
+          window.clearInterval(interval);
+        });
       };
 
       var hideOnClickOutside = function (event) {
@@ -182,12 +273,158 @@ from metadata.conf import has_navigator
         }
       };
 
+      var i18n = {
+        errorLoadingStats: "${ _('There was a problem loading the stats.') }",
+        errorRefreshingStats: "${ _('There was a problem refreshing the stats.') }",
+        errorLoadingTerms: "${ _('There was a problem loading the terms.') }"
+      };
+
+      function tableContextTabs(data, snippet) {
+        var self = this;
+        self.tabs = ko.observableArray([
+          { id: 'sample', label: '${ _("Sample") }', template: 'sql-context-table-sample' }
+        ]);
+        self.activeTab = ko.observable('sample');
+
+        var database = data.identifierChain.length > 1 ? data.identifierChain[0].name : snippet.database();
+        var tableName = data.identifierChain[data.identifierChain.length - 1].name;
+
+        self.tableStats = new TableStats({
+          i18n: i18n,
+          sourceType: snippet.type(),
+          databaseName: database,
+          tableName: tableName,
+          apiHelper: ApiHelper.getInstance(),
+          showViewMore: false
+        });
+
+        self.activeTab.subscribe(function (newValue) {
+          self.tableStats.activeTab(newValue);
+        });
+
+        self.tableStats.showAnalysis.subscribe(function (newValue) {
+          if (newValue && self.tabs().length === 1) {
+            self.tabs.push({ id: 'analysis', label: '${ _("Analysis") }', template: 'sql-context-table-analysis' });
+          }
+        });
+
+        self.formatAnalysisValue = function (type, val) {
+          if (type === 'last_modified_time' || type === 'transient_lastDdlTime') {
+            return localeFormat(val * 1000);
+          }
+          if (type.toLowerCase().indexOf('size') > -1) {
+            return filesize(val);
+          }
+          return val;
+        };
+
+        self.loadingSamples = ko.observable(true);
+
+        huePubSub.subscribe('sample.rendered', function (data) {
+          window.setTimeout(function () {
+            var $t = $('.samples-table');
+
+            if ($t.parent().hasClass('dataTables_wrapper')) {
+              if ($t.parent().data('scrollFnDt')) {
+                $t.parent().off('scroll', $t.parent().data('scrollFnDt'));
+              }
+              $t.unwrap();
+              if ($t.children('tbody').length > 0) {
+                $t.children('tbody').empty();
+              }
+              else {
+                $t.children('tr').remove();
+              }
+              $t.data('isScrollAttached', null);
+              $t.data('data', []);
+            }
+            var dt = $t.hueDataTable({
+              i18n: {
+                NO_RESULTS: "${_('No results found.')}",
+                OF: "${_('of')}"
+              },
+              fnDrawCallback: function (oSettings) {
+              },
+              scrollable: '.dataTables_wrapper',
+              forceInvisible: 10
+            });
+
+            $t.parents('.dataTables_wrapper').height(350);
+            $t.jHueTableExtender({
+              fixedHeader: true,
+              fixedFirstColumn: true,
+              fixedFirstColumnTopMargin: -1,
+              headerSorting: false,
+              includeNavigator: false,
+              parentId: 'sampleTab',
+              classToRemove: 'samples-table',
+              clonedContainerPosition: 'fixed'
+            });
+
+            $t.parents('.dataTables_wrapper').niceScroll({
+              cursorcolor: "#CCC",
+              cursorborder: "1px solid #CCC",
+              cursoropacitymin: 0,
+              cursoropacitymax: 0.75,
+              scrollspeed: 100,
+              mousescrollstep: 60,
+              cursorminheight: 20,
+              horizrailenabled: true
+            });
+
+            if (data && data.rows) {
+              var _tempData = [];
+              $.each(data.rows, function (index, row) {
+                var _row = row.slice(0); // need to clone the array otherwise it messes with the caches
+                _row.unshift(index + 1);
+                _tempData.push(_row);
+              });
+              if (_tempData.length > 0) {
+                dt.fnAddData(_tempData);
+              }
+            }
+          }, 0);
+        });
+      }
+
+      function columnContextTabs(data) {
+        var self = this;
+        self.tabs = [
+          { id: 'sample', label: '${ _("Sample") }', template: 'sql-context-column-sample' }
+        ];
+        self.activeTab = ko.observable('sample');
+      }
+
+      function functionContextTabs(data) {
+        var self = this;
+        self.tabs = [
+          { id: 'details', label: '${ _("Details") }', template: 'sql-context-function-details' }
+        ];
+        self.activeTab = ko.observable('details');
+      }
+
       function sqlContextPopoverViewModel(params) {
         var self = this;
         self.left = ko.observable();
         self.top = ko.observable();
         self.data = params.data;
+        self.snippet = params.snippet;
+        self.close = hidePopover;
         var orientation = params.orientation || 'bottom';
+        self.contents = null;
+
+        intervals.push(window.setInterval(function () {
+          var $t = $('.samples-table');
+          if ($t.length === 0) {
+            return;
+          }
+
+          if ($t.data('plugin_jHueTableExtender')) {
+            $t.data('plugin_jHueTableExtender').drawHeader();
+            $t.data('plugin_jHueTableExtender').drawFirstColumn();
+          }
+          $t.parents('.dataTables_wrapper').getNiceScroll().resize();
+        }, 300));
 
         switch (orientation) {
           case 'left':
@@ -208,13 +445,16 @@ from metadata.conf import has_navigator
         self.isFunction = params.data.type === 'function';
 
         if (self.isTable) {
-          self.title = params.data.identifierChain[params.data.identifierChain.length - 1].name;
+          self.contents = new tableContextTabs(self.data, self.snippet);
+          self.title = self.data.identifierChain[self.data.identifierChain.length - 1].name;
           self.iconClass = 'fa-table'
         } else if (self.isColumn) {
-          self.title = params.data.identifierChain[params.data.identifierChain.length - 1].name;
+          self.contents = new columnContextTabs(self.data);
+          self.title = self.data.identifierChain[self.data.identifierChain.length - 1].name;
           self.iconClass = 'fa-columns'
         } else if (self.isFunction) {
-          self.title = params.data.function;
+          self.contents = new functionContextTabs(self.data);
+          self.title = self.data.function;
           self.iconClass = 'fa-superscript'
         } else {
           self.title = '';
@@ -227,6 +467,17 @@ from metadata.conf import has_navigator
         hidePopover();
       };
 
+      sqlContextPopoverViewModel.prototype.openInMetastore = function () {
+        var self = this;
+        if (self.isTable) {
+          if (self.data.identifierChain.length === 1) {
+            window.open("/metastore/table/" + self.snippet.database() + "/" + self.data.identifierChain[0].name, '_blank');
+          } else if (token.identifierChain.length === 2) {
+            window.open("/metastore/table/" + self.data.identifierChain[0].name + '/' + self.data.identifierChain[1].name, '_blank');
+          }
+        }
+      };
+
       ko.components.register('sql-context-popover', {
         viewModel: sqlContextPopoverViewModel,
         template: { element: 'sql-context-popover-template' }
@@ -236,7 +487,7 @@ from metadata.conf import has_navigator
 
       huePubSub.subscribe('sql.context.popover.show', function (details) {
         hidePopover();
-        var $sqlContextPopover = $('<div id="sqlContextPopover" data-bind="component: { name: \'sql-context-popover\', params: { data: data, source: source } }" />');
+        var $sqlContextPopover = $('<div id="sqlContextPopover" data-bind="component: { name: \'sql-context-popover\', params: $data }" />');
         $('body').append($sqlContextPopover);
         ko.applyBindings(details, $sqlContextPopover[0]);
         window.setTimeout(function() {