You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by al...@apache.org on 2016/02/11 10:48:24 UTC

[1/2] ambari git commit: AMBARI-14993. Log Search: Add Host Log Metrics to Host Details -> Summary (alexantonenko)

Repository: ambari
Updated Branches:
  refs/heads/trunk b9a35f1ea -> 96bdecf84


AMBARI-14993. Log Search: Add Host Log Metrics to Host Details -> Summary (alexantonenko)


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

Branch: refs/heads/trunk
Commit: 96bdecf8474064465b2ecc1261152d9ba68730e2
Parents: 209ec33
Author: Alex Antonenko <hi...@gmail.com>
Authored: Wed Feb 10 16:31:52 2016 +0200
Committer: Alex Antonenko <hi...@gmail.com>
Committed: Thu Feb 11 11:48:18 2016 +0200

----------------------------------------------------------------------
 ambari-web/app/messages.js                      |   1 +
 ambari-web/app/routes/main.js                   |   5 +-
 .../app/templates/main/host/log_metrics.hbs     |  26 ++++
 ambari-web/app/templates/main/host/summary.hbs  |  24 +++-
 ambari-web/app/utils/ember_reopen.js            |  46 ++++++
 ambari-web/app/views.js                         |   1 +
 ambari-web/app/views/common/chart/pie.js        |  11 +-
 ambari-web/app/views/main/host/log_metrics.js   | 141 +++++++++++++++++++
 ambari-web/app/views/main/host/logs_view.js     |  12 ++
 ambari-web/test/utils/ember_reopen_test.js      |  57 ++++++++
 10 files changed, 311 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 00fb4b9..59877a5 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -2307,6 +2307,7 @@ Em.I18n.translations = {
   'hosts.host.summary.hostname':'Hostname',
   'hosts.host.summary.agentHeartbeat':'Heartbeat',
   'hosts.host.summary.hostMetrics':'Host Metrics',
+  'hosts.host.summary.hostLogMetrics':'Host Log Metrics',
   'hosts.host.summary.addComponent':'Add Component',
   'hosts.host.summary.currentVersion':'Current Version',
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/routes/main.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/routes/main.js b/ambari-web/app/routes/main.js
index 891bf34..419f845 100644
--- a/ambari-web/app/routes/main.js
+++ b/ambari-web/app/routes/main.js
@@ -265,13 +265,16 @@ module.exports = Em.Route.extend(App.RouterRedirections, {
       }),
 
       logs: Em.Route.extend({
-        route: '/logs',
+        route: '/logs:query',
         connectOutlets: function (router, context) {
           if (App.get('supports.logSearch')) {
             router.get('mainHostDetailsController').connectOutlet('mainHostLogs')
           } else {
             router.transitionTo('summary');
           }
+        },
+        serialize: function(router, params) {
+          return this.serializeQueryParams(router, params, 'mainHostDetailsController');
         }
       }),
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/templates/main/host/log_metrics.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/host/log_metrics.hbs b/ambari-web/app/templates/main/host/log_metrics.hbs
new file mode 100644
index 0000000..22a39be
--- /dev/null
+++ b/ambari-web/app/templates/main/host/log_metrics.hbs
@@ -0,0 +1,26 @@
+{{!
+* 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="row-fluid log-metrics-charts mtl">
+  {{#each item in view.logsData}}
+    <div class="span6 text-center mtl">
+      {{view view.chartView contentBinding="item"}}
+      <a href="#" {{action transitionByService item target="view"}}>{{item.service.displayName}}</a>
+    </div>
+  {{/each}}
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/templates/main/host/summary.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/host/summary.hbs b/ambari-web/app/templates/main/host/summary.hbs
index 6b4c7a5..17a0b69 100644
--- a/ambari-web/app/templates/main/host/summary.hbs
+++ b/ambari-web/app/templates/main/host/summary.hbs
@@ -168,19 +168,31 @@
             </div>
         </div>
     </div>
-  {{!metrics}}
-  {{#unless view.isNoHostMetricsService}}
-      <div class="span6">
+    <div class="span6">
+      {{!metrics}}
+      {{#unless view.isNoHostMetricsService}}
         <div class="box">
           <div class="box-header">
             <h4>{{t hosts.host.summary.hostMetrics}}</h4>
             {{view view.timeRangeListView}}
           </div>
           <div>
-           {{view App.MainHostMetricsView contentBinding="view.content"}}
+            {{view App.MainHostMetricsView contentBinding="view.content"}}
           </div>
         </div>
-      </div>
+      {{/unless}}
+
+      {{!logs metrics}}
+      {{#if App.supports.logSearch}}
+        <div class="box">
+          <div class="box-header">
+            <h4>{{t hosts.host.summary.hostLogMetrics}}</h4>
+          </div>
+          <div>
+            {{view App.MainHostLogMetrics contentBinding="view.content"}}
+          </div>
+        </div>
+      {{/if}}
     </div>
-  {{/unless}}
+  </div>
 </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/utils/ember_reopen.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/ember_reopen.js b/ambari-web/app/utils/ember_reopen.js
index 512b3da..bf091da 100644
--- a/ambari-web/app/utils/ember_reopen.js
+++ b/ambari-web/app/utils/ember_reopen.js
@@ -242,6 +242,23 @@ Ember.TextArea.reopen({
   attributeBindings: ['readonly']
 });
 
+/**
+ * Simply converts query string to object.
+ *
+ * @param  {string} queryString query string e.g. '?param1=value1&param2=value2'
+ * @return {object} converted object
+ */
+function parseQueryParams(queryString) {
+  if (!queryString) {
+    return {};
+  }
+  return queryString.replace(/^\?/, '').split('&').map(decodeURIComponent)
+    .reduce(function(p, c) {
+      var keyVal = c.split('=');
+      p[keyVal[0]] = keyVal[1];
+      return p;
+    }, {});
+};
 
 Ember.Route.reopen({
   /**
@@ -257,6 +274,35 @@ Ember.Route.reopen({
    */
   exitRoute: function (router, context, callback) {
     callback();
+  },
+
+  /**
+   * Query Params serializer. This method should be used inside <code>serialize</code> method.
+   * You need to specify `:query` dynamic sygment in your route's <code>route</code> attribute
+   * e.g. Em.Route.extend({ route: '/login:query'}) and return result of this method.
+   * This method will set <code>serializedQuery</code> property to specified controller by name.
+   * For concrete example see `app/routes/main.js`.
+   *
+   * @example
+   *  queryParams: Em.Route.extend({
+   *   route: '/queryDemo:query',
+   *   serialize: function(route, params) {
+   *     return this.serializeQueryParams(route, params, 'controllerNameToSetQueryObject');
+   *   }
+   *  });
+   *  // now when navigated to http://example.com/#/queryDemo?param1=value1&param2=value2
+   *  // App.router.get('controllerNameToSetQueryObject').get('serializedQuery')
+   *  // will return { param1: 'value1', param2: 'value2' }
+   *
+   * @param  {Em.Router} router router instance passed to <code>serialize</code> method
+   * @param  {object} params dynamic segment passed to <code>seriazlie</code>
+   * @param  {string} controllerName name of the controller to set `serializedQuery` as result
+   * @return {object}
+   */
+  serializeQueryParams: function(router, params, controllerName) {
+    var controller = router.get(controllerName);
+    controller.set('serializedQuery', parseQueryParams(params ? params.query : ''));
+    return params || { query: ''};
   }
 });
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js
index a4008f7..2440086 100644
--- a/ambari-web/app/views.js
+++ b/ambari-web/app/views.js
@@ -128,6 +128,7 @@ require('views/main/host/summary');
 require('views/main/host/configs');
 require('views/main/host/configs_service');
 require('views/main/host/configs_service_menu');
+require('views/main/host/log_metrics');
 require('views/main/host/metrics');
 require('views/main/host/stack_versions_view');
 require('views/main/host/add_view');

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views/common/chart/pie.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/chart/pie.js b/ambari-web/app/views/common/chart/pie.js
index 0280f87..ce9bda4 100644
--- a/ambari-web/app/views/common/chart/pie.js
+++ b/ambari-web/app/views/common/chart/pie.js
@@ -82,16 +82,15 @@ App.ChartPieView = Em.View.extend({
       .append("svg:g")
       .attr("transform", "translate(" + thisChart.get('w') / 2 + "," + thisChart.get('h') / 2 + ")"));
 
-    this.set('arcs', thisChart.get('svg').selectAll("path")
+    this.set('arcs', thisChart.get('svg').selectAll(".arc")
       .data(thisChart.donut(thisChart.get('data')))
-      .enter().append("svg:path")
+      .enter()
+      .append("svg:g").attr('class', 'arc')
+      .append('svg:path')
       .attr("fill", function (d, i) {
         return thisChart.palette.color(i);
       })
       .attr("d", thisChart.get('arc'))
-
     );
-
   }
-
-});
\ No newline at end of file
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views/main/host/log_metrics.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host/log_metrics.js b/ambari-web/app/views/main/host/log_metrics.js
new file mode 100644
index 0000000..20f5ec6
--- /dev/null
+++ b/ambari-web/app/views/main/host/log_metrics.js
@@ -0,0 +1,141 @@
+/**
+ * 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');
+
+/**
+ * @typedef {Ember.Object} LogLevelItemObject
+ * @property {string} level level name
+ * @property {number} counter
+ */
+/**
+ * @typedef {Object} ServiceLogMetricsObject
+ * @property {App.Service} service model instance
+ * @property {LogLevelItemObject[]} logs
+ */
+App.MainHostLogMetrics = Em.View.extend({
+  templateName: require('templates/main/host/log_metrics'),
+  classNames: ['host-log-metrics'],
+
+  /**
+   * @type {ServiceLogMetricsObject[]}
+   */
+  logsData: function() {
+    var services = this.get('content').get('hostComponents').mapProperty('service').uniq();
+    var logLevels = ['fatal', 'critical', 'error', 'warning', 'info', 'debug'];
+    return services.map(function(service) {
+      var levels = logLevels.map(function(level) {
+        return Em.Object.create({
+          level: level,
+          counter: Math.ceil(Math.random()*10)
+        });
+      });
+      return Em.Object.create({
+        service: service,
+        logs: levels
+      });
+    });
+  }.property('content'),
+
+  /**
+   * @type  {Ember.View} Pie Chart view
+   * @extends App.PieChartView
+   */
+  chartView: App.ChartPieView.extend({
+    classNames: ['log-metrics-chart'],
+    w: 150,
+    h: 150,
+    stroke: '#fff',
+    strokeWidth: 1,
+    levelColors: {
+      FATAL: '#B10202',
+      CRITICAL: '#E00505',
+      ERROR: App.healthStatusRed,
+      INFO: App.healthStatusGreen,
+      WARNING: App.healthStatusOrange,
+      DEBUG: '#1e61f7'
+    },
+    innerR: 36,
+    donut: d3.layout.pie().sort(null).value(function(d) { return d.get('counter'); }),
+
+    prepareChartData: function(content) {
+      this.set('data', content.get('logs'));
+    },
+
+    didInsertElement: function() {
+      this.prepareChartData(this.get('content'));
+      this._super();
+      this.appendLabels();
+      this.formatCenterText();
+      this.attachArcEvents();
+      this.colorizeArcs();
+    },
+
+    attachArcEvents: function() {
+      var self = this;
+      this.get('svg').selectAll('.arc')
+        .on('mouseover', function(d) {
+          self.get('svg').select('g.center-text').select('text')
+            .text(d.data.get('level').capitalize() + ": " + d.data.get('counter'));
+        })
+        .on('mouseout', function() {
+          self.get('svg').select('g.center-text').select('text').text('');
+        });
+    },
+
+    formatCenterText: function() {
+      this.get('svg')
+        .append('svg:g')
+        .attr('class', 'center-text')
+        .attr('render-order', 1)
+        .append('svg:text')
+        .attr('transform', "translate(0,0)")
+        .attr('text-anchor', 'middle')
+        .attr('stroke', '#000')
+        .attr('stroke-width', 0)
+    },
+
+    appendLabels: function() {
+      var labelArc = d3.svg.arc()
+        .outerRadius(this.get('outerR') - 15)
+        .innerRadius(this.get('outerR') - 15);
+      this.get('svg').selectAll('.arc')
+        .append('text')
+        .attr('transform', function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
+        .attr('stroke', '#000')
+        .attr('stroke-width', 0)
+        .attr('font-size', '12px')
+        .attr('dy', '.50em')
+        .text(function(d) { return d.data.get('counter'); });
+    },
+
+    colorizeArcs: function() {
+      var self = this;
+      this.get('svg').selectAll('.arc path')
+        .attr('fill', function(d) {
+          return self.get('levelColors')[d.data.get('level').toUpperCase()];
+        });
+    }
+  }),
+
+
+  transitionByService: function(e) {
+    var service = e.context;
+    App.router.transitionTo('logs', {query: '?service_name=' + service.get('service.serviceName')});
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/app/views/main/host/logs_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/host/logs_view.js b/ambari-web/app/views/main/host/logs_view.js
index dfe00e4..b51f955 100644
--- a/ambari-web/app/views/main/host/logs_view.js
+++ b/ambari-web/app/views/main/host/logs_view.js
@@ -67,6 +67,10 @@ App.MainHostLogsView = App.TableView.extend({
   serviceNameFilterView: filters.createSelectView({
     column: 1,
     fieldType: 'filter-input-width',
+    didInsertElement: function() {
+      this.setValue(Em.getWithDefault(this, 'controller.serializedQuery.service_name', ''));
+      this._super();
+    },
     content: function() {
       return [{
         value: '',
@@ -86,6 +90,10 @@ App.MainHostLogsView = App.TableView.extend({
   componentNameFilterView: filters.createSelectView({
     column: 2,
     fieldType: 'filter-input-width',
+    didInsertElement: function() {
+      this.setValue(Em.getWithDefault(this, 'controller.serializedQuery.component_name', ''));
+      this._super();
+    },
     content: function() {
       var hostName = this.get('parentView').get('host.hostName'),
         hostComponents = App.HostComponent.find().filterProperty('hostName', hostName),
@@ -108,6 +116,10 @@ App.MainHostLogsView = App.TableView.extend({
   fileExtensionsFilter: filters.createSelectView({
     column: 3,
     fieldType: 'filter-input-width',
+    didInsertElement: function() {
+      this.setValue(Em.getWithDefault(this, 'controller.serializedQuery.file_extension', ''));
+      this._super();
+    },
     content: function() {
       return [{
         value: '',

http://git-wip-us.apache.org/repos/asf/ambari/blob/96bdecf8/ambari-web/test/utils/ember_reopen_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/ember_reopen_test.js b/ambari-web/test/utils/ember_reopen_test.js
index eda5e81..aa50a50 100644
--- a/ambari-web/test/utils/ember_reopen_test.js
+++ b/ambari-web/test/utils/ember_reopen_test.js
@@ -78,4 +78,61 @@ describe('Ember functionality extension', function () {
 
   });
 
+  describe('#Em.Route', function() {
+    describe('#serializeQueryParams', function() {
+      var route,
+          cases = [
+        {
+          m: 'No query params',
+          params: undefined,
+          e: {
+            result: {query: ''},
+            serializedQuery: {}
+          }
+        },
+        {
+          m: 'Query params ?param1=value1&param2=value2',
+          params: { query: '?param1=value1&param2=value2'},
+          e: {
+            result: {query: '?param1=value1&param2=value2'},
+            serializedQuery: {param1: 'value1', param2: 'value2'}
+          }
+        },
+        {
+          m: 'Query params with encodedComponent ?param1=value1%30&param2=value2',
+          params: { query: '?param1=value1%30&param2=value2'},
+          e: {
+            result: {query: '?param1=value1%30&param2=value2'},
+            serializedQuery: {param1: 'value10', param2: 'value2'}
+          }
+        }
+      ];
+
+      beforeEach(function() {
+        route = Ember.Route.create({
+          route: 'demo:query',
+          serialize: function(router, params) {
+            return this.serializeQueryParams(router, params, 'testController');
+          }
+        });
+      });
+
+      afterEach(function() {
+        route.destroy();
+        route = null;
+      });
+
+      cases.forEach(function(test) {
+        it(test.m, function() {
+          var ctrl = Em.Object.create({});
+          var router = Em.Object.create({
+            testController: ctrl
+          });
+          var ret = route.serialize(router, test.params);
+          expect(ret).to.be.eql(test.e.result);
+          expect(ctrl.get('serializedQuery')).to.be.eql(test.e.serializedQuery);
+        });
+      });
+    });
+  });
 });


[2/2] ambari git commit: AMBARI-14992. Upon changing the time range for displaying graphs, UI does not show feedback (doesn't update and keeps showing the same graph) when data is not available (alexantonenko)

Posted by al...@apache.org.
AMBARI-14992. Upon changing the time range for displaying graphs, UI does not show feedback (doesn't update and keeps showing the same graph) when data is not available (alexantonenko)


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

Branch: refs/heads/trunk
Commit: 209ec330def90559aa767211f3b869d21c13b673
Parents: b9a35f1
Author: Alex Antonenko <hi...@gmail.com>
Authored: Wed Feb 10 15:47:16 2016 +0200
Committer: Alex Antonenko <hi...@gmail.com>
Committed: Thu Feb 11 11:48:18 2016 +0200

----------------------------------------------------------------------
 .../app/mixins/common/widgets/widget_mixin.js   |  49 +++++++-
 ambari-web/app/styles/application.less          |   5 +
 .../views/common/widget/graph_widget_view.js    |   4 +-
 .../test/mixins/common/widget_mixin_test.js     | 124 +++++++++++++++++++
 4 files changed, 176 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/209ec330/ambari-web/app/mixins/common/widgets/widget_mixin.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mixins/common/widgets/widget_mixin.js b/ambari-web/app/mixins/common/widgets/widget_mixin.js
index df7ff76..b6fe353 100644
--- a/ambari-web/app/mixins/common/widgets/widget_mixin.js
+++ b/ambari-web/app/mixins/common/widgets/widget_mixin.js
@@ -119,6 +119,7 @@ App.WidgetMixin = Ember.Mixin.create({
           context: this,
           startCallName: 'getHostComponentMetrics',
           successCallback: this.getHostComponentMetricsSuccessCallback,
+          errorCallback: this.getMetricsErrorCallback,
           completeCallback: function () {
             requestCounter--;
             if (requestCounter === 0) this.onMetricsLoaded();
@@ -130,6 +131,7 @@ App.WidgetMixin = Ember.Mixin.create({
           context: this,
           startCallName: 'getServiceComponentMetrics',
           successCallback: this.getMetricsSuccessCallback,
+          errorCallback: this.getMetricsErrorCallback,
           completeCallback: function () {
             requestCounter--;
             if (requestCounter === 0) this.onMetricsLoaded();
@@ -271,12 +273,41 @@ App.WidgetMixin = Ember.Mixin.create({
         if (!Em.isNone(metric_data)) {
           _metric.data = metric_data;
           this.get('metrics').pushObject(_metric);
+        } else if (this.get('graphView')) {
+          var graph = this.get('childViews') && this.get('childViews').findProperty('_showMessage');
+          if (graph) {
+            graph.set('hasData', false);
+            this.set('isExportButtonHidden', true);
+            graph._showMessage('info', this.t('graphs.noData.title'), this.t('graphs.noDataAtTime.message'));
+            this.get('metrics').clear();
+          }
         }
       }, this);
     }
   },
 
   /**
+   * error callback on getting aggregated metrics and host component metrics
+   * @param {object} xhr
+   * @param {string} textStatus
+   * @param {string} errorThrown
+   */
+  getMetricsErrorCallback: function (xhr, textStatus, errorThrown) {
+    if (this.get('graphView')) {
+      var graph = this.get('childViews') && this.get('childViews').findProperty('_showMessage');
+      if (graph) {
+        if (xhr.readyState == 4 && xhr.status) {
+          textStatus = xhr.status + " " + textStatus;
+        }
+        graph.set('hasData', false);
+        this.set('isExportButtonHidden', true);
+        graph._showMessage('warn', this.t('graphs.error.title'), this.t('graphs.error.message').format(textStatus, errorThrown));
+        this.get('metrics').clear();
+      }
+    }
+  },
+
+  /**
    * make GET call to get metrics value for all host components
    * @param {object} request
    * @return {$.ajax}
@@ -704,6 +735,7 @@ App.WidgetLoadAggregator = Em.Object.create({
         bulks[id].subRequests = [{
           context: request.context,
           successCallback: request.successCallback,
+          errorCallback: request.errorCallback,
           completeCallback: request.completeCallback
         }];
       } else {
@@ -711,6 +743,7 @@ App.WidgetLoadAggregator = Em.Object.create({
         bulks[id].subRequests.push({
           context: request.context,
           successCallback: request.successCallback,
+          errorCallback: request.errorCallback,
           completeCallback: request.completeCallback
         });
       }
@@ -732,11 +765,17 @@ App.WidgetLoadAggregator = Em.Object.create({
           _request.subRequests.forEach(function (subRequest) {
             subRequest.successCallback.call(subRequest.context, response);
           }, this);
-        }).complete(function () {
-          _request.subRequests.forEach(function (subRequest) {
-            subRequest.completeCallback.call(subRequest.context);
-          }, this);
-        });
+        }).fail(function (xhr, textStatus, errorThrown) {
+            _request.subRequests.forEach(function (subRequest) {
+              if (subRequest.errorCallback) {
+                subRequest.errorCallback.call(subRequest.context, xhr, textStatus, errorThrown);
+              }
+            }, this);
+          }).complete(function () {
+              _request.subRequests.forEach(function (subRequest) {
+                subRequest.completeCallback.call(subRequest.context);
+              }, this);
+            });
       })(bulks[id]);
     }
   }

http://git-wip-us.apache.org/repos/asf/ambari/blob/209ec330/ambari-web/app/styles/application.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/application.less b/ambari-web/app/styles/application.less
index 6c5d4d0..57b7e76 100644
--- a/ambari-web/app/styles/application.less
+++ b/ambari-web/app/styles/application.less
@@ -2245,6 +2245,7 @@ a:focus {
       left: 60px;
       overflow: visible;
       position: relative;
+      text-align: center;
     }
     .chart-y-axis {
       position: absolute;
@@ -2260,6 +2261,10 @@ a:focus {
         margin-top: 35px !important;
       }
     }
+    .alert {
+      display: inline-block;
+      padding-right: 14px;
+    }
   }
   position: relative;
   margin: 20px 15px 0 15px;

http://git-wip-us.apache.org/repos/asf/ambari/blob/209ec330/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 da30dca..6feaa28 100644
--- a/ambari-web/app/views/common/widget/graph_widget_view.js
+++ b/ambari-web/app/views/common/widget/graph_widget_view.js
@@ -311,7 +311,9 @@ App.GraphWidgetView = Em.View.extend(App.WidgetMixin, App.ExportMetricsMixin, {
         self.set('parentView.isExportMenuHidden', true);
       });
       this.setYAxisFormatter();
-      this.loadData();
+      if (!arguments.length || this.get('parentView.data.length')) {
+        this.loadData();
+      }
       var self = this;
       Em.run.next(function () {
         if (self.get('isPreview')) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/209ec330/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 00606f4..91f628f 100644
--- a/ambari-web/test/mixins/common/widget_mixin_test.js
+++ b/ambari-web/test/mixins/common/widget_mixin_test.js
@@ -384,6 +384,129 @@ describe('App.WidgetMixin', function () {
       });
     });
   });
+
+  describe('#getMetricsErrorCallback()', function () {
+
+    var obj,
+      view = Em.Object.create({
+        _showMessage: Em.K
+      }),
+      cases = [
+        {
+          graphView: null,
+          metricsLength: 1,
+          showMessageCallCount: 0,
+          isExportButtonHidden: false,
+          title: 'no graph view'
+        },
+        {
+          graphView: {},
+          metricsLength: 1,
+          showMessageCallCount: 0,
+          isExportButtonHidden: false,
+          title: 'no childViews property'
+        },
+        {
+          graphView: {},
+          childViews: [],
+          metricsLength: 1,
+          showMessageCallCount: 0,
+          isExportButtonHidden: false,
+          title: 'no child views'
+        },
+        {
+          graphView: {},
+          childViews: [Em.Object.create({})],
+          metricsLength: 1,
+          showMessageCallCount: 0,
+          isExportButtonHidden: false,
+          title: 'no view with _showMessage method'
+        },
+        {
+          graphView: {},
+          childViews: [Em.Object.create({}), view],
+          metricsLength: 0,
+          showMessageCallCount: 1,
+          isExportButtonHidden: true,
+          title: 'graph view is available'
+        }
+      ],
+      messageCases = [
+        {
+          readyState: 2,
+          status: 0,
+          textStatus: 'error',
+          title: 'incomplete request'
+        },
+        {
+          readyState: 4,
+          status: 0,
+          textStatus: 'error',
+          title: 'no status code'
+        },
+        {
+          readyState: 4,
+          status: 404,
+          textStatus: '404 error',
+          title: 'status code available'
+        }
+      ];
+
+    beforeEach(function () {
+      sinon.spy(view, '_showMessage');
+    });
+
+    afterEach(function () {
+      view._showMessage.restore();
+    });
+
+    cases.forEach(function (item) {
+
+      describe(item.title, function () {
+
+        beforeEach(function () {
+          obj = Em.Object.create(App.WidgetMixin, {
+            metrics: [{}],
+            isExportButtonHidden: false,
+            graphView: item.graphView,
+            childViews: item.childViews
+          });
+          obj.getMetricsErrorCallback({});
+        });
+
+        it('metrics array', function () {
+          expect(obj.get('metrics')).to.have.length(item.metricsLength);
+        });
+
+        it('error message', function () {
+          expect(view._showMessage.callCount).to.equal(item.showMessageCallCount);
+        });
+
+        it('export button display', function () {
+          expect(obj.get('isExportButtonHidden')).to.equal(item.isExportButtonHidden);
+        });
+
+      });
+
+    });
+
+    messageCases.forEach(function (item) {
+
+      it(item.title, function () {
+        obj = Em.Object.create(App.WidgetMixin, {
+          graphView: Em.Object.create({}),
+          childViews: [view]
+        });
+        obj.getMetricsErrorCallback({
+          readyState: item.readyState,
+          status: item.status
+        }, 'error', 'Not Found');
+        expect(view._showMessage.firstCall.args).to.eql(['warn', Em.I18n.t('graphs.error.title'), Em.I18n.t('graphs.error.message').format(item.textStatus, 'Not Found')]);
+      });
+
+    });
+
+  });
 });
 
 
@@ -502,6 +625,7 @@ describe('App.WidgetLoadAggregator', function () {
       f1: function () {
         return {
           done: Em.K,
+          fail: Em.K,
           complete: Em.K
         }
       },