Selaa lähdekoodia

AMBARI-10293 Implement Actions Dropdown menu for the widgets on service summary page. (atkach)

Andrii Tkach 10 vuotta sitten
vanhempi
commit
12598cff58

+ 16 - 0
ambari-web/app/assets/data/widget_layouts/HBASE/layouts.json

@@ -0,0 +1,16 @@
+{
+  "href": "http://c6401.ambari.apache.org:8080/api/v1/users?widget_layouts/section_name=HBASE_SUMMARY&widget_layouts/scope=CLUSTER",
+  "items": [
+    {
+      "href": "http://c6401.ambari.apache.org:8080/api/v1/users/jaimin",
+      "Users": {
+        "user_name": "jaimin"
+      },
+      "widget_layouts": {
+        "layout_name": "default_hbase_layout",
+        "section_name": "HBASE_SUMMARY",
+        "scope": "CLUSTER"
+      }
+    }
+  ]
+}

+ 3 - 0
ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json

@@ -17,6 +17,7 @@
             "display_name": "RegionServer Reads and Writes",
             "description": "This widget shows all the read requests and write requests on all regions for a RegionServer",
             "widget_type": "GRAPH",
+            "is_visible": "true",
             "metrics":[
               {
                 "name": "regionserver.Server.Append_num_ops",
@@ -54,6 +55,7 @@
             "display_name": "Files Local",
             "description": "This widget shows percentage of files local.",
             "widget_type": "NUMBER",
+            "is_visible": "true",
             "metrics":[
               {
                 "name": "regionserver.Server.percentFilesLocal",
@@ -77,6 +79,7 @@
             "widget_name": "NAMENODE_HEAP",
             "display_name": "NameNode Heap",
             "widget_type": "GAUGE",
+            "is_visible": "true",
             "description": "",
             "metrics":[
               {

+ 73 - 3
ambari-web/app/controllers/main/service/info/summary.js

@@ -40,6 +40,13 @@ App.MainServiceInfoSummaryController = Em.Controller.extend({
    */
   isPreviousRangerConfigsCallFailed: false,
 
+  /**
+   * UI section name
+   */
+  sectionName: function() {
+    return this.get('content.serviceName') + "_SUMMARY";
+  }.property('content.serviceName'),
+
   /**
    * Ranger plugins data
    * @type {array}
@@ -298,12 +305,45 @@ App.MainServiceInfoSummaryController = Em.Controller.extend({
   isWidgetsLoaded: false,
 
   /**
-   * @type Em.A
+   * @type {boolean}
+   */
+  isWidgetLayoutsLoaded: false,
+
+  /**
+   * @type {Em.A}
    */
   widgets: function() {
     return App.Widget.find().filterProperty('serviceName', this.get('content.serviceName'));
   }.property('isWidgetsLoaded'),
 
+  /**
+   * @type {Em.A}
+   */
+  widgetLayouts: function() {
+    return App.WidgetLayout.find().filterProperty('serviceName', this.get('content.serviceName'));
+  }.property('isWidgetLayoutsLoaded'),
+
+  /**
+   * load widget layouts across all users in CLUSTER scope
+   * @returns {$.ajax}
+   */
+  loadWidgetLayouts: function() {
+    this.set('isWidgetLayoutsLoaded', false);
+    return App.ajax.send({
+      name: 'widgets.layouts.get',
+      sender: this,
+      data: {
+        sectionName: this.get('sectionName')
+      },
+      success: 'loadWidgetLayoutsSuccessCallback'
+    });
+  },
+
+  loadWidgetLayoutsSuccessCallback: function(data) {
+    App.widgetLayoutMapper.map(data);
+    this.set('isWidgetLayoutsLoaded', true);
+  },
+
   /**
    * load widgets defined by user
    * @returns {$.ajax}
@@ -315,7 +355,7 @@ App.MainServiceInfoSummaryController = Em.Controller.extend({
       sender: this,
       data: {
         loginName: App.router.get('loginName'),
-        sectionName: this.get('content.serviceName') + "_SUMMARY"
+        sectionName: this.get('sectionName')
       },
       success: 'loadWidgetsSuccessCallback'
     });
@@ -355,8 +395,38 @@ App.MainServiceInfoSummaryController = Em.Controller.extend({
    * @param {object|null} data
    */
   loadStackWidgetsLayoutSuccessCallback: function (data) {
-    App.widgetMapper.map(data.artifact_data.layouts.findProperty('section_name', (this.get('content.serviceName') + "_SUMMARY")), this.get('content.serviceName'));
+    App.widgetMapper.map(data.artifact_data.layouts.findProperty('section_name', this.get('sectionName')), this.get('content.serviceName'));
     this.set('isWidgetsLoaded', true);
+  },
+
+  /**
+   * add widgets
+   */
+  addWidgets: function (event) {
+    var widgets = event.context.filterProperty('selected');
+
+  },
+
+  /**
+   * delete widgets
+   */
+  deleteWidgets: function (event) {
+    var widgets = event.context.filterProperty('selected');
+
+  },
+
+  /**
+   * save layout
+   */
+  saveLayout: function () {
+
+  },
+
+  /**
+   * create widget
+   */
+  createWidget: function () {
+
   }
 
 });

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

@@ -43,3 +43,4 @@ require('mappers/alert_groups_mapper');
 require('mappers/alert_notification_mapper');
 require('mappers/root_service_mapper');
 require('mappers/widget_mapper');
+require('mappers/widget_layout_mapper');

+ 10 - 27
ambari-web/app/styles/widget_layout.less → ambari-web/app/mappers/widget_layout_mapper.js

@@ -16,31 +16,14 @@
  * limitations under the License.
  */
 
-#widget_layout {
-  .widget {
-    .title {
-      padding: 5px 0 0 5px;
-      height: 25px;
-      font-weight: bold;
-      text-align: left;
-    }
-    .content {
-      text-align: center;
-      color: #5ab400;
-      padding-top: 35px;
-      font-weight: bold;
-      font-size: 35px;
-    }
-    .template-widget {
-      height: 150px;
-      width: 90%;
-    }
-    .gauge-widget {
-      height: 150px;
-      width: 90%;
-      .content {
-        padding-top: 5px;
-      }
-    }
+
+App.widgetLayoutMapper = App.QuickDataMapper.create({
+  model: App.WidgetLayout,
+  config: {
+    id: 'widget_layouts.layout_name',
+    layout_name: 'widget_layouts.layout_name',
+    section_name: 'widget_layouts.section_name',
+    scope: 'widget_layouts.scope',
+    user: 'Users.user_name'
   }
-}
+});

+ 2 - 1
ambari-web/app/mappers/widget_mapper.js

@@ -33,7 +33,8 @@ App.widgetMapper = App.QuickDataMapper.create({
     properties: 'properties',
     metrics: 'metrics',
     values: 'values',
-    description: 'description'
+    description: 'description',
+    is_visible: 'is_visible'
   },
   map: function (json, serviceName) {
     //TODO add service name to user layout API response

+ 3 - 0
ambari-web/app/messages.js

@@ -2236,6 +2236,9 @@ Em.I18n.translations = {
   'dashboard.button.switchShort': 'Switch',
   'dashboard.button.reset': 'Reset all widgets to default ',
   'dashboard.button.gangliaLink': 'View metrics in Ganglia',
+  'dashboard.widgets.create': 'Create New Widget',
+  'dashboard.widgets.layout.import': 'Import a layout',
+  'dashboard.widgets.layout.save': 'Save a layout',
 
   'dashboard.widgets.NameNodeHeap': 'NameNode Heap',
   'dashboard.widgets.NameNodeCpu': 'NameNode CPU WIO',

+ 21 - 5
ambari-web/app/mixins/common/widget_mixin.js

@@ -55,10 +55,26 @@ App.WidgetMixin = Ember.Mixin.create({
    */
   content: null,
 
+  beforeRender: function () {
+    this.loadMetrics();
+  },
+
+  /**
+   * callback on metrics loaded
+   */
+  onMetricsLoaded: function () {
+    var self = this;
+    this.set('isLoaded', true);
+    this.drawWidget();
+    setTimeout(function() {
+      self.loadMetrics();
+    }, App.contentUpdateInterval);
+  },
+
   /**
    * load metrics
    */
-  beforeRender: function () {
+  loadMetrics: function () {
     var requestData = this.getRequestData(this.get('content.metrics')),
         request,
         requestCounter = 0,
@@ -70,12 +86,12 @@ App.WidgetMixin = Ember.Mixin.create({
       if (request.host_component_criteria) {
         this.getHostComponentMetrics(request).complete(function () {
           requestCounter--;
-          if (requestCounter === 0) self.set('isLoaded', true);
+          if (requestCounter === 0) self.onMetricsLoaded();
         });
       } else {
         this.getServiceComponentMetrics(request).complete(function () {
           requestCounter--;
-          if (requestCounter === 0) self.set('isLoaded', true);
+          if (requestCounter === 0) self.onMetricsLoaded();
         });
       }
     }
@@ -111,7 +127,7 @@ App.WidgetMixin = Ember.Mixin.create({
     this.get('content.values').forEach(function (value) {
       var computeExpression = this.computeExpression(this.extractExpressions(value), metrics);
       value.computedValue = value.value.replace(this.get('EXPRESSION_REGEX'), function (match) {
-        return (computeExpression[match]) ? computeExpression[match] + (displayUnit || "") : Em.I18n.t('common.na');
+        return (!Em.isNone(computeExpression[match])) ? computeExpression[match] + (displayUnit || "") : Em.I18n.t('common.na');
       });
     }, this);
   },
@@ -194,7 +210,7 @@ App.WidgetMixin = Ember.Mixin.create({
     var metrics = [];
 
     this.get('content.metrics').forEach(function (_metric) {
-      if (Em.get(data, _metric.widget_id.replace(/\//g, '.'))) {
+      if (!Em.isNone(Em.get(data, _metric.widget_id.replace(/\//g, '.')))) {
         _metric.data = Em.get(data, _metric.widget_id.replace(/\//g, '.'));
         this.get('metrics').pushObject(_metric);
       }

+ 2 - 1
ambari-web/app/models.js

@@ -68,4 +68,5 @@ require('models/configs/config_property');
 require('models/configs/tab');
 require('models/configs/section');
 require('models/configs/sub_section');
-require('models/widget');
+require('models/widget');
+require('models/widget_layout');

+ 1 - 1
ambari-web/app/models/widget.js

@@ -27,7 +27,6 @@ App.Widget = DS.Model.extend({
    *  - HEATMAP
    *  - GRAPH (Line graph and stack graph)
    *  - NUMBER (e.g., “1 ms” for RPC latency)
-   *  - x / y (e.g., “2 / 3” DataNodes live)
    *  - LINKS
    *  - TEMPLATE
    */
@@ -42,6 +41,7 @@ App.Widget = DS.Model.extend({
   expression: DS.attr('array'),
   metrics: DS.attr('array'),
   values: DS.attr('array'),
+  isVisible: DS.attr('boolean'),
 
   /**
    * @type {number}

+ 4 - 2
ambari-web/app/models/widget_layout.js

@@ -20,8 +20,10 @@ var App = require('app');
 
 App.WidgetLayout = DS.Model.extend({
   layoutName: DS.attr('string'),
-  sectionName:DS.attr('string'),
-  widgetLayoutInfo: DS.attr('string')
+  sectionName: DS.attr('string'),
+  widgetLayoutInfo: DS.attr('string'),
+  scope: DS.attr('string'),
+  user: DS.attr('string')
 });
 
 

+ 46 - 1
ambari-web/app/styles/enhanced_service_dashboard.less

@@ -25,8 +25,53 @@
     .icon-plus {
       font-size: 70px;
       color: #ddd;
-      position: center;
     }
   }
 
+  .actions .dropdown-menu {
+    min-width: 270px;
+    label.checkbox {
+      padding: 0 20px 0 38px;
+      margin-bottom: 0;
+      margin-top: 5px;
+    }
+    li.btn-row {
+      padding: 5px 20px 10px 20px;
+    }
+    a.action {
+      border-bottom: 1px solid #ddd
+    }
+  }
+}
+
+#widget_layout {
+  .widget {
+    .spinner {
+      margin: 55px auto;
+    }
+    .title {
+      padding: 5px 0 0 5px;
+      height: 25px;
+      font-weight: bold;
+      text-align: left;
+    }
+    .content {
+      text-align: center;
+      color: #5ab400;
+      padding-top: 35px;
+      font-weight: bold;
+      font-size: 35px;
+    }
+    .template-widget {
+      height: 150px;
+      width: 90%;
+    }
+    .gauge-widget {
+      height: 150px;
+      width: 90%;
+      .content {
+        padding-top: 5px;
+      }
+    }
+  }
 }

+ 6 - 2
ambari-web/app/templates/common/widget/gauge_widget.hbs

@@ -17,6 +17,10 @@
 }}
 
 <div class="gauge-widget thumbnail">
-  <div class="caption title">{{view.title}}</div>
-  <div class="content"> {{view view.chartView}}</div>
+  {{#if view.isLoaded}}
+    <div class="caption title">{{view.title}}</div>
+    <div class="content"> {{view view.chartView}}</div>
+  {{else}}
+    <div class="spinner"></div>
+  {{/if}}
 </div>

+ 6 - 2
ambari-web/app/templates/common/widget/template_widget.hbs

@@ -17,6 +17,10 @@
 }}
 
 <div class="template-widget thumbnail">
-  <div class="caption title">{{view.title}}</div>
-  <div class="content"> {{view.value}}</div>
+  {{#if view.isLoaded}}
+    <div class="caption title">{{view.title}}</div>
+    <div class="content"> {{view.value}}</div>
+  {{else}}
+    <div class="spinner"></div>
+  {{/if}}
 </div>

+ 41 - 1
ambari-web/app/templates/main/service/info/summary.hbs

@@ -89,10 +89,50 @@
               </ul>
             </div>
             {{#if App.supports.customizedWidgets}}
-              <div class="btn-group pull-right">
+              <div class="btn-group pull-right actions">
                 <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                   {{t common.actions}} &nbsp;<span class="caret"></span>
                 </button>
+                <ul class="dropdown-menu">
+                  {{#each option in view.widgetActions}}
+                    <li {{bindAttr class="option.layouts:dropdown-submenu"}}>
+                      {{#if option.isAction}}
+                        <a href="javascript:void(0);" class="action" {{action doWidgetAction option.action target="view"}}>
+                          <i {{bindAttr class="option.class"}}></i>
+                          {{option.label}}
+                        </a>
+                        {{#if option.layouts}}
+                          <ul class="dropdown-menu">
+                            {{#each layout in option.layouts}}
+                              <li>
+                                <a href="javascript:void(0);">
+                                  {{layout.layoutName}}
+                                </a>
+                              </li>
+                            {{/each}}
+                          </ul>
+                        {{/if}}
+                      {{else}}
+                        <label class="checkbox">
+                          {{view Em.Checkbox checkedBinding="option.selected"}}
+                          {{option.label}}
+                        </label>
+                      {{/if}}
+                    </li>
+                  {{/each}}
+                  <li>
+                    <a href="javascript:void(0);">
+                      {{t hostPopup.serviceInfo.showMore}}
+                    </a>
+                  </li>
+                  <li class="btn-row">
+                    <div class="row-fluid">
+                      <button class="btn span4">{{t common.cancel}}</button>
+                      <button class="btn btn-danger span4" {{action deleteWidgets view.widgetActions target="controller"}}>{{t common.delete}}</button>
+                      <button class="btn btn-primary span4" {{action addWidgets view.widgetActions target="controller"}}>{{t common.add}}</button>
+                    </div>
+                  </li>
+                </ul>
               </div>
             {{/if}}
           </div>

+ 5 - 0
ambari-web/app/utils/ajax/ajax.js

@@ -2413,6 +2413,11 @@ var urls = {
     mock: '/data/widget_layouts/HBASE/empty_user_layout.json'
   },
 
+  'widgets.layouts.get': {
+    real: '/users?widget_layouts/section_name={sectionName}&widget_layouts/scope=CLUSTER',
+    mock: '/data/widget_layouts/HBASE/layouts.json'
+  },
+
   'widgets.serviceComponent.metrics.get': {
     real: '/clusters/{clusterName}/services/{serviceName}/components/{componentName}?fields={widgetIds}',
     mock: '/data/metrics/{serviceName}/Append_num_ops_&_Delete_num_ops.json'

+ 2 - 2
ambari-web/app/views/common/widget/gauge_widget_view.js

@@ -43,7 +43,7 @@ App.GaugeWidgetView = Em.View.extend(App.WidgetMixin, {
       this.set('title', this.get('content.values')[0].name);
       this.set('value', this.get('content.values')[0].computedValue);
     }
-  }.observes('isLoaded'),
+  },
 
   chartView: App.ChartPieView.extend({
     stroke: '#D6DDDF', //light grey
@@ -76,7 +76,7 @@ App.GaugeWidgetView = Em.View.extend(App.WidgetMixin, {
 
     data: function () {
       var data = parseFloat(this.get('parentView.value')) * this.get('FACTOR');
-      if (isNaN(data)) return [this.get('MAX_VALUE'), 0];
+      if (isNaN(data)) return [0, this.get('MAX_VALUE')];
       return [data, this.get('MAX_VALUE') - data];
     }.property('parentView.value'),
 

+ 1 - 1
ambari-web/app/views/common/widget/graph_widget_view.js

@@ -69,7 +69,7 @@ App.GraphWidgetView = App.ChartLinearTimeView.extend(App.WidgetMixin, {
     if (this.get('isLoaded')) {
       this._refreshGraph(this.calculateValues())
     }
-  }.observes('isLoaded'),
+  },
 
   /**
    * calculate series datasets for graph widgets

+ 1 - 1
ambari-web/app/views/common/widget/template_widget_view.js

@@ -43,5 +43,5 @@ App.TemplateWidgetView = Em.View.extend(App.WidgetMixin, {
       this.set('value', this.get('content.values')[0].computedValue);
       this.set('title', this.get('content.values')[0].name);
     }
-  }.observes('isLoaded')
+  }
 });

+ 53 - 0
ambari-web/app/views/main/service/info/summary.js

@@ -365,6 +365,58 @@ App.MainServiceInfoSummaryView = Em.View.extend(App.UserPref, {
       this.set('currentTimeRangeIndex', 0);
     }
   },
+  /**
+   * list of static actions of widget
+   * @type {Array}
+   */
+  staticWidgetActions: [
+    Em.Object.create({
+      label: Em.I18n.t('dashboard.widgets.layout.save'),
+      class: 'icon-download-alt',
+      action: 'saveLayout',
+      isAction: true
+    }),
+    Em.Object.create({
+      label: Em.I18n.t('dashboard.widgets.layout.import'),
+      class: 'icon-file',
+      isAction: true,
+      layouts: App.WidgetLayout.find()
+    }),
+    Em.Object.create({
+      label: Em.I18n.t('dashboard.widgets.create'),
+      class: 'icon-plus',
+      action: 'createWidget',
+      isAction: true
+    })
+  ],
+
+  /**
+   * @type {Array}
+   */
+  widgetActions: function() {
+    var options = [];
+
+    options.pushObjects(this.get('staticWidgetActions'));
+    this.get('controller.widgets').forEach(function (widget) {
+      options.push(Em.Object.create({
+        label: widget.get('displayName'),
+        isVisible: widget.get('isVisible'),
+        selected: true
+      }));
+    }, this);
+
+    return options;
+  }.property('controller.widgets.length'),
+
+  /**
+   * call action function defined in controller
+   * @param event
+   */
+  doWidgetAction: function(event) {
+    if($.isFunction(this.get('controller')[event.context])) {
+      this.get('controller')[event.context].apply(this.get('controller'));
+    }
+  },
 
   /**
    * time range options for service metrics, a dropdown will list all options
@@ -478,6 +530,7 @@ App.MainServiceInfoSummaryView = Em.View.extend(App.UserPref, {
       var stackService = App.StackService.find().findProperty('serviceName', serviceName);
       if (stackService.get('isServiceWithWidgets')) {
         this.get('controller').loadWidgets();
+        this.get('controller').loadWidgetLayouts();
       }
     }
 

+ 7 - 5
ambari-web/test/mixins/common/widget_mixin_test.js

@@ -21,7 +21,7 @@ var App = require('app');
 describe('App.WidgetMixin', function() {
   var mixinClass = Em.Object.extend(App.WidgetMixin, {metrics: [], content: {}});
 
-  describe('#beforeRender()', function () {
+  describe('#loadMetrics()', function () {
     var mixinObject = mixinClass.create();
     beforeEach(function () {
       this.mock = sinon.stub(mixinObject, 'getRequestData');
@@ -31,27 +31,29 @@ describe('App.WidgetMixin', function() {
       sinon.stub(mixinObject, 'getServiceComponentMetrics').returns({complete: function(callback){
         callback();
       }});
+      sinon.stub(mixinObject, 'onMetricsLoaded');
     });
     afterEach(function () {
       this.mock.restore();
       mixinObject.getHostComponentMetrics.restore();
       mixinObject.getServiceComponentMetrics.restore();
+      mixinObject.onMetricsLoaded.restore();
     });
     it('has host_component_criteria', function () {
       this.mock.returns({'key1': {host_component_criteria: 'criteria'}});
       mixinObject.set('isLoaded', false);
-      mixinObject.beforeRender();
+      mixinObject.loadMetrics();
 
       expect(mixinObject.getHostComponentMetrics.calledWith({host_component_criteria: 'criteria'})).to.be.true;
-      expect(mixinObject.get('isLoaded')).to.be.true;
+      expect(mixinObject.onMetricsLoaded.calledOnce).to.be.true;
     });
     it('host_component_criteria is absent', function () {
       this.mock.returns({'key1': {}});
       mixinObject.set('isLoaded', false);
-      mixinObject.beforeRender();
+      mixinObject.loadMetrics();
 
       expect(mixinObject.getServiceComponentMetrics.calledWith({})).to.be.true;
-      expect(mixinObject.get('isLoaded')).to.be.true;
+      expect(mixinObject.onMetricsLoaded.calledOnce).to.be.true;
     });
   });