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

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

Repository: ambari
Updated Branches:
  refs/heads/trunk 9301f9b16 -> 12598cff5


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


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

Branch: refs/heads/trunk
Commit: 12598cff5876fe4a2323828722a5869ed5373f97
Parents: 9301f9b
Author: Andrii Tkach <at...@hortonworks.com>
Authored: Tue Mar 31 19:12:33 2015 +0300
Committer: Andrii Tkach <at...@hortonworks.com>
Committed: Tue Mar 31 21:19:00 2015 +0300

----------------------------------------------------------------------
 .../data/widget_layouts/HBASE/layouts.json      | 16 +++++
 .../data/widget_layouts/HBASE/stack_layout.json |  3 +
 .../controllers/main/service/info/summary.js    | 76 +++++++++++++++++++-
 ambari-web/app/mappers.js                       |  1 +
 ambari-web/app/mappers/widget_layout_mapper.js  | 29 ++++++++
 ambari-web/app/mappers/widget_mapper.js         |  3 +-
 ambari-web/app/messages.js                      |  3 +
 ambari-web/app/mixins/common/widget_mixin.js    | 26 +++++--
 ambari-web/app/models.js                        |  3 +-
 ambari-web/app/models/widget.js                 |  2 +-
 ambari-web/app/models/widget_layout.js          |  6 +-
 .../app/styles/enhanced_service_dashboard.less  | 47 +++++++++++-
 ambari-web/app/styles/widget_layout.less        | 46 ------------
 .../templates/common/widget/gauge_widget.hbs    |  8 ++-
 .../templates/common/widget/template_widget.hbs |  8 ++-
 .../app/templates/main/service/info/summary.hbs | 42 ++++++++++-
 ambari-web/app/utils/ajax/ajax.js               |  5 ++
 .../views/common/widget/gauge_widget_view.js    |  4 +-
 .../views/common/widget/graph_widget_view.js    |  2 +-
 .../views/common/widget/template_widget_view.js |  2 +-
 .../app/views/main/service/info/summary.js      | 53 ++++++++++++++
 .../test/mixins/common/widget_mixin_test.js     | 12 ++--
 22 files changed, 323 insertions(+), 74 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/assets/data/widget_layouts/HBASE/layouts.json
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/data/widget_layouts/HBASE/layouts.json b/ambari-web/app/assets/data/widget_layouts/HBASE/layouts.json
new file mode 100644
index 0000000..ae33a84
--- /dev/null
+++ b/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"
+      }
+    }
+  ]
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json b/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json
index 6285138..bfab170 100644
--- a/ambari-web/app/assets/data/widget_layouts/HBASE/stack_layout.json
+++ b/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":[
               {

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/controllers/main/service/info/summary.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/service/info/summary.js b/ambari-web/app/controllers/main/service/info/summary.js
index d1d1be8..1eec9d1 100644
--- a/ambari-web/app/controllers/main/service/info/summary.js
+++ b/ambari-web/app/controllers/main/service/info/summary.js
@@ -41,6 +41,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,13 +305,46 @@ 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 () {
+
   }
 
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/mappers.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers.js b/ambari-web/app/mappers.js
index 6939c0f..b96fc50 100644
--- a/ambari-web/app/mappers.js
+++ b/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');
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/mappers/widget_layout_mapper.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers/widget_layout_mapper.js b/ambari-web/app/mappers/widget_layout_mapper.js
new file mode 100644
index 0000000..d46099d
--- /dev/null
+++ b/ambari-web/app/mappers/widget_layout_mapper.js
@@ -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.
+ */
+
+
+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'
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/mappers/widget_mapper.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers/widget_mapper.js b/ambari-web/app/mappers/widget_mapper.js
index 1df5003..c9b1aef 100644
--- a/ambari-web/app/mappers/widget_mapper.js
+++ b/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

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 92c0a74..86093fc 100644
--- a/ambari-web/app/messages.js
+++ b/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',

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/mixins/common/widget_mixin.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/widget_mixin.js b/ambari-web/app/mixins/common/widget_mixin.js
index 7d1abfe..5ed262c 100644
--- a/ambari-web/app/mixins/common/widget_mixin.js
+++ b/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);
       }

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/models.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models.js b/ambari-web/app/models.js
index 6f5da37..e9de30f 100644
--- a/ambari-web/app/models.js
+++ b/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');
\ No newline at end of file
+require('models/widget');
+require('models/widget_layout');
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/models/widget.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/widget.js b/ambari-web/app/models/widget.js
index b093a53..e00cd05 100644
--- a/ambari-web/app/models/widget.js
+++ b/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}

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/models/widget_layout.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/widget_layout.js b/ambari-web/app/models/widget_layout.js
index b7cf9bb..2c29670 100644
--- a/ambari-web/app/models/widget_layout.js
+++ b/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')
 });
 
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/styles/enhanced_service_dashboard.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/enhanced_service_dashboard.less b/ambari-web/app/styles/enhanced_service_dashboard.less
index 8555f44..4d74488 100644
--- a/ambari-web/app/styles/enhanced_service_dashboard.less
+++ b/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;
+      }
+    }
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/styles/widget_layout.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/widget_layout.less b/ambari-web/app/styles/widget_layout.less
deleted file mode 100644
index 8b4b94c..0000000
--- a/ambari-web/app/styles/widget_layout.less
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * 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.
- */
-
-#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;
-      }
-    }
-  }
-}

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/templates/common/widget/gauge_widget.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/widget/gauge_widget.hbs b/ambari-web/app/templates/common/widget/gauge_widget.hbs
index 03a975f..11e3565 100644
--- a/ambari-web/app/templates/common/widget/gauge_widget.hbs
+++ b/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>

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/templates/common/widget/template_widget.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/widget/template_widget.hbs b/ambari-web/app/templates/common/widget/template_widget.hbs
index fda71ea..02f7425 100644
--- a/ambari-web/app/templates/common/widget/template_widget.hbs
+++ b/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>

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/templates/main/service/info/summary.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/service/info/summary.hbs b/ambari-web/app/templates/main/service/info/summary.hbs
index e87ca6b..92f7cbd 100644
--- a/ambari-web/app/templates/main/service/info/summary.hbs
+++ b/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>

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/utils/ajax/ajax.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ajax/ajax.js b/ambari-web/app/utils/ajax/ajax.js
index 1118d37..7b7c6fb 100644
--- a/ambari-web/app/utils/ajax/ajax.js
+++ b/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'

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/views/common/widget/gauge_widget_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/widget/gauge_widget_view.js b/ambari-web/app/views/common/widget/gauge_widget_view.js
index 0964e11..7a31d14 100644
--- a/ambari-web/app/views/common/widget/gauge_widget_view.js
+++ b/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'),
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/views/common/widget/graph_widget_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/widget/graph_widget_view.js b/ambari-web/app/views/common/widget/graph_widget_view.js
index 2e4f0d9..9d2e4f5 100644
--- a/ambari-web/app/views/common/widget/graph_widget_view.js
+++ b/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

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/views/common/widget/template_widget_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/widget/template_widget_view.js b/ambari-web/app/views/common/widget/template_widget_view.js
index 9b9d844..572e4fb 100644
--- a/ambari-web/app/views/common/widget/template_widget_view.js
+++ b/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')
+  }
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/app/views/main/service/info/summary.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/service/info/summary.js b/ambari-web/app/views/main/service/info/summary.js
index 9e108bf..f4e123d 100644
--- a/ambari-web/app/views/main/service/info/summary.js
+++ b/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();
       }
     }
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/12598cff/ambari-web/test/mixins/common/widget_mixin_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mixins/common/widget_mixin_test.js b/ambari-web/test/mixins/common/widget_mixin_test.js
index 2ee9edd..edb5359 100644
--- a/ambari-web/test/mixins/common/widget_mixin_test.js
+++ b/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;
     });
   });