Browse Source

HUE-8155 [metastore] Make it possible to edit custom properties in the table browser

Johan Ahlen 7 years ago
parent
commit
ee3d7516f8

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


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


+ 9 - 1
desktop/core/src/desktop/static/desktop/js/apiHelper.js

@@ -1667,7 +1667,7 @@ var ApiHelper = (function () {
     return new CancellablePromise(deferred, request);
   };
 
-  ApiHelper.prototype.updateNavigatorMetadata = function (options) {
+  ApiHelper.prototype.updateNavigatorProperties = function (options) {
     var self = this;
     return self.simplePost(NAV_URLS.UPDATE_PROPERTIES, {
       id: ko.mapping.toJSON(options.identity),
@@ -1675,6 +1675,14 @@ var ApiHelper = (function () {
     }, options)
   };
 
+  ApiHelper.prototype.updateNavigatorCustomProperties = function (options) {
+    var self = this;
+    return self.simplePost(NAV_URLS.UPDATE_PROPERTIES, {
+      id: ko.mapping.toJSON(options.identity),
+      metadata: ko.mapping.toJSON(options.customProperties)
+    }, options)
+  };
+
   /**
    * Lists all available navigator tags
    *

+ 47 - 7
desktop/core/src/desktop/static/desktop/js/dataCatalog.js

@@ -1115,6 +1115,41 @@ var DataCatalog = (function () {
       return applyCancellable(new CancellablePromise(deferred, undefined, cancellablePromises), options);
     };
 
+    DataCatalogEntry.prototype.setNavigatorCustomProperties = function (customProperties, apiOptions) {
+      var self = this;
+      var deferred = $.Deferred();
+
+      if (self.canHaveNavigatorMetadata()) {
+        if (self.navigatorMeta === {} || (self.navigatorMeta && typeof self.navigatorMeta.identity === 'undefined')) {
+          if (!apiOptions) {
+            apiOptions = {};
+          }
+          apiOptions.refreshCache = true;
+        }
+        self.getNavigatorMeta(apiOptions).done(function (navigatorMeta) {
+          if (navigatorMeta) {
+            ApiHelper.getInstance().updateNavigatorCustomProperties({
+              identity: navigatorMeta.identity,
+              customProperties: customProperties
+            }).done(function (updateResponse) {
+              if (updateResponse.entity) {
+                self.navigatorMeta = updateResponse.entity;
+                self.navigatorMetaPromise = $.Deferred().resolve(self.navigatorMeta).promise();
+                self.saveLater();
+                deferred.resolve(self.navigatorMeta);
+              } else {
+                deferred.reject();
+              }
+            }).fail(deferred.reject);
+          }
+        }).fail(deferred.reject);
+      } else {
+        deferred.reject();
+      }
+
+      return deferred.promise();
+    };
+
     /**
      * Sets the comment in the proper source
      *
@@ -1138,17 +1173,22 @@ var DataCatalog = (function () {
         }
         self.getNavigatorMeta(apiOptions).done(function (navigatorMeta) {
           if (navigatorMeta) {
-            ApiHelper.getInstance().updateNavigatorMetadata({
+            ApiHelper.getInstance().updateNavigatorProperties({
               identity: navigatorMeta.identity,
               properties: {
                 description: comment
               }
-            }).done(function () {
-              reloadNavigatorMeta(self, {
-                silenceErrors: apiOptions && apiOptions.silenceErrors,
-                refreshCache: true
-              }).done(function() {
-                self.getComment(apiOptions).done(deferred.resolve);
+            }).done(function (updateResponse) {
+              if (updateResponse.entity) {
+                self.navigatorMeta = updateResponse.entity;
+                self.navigatorMetaPromise = $.Deferred().resolve(self.navigatorMeta).promise();
+                self.saveLater();
+              }
+              self.getComment(apiOptions).done(function (comment) {
+                if (self.commentObservable && self.commentObservable() !== comment) {
+                  self.commentObservable(comment);
+                }
+                deferred.resolve(comment);
               });
             }).fail(deferred.reject);
           }

+ 78 - 38
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -1420,18 +1420,23 @@
 
       var $element = $(element);
 
-      var visible = false;
+      var visible = options.visible || ko.observable(false);
+
+      var trackElementInterval = -1;
 
       var hidePopover = function () {
-        $element.popover('hide');
-        visible = false;
-        $(document).off('click', hideOnClickOutside)
+        if (visible()) {
+          window.clearInterval(trackElementInterval);
+          $element.popover('hide');
+          visible(false);
+          $(document).off('click', hideOnClickOutside);
+        }
       };
 
-      huePubSub.subscribe('close.popover', hidePopover);
+      var closeSub = huePubSub.subscribe('close.popover', hidePopover);
 
       var hideOnClickOutside = function (event) {
-        if (visible && $element.data('popover') && ! $.contains($element.data('popover').$tip[0], event.target)) {
+        if (visible() && $element.data('popover') && ! $.contains($element.data('popover').$tip[0], event.target)) {
           hidePopover();
         }
       };
@@ -1440,49 +1445,84 @@
         if (visible && $element.data('popover')) {
           hidePopover();
         }
+        closeSub.remove();
       });
 
-      var showPopover = function () {
+      var afterRender = function () {
+        options.content = $content.html();
+        options.title = $title.html();
+        $element.popover(options);
+        $element.popover('show');
+        var $tip = $element.data('popover').$tip;
+        if (HUE_CONTAINER !== 'body') {
+          $tip.appendTo(HUE_CONTAINER);
+          var tipOffset = $tip.offset();
+          var containerOffset = $(HUE_CONTAINER).offset();
+          $tip.offset({ left: tipOffset.left - containerOffset.left, top: tipOffset.top - containerOffset.top });
+        }
+        ko.cleanNode($tip.get(0));
+        ko.applyBindings(viewModel, $tip.get(0));
+        $tip.find(".close-popover").click(function (event) {
+          hidePopover();
+          event.stopPropagation();
+        });
+        if (options.minWidth) {
+          var heightBefore = $tip.height();
+          $tip.css('min-width', options.minWidth);
+          // The width might affect the height in which case we need to reposition the popover
+          var diff = (heightBefore - $tip.height()) / 2;
+          if (diff !== 0) {
+            $tip.css('top', ($tip.position().top + diff) + 'px');
+          }
+        }
+        var lastWidth = $element.outerWidth(true);
+        var lastOffset = $element.offset();
+        var lastHeight = $element.outerHeight(true);
+        trackElementInterval = window.setInterval(function () {
+          var elementWidth = $element.outerWidth(true);
+          var elementHeight = $element.outerHeight(true);
+          var elementOffset = $element.offset();
+          if (lastHeight !== elementHeight || lastWidth !== $element.outerWidth(true) || lastOffset.top !== elementOffset.top || lastOffset.left !== elementOffset.left) {
+            $tip.css({ 'left': elementOffset.left + (elementWidth / 2) - ($tip.outerWidth(true) / 2) , 'top': elementOffset.top + elementHeight + 10 });
+            lastWidth = elementWidth;
+            lastOffset = elementOffset;
+            lastHeight = elementHeight;
+          }
+        }, 50);
+        $content.empty();
+        $title.empty();
+        $(document).on('click', hideOnClickOutside);
+        visible(true);
+      };
+
+      var showPopover = function (preventClose) {
+        if (!preventClose) {
+          huePubSub.publish('close.popover');
+        }
         ko.renderTemplate(options.contentTemplate, viewModel, {
           afterRender: function () {
-            ko.renderTemplate(options.titleTemplate, viewModel, {
-              afterRender: function () {
-                options.content = $content.html();
-                options.title = $title.html();
-                $element.popover(options);
-                $element.popover('show');
-                var $tip = $element.data('popover').$tip;
-                if (HUE_CONTAINER !== 'body') {
-                  $tip.appendTo(HUE_CONTAINER);
-                  var tipOffset = $tip.offset();
-                  var containerOffset = $(HUE_CONTAINER).offset();
-                  $tip.offset({ left: tipOffset.left - containerOffset.left, top: tipOffset.top - containerOffset.top });
+            if (options.titleTemplate) {
+              ko.renderTemplate(options.titleTemplate, viewModel, {
+                afterRender: function () {
+                  afterRender();
                 }
-                ko.cleanNode($tip.get(0));
-                ko.applyBindings(viewModel, $tip.get(0));
-                $tip.find(".close-popover").click(hidePopover);
-                if (options.minWidth) {
-                  var heightBefore = $tip.height();
-                  $tip.css('min-width', options.minWidth);
-                  // The width might affect the height in which case we need to reposition the popover
-                  var diff = (heightBefore - $tip.height()) / 2;
-                  if (diff !== 0) {
-                    $tip.css('top', ($tip.position().top + diff) + 'px');
-                  }
-                }
-                $content.empty();
-                $title.empty();
-                $(document).on('click', hideOnClickOutside);
-                visible = true;
-              }
-            }, $title.get(0), 'replaceChildren');
+              }, $title.get(0), 'replaceChildren');
+            } else {
+              afterRender();
+            }
           }
         }, $content.get(0), 'replaceChildren');
       };
 
+      if (visible()) {
+        window.setTimeout(function () {
+          showPopover(true);
+        }, 0);
+      }
+
       if (clickTrigger) {
         $element.click(function (e) {
-          if (visible) {
+          if (visible()) {
             hidePopover();
           } else {
             showPopover();

+ 103 - 4
desktop/core/src/desktop/static/desktop/less/components/hue-nav-properties.less

@@ -17,14 +17,82 @@
 */
 
 .hue-nav-properties {
+  position: relative;
   margin: 5px 14px;
   font-size: 14px;
+  line-height: 20px;
+
+  &.hue-nav-properties-edit {
+    padding-right: 40px;
+
+    .hue-nav-properties-edit-actions {
+      position: absolute;
+      font-size: 18px;
+      right: 0;
+      top: 0;
+
+      a {
+        color: @cui-gray-600;
+
+        &:hover {
+          color: @hue-primary-color-dark;
+        }
+      }
+    }
+
+    .hue-nav-property-add {
+      line-height: 26px;
+      display: inline-block;
+      overflow: hidden; // Somehow vertical align is affected by overflow on the property keys and values
+      margin-left: 5px;
+    }
+  }
 
   .hue-nav-property {
     display: inline-block;
-    border-radius: 4px;
-    padding: 2px 4px 2px 0;
-    white-space: pre;
+    padding: 2px;
+    height: 20px;
+    overflow: hidden;
+
+    &.hue-nav-property-edit {
+      background-color: @cui-gray-050;
+      color: @cui-gray-700;
+      border-radius: 9px;
+      border: 1px solid @cui-gray-050;
+      font-size: 13px;
+      padding: 2px 4px;
+
+      &.hue-nav-property-invalid {
+        background-color: @cui-red-100;
+        border-color: @cui-red-300;
+      }
+
+      &:hover {
+        border-color: @cui-gray-200;
+      }
+
+      .hue-nav-property-key {
+        cursor: pointer;
+        padding: 0 5px;
+        color: @cui-gray-700;
+        font-weight: bold;
+      }
+
+      .hue-nav-property-value {
+        cursor: pointer;
+        background-color: @hue-primary-color-dark;
+        color: @cui-gray-050;
+        font-size: 12px;
+        padding: 0 9px;
+        border-radius: 4px;
+      }
+
+      .hue-nav-property-remove {
+        float:right;
+        margin: 0 3px;
+        height: 20px;
+      }
+    }
 
     &:not(:last-child) {
       margin-right: 10px;
@@ -34,11 +102,42 @@
       display: inline-block;
       color: @cui-gray-700;
       margin-right: 3px;
+      max-width: 180px;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      overflow: hidden;
     }
 
     .hue-nav-property-value {
       display: inline-block;
-      background-color: #FFF;
+      background-color: @cui-white;
+      max-width: 220px;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      overflow: hidden;
+    }
+  }
+
+  .editable-inline-action {
+    overflow: hidden;
+    display: inline-block;
+    line-height: 26px;
+  }
+}
+
+.hue-nav-property-edit-content {
+  position:relative;
+  padding-top: 5px;
+
+  a.close-popover {
+    position: absolute;
+    top: 0;
+    right: 0;
+
+    color: @cui-gray-600;
+
+    &:hover {
+      color: @hue-primary-color-dark;
     }
   }
 }

+ 153 - 8
desktop/core/src/desktop/templates/ko_components/ko_nav_properties.mako

@@ -20,24 +20,102 @@ from django.utils.translation import ugettext as _
 %>
 
 <%def name="navProperties()">
+
+  <script type="text/html" id="nav-property-edit-popover-content">
+    <div class="hue-nav-property-edit-content">
+      <a href="javascript: void(0);" class="close-popover"><i class="fa fa-times"></i></a>
+      <div class="control-group" data-bind="css: { 'error': keyInvalid }">
+        <label class="control-label">${ _("Key") }</label>
+        <div class="controls">
+          <input type="text" placeholder="${ _("Key") }" data-bind="textInput: key">
+        </div>
+      </div>
+      <div class="control-group" data-bind="css: { 'error': valueInvalid }">
+        <label class="control-label">${ _("Value") }</label>
+        <div class="controls">
+          <input type="text" placeholder="${ _("Value") }" data-bind="textInput: value">
+        </div>
+      </div>
+    </div>
+  </script>
+
   <script type="text/html" id="nav-properties-template">
      <!-- ko if: loading -->
      <div class="hue-nav-properties"><div data-bind="hueSpinner: { spin: loading, inline: true }"></div></div>
      <!-- /ko -->
      <!-- ko ifnot: loading -->
-     <div class="hue-nav-properties" data-bind="foreach: properties">
-       <div class="hue-nav-property"><div class="hue-nav-property-key" data-bind="text: key"></div><div class="hue-nav-property-value" data-bind="html: value"></div></div>
+     <!-- ko ifnot: editMode -->
+
+     <div class="hue-nav-properties" data-bind="click: startEdit, visibleOnHover: { selector: '.editable-inline-action' }">
+       <!-- ko ifnot: properties().length -->
+       ${ _("No properties") }
+       <!-- /ko -->
+       <!-- ko foreach: properties -->
+       <div class="hue-nav-property"><div class="hue-nav-property-key" data-bind="text: key, attr: { 'title': key }"></div><div class="hue-nav-property-value" data-bind="text: value, attr: { 'title': value }"></div></div>
+       <!-- /ko -->
+       <div class="editable-inline-action" title="${ _("Edit") }"><a href="javascript: void(0);" data-bind="click: startEdit"><i class="fa fa-pencil"></i></a></div>
+     </div>
+     <!-- /ko -->
+     <!-- ko if: editMode -->
+     <div class="hue-nav-properties hue-nav-properties-edit">
+       <!-- ko foreach: editProperties -->
+       <div class="hue-nav-property hue-nav-property-edit" data-bind="css: { 'hue-nav-property-invalid': invalid }, templatePopover: { trigger: 'click', placement: 'bottom', visible: editPropertyVisible, contentTemplate: 'nav-property-edit-popover-content' }">
+         <div class="hue-nav-property-key" data-bind="text: key, attr: { 'title': key }"></div><div class="hue-nav-property-value" data-bind="text: value, attr: { 'title': value }"></div>
+         <div class="hue-nav-property-remove"><a href="javascript: void(0);" title="${ _("Remove") }" data-bind="click: function (entry) { $parent.removeProperty(entry); }"><i class="fa fa-times"></i></a></div>
+       </div>
+       <!-- /ko -->
+       <div class="hue-nav-property-add"><a href="javascript: void(0);" title="${ _("Add") }" data-bind="click: addProperty"><i class="fa fa-plus"></i></a></div>
+       <div class="hue-nav-properties-edit-actions">
+         <a href="javascript: void(0);" title="${ _("Save") }" data-bind="click: saveEdit"><i class="fa fa-check"></i></a>
+         <a href="javascript: void(0);" title="${ _("Cancel") }" data-bind="click: cancelEdit"><i class="fa fa-close"></i></a>
+       </div>
      </div>
      <!-- /ko -->
+     <!-- /ko -->
   </script>
 
   <script type="text/javascript">
     (function () {
 
-      function NavProperty(key, value) {
+      function NavProperty(key, value, isNew) {
         var self = this;
-        self.key = key;
-        self.value = value;
+        self.key = ko.observable(key);
+        self.keyEdited = ko.observable(false);
+
+        self.value = ko.observable(value);
+        self.valueEdited = ko.observable(false);
+
+        self.editPropertyVisible = ko.observable(isNew);
+
+        var keySub = self.key.subscribe(function () {
+          keySub.dispose();
+          self.keyEdited(true);
+        });
+
+        var valueSub = self.value.subscribe(function () {
+          valueSub.dispose();
+          self.valueEdited(true);
+        });
+
+        var editSub = self.editPropertyVisible.subscribe(function () {
+          editSub.dispose();
+          keySub.dispose();
+          valueSub.dispose();
+          self.keyEdited(true);
+          self.valueEdited(true);
+        });
+
+        self.keyInvalid = ko.pureComputed(function () {
+          return self.keyEdited() && !self.key();
+        });
+
+        self.valueInvalid = ko.pureComputed(function () {
+          return self.valueEdited() && !self.value();
+        });
+
+        self.invalid = ko.pureComputed(function () {
+          return self.keyInvalid() || self.valueInvalid() || (!self.editPropertyVisible() && !self.key() && !self.value());
+        })
       }
 
       /**
@@ -46,14 +124,17 @@ from django.utils.translation import ugettext as _
        *
        * @constructor
        */
-      function NavProperties(params) {
+      function NavProperties(params, element) {
         var self = this;
 
+        self.element = element;
         self.hasErrors = ko.observable(false);
         self.loading = ko.observable(true);
         self.properties = ko.observableArray();
+        self.editProperties = ko.observableArray();
 
         self.catalogEntry = params.catalogEntry;
+        self.editMode = ko.observable(false);
 
         self.loadProperties();
 
@@ -64,6 +145,66 @@ from django.utils.translation import ugettext as _
         });
       }
 
+      NavProperties.prototype.startEdit = function () {
+        var self = this;
+        var editProperties = [];
+        self.properties().forEach(function (property) {
+          editProperties.push(new NavProperty(property.key(), property.value()));
+        });
+        self.editProperties(editProperties);
+        self.editMode(true);
+
+        $(document).on('click.navProperties', function (event) {
+          if ($.contains(document, event.target) && !$.contains(self.element, event.target) &&
+              !self.editProperties().some(function (prop) { return prop.editPropertyVisible() })) {
+            self.saveEdit();
+          }
+        });
+      };
+
+      NavProperties.prototype.cancelEdit = function () {
+        var self = this;
+        $(document).off('click.navProperties');
+        self.editMode(false);
+      };
+
+      NavProperties.prototype.saveEdit = function () {
+        var self = this;
+
+        var someInvalid = self.editProperties().some(function (property) {
+          return !property.key() || !property.value();
+        });
+
+        if (someInvalid) {
+          return;
+        }
+
+        $(document).off('click.navProperties');
+        self.editMode(false);
+        if (ko.mapping.toJSON(self.editProperties()) !== ko.mapping.toJSON(self.properties())) {
+          var newProperties = {};
+          self.editProperties().forEach(function(property) {
+            newProperties[property.key()] = property.value();
+          });
+          self.loading(true);
+          self.catalogEntry.setNavigatorCustomProperties(newProperties).always(function () {
+            self.loadProperties();
+          });
+        }
+      };
+
+      NavProperties.prototype.removeProperty = function (property) {
+        var self = this;
+        self.editProperties.remove(property);
+      };
+
+      NavProperties.prototype.addProperty = function () {
+        var self = this;
+        var newProperty = new NavProperty('', '', true);
+        newProperty.editPropertyVisible(true);
+        self.editProperties.push(newProperty);
+      };
+
       NavProperties.prototype.dispose = function () {
         var self = this;
         self.refreshSub.remove();
@@ -83,7 +224,7 @@ from django.utils.translation import ugettext as _
               }
             });
             newProps.sort(function (a, b) {
-              return a.key.localeCompare(b.key);
+              return a.key().localeCompare(b.key());
             })
           }
           self.properties(newProps);
@@ -95,7 +236,11 @@ from django.utils.translation import ugettext as _
       };
 
       ko.components.register('nav-properties', {
-        viewModel: NavProperties,
+        viewModel: {
+          createViewModel: function(params, componentInfo) {
+            return new NavProperties(params, componentInfo.element);
+          }
+        },
         template: { element: 'nav-properties-template' }
       });
     })();

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