Quellcode durchsuchen

AMBARI-10153. Implement 'slider' UI widget for enhanced configs (onechiporenko)

Oleg Nechiporenko vor 10 Jahren
Ursprung
Commit
91926434a0

+ 3 - 0
ambari-web/app/assets/licenses/NOTICE.txt

@@ -45,3 +45,6 @@ Copyright (c) 2011 Felix Gnass [fgnass at neteye dot de]
 This product includes Moment.js (https://github.com/moment/moment/ - MIT license)
 
 This product includes Ember QUnit (https://github.com/rwjblue/ember-qunit - MIT license)
+
+This product includes bootstrap-slider 4.5.6 (https://github.com/seiyria/bootstrap-slider - MIT License)
+Copyright (C) 2015 by Kyle Kemp, Rohit Kalkur, and contributors

+ 1 - 0
ambari-web/app/assets/test/tests.js

@@ -164,6 +164,7 @@ var files = ['test/init_model_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/configs/widgets/slider_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',

+ 45 - 1
ambari-web/app/styles/widgets.less

@@ -25,5 +25,49 @@
       color: #333 !important;
     }
   }
-
 }
+
+.slider-widget {
+
+  @slider-widget-border-color: #999;
+
+  .ui-slider-wrapper {
+    margin: 0 30px;
+  }
+
+  .slider-track {
+    height: 20px !important;
+    border: 1px solid @slider-widget-border-color;
+    border-radius: 10px;
+  }
+  .slider-tick {
+    margin-top: 6px !important;
+    width: 0px;
+    font-size: 8px;
+    margin-left: 0px !important;
+    &:before {
+      content: '|';
+    }
+    &:nth-of-type(1):before {
+      content: '$$$' !important;
+    }
+  }
+  .slider-handle {
+    margin-top: -2px !important;
+    height: 22px;
+    width: 22px;
+    margin-left: -12px !important;
+    background-color: transparent !important;
+    background-image: radial-gradient(#aaa 5px, #eee 5px) !important;
+    border: 1px solid @slider-widget-border-color;
+  }
+  .slider-tick-label {
+    font-size: 10px;
+  }
+  .slider-selection {
+    background-image: none;
+    background-color: lighten(@blue, 20%);
+    border-radius: 11px;
+    box-shadow: none;
+  }
+}

+ 10 - 6
ambari-web/app/templates/common/configs/widgets/list_config_widget.hbs

@@ -16,20 +16,24 @@
 * limitations under the License.
 }}
 
-{{view.config.name}}
 <div class="widget list-widget">
+  {{view.config.name}}
   <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>
+      <div class="undo-button">
+        <a class="btn btn-small" href="#" {{action "restoreValue" target="view"}}>
+          <i class="icon-undo"></i>
+        </a>
+      </div>
     {{/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 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}}

+ 40 - 0
ambari-web/app/templates/common/configs/widgets/slider_config_widget.hbs

@@ -0,0 +1,40 @@
+{{!
+* 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.
+}}
+
+<div class="widget slider-widget">
+  <p>{{view.config.description}}</p>
+  <div {{bindAttr class="view.isMirrorValueValid::error :control-group :pull-left"}}>
+    <div {{bindAttr class="view.config.stackConfigProperty.valueAttributes.unit:input-append"}}>
+      {{view Ember.TextField valueBinding="view.mirrorValue" class="input-mini"}}
+      {{#if view.config.stackConfigProperty.valueAttributes.unit}}
+        <span class="add-on">{{view.config.stackConfigProperty.valueAttributes.unit}}</span>
+      {{/if}}
+    </div>
+  </div>
+  <div class="pull-left ui-slider-wrapper">
+    {{view Ember.TextField valueBinding="view.config.value" class="input-mini slider-input"}}
+  </div>
+  {{#if view.valueIsChanged}}
+    <div class="pull-left undo-button">
+      <a class="btn btn-small" href="#" {{action "restoreValue" target="view"}}>
+        <i class="icon-undo"></i>
+      </a>
+    </div>
+  {{/if}}
+  <div class="clearfix"></div>
+</div>

+ 1 - 0
ambari-web/app/views.js

@@ -51,6 +51,7 @@ 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/configs/widgets/slider_config_widget_view');
 require('views/common/filter_combobox');
 require('views/common/filter_combo_cleanable');
 require('views/common/table_view');

+ 2 - 2
ambari-web/app/views/common/configs/widgets/config_widget_view.js

@@ -25,7 +25,7 @@ var App = require('app');
 App.ConfigWidgetView = Em.View.extend({
 
   /**
-   * @type {App.StackConfigProperty}
+   * @type {App.ConfigProperty}
    */
   config: null,
 
@@ -34,7 +34,7 @@ App.ConfigWidgetView = Em.View.extend({
    * @type {boolean}
    */
   valueIsChanged: function () {
-    return this.get('config.value') !== this.get('config.defaultValue');
+    return this.get('config.value') != this.get('config.defaultValue');
   }.property('config.value'),
 
   /**

+ 2 - 2
ambari-web/app/views/common/configs/widgets/list_config_widget_view.js

@@ -124,7 +124,7 @@ App.ListConfigWidgetView = App.ConfigWidgetView.extend({
    * @method calculateOptions
    */
   calculateOptions: function () {
-    var valueAttributes = this.get('config.valueAttributes'),
+    var valueAttributes = this.get('config.stackConfigProperty.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) {
@@ -193,7 +193,7 @@ App.ListConfigWidgetView = App.ConfigWidgetView.extend({
    * @method parseCardinality
    */
   parseCardinality: function () {
-    var cardinality = numberUtils.getCardinalityValue(this.get('config.valueAttributes.selection_cardinality'), true);
+    var cardinality = numberUtils.getCardinalityValue(this.get('config.stackConfigProperty.valueAttributes.selection_cardinality'), true);
     this.set('allowedToSelect', cardinality);
   },
 

+ 137 - 0
ambari-web/app/views/common/configs/widgets/slider_config_widget_view.js

@@ -0,0 +1,137 @@
+/**
+ * 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 validator = require('utils/validator');
+
+/**
+ * Slider-view for configs
+ * Used to numeric values
+ * Config value attributes should contain minimum and maximum limits for value
+ * @type {App.ConfigWidgetView}
+ */
+App.SliderConfigWidgetView = App.ConfigWidgetView.extend({
+
+  templateName: require('templates/common/configs/widgets/slider_config_widget'),
+
+  /**
+   * Slider-object created on the <code>initSlider</code>
+   * @type {Object}
+   */
+  slider: null,
+
+  /**
+   * Mirror of the config-value shown in the input on the left of the slider
+   * @type {number}
+   */
+  mirrorValue: 0,
+
+  /**
+   * Determines if used-input <code>mirrorValue</code> is valid
+   * Calculated on the <code>mirrorValueObs</code>
+   * @type {boolean}
+   */
+  isMirrorValueValid: true,
+
+  willInsertElement: function () {
+    this._super();
+    this.addObserver('mirrorValue', this, this.mirrorValueObs);
+  },
+
+  didInsertElement: function () {
+    this._super();
+    this.set('mirrorValue', this.get('config.value'));
+    this.initSlider();
+  },
+
+  /**
+   * Check if <code>mirrorValue</code> was updated by user
+   * Validate it. If value is correct, set it to slider and config.value
+   * @method mirrorValueObs
+   */
+  mirrorValueObs: function () {
+    var mirrorValue = this.get('mirrorValue'),
+      slider = this.get('slider'),
+      min = this.get('config.stackConfigProperty.valueAttributes.minimum'),
+      max = this.get('config.stackConfigProperty.valueAttributes.maximum'),
+      validationFunction = this.get('config.stackConfigProperty.valueAttributes.type') == 'int' ? validator.isValidInt : validator.isValidFloat,
+      parseFunction = this.get('config.stackConfigProperty.valueAttributes.type') == 'int' ? parseInt : parseFloat;
+    if (validationFunction(mirrorValue)) {
+      var parsed = parseFunction(mirrorValue);
+      if (parsed >= min && parsed <=max) {
+        this.set('isMirrorValueValid', true);
+        this.set('config.value', parsed);
+        if (slider) {
+          slider.setValue(this.get('config.value'));
+        }
+      }
+      else {
+        this.set('isMirrorValueValid', false);
+      }
+    }
+    else {
+      this.set('isMirrorValueValid', false);
+    }
+  },
+
+  /**
+   * Draw slider for current config
+   * @method initSlider
+   */
+  initSlider: function () {
+    var self = this,
+      config = this.get('config'),
+      valueAttributes = config.get('stackConfigProperty.valueAttributes'),
+      unit = valueAttributes.get('unit'),
+      ticks = [valueAttributes.minimum],
+      ticksLabels = [];
+    for (var i = 1; i <= 3; i++) {
+      ticks.push(Math.round((valueAttributes.minimum + valueAttributes.maximum) / 4 * i));
+    }
+    ticks.push(valueAttributes.maximum);
+    ticks.forEach(function (tick, indx) {
+      ticksLabels.push(indx  % 2===0 ? tick + ' ' + unit : '');
+    });
+    var slider = new Slider('#' + this.get('elementId') + ' input.slider-input', {
+      value: this.get('config.value'),
+      ticks: ticks,
+      tooltip: 'hide',
+      ticks_labels: ticksLabels,
+      ticks_snap_bounds: valueAttributes.get('type') === 'int' ? 1 : 0.1,
+      step: valueAttributes.get('type') === 'int' ? 1 : 0.1
+    });
+    slider.on('slide', function (newValue) {
+      self.set('config.value', newValue);
+      self.set('mirrorValue', newValue);
+    });
+    this.set('slider', slider);
+    this.$('.slider-tick:first, .slider-tick:last').hide(); // hide some ticks. can't do this via css
+  },
+
+  /**
+   * Restore <code>defaultValue</code> for config
+   * Restore <code>mirrorValue</code> too
+   * @method restoreValue
+   */
+  restoreValue: function () {
+    this._super();
+    this.get('slider').setValue(this.get('config.value'));
+    this.set('mirrorValue', this.get('config.value'));
+  }
+
+});

+ 4 - 2
ambari-web/config.coffee

@@ -39,7 +39,8 @@ exports.config =
           'vendor/scripts/ember-data-latest.js',
           'vendor/scripts/ember-i18n-1.4.1.js',
           'vendor/scripts/bootstrap.js',
-          'vendor/scripts/bootstrap-combobox.js'
+          'vendor/scripts/bootstrap-combobox.js',
+          'vendor/scripts/bootstrap-slider.min.js',
           'vendor/scripts/d3.v2.js',
           'vendor/scripts/cubism.v1.js',
           'vendor/scripts/jquery.ui.core.js',
@@ -74,7 +75,8 @@ exports.config =
           'vendor/styles/font-awesome-ie7.css',
           'vendor/styles/cubism.css',
           'vendor/styles/rickshaw.css'
-          'vendor/styles/bootstrap-combobox.css'
+          'vendor/styles/bootstrap-combobox.css',
+          'vendor/styles/bootstrap-slider.min.css'
         ]
 
     templates:

+ 10 - 8
ambari-web/test/views/common/configs/widgets/list_config_widget_view_test.js

@@ -28,12 +28,14 @@ describe('App.ListConfigWidgetView', function () {
         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'
-        }
+        stackConfigProperty: Em.Object.create({
+          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();
@@ -62,14 +64,14 @@ describe('App.ListConfigWidgetView', function () {
   describe('#calculateOptions', function () {
 
     it('should trigger error', function () {
-      view.set('config.valueAttributes.entry_descriptions', ['1', '2', '3', '4']);
+      view.set('config.stackConfigProperty.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'));
+      expect(view.get('options.length')).to.equal(view.get('config.stackConfigProperty.valueAttributes.entries.length'));
     });
 
     it('should selected options basing on `value`-property', function () {

+ 97 - 0
ambari-web/test/views/common/configs/widgets/slider_config_widget_view_test.js

@@ -0,0 +1,97 @@
+/**
+ * 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 viewInt, viewFloat;
+
+describe('App.SliderConfigWidgetView', function () {
+
+  beforeEach(function () {
+    viewInt = App.SliderConfigWidgetView.create({
+      initSlider: Em.K,
+      config: Em.Object.create({
+        name: 'a.b.c',
+        description: 'A B C',
+        value: 486,
+        defaultValue: 486,
+        stackConfigProperty: Em.Object.create({
+          valueAttributes: Em.Object.create({
+            type: 'int',
+            minimum: 0,
+            maximum: 2096,
+            unit: 'MB'
+          })
+        })
+      })
+    });
+    viewInt.willInsertElement();
+    viewInt.didInsertElement();
+    viewFloat = App.SliderConfigWidgetView.create({
+      initSlider: Em.K,
+      config: Em.Object.create({
+        name: 'a.b.c2',
+        description: 'A B C 2',
+        value: 72.2,
+        defaultValue: 72.2,
+        stackConfigProperty: Em.Object.create({
+          valueAttributes: Em.Object.create({
+            type: 'float',
+            minimum: 0,
+            maximum: 100,
+            unit: '%'
+          })
+        })
+      })
+    });
+    viewFloat.willInsertElement();
+    viewFloat.didInsertElement();
+  });
+
+  describe('#mirrorValue', function () {
+    it('should be equal to config.value after init', function () {
+      expect(viewInt.get('mirrorValue')).to.equal(viewInt.get('config.value'));
+      expect(viewFloat.get('mirrorValue')).to.equal(viewFloat.get('config.value'));
+    });
+  });
+
+  describe('#mirrorValueObs', function () {
+
+    it('check int', function () {
+      viewInt.set('mirrorValue', 1000);
+      expect(viewInt.get('isMirrorValueValid')).to.be.true;
+      expect(viewInt.get('config.value')).to.equal(1000);
+
+      viewInt.set('mirrorValue', 100500);
+      expect(viewInt.get('isMirrorValueValid')).to.be.false;
+      expect(viewInt.get('config.value')).to.equal(1000);
+    });
+
+    it('check float', function () {
+      viewFloat.set('mirrorValue', 55.5);
+      expect(viewFloat.get('isMirrorValueValid')).to.be.true;
+      expect(viewFloat.get('config.value')).to.equal(55.5);
+
+      viewFloat.set('mirrorValue', 100500.5);
+      expect(viewFloat.get('isMirrorValueValid')).to.be.false;
+      expect(viewFloat.get('config.value')).to.equal(55.5);
+    });
+
+  });
+
+});

Datei-Diff unterdrückt, da er zu groß ist
+ 28 - 0
ambari-web/vendor/scripts/bootstrap-slider.min.js


Datei-Diff unterdrückt, da er zu groß ist
+ 27 - 0
ambari-web/vendor/styles/bootstrap-slider.min.css


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.