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;
+ });
+
+ });
+
+});