Browse Source

HUE-8237 [assist] New context popover for files V1

- Navigation using breadcrumbs
- Listing of directories
- Preview of files
Johan Ahlen 7 years ago
parent
commit
dcdd595850

File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue3-extra.css


+ 42 - 0
desktop/core/src/desktop/static/desktop/js/apiHelper.js

@@ -498,6 +498,48 @@ var ApiHelper = (function () {
     });
     });
   };
   };
 
 
+
+  /**
+   *
+   * @param {Object} options
+   * @param {string[]} options.path
+   * @param {string} options.type - 's3', 'adls' or 'hdfs'
+   * @param {number} [options.offset]
+   * @param {number} [options.length]
+   * @param {boolean} [options.silenceErrors]
+   */
+  ApiHelper.prototype.fetchStoragePreview = function (options) {
+    var self = this;
+    var url;
+    if (options.type === 's3') {
+      url = S3_API_PREFIX;
+    } else if (options.type === 'adls') {
+      url = ADLS_API_PREFIX;
+    } else {
+      url = HDFS_API_PREFIX;
+    }
+
+    url += options.path.join('/') + '?compression=none&mode=text';
+    url += '&offset=' + (options.offset || 0);
+    url += '&length=' + (options.length || 118784);
+
+    var deferred = $.Deferred();
+    $.ajax({
+      dataType: "json",
+      url: url,
+      success: function (data) {
+        if (self.successResponseIsError(data)) {
+          deferred.reject(self.assistErrorCallback(options)(data))
+        } else {
+          deferred.resolve(data);
+        }
+      },
+      fail: deferred.reject
+    });
+
+    return deferred.promise();
+  };
+
   /**
   /**
    * @param {Object} options
    * @param {Object} options
    * @param {Function} options.successCallback
    * @param {Function} options.successCallback

+ 49 - 4
desktop/core/src/desktop/static/desktop/js/assist/assistStorageEntry.js

@@ -65,6 +65,7 @@ var AssistStorageEntry = (function () {
     self.path += self.definition.name;
     self.path += self.definition.name;
     self.currentPage = 1;
     self.currentPage = 1;
     self.hasMorePages = true;
     self.hasMorePages = true;
+    self.preview = ko.observable();
 
 
     self.filter = ko.observable('').extend({ rateLimit: 400 });
     self.filter = ko.observable('').extend({ rateLimit: 400 });
 
 
@@ -79,12 +80,17 @@ var AssistStorageEntry = (function () {
     self.loaded = false;
     self.loaded = false;
     self.loading = ko.observable(false);
     self.loading = ko.observable(false);
     self.loadingMore = ko.observable(false);
     self.loadingMore = ko.observable(false);
+    self.errorText = ko.observable();
     self.hasErrors = ko.observable(false);
     self.hasErrors = ko.observable(false);
     self.open = ko.observable(false);
     self.open = ko.observable(false);
 
 
     self.open.subscribe(function(newValue) {
     self.open.subscribe(function(newValue) {
-      if (newValue && self.entries().length == 0) {
-        self.loadEntries();
+      if (newValue && self.entries().length === 0) {
+        if (self.definition.type === 'dir') {
+          self.loadEntries();
+        } else {
+          self.loadPreview();
+        }
       }
       }
     });
     });
 
 
@@ -98,6 +104,23 @@ var AssistStorageEntry = (function () {
     huePubSub.publish(TYPE_SPECIFICS[self.type].dblClickPubSubId, self);
     huePubSub.publish(TYPE_SPECIFICS[self.type].dblClickPubSubId, self);
   };
   };
 
 
+  AssistStorageEntry.prototype.loadPreview = function () {
+    var self = this;
+    self.loading(true);
+    ApiHelper.getInstance().fetchStoragePreview({
+      path: self.getHierarchy(),
+      type: self.type,
+      silenceErrors: true
+    }).done(function (data) {
+      self.preview(data);
+    }).fail(function (errorText) {
+      self.hasErrors(true);
+      self.errorText(errorText);
+    }).always(function () {
+      self.loading(false);
+    })
+  };
+
   AssistStorageEntry.prototype.loadEntries = function(callback) {
   AssistStorageEntry.prototype.loadEntries = function(callback) {
     var self = this;
     var self = this;
     if (self.loading()) {
     if (self.loading()) {
@@ -126,8 +149,9 @@ var AssistStorageEntry = (function () {
       }
       }
     };
     };
 
 
-    var errorCallback = function () {
+    var errorCallback = function (errorText) {
       self.hasErrors(true);
       self.hasErrors(true);
+      self.errorText(errorText);
       self.loading(false);
       self.loading(false);
       if (callback) {
       if (callback) {
         callback();
         callback();
@@ -162,7 +186,7 @@ var AssistStorageEntry = (function () {
     var findNextAndLoadDeep = function () {
     var findNextAndLoadDeep = function () {
 
 
       var foundEntry = $.grep(self.entries(), function (entry) {
       var foundEntry = $.grep(self.entries(), function (entry) {
-        return entry.definition.name === nextName && entry.definition.type === 'dir';
+        return entry.definition.name === nextName;
       });
       });
       var passedAlphabetically = self.entries().length > 0 && self.entries()[self.entries().length - 1].definition.name.localeCompare(nextName) > 0;
       var passedAlphabetically = self.entries().length > 0 && self.entries()[self.entries().length - 1].definition.name.localeCompare(nextName) > 0;
 
 
@@ -264,6 +288,27 @@ var AssistStorageEntry = (function () {
     });
     });
   };
   };
 
 
+  AssistStorageEntry.prototype.showContextPopover = function (entry, event) {
+    var $source = $(event.target);
+    var offset = $source.offset();
+
+    huePubSub.publish('context.popover.show', {
+      data: {
+        type: 'storageEntry',
+        storageEntry: entry
+      },
+      pinEnabled: true,
+      orientation: 'right',
+      source: {
+        element: event.target,
+        left: offset.left,
+        top: offset.top - 3,
+        right: offset.left + $source.width() + 3,
+        bottom: offset.top + $source.height() - 3
+      }
+    });
+  };
+
   AssistStorageEntry.prototype.openInImporter = function () {
   AssistStorageEntry.prototype.openInImporter = function () {
     huePubSub.publish('open.in.importer', this.definition.path);
     huePubSub.publish('open.in.importer', this.definition.path);
   };
   };

+ 1 - 1
desktop/core/src/desktop/static/desktop/js/autocomplete/jison/sql.jisonlex

@@ -470,7 +470,7 @@ ROW_NUMBER\s*\(                            { yy.lexer.unput('('); yytext = 'row_
 <hdfs>'\u2020'                             { parser.yy.cursorFound = true; return 'CURSOR'; }
 <hdfs>'\u2020'                             { parser.yy.cursorFound = true; return 'CURSOR'; }
 <hdfs>'\u2021'                             { parser.yy.cursorFound = true; return 'PARTIAL_CURSOR'; }
 <hdfs>'\u2021'                             { parser.yy.cursorFound = true; return 'PARTIAL_CURSOR'; }
 <hdfs>\s+[']                               { return 'HDFS_START_QUOTE'; }
 <hdfs>\s+[']                               { return 'HDFS_START_QUOTE'; }
-<hdfs>[^'\u2020\u2021]+                    { parser.addHdfsLocation(yylloc, yytext); return 'HDFS_PATH'; }
+<hdfs>[^'\u2020\u2021]+                    { parser.addFileLocation(yylloc, yytext); return 'HDFS_PATH'; }
 <hdfs>[']                                  { this.popState(); return 'HDFS_END_QUOTE'; }
 <hdfs>[']                                  { this.popState(); return 'HDFS_END_QUOTE'; }
 <hdfs><<EOF>>                              { return 'EOF'; }
 <hdfs><<EOF>>                              { return 'EOF'; }
 
 

+ 3 - 3
desktop/core/src/desktop/static/desktop/js/autocomplete/spec/sqlSpecLocations.js

@@ -546,14 +546,14 @@
       });
       });
     });
     });
 
 
-    describe('HDFS paths', function () {
+    describe('File paths', function () {
       it('should report locations for "LOAD DATA LOCAL INPATH \'/some/path/file.ble\' OVERWRITE INTO TABLE bla; |"', function () {
       it('should report locations for "LOAD DATA LOCAL INPATH \'/some/path/file.ble\' OVERWRITE INTO TABLE bla; |"', function () {
         assertLocations({
         assertLocations({
           dialect: 'hive',
           dialect: 'hive',
           beforeCursor: 'LOAD DATA LOCAL INPATH \'/some/path/file.ble\' OVERWRITE INTO TABLE bla;',
           beforeCursor: 'LOAD DATA LOCAL INPATH \'/some/path/file.ble\' OVERWRITE INTO TABLE bla;',
           expectedLocations: [
           expectedLocations: [
             { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 70 } },
             { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 70 } },
-            { type: 'hdfs', location: {first_line: 1, last_line: 1, first_column: 25, last_column: 44}, path: '/some/path/file.ble' },
+            { type: 'file', location: {first_line: 1, last_line: 1, first_column: 25, last_column: 44}, path: '/some/path/file.ble' },
             { type: 'table', location: {first_line: 1, last_line: 1, first_column: 67, last_column: 70}, identifierChain: [{name: 'bla'}] }
             { type: 'table', location: {first_line: 1, last_line: 1, first_column: 67, last_column: 70}, identifierChain: [{name: 'bla'}] }
           ]
           ]
         });
         });
@@ -565,7 +565,7 @@
           beforeCursor: 'CREATE TABLE bla (id INT) LOCATION \'/bla/bla/\';',
           beforeCursor: 'CREATE TABLE bla (id INT) LOCATION \'/bla/bla/\';',
           expectedLocations: [
           expectedLocations: [
             { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 47 } },
             { type: 'statement', location: { first_line: 1, last_line: 1, first_column: 1, last_column: 47 } },
-            { type: 'hdfs', location: {first_line: 1, last_line: 1, first_column: 37, last_column: 46}, path: '/bla/bla/' }
+            { type: 'file', location: {first_line: 1, last_line: 1, first_column: 37, last_column: 46}, path: '/bla/bla/' }
           ]
           ]
         });
         });
       });
       });

File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/js/autocomplete/sqlAutocompleteParser.js


+ 3 - 3
desktop/core/src/desktop/static/desktop/js/autocomplete/sqlParseSupport.js

@@ -1479,9 +1479,9 @@ var SqlParseSupport = (function () {
       parser.yy.locations.push(location)
       parser.yy.locations.push(location)
     };
     };
 
 
-    parser.addHdfsLocation = function (location, path) {
+    parser.addFileLocation = function (location, path) {
       parser.yy.locations.push({
       parser.yy.locations.push({
-        type: 'hdfs',
+        type: 'file',
         location: adjustLocationForCursor(location),
         location: adjustLocationForCursor(location),
         path: path
         path: path
       });
       });
@@ -1856,7 +1856,7 @@ var SqlParseSupport = (function () {
     'getSubQuery', 'addTablePrimary', 'suggestFileFormats', 'suggestDdlAndDmlKeywords', 'checkForSelectListKeywords', 'checkForKeywords',
     'getSubQuery', 'addTablePrimary', 'suggestFileFormats', 'suggestDdlAndDmlKeywords', 'checkForSelectListKeywords', 'checkForKeywords',
     'suggestKeywords', 'suggestColRefKeywords', 'suggestTablesOrColumns', 'suggestFunctions', 'suggestAggregateFunctions', 'suggestAnalyticFunctions',
     'suggestKeywords', 'suggestColRefKeywords', 'suggestTablesOrColumns', 'suggestFunctions', 'suggestAggregateFunctions', 'suggestAnalyticFunctions',
     'suggestColumns', 'suggestGroupBys', 'suggestIdentifiers', 'suggestOrderBys', 'suggestFilters', 'suggestKeyValues', 'suggestTables', 'addFunctionLocation',
     'suggestColumns', 'suggestGroupBys', 'suggestIdentifiers', 'suggestOrderBys', 'suggestFilters', 'suggestKeyValues', 'suggestTables', 'addFunctionLocation',
-    'addStatementLocation', 'firstDefined', 'addClauseLocation', 'addHdfsLocation', 'addDatabaseLocation', 'addColumnAliasLocation', 'addTableAliasLocation',
+    'addStatementLocation', 'firstDefined', 'addClauseLocation', 'addFileLocation', 'addDatabaseLocation', 'addColumnAliasLocation', 'addTableAliasLocation',
     'addSubqueryAliasLocation', 'addTableLocation', 'addAsteriskLocation', 'addVariableLocation', 'addColumnLocation', 'addCteAliasLocation', 'addUnknownLocation',
     'addSubqueryAliasLocation', 'addTableLocation', 'addAsteriskLocation', 'addVariableLocation', 'addColumnLocation', 'addCteAliasLocation', 'addUnknownLocation',
     'addColRefToVariableIfExists', 'suggestDatabases', 'suggestHdfs', 'suggestValues'];
     'addColRefToVariableIfExists', 'suggestDatabases', 'suggestHdfs', 'suggestValues'];
 
 

File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/js/autocomplete/sqlStatementsParser.js


File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/js/autocomplete/sqlSyntaxParser.js


+ 57 - 7
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -170,6 +170,27 @@
     }
     }
   };
   };
 
 
+  ko.bindingHandlers.fetchMore = {
+    init: function (element, valueAccessor) {
+      var options = valueAccessor();
+      var $element = $(element);
+
+      var throttle = -1;
+      $element.on('scroll.fetchMore', function () {
+        window.clearTimeout(throttle);
+        throttle = window.setTimeout(function () {
+          if ((element.scrollTop + $element.innerHeight() >= element.scrollHeight - 10) && ko.unwrap(options.hasMore) && !ko.unwrap(options.loadingMore)) {
+            options.fetchMore();
+          }
+        }, 100);
+      });
+
+      ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+        $element.off('scroll.fetchMore');
+      });
+    }
+  };
+
   ko.bindingHandlers.autocomplete = {
   ko.bindingHandlers.autocomplete = {
     init: function (element, valueAccessor) {
     init: function (element, valueAccessor) {
       var options = valueAccessor();
       var options = valueAccessor();
@@ -5116,13 +5137,42 @@
                 });
                 });
               } else if (token.parseLocation && !token.notFound) {
               } else if (token.parseLocation && !token.notFound) {
                 // Asterisk, function etc.
                 // Asterisk, function etc.
-                huePubSub.publish('context.popover.show', {
-                  data: token.parseLocation,
-                  sourceType: snippet.type(),
-                  defaultDatabase: snippet.database(),
-                  pinEnabled: true,
-                  source: source
-                });
+                if (token.parseLocation.type === 'file') {
+                  var typeMatch = token.parseLocation.path.match(/^([^:]+):\/(\/.*)\/?/i);
+                  var type = typeMatch ? typeMatch[1] : 'hdfs';
+                  type.replace(/s3.*/i, 's3');
+                  var rootEntry = new AssistStorageEntry({
+                    type: type.toLowerCase(),
+                    definition: {
+                      name: '/',
+                      type: 'dir'
+                    },
+                    parent: null,
+                    apiHelper: ApiHelper.getInstance()
+                  });
+
+                  var path = (typeMatch ? typeMatch[2] : token.parseLocation.path).replace(/(?:^\/)|(?:\/$)/g, '').split('/');
+
+                  rootEntry.loadDeep(path, function (entry) {
+                    entry.open(true);
+                    huePubSub.publish('context.popover.show', {
+                      data: {
+                        type: 'storageEntry',
+                        storageEntry: entry
+                      },
+                      pinEnabled: true,
+                      source: source
+                    });
+                  });
+                } else {
+                  huePubSub.publish('context.popover.show', {
+                    data: token.parseLocation,
+                    sourceType: snippet.type(),
+                    defaultDatabase: snippet.database(),
+                    pinEnabled: true,
+                    source: source
+                  });
+                }
               } else if (token.syntaxError) {
               } else if (token.syntaxError) {
                 huePubSub.publish('sql.syntax.dropdown.show', {
                 huePubSub.publish('sql.syntax.dropdown.show', {
                   snippet: snippet,
                   snippet: snippet,

+ 9 - 0
desktop/core/src/desktop/static/desktop/less/components/hue-popover.less

@@ -467,4 +467,13 @@
 .context-popover-close {
 .context-popover-close {
   .flex(1 0 15px);
   .flex(1 0 15px);
   padding: 5px;
   padding: 5px;
+}
+
+.storage-entry-container {
+  padding: 5px;
+
+  .empty-file-contents {
+    font-style: italic;
+    color: @cui-gray-400;
+  }
 }
 }

+ 3 - 34
desktop/core/src/desktop/templates/assist.mako

@@ -340,19 +340,6 @@ from desktop.views import _ko
     <!-- /ko -->
     <!-- /ko -->
   </script>
   </script>
 
 
-  <script type="text/html" id="s3-details-content">
-    <!-- ko with: definition -->
-    <div class="assist-details-wrap">
-      <div><div class="assist-details-header">${ _('Size') }</div><div class="assist-details-value" data-bind="text: humansize"></div></div>
-      <div><div class="assist-details-header">${ _('Permissions') }</div><div class="assist-details-value" data-bind="text: rwx"></div></div>
-    </div>
-    <!-- /ko -->
-  </script>
-
-  <script type="text/html" id="s3-details-title">
-    <span data-bind="text: definition.name"></span>
-  </script>
-
   <script type="text/html" id="assist-s3-header-actions">
   <script type="text/html" id="assist-s3-header-actions">
     <div class="assist-db-header-actions">
     <div class="assist-db-header-actions">
       <a class="inactive-action" href="javascript:void(0)" data-bind="click: function () { huePubSub.publish('assist.s3.refresh'); }"><i class="pointer fa fa-refresh" data-bind="css: { 'fa-spin blue' : loading }" title="${_('Manual refresh')}"></i></a>
       <a class="inactive-action" href="javascript:void(0)" data-bind="click: function () { huePubSub.publish('assist.s3.refresh'); }"><i class="pointer fa fa-refresh" data-bind="css: { 'fa-spin blue' : loading }" title="${_('Manual refresh')}"></i></a>
@@ -387,7 +374,7 @@ from desktop.views import _ko
         <ul class="assist-tables" data-bind="foreachVisible: { data: entries, minHeight: 22, container: '.assist-s3-scrollable', fetchMore: $data.fetchMore.bind($data) }">
         <ul class="assist-tables" data-bind="foreachVisible: { data: entries, minHeight: 22, container: '.assist-s3-scrollable', fetchMore: $data.fetchMore.bind($data) }">
           <li class="assist-entry assist-table-link" style="position: relative;" data-bind="appAwareTemplateContextMenu: { template: 'hdfs-context-items', scrollContainer: '.assist-s3-scrollable' }, visibleOnHover: { 'selector': '.assist-actions' }">
           <li class="assist-entry assist-table-link" style="position: relative;" data-bind="appAwareTemplateContextMenu: { template: 'hdfs-context-items', scrollContainer: '.assist-s3-scrollable' }, visibleOnHover: { 'selector': '.assist-actions' }">
             <div class="assist-actions table-actions" style="opacity: 0;" >
             <div class="assist-actions table-actions" style="opacity: 0;" >
-              <a style="padding: 0 3px;" class="inactive-action" href="javascript:void(0);" data-bind="templatePopover : { contentTemplate: 's3-details-content', titleTemplate: 's3-details-title', minWidth: '320px' }">
+              <a style="padding: 0 3px;" class="inactive-action" href="javascript:void(0);" data-bind="click: showContextPopover">
                 <i class='fa fa-info' title="${ _('Details') }"></i>
                 <i class='fa fa-info' title="${ _('Details') }"></i>
               </a>
               </a>
             </div>
             </div>
@@ -476,24 +463,6 @@ from desktop.views import _ko
     <!-- /ko -->
     <!-- /ko -->
   </script>
   </script>
 
 
-  <script type="text/html" id="hdfs-details-content">
-    <!-- ko with: definition -->
-    <div class="assist-details-wrap">
-      <div><div class="assist-details-header">${ _('Size') }</div><div class="assist-details-value" data-bind="text: humansize"></div></div>
-      <!-- ko with: stats -->
-      <div><div class="assist-details-header">${ _('User') }</div><div class="assist-details-value" data-bind="text: user"></div></div>
-      <div><div class="assist-details-header">${ _('Group') }</div><div class="assist-details-value" data-bind="text: group"></div></div>
-      <!-- /ko -->
-      <div><div class="assist-details-header">${ _('Permissions') }</div><div class="assist-details-value" data-bind="text: rwx"></div></div>
-      <div><div class="assist-details-header">${ _('Date') }</div><div class="assist-details-value" data-bind="text: mtime"></div></div>
-    </div>
-    <!-- /ko -->
-  </script>
-
-  <script type="text/html" id="hdfs-details-title">
-    <span data-bind="text: definition.name"></span>
-  </script>
-
   <script type="text/html" id="assist-hdfs-header-actions">
   <script type="text/html" id="assist-hdfs-header-actions">
     <div class="assist-db-header-actions">
     <div class="assist-db-header-actions">
       <a class="inactive-action" href="javascript:void(0)" data-bind="click: goHome" title="Go to ${ home_dir }"><i class="pointer fa fa-home"></i></a>
       <a class="inactive-action" href="javascript:void(0)" data-bind="click: goHome" title="Go to ${ home_dir }"><i class="pointer fa fa-home"></i></a>
@@ -552,7 +521,7 @@ from desktop.views import _ko
         <ul class="assist-tables" data-bind="foreachVisible: { data: entries, minHeight: 22, container: '.assist-hdfs-scrollable', fetchMore: $data.fetchMore.bind($data) }">
         <ul class="assist-tables" data-bind="foreachVisible: { data: entries, minHeight: 22, container: '.assist-hdfs-scrollable', fetchMore: $data.fetchMore.bind($data) }">
           <li class="assist-entry assist-table-link" style="position: relative;" data-bind="appAwareTemplateContextMenu: { template: 'hdfs-context-items', scrollContainer: '.assist-hdfs-scrollable' }, visibleOnHover: { 'selector': '.assist-actions' }">
           <li class="assist-entry assist-table-link" style="position: relative;" data-bind="appAwareTemplateContextMenu: { template: 'hdfs-context-items', scrollContainer: '.assist-hdfs-scrollable' }, visibleOnHover: { 'selector': '.assist-actions' }">
             <div class="assist-actions table-actions" style="opacity: 0;" >
             <div class="assist-actions table-actions" style="opacity: 0;" >
-              <a style="padding: 0 3px;" class="inactive-action" href="javascript:void(0);" data-bind="templatePopover : { contentTemplate: 'hdfs-details-content', titleTemplate: 'hdfs-details-title', minWidth: '320px' }">
+              <a style="padding: 0 3px;" class="inactive-action" href="javascript:void(0);" data-bind="click: showContextPopover">
                 <i class='fa fa-info' title="${ _('Details') }"></i>
                 <i class='fa fa-info' title="${ _('Details') }"></i>
               </a>
               </a>
             </div>
             </div>
@@ -610,7 +579,7 @@ from desktop.views import _ko
         <ul class="assist-tables" data-bind="foreachVisible: { data: entries, minHeight: 22, container: '.assist-adls-scrollable', fetchMore: $data.fetchMore.bind($data) }">
         <ul class="assist-tables" data-bind="foreachVisible: { data: entries, minHeight: 22, container: '.assist-adls-scrollable', fetchMore: $data.fetchMore.bind($data) }">
           <li class="assist-entry assist-table-link" style="position: relative;" data-bind="appAwareTemplateContextMenu: { template: 'hdfs-context-items', scrollContainer: '.assist-adls-scrollable' }, visibleOnHover: { 'selector': '.assist-actions' }">
           <li class="assist-entry assist-table-link" style="position: relative;" data-bind="appAwareTemplateContextMenu: { template: 'hdfs-context-items', scrollContainer: '.assist-adls-scrollable' }, visibleOnHover: { 'selector': '.assist-actions' }">
             <div class="assist-actions table-actions" style="opacity: 0;" >
             <div class="assist-actions table-actions" style="opacity: 0;" >
-              <a style="padding: 0 3px;" class="inactive-action" href="javascript:void(0);" data-bind="templatePopover : { contentTemplate: 'hdfs-details-content', titleTemplate: 'hdfs-details-title', minWidth: '320px' }">
+              <a style="padding: 0 3px;" class="inactive-action" href="javascript:void(0);" data-bind="click: showContextPopover">
                 <i class='fa fa-info' title="${ _('Details') }"></i>
                 <i class='fa fa-info' title="${ _('Details') }"></i>
               </a>
               </a>
             </div>
             </div>

+ 119 - 53
desktop/core/src/desktop/templates/ko_components/ko_context_popover.mako

@@ -53,16 +53,6 @@ from metadata.conf import has_navigator
     </div>
     </div>
   </script>
   </script>
 
 
-  <script type="text/html" id="context-popover-hdfs-details">
-    <div class="context-popover-flex-fill" data-bind="with: details">
-      <div style="padding: 8px">
-        <div style="margin: 10px 10px 18px 10px;">
-          <div data-bind="hdfsTree: { isS3: $data.path.indexOf('s3a://') === 0, path: $data.path, selectedPath: $parent.selectedPath }"></div>
-        </div>
-      </div>
-    </div>
-  </script>
-
   <script type="text/html" id="context-popover-collection-stats-details">
   <script type="text/html" id="context-popover-collection-stats-details">
     <div class="context-popover-flex-fill">
     <div class="context-popover-flex-fill">
       <div style="padding: 8px">
       <div style="padding: 8px">
@@ -382,6 +372,84 @@ from metadata.conf import has_navigator
     </div>
     </div>
   </script>
   </script>
 
 
+
+  <script type="text/html" id="context-storage-entry-title">
+    <div class="hue-popover-title">
+      <i class="hue-popover-title-icon fa muted" data-bind="css: storageEntry() && storageEntry().definition.type === 'dir' ? 'fa-folder-o' : 'fa-file-o'"></i>
+      <span class="hue-popover-title-text" data-bind="foreach: breadCrumbs">
+        <!-- ko ifnot: isActive --><div><a href="javascript: void(0);" data-bind="click: makeActive, text: name"></a><!-- ko if: $index() > 0 -->/<!-- /ko --></div><!-- /ko -->
+        <!-- ko if: isActive -->
+        <div>
+          <span data-bind="text: name"></span>
+        </div>
+        <!-- /ko -->
+      </span>
+      <div class="hue-popover-title-actions">
+        <!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
+##         <a class="pointer inactive-action" title="${ _('Refresh') }" data-bind="visible: !loading(), click: refresh"><i class="fa fa-fw fa-refresh"></i></a>
+##         <a class="pointer inactive-action" title="${ _('Pin') }" data-bind="visible: popover.pinEnabled, click: popover.pin"><i class="fa fa-fw fa-thumb-tack"></i></a>
+        <a class="pointer inactive-action" title="${ _('Close') }" data-bind="visible: !popover.closeDisabled, click: popover.close"><i class="fa fa-fw fa-times"></i></a>
+      </div>
+    </div>
+  </script>
+
+  <script type="text/html" id="context-storage-entry-contents">
+    <div class="context-popover-content" data-bind="with: storageEntry">
+      <div class="context-popover-flex-fill" data-bind="visible: loading"><!-- ko hueSpinner: { spin: loading, center: true, size: 'xlarge' } --><!-- /ko --></div>
+      <!-- ko ifnot: loading -->
+        <!-- ko if: hasErrors -->
+        <div class="context-popover-flex-fill">
+          <div class="alert" data-bind="text: errorText"></div>
+        </div>
+        <!-- /ko -->
+        <!-- ko ifnot: hasErrors -->
+          <div class="context-popover-flex-fill storage-entry-container" data-bind="fetchMore: { fetchMore: fetchMore.bind($data), hasMore: hasMorePages, loadingMore: loadingMore.bind($data) }">
+            <!-- ko if: definition.type === 'dir' -->
+            <table class="table table-condensed table-nowrap">
+              <thead>
+                <tr>
+                  <th width="1%"></th>
+                  <th>${ _('Name') }</th>
+                  <th>${ _('Size') }</th>
+                  <th>${ _('Permissions') }</th>
+                </tr>
+              </thead>
+              <tbody>
+                <!-- ko if: $parent.storageEntry().parent -->
+                <tr>
+                  <td><i class="fa fa-folder-o"></i></td>
+                  <td><a href="javascript: void(0);" data-bind="click: function () { $parent.storageEntry($parent.storageEntry().parent) }">..</a></td>
+                  <td data-bind="text: $parent.storageEntry().definition.humansize"></td>
+                  <td data-bind="text: $parent.storageEntry().definition.rwx"></td>
+                </tr>
+                <!-- /ko -->
+                <!-- ko foreach: entries -->
+                <tr>
+                  <td><i class="fa" data-bind="css: definition.type === 'dir' ? 'fa-folder-o' : 'fa-file-o'"></i></td>
+                  <td><a href="javascript: void(0);" data-bind="click: function () { $parents[1].storageEntry($data) }, text: definition.name"></a></td>
+                  <td data-bind="text: definition.humansize"></td>
+                  <td data-bind="text: definition.rwx"></td>
+                </tr>
+                <!-- /ko -->
+              </tbody>
+            </table>
+            <!-- /ko -->
+            <!-- ko if: definition.type !== 'dir' -->
+            <div data-bind="with: preview">
+              <!-- ko if: view && view.contents -->
+              <pre data-bind="text: view.contents"></pre>
+              <!-- /ko -->
+              <!-- ko if: view && !view.contents -->
+              <div class="empty-file-contents">${ _('Empty file...') }</div>
+              <!-- /ko -->
+            </div>
+            <!-- /ko -->
+          </div>
+        <!-- /ko -->
+      <!-- /ko -->
+    </div>
+  </script>
+
   <script type="text/html" id="global-search-context">
   <script type="text/html" id="global-search-context">
     <!-- ko if: isCatalogEntry -->
     <!-- ko if: isCatalogEntry -->
     <!-- ko with: contents -->
     <!-- ko with: contents -->
@@ -646,48 +714,46 @@ from metadata.conf import has_navigator
         self.activeTab = ko.observable('details');
         self.activeTab = ko.observable('details');
       }
       }
 
 
-      function HdfsContextTabs(data) {
+      var StorageContext = function (options) {
         var self = this;
         var self = this;
 
 
-        self.disposals = [];
+        self.popover = options.popover;
 
 
-        // TODO: Update Ace token with selected path
-        self.data = ko.observable({
-          details: data,
-          loading: ko.observable(false),
-          hasErrors: ko.observable(false),
-          selectedPath: ko.observable(data.path)
-        });
+        self.storageEntry = ko.observable();
 
 
-        var showInFileBrowserPubSub = huePubSub.subscribe('context.popover.open.in.file.browser', function () {
-          window.open((data.path.indexOf('/') === 0 ? '/filebrowser/#' : '/filebrowser/#/') + data.path, '_blank');
+        self.loading = ko.pureComputed(function () {
+          return self.storageEntry() && self.storageEntry().loading();
         });
         });
 
 
-        self.disposals.push(function () {
-          showInFileBrowserPubSub.remove();
+        self.storageEntry.subscribe(function (newVal) {
+          if (!newVal.loaded && !newVal.loading()) {
+            if (newVal.definition.type === 'dir') {
+              newVal.open(true);
+            } else {
+              newVal.loadPreview();
+            }
+          }
         });
         });
 
 
-        var replaceInEditorPubSub = huePubSub.subscribe('context.popover.replace.in.editor', function () {
-          huePubSub.publish('ace.replace', {
-            location: data.location,
-            text: self.data().selectedPath()
-          });
-        });
-        self.disposals.push(function () {
-          replaceInEditorPubSub.remove();
-        });
+        self.storageEntry(options.storageEntry);
 
 
-        self.tabs = [
-          { id: 'details', label: '${ _("Details") }', template: 'context-popover-hdfs-details', templateData: self.data }
-        ];
-        self.activeTab = ko.observable('details');
-      }
+        self.breadCrumbs = ko.pureComputed(function () {
+          var result = [];
+          var currentEntry = self.storageEntry();
+          do {
+            result.unshift({
+              name: currentEntry.definition.name,
+              isActive: currentEntry === self.storageEntry(),
+              storageEntry: currentEntry,
+              makeActive: function () {
+                self.storageEntry(this.storageEntry);
+              }
+            });
 
 
-      HdfsContextTabs.prototype.dispose = function () {
-        var self = this;
-        while (self.disposals.length) {
-          self.disposals.pop()();
-        }
+            currentEntry = currentEntry.parent;
+          } while (currentEntry);
+          return result;
+        });
       };
       };
 
 
       function FunctionContextTabs(data, sourceType) {
       function FunctionContextTabs(data, sourceType) {
@@ -1152,7 +1218,7 @@ from metadata.conf import has_navigator
 
 
         self.isComplex = params.data.type === 'complex';
         self.isComplex = params.data.type === 'complex';
         self.isFunction = params.data.type === 'function';
         self.isFunction = params.data.type === 'function';
-        self.isHdfs = params.data.type === 'hdfs';
+        self.isStorageEntry = params.data.type === 'storageEntry';
         self.isAsterisk = params.data.type === 'asterisk';
         self.isAsterisk = params.data.type === 'asterisk';
         self.isDocument = params.data.type.toLowerCase() === 'hue';
         self.isDocument = params.data.type.toLowerCase() === 'hue';
         self.isCollection = params.data.type === 'collection';
         self.isCollection = params.data.type === 'collection';
@@ -1167,11 +1233,11 @@ from metadata.conf import has_navigator
                 && (self.isDocument || self.isCollection || self.isCatalogEntry);
                 && (self.isDocument || self.isCollection || self.isCatalogEntry);
         self.openInDashboardEnabled = self.isCatalogEntry && params.data.catalogEntry.path.length <= 2;
         self.openInDashboardEnabled = self.isCatalogEntry && params.data.catalogEntry.path.length <= 2;
         self.openInTableBrowserEnabled = self.isCatalogEntry && params.data.catalogEntry.path.length <= 2;
         self.openInTableBrowserEnabled = self.isCatalogEntry && params.data.catalogEntry.path.length <= 2;
-        self.replaceEditorContentEnabled = self.isHdfs;
-        self.openInFileBrowserEnabled = self.isHdfs;
+        self.replaceEditorContentEnabled = self.isStorageEntry;
+        self.openInFileBrowserEnabled = self.isStorageEntry;
         self.expandColumnsEnabled = self.isAsterisk;
         self.expandColumnsEnabled = self.isAsterisk;
 
 
-        self.pinEnabled = params.pinEnabled && !self.isFunction && !self.isAsterisk && !self.isHdfs && !self.isCatalogEntry;
+        self.pinEnabled = params.pinEnabled && !self.isFunction && !self.isAsterisk && !self.isStorageEntry && !self.isCatalogEntry;
 
 
         if (self.isCatalogEntry) {
         if (self.isCatalogEntry) {
           self.contents = new DataCatalogContext({ popover: self, catalogEntry: params.data.catalogEntry });
           self.contents = new DataCatalogContext({ popover: self, catalogEntry: params.data.catalogEntry });
@@ -1181,10 +1247,10 @@ from metadata.conf import has_navigator
           self.contents = new FunctionContextTabs(self.data, self.sourceType);
           self.contents = new FunctionContextTabs(self.data, self.sourceType);
           self.title = self.data.function;
           self.title = self.data.function;
           self.iconClass = 'fa-superscript';
           self.iconClass = 'fa-superscript';
-        } else if (self.isHdfs) {
-          self.contents = new HdfsContextTabs(self.data);
-          self.title = self.data.path;
-          self.iconClass = 'fa-folder-o';
+        } else if (self.isStorageEntry) {
+          self.contents = new StorageContext({ popover: self, storageEntry: params.data.storageEntry });
+          self.titleTemplate = 'context-storage-entry-title';
+          self.contentsTemplate = 'context-storage-entry-contents';
         } else if (self.isAsterisk) {
         } else if (self.isAsterisk) {
           self.contents = new AsteriskContextTabs(self.data, self.sourceType, self.defaultDatabase);
           self.contents = new AsteriskContextTabs(self.data, self.sourceType, self.defaultDatabase);
           self.title = '*';
           self.title = '*';
@@ -1313,7 +1379,7 @@ from metadata.conf import has_navigator
         self.close = params.globalSearch.close.bind(params.globalSearch);
         self.close = params.globalSearch.close.bind(params.globalSearch);
 
 
         // These are currently not in the global search results
         // These are currently not in the global search results
-        self.isHdfs = false;
+        self.isStorageEntry = false;
         self.isAsterisk = false;
         self.isAsterisk = false;
         self.isComplex = false;
         self.isComplex = false;
         self.isFunction = false;
         self.isFunction = false;
@@ -1321,8 +1387,8 @@ from metadata.conf import has_navigator
         self.showInAssistEnabled = !self.isHueApp;
         self.showInAssistEnabled = !self.isHueApp;
         self.openInDashboardEnabled = false;
         self.openInDashboardEnabled = false;
         self.openInTableBrowserEnabled = false;
         self.openInTableBrowserEnabled = false;
-        self.replaceEditorContentEnabled = self.isHdfs;
-        self.openInFileBrowserEnabled = self.isHdfs;
+        self.replaceEditorContentEnabled = self.isStorageEntry;
+        self.openInFileBrowserEnabled = self.isStorageEntry;
         self.expandColumnsEnabled = self.isAsterisk;
         self.expandColumnsEnabled = self.isAsterisk;
         self.closeDisabled = true; // Global search has it's own close
         self.closeDisabled = true; // Global search has it's own close
 
 

Some files were not shown because too many files changed in this diff