You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by on...@apache.org on 2015/03/18 18:11:11 UTC

ambari git commit: AMBARI-10121. Implement 'list' UI control (onechiporenko)

Repository: ambari
Updated Branches:
  refs/heads/trunk 21602caf6 -> 806d31f5f


AMBARI-10121. Implement 'list' UI control (onechiporenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/806d31f5
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/806d31f5
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/806d31f5

Branch: refs/heads/trunk
Commit: 806d31f5f39aaa13cbd7872f41a00b7f31750d64
Parents: 21602ca
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Wed Mar 18 18:03:36 2015 +0200
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Wed Mar 18 19:10:59 2015 +0200

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |   1 +
 ambari-web/app/messages.js                      |   2 +
 ambari-web/app/styles/widgets.less              |  29 +++
 .../configs/widgets/list_config_widget.hbs      |  38 +++
 ambari-web/app/views.js                         |   2 +
 .../configs/widgets/config_widget_view.js       |  48 ++++
 .../configs/widgets/list_config_widget_view.js  | 238 +++++++++++++++++++
 .../widgets/list_config_widget_view_test.js     | 139 +++++++++++
 8 files changed, 497 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/app/assets/test/tests.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js
index ee6d81c..9f3596a 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -163,6 +163,7 @@ var files = ['test/init_model_test',
   'test/utils/ui_effects_test',
   'test/utils/updater_test',
   'test/views/common/chart/linear_time_test',
+  'test/views/common/configs/widgets/list_config_widget_view_test',
   'test/views/common/ajax_default_error_popup_body_test',
   'test/views/common/filter_combo_cleanable_test',
   'test/views/common/filter_view_test',

http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index b909f2b..28c2da5 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -1670,6 +1670,8 @@ Em.I18n.translations = {
   'services.service.config.configHistory.makeCurrent.message': 'Created from service config version {0}',
   'services.service.config.configHistory.comparing': 'Comparing',
 
+  'services.service.widgets.list-widget.nothingSelected': 'Nothing selected',
+
   'services.add.header':'Add Service Wizard',
   'services.reassign.header':'Move Master Wizard',
   'services.service.add':'Add Service',

http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/app/styles/widgets.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/widgets.less b/ambari-web/app/styles/widgets.less
new file mode 100644
index 0000000..d8ef400
--- /dev/null
+++ b/ambari-web/app/styles/widgets.less
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import 'common.less';
+
+.list-widget {
+  li, li:active {
+    a, a:hover, a:focus, a:active, a:visited {
+      background-color: #fff !important;
+      background-image: none !important;
+      color: #333 !important;
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/app/templates/common/configs/widgets/list_config_widget.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/configs/widgets/list_config_widget.hbs b/ambari-web/app/templates/common/configs/widgets/list_config_widget.hbs
new file mode 100644
index 0000000..42bc923
--- /dev/null
+++ b/ambari-web/app/templates/common/configs/widgets/list_config_widget.hbs
@@ -0,0 +1,38 @@
+{{!
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+}}
+
+{{view.config.name}}
+<div class="widget list-widget">
+  <div class="btn-group">
+    <a class="btn dropdown-toggle" data-toggle="dropdown">{{view.displayVal}} <span class="caret"></span></a>
+    {{#if view.valueIsChanged}}
+    <a class="btn btn-small" href="#" {{action "restoreValue" target="view"}}>
+      <i class="icon-undo"></i>
+    </a>
+    {{/if}}
+    <ul class="dropdown-menu">
+      {{#each option in view.options}}
+        <li>
+          <a rel="tooltip" href="javascript:void(0);" {{action "toggleOption" option target="view"}} {{bindAttr data-original-title="option.description"}}>
+            <label class="checkbox">{{view view.checkBoxWithoutAction valueBinding="option.value" disabledBinding="option.isDisabled" checkedBinding="option.isSelected"}} {{option.label}}</label>
+          </a>
+        </li>
+      {{/each}}
+    </ul>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/app/views.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js
index 1089ce7..26b5a7f 100644
--- a/ambari-web/app/views.js
+++ b/ambari-web/app/views.js
@@ -49,6 +49,8 @@ require('views/common/configs/overriddenProperty_view');
 require('views/common/configs/compare_property_view');
 require('views/common/configs/config_history_flow');
 require('views/common/configs/custom_category_views/notification_configs_view');
+require('views/common/configs/widgets/config_widget_view');
+require('views/common/configs/widgets/list_config_widget_view');
 require('views/common/filter_combobox');
 require('views/common/filter_combo_cleanable');
 require('views/common/table_view');

http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/app/views/common/configs/widgets/config_widget_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/configs/widgets/config_widget_view.js b/ambari-web/app/views/common/configs/widgets/config_widget_view.js
new file mode 100644
index 0000000..1dfca68
--- /dev/null
+++ b/ambari-web/app/views/common/configs/widgets/config_widget_view.js
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+/**
+ * Common view for config widgets
+ * @type {Em.View}
+ */
+App.ConfigWidgetView = Em.View.extend({
+
+  /**
+   * @type {App.StackConfigProperty}
+   */
+  config: null,
+
+  /**
+   * Determines if config-value was changed
+   * @type {boolean}
+   */
+  valueIsChanged: function () {
+    return this.get('config.value') !== this.get('config.defaultValue');
+  }.property('config.value'),
+
+  /**
+   * Reset config-value to its default
+   * @method restoreValue
+   */
+  restoreValue: function () {
+    this.set('config.value', this.get('config.defaultValue'));
+  }
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/app/views/common/configs/widgets/list_config_widget_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/configs/widgets/list_config_widget_view.js b/ambari-web/app/views/common/configs/widgets/list_config_widget_view.js
new file mode 100644
index 0000000..669bede
--- /dev/null
+++ b/ambari-web/app/views/common/configs/widgets/list_config_widget_view.js
@@ -0,0 +1,238 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+var numberUtils = require('utils/number_utils');
+
+/**
+ * Template for list-option
+ * @type {Em.object}
+ */
+var configOption = Em.Object.extend({
+  label: '',
+  value: '',
+  description: '',
+  isSelected: false,
+  isDisabled: false,
+  order: 0
+});
+
+/**
+ * Config Widget for List
+ * Usage:
+ *  <code>{{view App.ListConfigWidgetView configBinding="someObject"}}</code>
+ * @type {App.ConfigWidgetView}
+ */
+App.ListConfigWidgetView = App.ConfigWidgetView.extend({
+
+  /**
+   * Counter used to determine order of options selection (<code>order<code>-field in the <code>configOption</code>)
+   * Greater number - later selection
+   * @type {number}
+   */
+  orderCounter: 1,
+
+  /**
+   * Maximum length of the <code>displayVal</code>
+   * If its length is greater, it will cut to current value and ' ...' will be added to the end
+   * @type {number}
+   */
+  maxDisplayValLength: 45,
+
+  /**
+   * <code>options</code> where <code>isSelected</code> is true
+   * @type {configOption[]}
+   */
+  val: [],
+
+  /**
+   * List of options for <code>config.value</code>
+   * @type {configOption[]}
+   */
+  options: [],
+
+  /**
+   * String with selected options labels separated with ', '
+   * If result string is too long (@see maxDisplayValLength) it's cut and ' ...' is added to the end
+   * If nothing is selected, default placeholder is used
+   * @type {string}
+   */
+  displayVal: function () {
+    var v = this.get('val').sortProperty('order').mapProperty('label');
+    if (v.length > 0) {
+      var output = v.join(', '),
+        maxDisplayValLength = this.get('maxDisplayValLength');
+      if (output.length > maxDisplayValLength - 3) {
+        return output.substring(0, maxDisplayValLength - 3) + ' ...';
+      }
+      return output;
+    }
+    return Em.I18n.t('services.service.widgets.list-widget.nothingSelected');
+  }.property('val.[]'),
+
+  /**
+   * Config-object bound on the template
+   * @type {App.StackConfigProperty}
+   */
+  config: null,
+
+  /**
+   * Maximum number of options allowed to select (based on <code>config.valueAttributes.selection_cardinality</code>)
+   * @type {number}
+   */
+  allowedToSelect: 1,
+
+  templateName: require('templates/common/configs/widgets/list_config_widget'),
+
+  willInsertElement: function () {
+    this._super();
+    this.parseCardinality();
+    this.calculateOptions();
+    this.calculateInitVal();
+  },
+
+  didInsertElement: function () {
+    this._super();
+    this.addObserver('options.@each.isSelected', this, this.calculateVal);
+    this.addObserver('options.@each.isSelected', this, this.checkSelectedItemsCount);
+    this.calculateVal();
+    this.checkSelectedItemsCount();
+    Em.run.next(function () {
+      App.tooltip(this.$('[rel="tooltip"]'));
+    });
+  },
+
+  /**
+   * Get list of <code>options</code> basing on <code>config.valueAttributes</code>
+   * <code>configOption</code> is used
+   * @method calculateOptions
+   */
+  calculateOptions: function () {
+    var valueAttributes = this.get('config.valueAttributes'),
+      options = [];
+    Em.assert('valueAttributes `entries`, `entry_label` and `entry_descriptions` should have the same length', valueAttributes.entries.length == valueAttributes.entry_labels.length && valueAttributes.entries.length == valueAttributes.entry_descriptions.length);
+    valueAttributes.entries.forEach(function (entryValue, indx) {
+      options.pushObject(configOption.create({
+        value: entryValue,
+        label: valueAttributes.entry_labels[indx],
+        description: valueAttributes.entry_descriptions[indx]
+      }));
+    });
+    this.set('options', options);
+  },
+
+  /**
+   * Get initial value for <code>val</code> using calculated earlier <code>options</code>
+   * Used on <code>willInsertElement</code> and when user click on "Undo"-button (to restore default value)
+   * @method calculateInitVal
+   */
+  calculateInitVal: function () {
+    var config = this.get('config'),
+      options = this.get('options'),
+      value = config.get('value'),
+      self = this;
+    if ('string' === Em.typeOf(value)) {
+      value = value.split(',');
+    }
+    options.invoke('setProperties', {isSelected: false, isDisabled: false});
+    var val = value.map(function (v) {
+      var option = options.findProperty('value', v.trim());
+      Em.assert('option with value `%@` is missing for config `%@`'.fmt(v, config.get('name')), option);
+      option.setProperties({
+        order: self.get('orderCounter'),
+        isSelected: true
+      });
+      self.incrementProperty('orderCounter');
+      return option;
+    });
+    this.set('val', val);
+  },
+
+  /**
+   * Get config-value basing on selected <code>options</code> sorted by <code>order</code>-field
+   * Triggers on each option select/deselect
+   * @method calculateVal
+   */
+  calculateVal: function () {
+    var val = this.get('options').filterProperty('isSelected').sortProperty('order');
+    this.set('val', val);
+    this.set('config.value', val.mapProperty('value').join(','));
+  },
+
+  /**
+   * If user already selected maximum of allowed options, disable other options
+   * If user deselect some option, all disabled options become enabled
+   * Triggers on each option select/deselect
+   * @method checkSelectedItemsCount
+   */
+  checkSelectedItemsCount: function () {
+    var allowedToSelect = this.get('allowedToSelect'),
+      currentlySelected = this.get('options').filterProperty('isSelected').length,
+      selectionDisabled = allowedToSelect <= currentlySelected;
+    this.get('options').filterProperty('isSelected', false).setEach('isDisabled', selectionDisabled);
+  },
+
+  /**
+   * Get maximum number of options allowed to select basing on config cardinality value
+   * @method parseCardinality
+   */
+  parseCardinality: function () {
+    var cardinality = numberUtils.getCardinalityValue(this.get('config.valueAttributes.selection_cardinality'), true);
+    this.set('allowedToSelect', cardinality);
+  },
+
+  /**
+   * Option click-handler
+   * toggle selection for current option and increment <code>orderCounter</code> for proper options selection order
+   * @param {{context: Object}} e
+   * @returns {boolean} always returns false to avoid list hiding
+   */
+  toggleOption: function (e) {
+    if (e.context.get('isDisabled')) return false;
+    var orderCounter = this.get('orderCounter'),
+      option = this.get('options').findProperty('value', e.context.get('value'));
+    option.set('order', orderCounter);
+    option.toggleProperty('isSelected');
+    this.incrementProperty('orderCounter');
+    return false;
+  },
+
+  /**
+   * Restore config value
+   * @method restoreValue
+   */
+  restoreValue: function() {
+    this._super();
+    this.calculateInitVal();
+  },
+
+  /**
+   * Just a small checkbox-wrapper with improved click-handler
+   * Should call <code>parentView.toggleOption</code>
+   * User may click on the checkbox or on the link which wraps it, but action in both cases should be the same (<code>toggleOption</code>)
+   * @type {Em.Checkbox}
+   */
+  checkBoxWithoutAction: Em.Checkbox.extend({
+    _updateElementValue: function () {
+      var option = this.get('parentView.options').findProperty('value', this.get('value'));
+      this.get('parentView').toggleOption({context: option});
+    }
+  })
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/806d31f5/ambari-web/test/views/common/configs/widgets/list_config_widget_view_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/common/configs/widgets/list_config_widget_view_test.js b/ambari-web/test/views/common/configs/widgets/list_config_widget_view_test.js
new file mode 100644
index 0000000..167389c
--- /dev/null
+++ b/ambari-web/test/views/common/configs/widgets/list_config_widget_view_test.js
@@ -0,0 +1,139 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var App = require('app');
+
+var view;
+describe('App.ListConfigWidgetView', function () {
+
+  beforeEach(function () {
+
+    view = App.ListConfigWidgetView.create({
+      config: Em.Object.create({
+        name: 'a.b.c',
+        defaultValue: '2,1',
+        value: '2,1',
+        valueAttributes: {
+          entries: ['1', '2', '3', '4', '5'],
+          entry_labels: ['first label', 'second label', 'third label', '4th label', '5th label'],
+          entry_descriptions: ['1', '2', '3', '4', '5'],
+          selection_cardinality: '3'
+        }
+      })
+    });
+    view.willInsertElement();
+    view.didInsertElement();
+
+  });
+
+  describe('#displayVal', function () {
+
+    it('init value', function () {
+      expect(view.get('displayVal')).to.equal('second label, first label');
+    });
+
+    it('deselect all', function () {
+      view.get('options').setEach('isSelected', false);
+      expect(view.get('displayVal')).to.equal(Em.I18n.t('services.service.widgets.list-widget.nothingSelected'));
+    });
+
+    it('check that value is trimmed', function () {
+      view.get('options').setEach('isSelected', true);
+      expect(view.get('displayVal').endsWith(' ...')).to.be.true;
+    });
+
+  });
+
+  describe('#calculateOptions', function () {
+
+    it('should trigger error', function () {
+      view.set('config.valueAttributes.entry_descriptions', ['1', '2', '3', '4']);
+      expect(view.calculateOptions.bind(view)).to.throw(Error, 'assertion failed');
+    });
+
+    it('should create options for each entry', function () {
+      view.set('options', []);
+      view.calculateOptions();
+      expect(view.get('options.length')).to.equal(view.get('config.valueAttributes.entries.length'));
+    });
+
+    it('should selected options basing on `value`-property', function () {
+      expect(view.get('options').mapProperty('isSelected')).to.eql([true, true, false, false, false]);
+    });
+
+    it('should set order to the options basing on `value`-property', function () {
+      expect(view.get('options').mapProperty('order')).to.eql([2, 1, 0, 0, 0]);
+    });
+
+    it('should disable options basing on `valueAttributes.selection_cardinality`-property', function () {
+      expect(view.get('options').everyProperty('isDisabled', false)).to.be.true;
+    });
+
+  });
+
+  describe('#calculateInitVal', function () {
+
+    it('should take only selected options', function () {
+      expect(view.get('val').length).to.equal(2);
+    });
+
+  });
+
+  describe('#calculateVal', function () {
+
+    it('value updates if some option', function () {
+      view.toggleOption({context: view.get('options')[2]});
+      expect(view.get('config.value')).to.equal('2,1,3');
+      view.toggleOption({context: view.get('options')[1]});
+      expect(view.get('config.value')).to.equal('1,3');
+      view.toggleOption({context: view.get('options')[1]});
+      expect(view.get('config.value')).to.equal('1,3,2');
+    });
+
+  });
+
+  describe('#restoreValue', function () {
+
+    it('should restore default value', function () {
+      view.toggleOption({context: view.get('options')[0]});
+      view.toggleOption({context: view.get('options')[1]});
+      view.toggleOption({context: view.get('options')[2]});
+      expect(view.get('config.value')).to.equal('3');
+      view.restoreValue();
+      expect(view.get('config.value')).to.equal('2,1');
+    });
+
+  });
+
+  describe('#toggleOption', function () {
+
+    it('should doesn\'t do nothing if maximum number of options is selected', function () {
+      view.toggleOption({context: view.get('options')[2]});
+      expect(view.get('options')[2].get('isSelected')).to.be.true;
+      expect(view.get('options')[3].get('isDisabled')).to.be.true;
+      expect(view.get('options')[3].get('isSelected')).to.be.false;
+      expect(view.get('options')[4].get('isDisabled')).to.be.true;
+      expect(view.get('options')[4].get('isSelected')).to.be.false;
+      view.toggleOption({context: view.get('options')[3]});
+      expect(view.get('options')[3].get('isDisabled')).to.be.true;
+      expect(view.get('options')[3].get('isSelected')).to.be.false;
+    });
+
+  });
+
+});