AMBARI-7238 Slider View: FE - Show alerts provided by alerts API. (atkach)

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

Branch: refs/heads/branch-alerts-dev
Commit: c91fab72a2290b537e2a4036d02239b8f23e2d0f
Parents: d3bfd49
Author: atkach <atk...@hortonworks.com>
Authored: Wed Sep 10 14:35:58 2014 +0300
Committer: atkach <atk...@hortonworks.com>
Committed: Wed Sep 10 14:35:58 2014 +0300

----------------------------------------------------------------------
 .../resources/ui/app/assets/data/apps/apps.json | 124 +++++++++++
 .../src/main/resources/ui/app/helpers/helper.js |  47 +++-
 .../ui/app/mappers/slider_apps_mapper.js        |  79 ++++---
 .../main/resources/ui/app/models/slider_app.js  |   5 +
 .../resources/ui/app/models/slider_app_alert.js | 172 +++++++++++++++
 .../resources/ui/app/styles/application.less    |  60 ++++++
 .../ui/app/templates/slider_app/summary.hbs     |  27 ++-
 .../src/main/resources/ui/app/translations.js   |  14 ++
 .../ui/app/views/slider_app/summary_view.js     |  20 +-
 .../slider/src/main/resources/ui/config.js      |   1 +
 .../ui/vendor/scripts/common/jquery.timeago.js  | 214 +++++++++++++++++++
 11 files changed, 732 insertions(+), 31 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/assets/data/apps/apps.json
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/app/assets/data/apps/apps.json 
b/contrib/views/slider/src/main/resources/ui/app/assets/data/apps/apps.json
index 60cc831..d774387 100644
--- a/contrib/views/slider/src/main/resources/ui/app/assets/data/apps/apps.json
+++ b/contrib/views/slider/src/main/resources/ui/app/assets/data/apps/apps.json
@@ -18,6 +18,68 @@
       "version" : "1.0.0",
       "view_name" : "SLIDER",
       "yarnId" : "application_1409333994422_0005",
+      "alerts" : {
+        "detail" : [
+          {
+            "description" : "alert1",
+            "host_name" : "с6402.ambari.apache.org",
+            "last_status" : "CRITICAL",
+            "last_status_time" : 1409689349,
+            "service_name" : "HBASE",
+            "status" : "OK",
+            "status_time" : 1409703869,
+            "output" : "Connection refused",
+            "actual_status" : "CRITICAL"
+          },
+          {
+            "description" : "alert2",
+            "host_name" : "с6402.ambari.apache.org",
+            "last_status" : "OK",
+            "last_status_time" : 1409334745,
+            "service_name" : "HBASE",
+            "status" : "WARNING",
+            "status_time" : 1409703865,
+            "output" : "TCP OK - 0.001 second response time on port 9083",
+            "actual_status" : "OK"
+          },
+          {
+            "description" : "NameNode edit logs directory status on 
dev01.hortonworks.com",
+            "host_name" : "dev01.hortonworks.com",
+            "last_status" : "WARNING",
+            "last_status_time" : 1389788011,
+            "service_name" : "HBASE",
+            "status" : "CRITICAL",
+            "status_time" : 1389793171,
+            "output" : "WARNING: NameNode directory status not available via 
http://dev01.hortonworks.com:50070/jmx url, code 0"
+          },
+          {
+            "description" : "NameNode host CPU utilization on 
dev01.hortonworks.com",
+            "host_name" : "dev01.hortonworks.com",
+            "last_status" : "OK",
+            "last_status_time" : 1389636534,
+            "service_name" : "HBASE",
+            "status" : "PASSIVE",
+            "status_time" : 1389793060,
+            "output" : "1 CPU, load 19.0% &lt; 200% : OK"
+          },
+          {
+            "description" : "App Timeline Server Web UI",
+            "host_name" : "dev01.hortonworks.com",
+            "last_status" : "WARNING",
+            "last_status_time" : 1389636534,
+            "service_name" : "HBASE",
+            "status" : "UNKNOWN",
+            "status_time" : 1389793060,
+            "output" : "1 CPU, load 19.0% &lt; 200% : OK"
+          }
+        ],
+        "summary" : {
+          "CRITICAL" : 1,
+          "OK" : 1,
+          "PASSIVE" : 0,
+          "WARNING" : 0
+        }
+      },
       "components" : {
         "HBASE_MASTER" : {
           "componentName" : "HBASE_MASTER",
@@ -403,6 +465,68 @@
       "version" : "1.0.0",
       "view_name" : "SLIDER",
       "yarnId" : "application_1409333994422_0009",
+      "alerts" : {
+        "detail" : [
+          {
+            "description" : "alert1",
+            "host_name" : "с6402.ambari.apache.org",
+            "last_status" : "CRITICAL",
+            "last_status_time" : 1409689349,
+            "service_name" : "HBASE",
+            "status" : "OK",
+            "status_time" : 1409703869,
+            "output" : "Connection refused",
+            "actual_status" : "CRITICAL"
+          },
+          {
+            "description" : "alert2",
+            "host_name" : "с6402.ambari.apache.org",
+            "last_status" : "OK",
+            "last_status_time" : 1409334745,
+            "service_name" : "HBASE",
+            "status" : "WARNING",
+            "status_time" : 1409703865,
+            "output" : "TCP OK - 0.001 second response time on port 9083",
+            "actual_status" : "OK"
+          },
+          {
+            "description" : "NameNode edit logs directory status on 
dev01.hortonworks.com",
+            "host_name" : "dev01.hortonworks.com",
+            "last_status" : "WARNING",
+            "last_status_time" : 1389788011,
+            "service_name" : "HBASE",
+            "status" : "CRITICAL",
+            "status_time" : 1389793171,
+            "output" : "WARNING: NameNode directory status not available via 
http://dev01.hortonworks.com:50070/jmx url, code 0"
+          },
+          {
+            "description" : "NameNode host CPU utilization on 
dev01.hortonworks.com",
+            "host_name" : "dev01.hortonworks.com",
+            "last_status" : "OK",
+            "last_status_time" : 1389636534,
+            "service_name" : "HBASE",
+            "status" : "PASSIVE",
+            "status_time" : 1389793060,
+            "output" : "1 CPU, load 19.0% &lt; 200% : OK"
+          },
+          {
+            "description" : "App Timeline Server Web UI",
+            "host_name" : "dev01.hortonworks.com",
+            "last_status" : "WARNING",
+            "last_status_time" : 1389636534,
+            "service_name" : "HBASE",
+            "status" : "UNKNOWN",
+            "status_time" : 1389793060,
+            "output" : "1 CPU, load 19.0% &lt; 200% : OK"
+          }
+        ],
+        "summary" : {
+          "CRITICAL" : 1,
+          "OK" : 1,
+          "PASSIVE" : 0,
+          "WARNING" : 0
+        }
+      },
       "components" : {
         "HBASE_MASTER" : {
           "componentName" : "HBASE_MASTER",

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/helpers/helper.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/helpers/helper.js 
b/contrib/views/slider/src/main/resources/ui/app/helpers/helper.js
index 88dfcb7..3f47ffd 100644
--- a/contrib/views/slider/src/main/resources/ui/app/helpers/helper.js
+++ b/contrib/views/slider/src/main/resources/ui/app/helpers/helper.js
@@ -100,4 +100,49 @@ App.registerBoundHelper('humanize', Em.View.extend({
     var content = this.get('content');
     return content && (content[0].toUpperCase() + 
content.slice(1)).replace(/([A-Z])/g, ' $1').replace(/_/g, ' ');
   }.property('content')
-}));
\ No newline at end of file
+}));
+
+/**
+ * Ambari overrides the default date transformer.
+ * This is done because of the non-standard data
+ * sent. For example Nagios sends date as "12345678".
+ * The problem is that it is a String and is represented
+ * only in seconds whereas Javascript's Date needs
+ * milliseconds representation.
+ */
+DS.attr.transforms = {
+  date: {
+    from: function (serialized) {
+      var type = typeof serialized;
+      if (type === 'string') {
+        serialized = parseInt(serialized);
+        type = typeof serialized;
+      }
+      if (type === 'number') {
+        if (!serialized) {  //serialized timestamp = 0;
+          return 0;
+        }
+        // The number could be seconds or milliseconds.
+        // If seconds, then the length is 10
+        // If milliseconds, the length is 13
+        if (serialized.toString().length < 13) {
+          serialized = serialized * 1000;
+        }
+        return new Date(serialized);
+      } else if (serialized === null || serialized === undefined) {
+        // if the value is not present in the data,
+        // return undefined, not null.
+        return serialized;
+      } else {
+        return null;
+      }
+    },
+    to: function (deserialized) {
+      if (deserialized instanceof Date) {
+        return deserialized.getTime();
+      } else {
+        return null;
+      }
+    }
+  }
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/mappers/slider_apps_mapper.js
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/app/mappers/slider_apps_mapper.js 
b/contrib/views/slider/src/main/resources/ui/app/mappers/slider_apps_mapper.js
index 23d8a8e..6b83eea 100644
--- 
a/contrib/views/slider/src/main/resources/ui/app/mappers/slider_apps_mapper.js
+++ 
b/contrib/views/slider/src/main/resources/ui/app/mappers/slider_apps_mapper.js
@@ -27,7 +27,7 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
    * @method load
    * @return {$.ajax}
    */
-  load: function() {
+  load: function () {
     return App.ajax.send({
       name: 'mapper.applicationApps',
       sender: this,
@@ -37,18 +37,47 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
 
   /**
    * Parse loaded data
+   * Load <code>App.Alert</code> model
+   * @param {object} data received from server data
+   * @method parse
+   */
+  parseAlerts: function (data) {
+    var alerts = [],
+      appId = data.id;
+
+    if (data.alerts && data.alerts.detail) {
+      data.alerts.detail.forEach(function (alert) {
+        alerts.push({
+          id: appId + alert.description,
+          title: alert.description,
+          serviceName: alert.service_name,
+          status: alert.status,
+          message: alert.output,
+          hostName: alert.host_name,
+          lastTime: alert.status_time,
+          appId: appId,
+          lastCheck: alert.last_status_time
+        });
+      });
+      App.SliderApp.store.pushMany('sliderAppAlert', alerts);
+    }
+    return alerts.mapProperty('id');
+  },
+
+  /**
+   * Parse loaded data
    * Load <code>App.SliderAppComponent</code> model
    * @param {object} data received from server data
    * @method parse
    */
-  parseComponents: function(data) {
+  parseComponents: function (data) {
     var components = [],
-    appId = data.id;
+      appId = data.id;
 
     Object.keys(data.components).forEach(function (key) {
       var component = data.components[key];
       activeContainers = Object.keys(component.activeContainers);
-      for(var i= 0; i < component.instanceCount; i++){
+      for (var i = 0; i < component.instanceCount; i++) {
         components.pushObject(
           Ember.Object.create({
             id: appId + component.componentName + i,
@@ -70,7 +99,7 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
    * @param {object} data received from server data
    * @method parse
    */
-  parseConfigs : function(data) {
+  parseConfigs: function (data) {
     var configs = {};
     Object.keys(data.configs).forEach(function (key) {
       configs[key] = data.configs[key];
@@ -84,25 +113,25 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
    * @param {object} data received from server data
    * @method parse
    */
-  parseQuickLinks : function(data) {
+  parseQuickLinks: function (data) {
     var quickLinks = [],
-    appId = data.id;
+      appId = data.id;
     quickLinks.push(
       Ember.Object.create({
         id: 'YARN application',
         label: 'YARN application',
-        url: "http://"+window.location.hostname+":8088";
+        url: "http://"; + window.location.hostname + ":8088"
       })
     );
 
-    if(!data.urls){
+    if (!data.urls) {
       return quickLinks.mapProperty('id');
     }
 
     Object.keys(data.urls).forEach(function (key) {
       quickLinks.push(
         Ember.Object.create({
-          id: appId+key,
+          id: appId + key,
           label: key,
           url: data.urls[key]
         })
@@ -112,9 +141,9 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
     return quickLinks.mapProperty('id');
   },
 
-  parseObject: function(o) {
+  parseObject: function (o) {
     if (Ember.typeOf(o) !== 'object') return [];
-    return Ember.keys(o).map(function(key) {
+    return Ember.keys(o).map(function (key) {
       return {key: key, value: o[key]};
     });
   },
@@ -125,22 +154,23 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
    * @param {object} data received from server data
    * @method parse
    */
-  parse: function(data) {
+  parse: function (data) {
     var apps = [],
-    self = this,
-    appsToDelete = 
App.SliderApp.store.all('sliderApp').get('content').mapProperty('id');
+      self = this,
+      appsToDelete = 
App.SliderApp.store.all('sliderApp').get('content').mapProperty('id');
 
-    data.items.forEach(function(app) {
+    data.items.forEach(function (app) {
       var componentsId = app.components ? self.parseComponents(app) : [],
-      configs = app.configs ? self.parseConfigs(app) : {},
-      quickLinks = self.parseQuickLinks(app),
-      jmx = self.parseObject(app.jmx),
-      masterActiveTime = jmx.findProperty('key', 'MasterActiveTime'),
-      masterStartTime = jmx.findProperty('key', 'MasterStartTime');
-      if(masterActiveTime){
+        configs = app.configs ? self.parseConfigs(app) : {},
+        quickLinks = self.parseQuickLinks(app),
+        alerts = self.parseAlerts(app),
+        jmx = self.parseObject(app.jmx),
+        masterActiveTime = jmx.findProperty('key', 'MasterActiveTime'),
+        masterStartTime = jmx.findProperty('key', 'MasterStartTime');
+      if (masterActiveTime) {
         masterActiveTime.value = new Date(Date.now() - 
masterActiveTime.value).getHours() + "h:" + new Date(Date.now() - 
masterActiveTime.value).getMinutes() + "m";
       }
-      if(masterStartTime){
+      if (masterStartTime) {
         masterStartTime.value = (new 
Date(parseInt(masterStartTime.value)).toUTCString());
       }
       apps.push(
@@ -157,6 +187,7 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
           description: app.description || "-",
           components: componentsId,
           quickLinks: quickLinks,
+          alerts: alerts,
           configs: configs,
           jmx: jmx,
           runtimeProperties: app.configs
@@ -168,7 +199,7 @@ App.SliderAppsMapper = 
App.Mapper.createWithMixins(App.RunPeriodically, {
 
     appsToDelete.forEach(function (app) {
       var appRecord = App.SliderApp.store.getById('sliderApp', app);
-      if(appRecord){
+      if (appRecord) {
         appRecord.destroyRecord();
       }
     });

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/models/slider_app.js
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/app/models/slider_app.js 
b/contrib/views/slider/src/main/resources/ui/app/models/slider_app.js
index 69f2479..6d6bfcc 100644
--- a/contrib/views/slider/src/main/resources/ui/app/models/slider_app.js
+++ b/contrib/views/slider/src/main/resources/ui/app/models/slider_app.js
@@ -74,6 +74,11 @@ App.SliderApp = DS.Model.extend({
   quickLinks: DS.hasMany('quickLink', {async:true}),
 
   /**
+   * @type {App.SliderAppAlert[]}
+   */
+  alerts: DS.hasMany('sliderAppAlert', {async:true}),
+
+  /**
    * @type {App.TypedProperty[]}
    */
   runtimeProperties: DS.hasMany('typedProperty', {async:true}),

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/models/slider_app_alert.js
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/app/models/slider_app_alert.js 
b/contrib/views/slider/src/main/resources/ui/app/models/slider_app_alert.js
new file mode 100644
index 0000000..c8cf0bd
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/models/slider_app_alert.js
@@ -0,0 +1,172 @@
+/**
+ * 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.SliderAppAlert = DS.Model.extend({
+  /**
+   * @type {string}
+   */
+  title: DS.attr('string'),
+
+  /**
+   * @type {string}
+   */
+  serviceName: DS.attr('string'),
+
+  /**
+   * @type {string}
+   */
+  status: DS.attr('string'),
+
+  /**
+   * @type {string}
+   */
+  message: DS.attr('string'),
+
+  /**
+   * @type {string}
+   */
+  hostName: DS.attr('string'),
+
+  /**
+   * @type {number}
+   */
+  lastTime: DS.attr('number'),
+
+  /**
+   * @type {number}
+   */
+  lastCheck: DS.attr('number'),
+
+  /**
+   * @type {App.SliderApp}
+   */
+  appId: DS.belongsTo('sliderApp'),
+
+  /**
+   * @type {string}
+   */
+  iconClass: function () {
+    var statusMap = Em.Object.create({
+      'OK': 'icon-ok',
+      'WARNING': 'icon-warning-sign',
+      'CRITICAL': 'icon-remove',
+      'PASSIVE': 'icon-medkit'
+    });
+    return statusMap.getWithDefault(this.get('status'), 'icon-question-sign');
+  }.property('status'),
+
+  /**
+   * @type {object}
+   */
+  date: function () {
+    return DS.attr.transforms.date.from(this.get('lastTime'));
+  }.property('lastTime'),
+
+  /**
+   * Provides how long ago this alert happened.
+   *
+   * @type {String}
+   */
+  timeSinceAlert: function () {
+    var d = this.get('date');
+    var timeFormat;
+    if (d) {
+      switch (this.get('status')) {
+        case "0":
+          timeFormat = Em.I18n.t('sliderApp.alerts.OK.timePrefix');
+          break;
+        case "1":
+          timeFormat = Em.I18n.t('sliderApp.alerts.WARN.timePrefix');
+          break;
+        case "2":
+          timeFormat = Em.I18n.t('sliderApp.alerts.CRIT.timePrefix');
+          break;
+        case "3":
+          timeFormat = Em.I18n.t('sliderApp.alerts.MAINT.timePrefix');
+          break;
+        default:
+          timeFormat = Em.I18n.t('sliderApp.alerts.UNKNOWN.timePrefix');
+          break;
+      }
+      var prevSuffix = $.timeago.settings.strings.suffixAgo;
+      $.timeago.settings.strings.suffixAgo = '';
+      var since = 
timeFormat.format($.timeago(this.makeTimeAtleastMinuteAgo(d)));
+      $.timeago.settings.strings.suffixAgo = prevSuffix;
+      return since;
+    } else if (d == 0) {
+      switch (this.get('status')) {
+        case "0":
+          timeFormat = Em.I18n.t('sliderApp.alerts.OK.timePrefixShort');
+          break;
+        case "1":
+          timeFormat = Em.I18n.t('sliderApp.alerts.WARN.timePrefixShort');
+          break;
+        case "2":
+          timeFormat = Em.I18n.t('sliderApp.alerts.CRIT.timePrefixShort');
+          break;
+        case "3":
+          timeFormat = Em.I18n.t('sliderApp.alerts.MAINT.timePrefixShort');
+          break;
+        default:
+          timeFormat = Em.I18n.t('sliderApp.alerts.UNKNOWN.timePrefixShort');
+      }
+      return timeFormat;
+    } else {
+      return "";
+    }
+  }.property('date', 'status'),
+
+  /**
+   *
+   * @param d
+   * @return {object}
+   */
+  makeTimeAtleastMinuteAgo: function (d) {
+    var diff = (new Date).getTime() - d.getTime();
+    if (diff < 60000) {
+      diff = 60000 - diff;
+      return new Date(d.getTime() - diff);
+    }
+    return d;
+  },
+
+  /**
+   * Provides more details about when this alert happened.
+   *
+   * @type {String}
+   */
+  timeSinceAlertDetails: function () {
+    var details = "";
+    var date = this.get('date');
+    if (date) {
+      var dateString = date.toDateString();
+      dateString = dateString.substr(dateString.indexOf(" ") + 1);
+      dateString = Em.I18n.t('sliderApp.alerts.occurredOn').format(dateString, 
date.toLocaleTimeString());
+      details += dateString;
+    }
+    var lastCheck = this.get('lastCheck');
+    if (lastCheck) {
+      lastCheck = new Date(lastCheck * 1000);
+      details = details ? details + 
Em.I18n.t('sliderApp.alerts.brLastCheck').format($.timeago(lastCheck)) : 
Em.I18n.t('sliderApp.alerts.lastCheck').format($.timeago(lastCheck));
+    }
+    return details;
+  }.property('lastCheck', 'date')
+
+});
+
+App.SliderAppAlert.FIXTURES = [];
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/styles/application.less
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/app/styles/application.less 
b/contrib/views/slider/src/main/resources/ui/app/styles/application.less
index 3eb6d55..11ec39b 100644
--- a/contrib/views/slider/src/main/resources/ui/app/styles/application.less
+++ b/contrib/views/slider/src/main/resources/ui/app/styles/application.less
@@ -608,3 +608,63 @@ a {
     margin-top: 20px;
   }
 }
+
+.app-alerts {
+  overflow-y: auto;
+  ul {
+    padding-left: 0;
+    margin-bottom: 0;
+  }
+  li {
+    border-bottom: 1px solid #eee;
+    list-style: none;
+    padding: 5px;
+    background-position: 14px 9px;
+    background-repeat: no-repeat;
+    .date-time {
+      float: right;
+    }
+    p {
+      margin-bottom: 2px;
+    }
+    .container-fluid {
+      padding-left: 10px;
+      padding-right: 10px;
+    }
+    .title {
+      font-weight: normal;
+      font-size: 13px;
+    }
+    .row-fluid [class*="span"] {
+      min-height: 0px;
+    }
+    .status-icon {
+      padding-top: 7px;
+    }
+    .date-time {
+      color: #999;
+      font-style: italic;
+      font-size: small;
+      text-align: right;
+    }
+    .message {
+      font-size: 12px;
+      color: #777;
+    }
+    .serviceLink {
+      padding-left: 7px;
+    }
+  }
+  .icon-ok {
+    color: #5AB400;
+  }
+  .icon-remove {
+    color: #FF4B4B;
+  }
+  .icon-warning-sign {
+    color: #FDB82F;
+  }
+  .icon-question-sign {
+    color: #999;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/templates/slider_app/summary.hbs
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/app/templates/slider_app/summary.hbs
 
b/contrib/views/slider/src/main/resources/ui/app/templates/slider_app/summary.hbs
index 974186d..0ba3998 100644
--- 
a/contrib/views/slider/src/main/resources/ui/app/templates/slider_app/summary.hbs
+++ 
b/contrib/views/slider/src/main/resources/ui/app/templates/slider_app/summary.hbs
@@ -69,14 +69,35 @@
         <div class="panel-heading">
           Alerts
           <div class="btn-group pull-right panel-link">
-            <a class="btn btn-default btn-sm" target="_blank" rel="tooltip"
+{{!--            <a class="btn btn-default btn-sm" target="_blank" 
rel="tooltip"
               {{translateAttr title="sliderApp.summary.go_to_nagios"}}
               {{bindAttr href="view.nagiosUrl"}}>
                 <i class="icon-link"></i>
-            </a>
+            </a>--}}
           </div>
         </div>
-        <div class="panel-body">
+        <div class="app-alerts">
+            <ul>
+              {{#each controller.model.alerts}}
+                {{#view view.AlertView contentBinding="this"}}
+                      <div class="container-fluid">
+                          <div class="row">
+                              <div class="col-md-1 status-icon">
+                                  <i {{bindAttr class="iconClass 
:icon-large"}}></i>
+                              </div>
+                              <div class="col-md-11">
+                                  <div class="row">
+                                      <div class="col-md-7 
title">{{title}}</div>
+
+                                      <div {{bs-bind-tooltip view.tooltip}} 
data-placement="right" class="col-md-5 date-time">{{timeSinceAlert}}</div>
+                                  </div>
+                                  <div class="message">{{message}}</div>
+                              </div>
+                          </div>
+                      </div>
+                  {{/view}}
+              {{/each}}
+            </ul>
         </div>
       </div>
     </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/translations.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/translations.js 
b/contrib/views/slider/src/main/resources/ui/app/translations.js
index 4af412a..e5e6a79 100644
--- a/contrib/views/slider/src/main/resources/ui/app/translations.js
+++ b/contrib/views/slider/src/main/resources/ui/app/translations.js
@@ -79,6 +79,20 @@ Em.I18n.translations = {
   'sliderApp.summary.go_to_nagios': 'Go to Nagios',
   'sliderApp.summary.go_to_ganglia': 'Go to Ganglia',
 
+  'sliderApp.alerts.OK.timePrefixShort': 'OK',
+  'sliderApp.alerts.WARN.timePrefixShort': 'WARN',
+  'sliderApp.alerts.CRIT.timePrefixShort': 'CRIT',
+  'sliderApp.alerts.MAINT.timePrefixShort': 'MAINT',
+  'sliderApp.alerts.UNKNOWN.timePrefixShort': 'UNKNOWN',
+  'sliderApp.alerts.OK.timePrefix': 'OK for {0}',
+  'sliderApp.alerts.WARN.timePrefix': 'WARN for {0}',
+  'sliderApp.alerts.CRIT.timePrefix': 'CRIT for {0}',
+  'sliderApp.alerts.MAINT.timePrefix': 'MAINT for {0}',
+  'sliderApp.alerts.UNKNOWN.timePrefix': 'UNKNOWN for {0}',
+  'sliderApp.alerts.lastCheck': 'Last Checked {0}',
+  'sliderApp.alerts.brLastCheck': "\nLast Checked {0}",
+  'sliderApp.alerts.occurredOn': 'Occurred on {0}, {1}',
+
   'wizard.name': 'Create Slider App',
   'wizard.step1.name': 'Select Type',
   'wizard.step1.header': 'Available Types',

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
 
b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
index cb3b64a..1be586d 100644
--- 
a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
+++ 
b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
@@ -38,9 +38,23 @@ App.SliderAppSummaryView = Ember.View.extend({
     return 'http://' + App.get('nagiosHost') + '/nagios';
   }.property(),
 
-  fitPanels:function () {
+  fitPanels: function () {
     var heightLeft = parseInt(this.$('.panel-summury').css('height'));
-    this.$('.panel-components, 
.panel-alerts').css('height',((heightLeft<200)?200:heightLeft-20)/2);
-  }.on('didInsertElement')
+    this.$('.panel-components').css('height', ((heightLeft < 200) ? 200 : 
heightLeft - 20) / 2);
+    this.$('.panel-alerts .app-alerts').css('height', ((heightLeft < 200) ? 
200 : heightLeft - 106) / 2);
+  }.on('didInsertElement'),
+
+  AlertView: Em.View.extend({
+    content: null,
+    tagName: 'li',
+    tooltip: function () {
+      var self = this;
+      return Ember.Object.create({
+        trigger: 'hover',
+        content: this.get('content.timeSinceAlertDetails'),
+        placement: "right"
+      });
+    }.property('content')
+  })
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/config.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/config.js 
b/contrib/views/slider/src/main/resources/ui/config.js
index 7acda84..e464128 100755
--- a/contrib/views/slider/src/main/resources/ui/config.js
+++ b/contrib/views/slider/src/main/resources/ui/config.js
@@ -37,6 +37,7 @@ exports.config = {
           'vendor/scripts/common/jquery.ui.widget.js',
           'vendor/scripts/common/jquery.ui.mouse.js',
           'vendor/scripts/common/jquery.ui.sortable.js',
+          'vendor/scripts/common/jquery.timeago.js',
           'vendor/scripts/common/handlebars.js',
           'vendor/scripts/development/ember.js',
           'vendor/scripts/production/ember-data.js',

http://git-wip-us.apache.org/repos/asf/ambari/blob/c91fab72/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/jquery.timeago.js
----------------------------------------------------------------------
diff --git 
a/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/jquery.timeago.js
 
b/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/jquery.timeago.js
new file mode 100644
index 0000000..90e9553
--- /dev/null
+++ 
b/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/jquery.timeago.js
@@ -0,0 +1,214 @@
+/**
+ * Timeago is a jQuery plugin that makes it easy to support automatically
+ * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
+ *
+ * @name timeago
+ * @version 1.4.1
+ * @requires jQuery v1.2.3+
+ * @author Ryan McGeary
+ * @license MIT License - http://www.opensource.org/licenses/mit-license.php
+ *
+ * For usage and examples, visit:
+ * http://timeago.yarp.com/
+ *
+ * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
+ */
+
+(function (factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else {
+    // Browser globals
+    factory(jQuery);
+  }
+}(function ($) {
+  $.timeago = function(timestamp) {
+    if (timestamp instanceof Date) {
+      return inWords(timestamp);
+    } else if (typeof timestamp === "string") {
+      return inWords($.timeago.parse(timestamp));
+    } else if (typeof timestamp === "number") {
+      return inWords(new Date(timestamp));
+    } else {
+      return inWords($.timeago.datetime(timestamp));
+    }
+  };
+  var $t = $.timeago;
+
+  $.extend($.timeago, {
+    settings: {
+      refreshMillis: 60000,
+      allowPast: true,
+      allowFuture: false,
+      localeTitle: false,
+      cutoff: 0,
+      strings: {
+        prefixAgo: null,
+        prefixFromNow: null,
+        suffixAgo: "ago",
+        suffixFromNow: "from now",
+        inPast: 'any moment now',
+        seconds: "less than a minute",
+        minute: "about a minute",
+        minutes: "%d minutes",
+        hour: "about an hour",
+        hours: "about %d hours",
+        day: "a day",
+        days: "%d days",
+        month: "about a month",
+        months: "%d months",
+        year: "about a year",
+        years: "%d years",
+        wordSeparator: " ",
+        numbers: []
+      }
+    },
+
+    inWords: function(distanceMillis) {
+      if(!this.settings.allowPast && ! this.settings.allowFuture) {
+        throw 'timeago allowPast and allowFuture settings can not both be set 
to false.';
+      }
+
+      var $l = this.settings.strings;
+      var prefix = $l.prefixAgo;
+      var suffix = $l.suffixAgo;
+      if (this.settings.allowFuture) {
+        if (distanceMillis < 0) {
+          prefix = $l.prefixFromNow;
+          suffix = $l.suffixFromNow;
+        }
+      }
+
+      if(!this.settings.allowPast && distanceMillis >= 0) {
+        return this.settings.strings.inPast;
+      }
+
+      var seconds = Math.abs(distanceMillis) / 1000;
+      var minutes = seconds / 60;
+      var hours = minutes / 60;
+      var days = hours / 24;
+      var years = days / 365;
+
+      function substitute(stringOrFunction, number) {
+        var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, 
distanceMillis) : stringOrFunction;
+        var value = ($l.numbers && $l.numbers[number]) || number;
+        return string.replace(/%d/i, value);
+      }
+
+      var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) 
||
+        seconds < 90 && substitute($l.minute, 1) ||
+        minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
+        minutes < 90 && substitute($l.hour, 1) ||
+        hours < 24 && substitute($l.hours, Math.round(hours)) ||
+        hours < 42 && substitute($l.day, 1) ||
+        days < 30 && substitute($l.days, Math.round(days)) ||
+        days < 45 && substitute($l.month, 1) ||
+        days < 365 && substitute($l.months, Math.round(days / 30)) ||
+        years < 1.5 && substitute($l.year, 1) ||
+        substitute($l.years, Math.round(years));
+
+      var separator = $l.wordSeparator || "";
+      if ($l.wordSeparator === undefined) { separator = " "; }
+      return $.trim([prefix, words, suffix].join(separator));
+    },
+
+    parse: function(iso8601) {
+      var s = $.trim(iso8601);
+      s = s.replace(/\.\d+/,""); // remove milliseconds
+      s = s.replace(/-/,"/").replace(/-/,"/");
+      s = s.replace(/T/," ").replace(/Z/," UTC");
+      s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
+      s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
+      return new Date(s);
+    },
+    datetime: function(elem) {
+      var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : 
$(elem).attr("title");
+      return $t.parse(iso8601);
+    },
+    isTime: function(elem) {
+      // jQuery's `is()` doesn't play well with HTML5 in IE
+      return $(elem).get(0).tagName.toLowerCase() === "time"; // 
$(elem).is("time");
+    }
+  });
+
+  // functions that can be called via $(el).timeago('action')
+  // init is default when no action is given
+  // functions are called with context of a single element
+  var functions = {
+    init: function(){
+      var refresh_el = $.proxy(refresh, this);
+      refresh_el();
+      var $s = $t.settings;
+      if ($s.refreshMillis > 0) {
+        this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
+      }
+    },
+    update: function(time){
+      var parsedTime = $t.parse(time);
+      $(this).data('timeago', { datetime: parsedTime });
+      if($t.settings.localeTitle) $(this).attr("title", 
parsedTime.toLocaleString());
+      refresh.apply(this);
+    },
+    updateFromDOM: function(){
+      $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? 
$(this).attr("datetime") : $(this).attr("title") ) });
+      refresh.apply(this);
+    },
+    dispose: function () {
+      if (this._timeagoInterval) {
+        window.clearInterval(this._timeagoInterval);
+        this._timeagoInterval = null;
+      }
+    }
+  };
+
+  $.fn.timeago = function(action, options) {
+    var fn = action ? functions[action] : functions.init;
+    if(!fn){
+      throw new Error("Unknown function name '"+ action +"' for timeago");
+    }
+    // each over objects here and call the requested function
+    this.each(function(){
+      fn.call(this, options);
+    });
+    return this;
+  };
+
+  function refresh() {
+    var data = prepareData(this);
+    var $s = $t.settings;
+
+    if (!isNaN(data.datetime)) {
+      if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
+        $(this).text(inWords(data.datetime));
+      }
+    }
+    return this;
+  }
+
+  function prepareData(element) {
+    element = $(element);
+    if (!element.data("timeago")) {
+      element.data("timeago", { datetime: $t.datetime(element) });
+      var text = $.trim(element.text());
+      if ($t.settings.localeTitle) {
+        element.attr("title", 
element.data('timeago').datetime.toLocaleString());
+      } else if (text.length > 0 && !($t.isTime(element) && 
element.attr("title"))) {
+        element.attr("title", text);
+      }
+    }
+    return element.data("timeago");
+  }
+
+  function inWords(date) {
+    return $t.inWords(distance(date));
+  }
+
+  function distance(date) {
+    return (new Date().getTime() - date.getTime());
+  }
+
+  // fix for IE6 suckage
+  document.createElement("abbr");
+  document.createElement("time");
+}));
\ No newline at end of file

Reply via email to