浏览代码

HUE-3851 [core] Support many-to-many for group configurations

For this I've also created a ko component for the jHueSelector
Johan Ahlen 9 年之前
父节点
当前提交
9c1b722

+ 1 - 0
apps/useradmin/src/useradmin/static/useradmin/css/useradmin.css

@@ -58,6 +58,7 @@ input[type=submit] {
 
 
 @media all and (max-height: 800px) {
 @media all and (max-height: 800px) {
   .form-actions {
   .form-actions {
+    border-top: 1px solid #F1F1F1;
     position: fixed;
     position: fixed;
     bottom: 0;
     bottom: 0;
     left: 0;
     left: 0;

+ 86 - 66
apps/useradmin/src/useradmin/templates/list_configurations.mako

@@ -33,13 +33,8 @@ ${layout.menubar(section='configurations')}
 <script id="app-list" type="text/html">
 <script id="app-list" type="text/html">
   <div class="card card-small">
   <div class="card card-small">
     <h1 class="card-heading simple">${ _('Configurations') }</h1>
     <h1 class="card-heading simple">${ _('Configurations') }</h1>
-    <%actionbar:render>
-      <%def name="search()">
-        <input id="filterInput" type="text" class="input-xlarge search-query" placeholder="${_('Search for application, group, etc...')}" data-bind="textInput: searchQuery">
-      </%def>
-    </%actionbar:render>
 
 
-    <table class="table table-striped table-condensed datatables">
+    <table class="table table-striped table-condensed datatables margin-top-20">
       <thead>
       <thead>
       <tr>
       <tr>
         <th>${ _('Application') }</th>
         <th>${ _('Application') }</th>
@@ -81,30 +76,51 @@ ${layout.menubar(section='configurations')}
   <!-- ko with: selectedApp -->
   <!-- ko with: selectedApp -->
   <div class="card card-small" style="padding-bottom: 68px;">
   <div class="card card-small" style="padding-bottom: 68px;">
     <h1 class="card-heading simple">${ _('Configuration') } - <!-- ko text: name --><!-- /ko --></h1>
     <h1 class="card-heading simple">${ _('Configuration') } - <!-- ko text: name --><!-- /ko --></h1>
-    <h4 class="margin-left-20 simple">${ _('Global') }</h4>
-    <div class="form-horizontal" style="width:100%;">
-      <!-- ko component: { name: 'property-selector', params: { properties: $data.default } } --><!-- /ko -->
+    <div class="margin-top-20 form-horizontal">
+      <div class="control-group">
+        <label class="control-label">${ _('Global Properties') }</label>
+        <div class="controls">
+          <!-- ko component: { name: 'property-selector', params: { properties: $data.default } } --><!-- /ko -->
+        </div>
+      </div>
     </div>
     </div>
 
 
     <!-- ko foreach: groups -->
     <!-- ko foreach: groups -->
-    <h4 class="margin-left-20 simple" style="    border-bottom: 1px solid #e5e5e5;">${ _('Group configuration - ') }<!-- ko text: group.name --><!-- /ko --></h4>
-    <div class="margin-left-20 form-horizontal" style="width:100%;">
-      <!-- ko component: { name: 'property-selector', params: { properties: properties } } --><!-- /ko -->
+    <h4 class="margin-left-20 simple" style="border-bottom: 1px solid #e5e5e5;">${ _('Group override') }</h4>
+    <div class="form-horizontal margin-top-20">
+      <div class="control-group">
+        <label class="control-label">${ _('Groups') }</label>
+        <div class="controls">
+          <!-- ko component: { name: 'multi-group-selector',
+            params: {
+              width: 500,
+              height: 198,
+              options: allGroups,
+              optionsValue: 'id',
+              optionsText: 'name',
+              selectedOptions: group_ids,
+            }
+          } --><!-- /ko -->
+        </div>
+      </div>
+      <div class="control-group">
+        <label class="control-label">${ _('Properties') }</label>
+        <div class="controls">
+          <!-- ko component: { name: 'property-selector', params: { properties: properties } } --><!-- /ko -->
+        </div>
+      </div>
     </div>
     </div>
     <!-- /ko -->
     <!-- /ko -->
-    <div class="margin-left-20 margin-top-20" data-bind="visible: availableGroups().length > 0">
-      <select data-bind="options: availableGroups, optionsText: 'name', optionsCaption: '${ _ko('Add group override...') }', value: groupToAdd"></select>
-      <div style="display: inline-block; vertical-align: top; margin-top: 6px; margin-left: 6px;">
-        <a class="inactive-action pointer" data-bind="click: addGroup">
-          <i class="fa fa-plus"></i>
-        </a>
-      </div>
+    <div class="margin-left-20 margin-top-20">
+      <a class="inactive-action pointer" href="javascript:void(0)" data-bind="click: addGroupOverride">
+        <i class="fa fa-plus"></i> ${ _('Add group override') }
+      </a>
     </div>
     </div>
   </div>
   </div>
   <!-- /ko -->
   <!-- /ko -->
   <div class="form-actions">
   <div class="form-actions">
-    <button class="btn btn-primary" data-bind="click: save">${_('Update configuration')}</button>
-    <button class="btn" data-bind="click: function () { selectedApp(null) }">${_('Cancel')}</button>
+    <button class="btn btn-primary" data-bind="click: save">${ _('Update configuration') }</button>
+    <button class="btn" data-bind="click: function () { selectedApp(null) }">${ _('Cancel') }</button>
   </div>
   </div>
 </script>
 </script>
 
 
@@ -127,23 +143,24 @@ ${ configKoComponents.config() }
     'knockout-sortable'
     'knockout-sortable'
   ], function (ko, apiHelper) {
   ], function (ko, apiHelper) {
 
 
+    var GroupOverride = function (group, allGroups) {
+      var self = this;
+      self.allGroups = allGroups;
+      ko.mapping.fromJS(group, {}, self);
+    };
+
     var AppConfiguration = function (app, allGroups) {
     var AppConfiguration = function (app, allGroups) {
       var self = this;
       var self = this;
       self.rawProperties = app.properties;
       self.rawProperties = app.properties;
       self.rawApp = app;
       self.rawApp = app;
-      ko.mapping.fromJS(app, {}, self);
-
-      self.availableGroups = ko.pureComputed(function () {
-        var selectedGroupIndex = {};
-        self.groups().forEach(function (groupConfig) {
-          selectedGroupIndex[groupConfig.group.id()] = true;
-        });
-        return $.grep(allGroups, function(group) {
-          return !selectedGroupIndex[group.id];
-        });
-      });
-
-      self.groupToAdd = ko.observable();
+      self.allGroups = allGroups;
+      ko.mapping.fromJS(app, {
+        'groups': {
+          create: function(options) {
+            return new GroupOverride(options.data, self.allGroups);
+          }
+        }
+      }, self);
 
 
       self.actualDefaultOverrides = ko.pureComputed(function () {
       self.actualDefaultOverrides = ko.pureComputed(function () {
         return $.grep(self.default(), function (defaultOverride) {
         return $.grep(self.default(), function (defaultOverride) {
@@ -152,21 +169,27 @@ ${ configKoComponents.config() }
       });
       });
 
 
       self.overriddenGroupNames = ko.pureComputed(function () {
       self.overriddenGroupNames = ko.pureComputed(function () {
-        return $.map(self.groups(), function (groupConfig) {
-          return groupConfig.group.name();
-        }).join(', ');
+        var groups = {};
+        var groupIndex = {};
+        self.allGroups().forEach(function (group) {
+          groupIndex[group.id] = group.name;
+        })
+
+        self.groups().forEach(function (groupOverride) {
+          groupOverride.group_ids().forEach(function (id) {
+            groups[groupIndex[id]] = true;
+          })
+        })
+        return Object.keys(groups).sort().join(', ');
       });
       });
     };
     };
 
 
-    AppConfiguration.prototype.addGroup = function () {
+    AppConfiguration.prototype.addGroupOverride = function () {
       var self = this;
       var self = this;
-      if (self.groupToAdd()) {
-        self.groups.push(ko.mapping.fromJS({
-          group: self.groupToAdd(),
-          properties: self.rawProperties
-        }));
-        self.groupToAdd(null);
-      }
+      self.groups.push(new GroupOverride({
+        group_ids: [],
+        properties: self.rawProperties
+      }, self.allGroups));
     };
     };
 
 
     var ConfigurationsViewModel = function () {
     var ConfigurationsViewModel = function () {
@@ -177,7 +200,7 @@ ${ configKoComponents.config() }
       self.hasErrors = ko.observable(false);
       self.hasErrors = ko.observable(false);
       self.loading = ko.observable(false);
       self.loading = ko.observable(false);
       self.apps = ko.observableArray();
       self.apps = ko.observableArray();
-      self.groups = {};
+      self.allGroups = ko.observableArray();
       self.searchQuery = ko.observable();
       self.searchQuery = ko.observable();
       self.selectedApp = ko.observable();
       self.selectedApp = ko.observable();
       self.filteredApps = ko.pureComputed(function () {
       self.filteredApps = ko.pureComputed(function () {
@@ -188,7 +211,7 @@ ${ configKoComponents.config() }
 
 
     ConfigurationsViewModel.prototype.edit = function (app) {
     ConfigurationsViewModel.prototype.edit = function (app) {
       var self = this;
       var self = this;
-      self.selectedApp(new AppConfiguration(app.rawApp, self.groups));
+      self.selectedApp(new AppConfiguration(app.rawApp, self.allGroups));
     };
     };
 
 
     ConfigurationsViewModel.prototype.save = function () {
     ConfigurationsViewModel.prototype.save = function () {
@@ -202,13 +225,16 @@ ${ configKoComponents.config() }
 
 
       $.each(data, function (app, appConfig) {
       $.each(data, function (app, appConfig) {
         appConfig.default(appConfig.actualDefaultOverrides());
         appConfig.default(appConfig.actualDefaultOverrides());
-        var actualGroups = {};
+        var actualGroups = [];
         appConfig.groups().forEach(function (groupConfig) {
         appConfig.groups().forEach(function (groupConfig) {
           var actualGroupOverrides = $.grep(groupConfig.properties(), function (property) {
           var actualGroupOverrides = $.grep(groupConfig.properties(), function (property) {
-            return ko.mapping.toJSON(property.defaultValue) !== ko.mapping.toJSON(property.value())
+            return property.defaultValue().toString() !== property.value().toString()
           });
           });
-          if (actualGroupOverrides.length > 0) {
-            actualGroups[groupConfig.group.id()] = actualGroupOverrides;
+          if (actualGroupOverrides.length > 0 && groupConfig.group_ids().length > 0) {
+            actualGroups.push({
+              group_ids: ko.mapping.toJS(groupConfig.group_ids),
+              properties: actualGroupOverrides
+            });
           }
           }
         });
         });
         data[app] = ko.mapping.toJS(appConfig);
         data[app] = ko.mapping.toJS(appConfig);
@@ -228,9 +254,10 @@ ${ configKoComponents.config() }
       var self = this;
       var self = this;
       var apps = [];
       var apps = [];
       var groupIndex = {};
       var groupIndex = {};
-      self.groups.forEach(function (group) {
+      self.allGroups().forEach(function (group) {
         groupIndex[group.id] = group;
         groupIndex[group.id] = group;
       });
       });
+
       $.each(data.configuration, function (appName, app) {
       $.each(data.configuration, function (appName, app) {
 
 
         app.name = appName;
         app.name = appName;
@@ -253,27 +280,20 @@ ${ configKoComponents.config() }
           app.groups = [];
           app.groups = [];
         } else {
         } else {
           var groups = [];
           var groups = [];
-          $.each(app.groups, function (groupId, groupValues) {
+          app.groups.forEach(function (group) {
             var groupPropertyIndex = {};
             var groupPropertyIndex = {};
-            groupValues.forEach(function (groupValue) {
-              groupPropertyIndex[groupValue.name] = groupValue;
-            });
+            group.properties.forEach(function (groupProperty) {
+              groupPropertyIndex[groupProperty.name] = groupProperty;
+            })
             // Merge the base properties into any existing group config
             // Merge the base properties into any existing group config
             app.properties.forEach(function (property) {
             app.properties.forEach(function (property) {
               if (!groupPropertyIndex[property.name]){
               if (!groupPropertyIndex[property.name]){
-                groupValues.push(property);
+                group.properties.push(property);
               }
               }
             });
             });
-
-            groups.push({
-              group: groupIndex[groupId],
-              properties: groupValues
-            })
           });
           });
-          delete app.groups;
-          app.groups = groups;
         }
         }
-        apps.push(new AppConfiguration(app, self.groups));
+        apps.push(new AppConfiguration(app, self.allGroups));
       });
       });
       self.apps(apps);
       self.apps(apps);
     };
     };
@@ -294,7 +314,7 @@ ${ configKoComponents.config() }
 
 
       self.apiHelper.fetchUsersAndGroups({
       self.apiHelper.fetchUsersAndGroups({
         successCallback: function (usersAndGroups) {
         successCallback: function (usersAndGroups) {
-          self.groups = usersAndGroups.groups;
+          self.allGroups(usersAndGroups.groups);
           self.apiHelper.fetchConfigurations({
           self.apiHelper.fetchConfigurations({
             successCallback: function (data) {
             successCallback: function (data) {
               self.updateFromData(data);
               self.updateFromData(data);

+ 130 - 8
desktop/core/src/desktop/templates/config_ko_components.mako

@@ -27,11 +27,14 @@ from desktop.views import _ko
     .config-property {
     .config-property {
       display: inline-block;
       display: inline-block;
       vertical-align: top;
       vertical-align: top;
-      padding-top:10px;
       margin-bottom: 20px;
       margin-bottom: 20px;
       position: relative;
       position: relative;
     }
     }
 
 
+    :not(.controls) > .config-property {
+      padding-top: 10px;
+    }
+
     .config-property-remove {
     .config-property-remove {
       position: absolute;
       position: absolute;
       top: -8px;
       top: -8px;
@@ -72,9 +75,128 @@ from desktop.views import _ko
     </div>
     </div>
   </script>
   </script>
 
 
+  <script type="text/html" id="multi-group-selector-template">
+    <div class="jHueSelector" style="position: relative;" data-bind="style: { 'width': width + 'px' }">
+      <div class="jHueSelectorHeader" data-bind="style: { 'width': (width-8) + 'px' }">
+        <input style="float:right;position: relative; margin: 0;" type="text" placeholder="${_('Search')}" data-bind="textInput: searchQuery">
+        <label><input type="checkbox" data-bind="checked: allSelected">${_('Select all')}</label>
+      </div>
+      <div class="jHueSelectorBody" data-bind="style: { 'height': (height - 33) + 'px' }">
+        <ul>
+          <!-- ko foreach: searchResultKeys -->
+          <li class="selectorDivider"><strong data-bind="text: $data"></strong></li>
+          <!-- ko foreach: $parent.searchResult()[$data] -->
+          <li><label><input type="checkbox" class="selector" data-bind="checked: altChecked"><!-- ko text: label --><!-- /ko --></label></li>
+          <!-- /ko -->
+          <!-- /ko -->
+        </ul>
+      </div>
+    </div>
+  </script>
+
+  <script type="text/javascript" charset="utf-8">
+    (function (factory) {
+      if (typeof require === "function") {
+        require(['knockout'], factory);
+      } else {
+        factory(ko);
+      }
+    }(function (ko) {
+      (function () {
+
+        function MultiGroupAlternative(alt, params, initiallyChecked) {
+          var self = this;
+          self.altChecked = ko.observable(initiallyChecked || false);
+          self.label = params.optionsText ? alt[params.optionsText] : alt;
+          var value = params.optionsValue ? alt[params.optionsValue] : alt;
+          self.altChecked.subscribe(function (newValue) {
+            if (newValue) {
+              params.selectedOptions.push(value);
+            } else {
+              params.selectedOptions.remove(value);
+            }
+          });
+        }
+
+        function MultiGroupSelectorViewModel(params) {
+          var self = this;
+          self.width = params.width || 600;
+          self.height = params.height || 300;
+
+          var textAccessor = function (alt) {
+            if (params.optionsText) {
+              return alt[params.optionsText];
+            }
+            return alt;
+          }
+
+          self.searchQuery = ko.observable();
+
+          var addToIndexedLists = function (index, key, value) {
+            if (! index[key]) {
+              index[key] = [];
+            }
+            index[key].push(value);
+          }
+
+          self.searchResult = ko.pureComputed(function () {
+            if (self.searchQuery()) {
+              var lowerQuery = self.searchQuery().toLowerCase();
+              var result = {};
+              Object.keys(self.addressBook()).forEach(function (key) {
+                self.addressBook()[key].forEach(function (alt) {
+                  if (alt.label.toLowerCase().indexOf(lowerQuery) !== -1) {
+                    addToIndexedLists(result, key, alt);
+                  }
+                });
+              });
+              return result;
+            }
+            return self.addressBook();
+          });
+
+          var initiallyCheckedIndex = {};
+          params.selectedOptions().forEach(function (alt) {
+            initiallyCheckedIndex[alt] = true;
+          });
+
+          self.addressBook = ko.pureComputed(function () {
+            var result = {}
+            ko.unwrap(params.options).forEach(function (alt) {
+              addToIndexedLists(result, textAccessor(alt).charAt(0).toUpperCase(), new MultiGroupAlternative(alt, params, initiallyCheckedIndex[params.optionsValue ? alt[params.optionsValue] : alt]));
+            });
+            Object.keys(result).forEach(function (key) {
+              result[key].sort();
+            })
+            return result;
+          });
+
+          self.searchResultKeys = ko.pureComputed(function () {
+            return Object.keys(self.searchResult()).sort();
+          })
+
+          self.allSelected = ko.observable(false);
+
+          self.allSelected.subscribe(function (newValue) {
+            self.searchResultKeys().forEach(function (key) {
+              self.searchResult()[key].forEach(function (alt) {
+                alt.altChecked(newValue);
+              })
+            })
+          })
+        }
+
+        ko.components.register('multi-group-selector', {
+          viewModel: MultiGroupSelectorViewModel,
+          template: {element: 'multi-group-selector-template'}
+        });
+      }());
+    }));
+  </script>
+
   <script type="text/javascript" charset="utf-8">
   <script type="text/javascript" charset="utf-8">
     (function (factory) {
     (function (factory) {
-      if(typeof require === "function") {
+      if (typeof require === "function") {
         require(['knockout'], factory);
         require(['knockout'], factory);
       } else {
       } else {
         factory(ko);
         factory(ko);
@@ -123,7 +245,7 @@ from desktop.views import _ko
 
 
         ko.components.register('property-selector', {
         ko.components.register('property-selector', {
           viewModel: PropertySelectorViewModel,
           viewModel: PropertySelectorViewModel,
-          template: { element: 'property-selector-template' }
+          template: {element: 'property-selector-template'}
         });
         });
       }());
       }());
     }));
     }));
@@ -189,7 +311,7 @@ from desktop.views import _ko
 
 
   <script type="text/javascript" charset="utf-8">
   <script type="text/javascript" charset="utf-8">
     (function (factory) {
     (function (factory) {
-      if(typeof require === "function") {
+      if (typeof require === "function") {
         require(['knockout'], factory);
         require(['knockout'], factory);
       } else {
       } else {
         factory(ko);
         factory(ko);
@@ -251,7 +373,7 @@ from desktop.views import _ko
 
 
   <script type="text/javascript" charset="utf-8">
   <script type="text/javascript" charset="utf-8">
     (function (factory) {
     (function (factory) {
-      if(typeof require === "function") {
+      if (typeof require === "function") {
         require(['knockout'], factory);
         require(['knockout'], factory);
       } else {
       } else {
         factory(ko);
         factory(ko);
@@ -313,7 +435,7 @@ from desktop.views import _ko
 
 
   <script type="text/javascript" charset="utf-8">
   <script type="text/javascript" charset="utf-8">
     (function (factory) {
     (function (factory) {
-      if(typeof require === "function") {
+      if (typeof require === "function") {
         require(['knockout'], factory);
         require(['knockout'], factory);
       } else {
       } else {
         factory(ko);
         factory(ko);
@@ -374,7 +496,7 @@ from desktop.views import _ko
 
 
   <script type="text/javascript" charset="utf-8">
   <script type="text/javascript" charset="utf-8">
     (function (factory) {
     (function (factory) {
-      if(typeof require === "function") {
+      if (typeof require === "function") {
         require(['knockout'], factory);
         require(['knockout'], factory);
       } else {
       } else {
         factory(ko);
         factory(ko);
@@ -461,7 +583,7 @@ from desktop.views import _ko
 
 
   <script type="text/javascript" charset="utf-8">
   <script type="text/javascript" charset="utf-8">
     (function (factory) {
     (function (factory) {
-      if(typeof require === "function") {
+      if (typeof require === "function") {
         require(['knockout'], factory);
         require(['knockout'], factory);
       } else {
       } else {
         factory(ko);
         factory(ko);