ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From xiw...@apache.org
Subject ambari git commit: AMBARI-8221. Alerts UI: Create Manage Alerts Group dialog.(xiwang)
Date Fri, 14 Nov 2014 00:06:12 GMT
Repository: ambari
Updated Branches:
  refs/heads/trunk d6b0db0b5 -> 43704d451


AMBARI-8221. Alerts UI: Create Manage Alerts Group dialog.(xiwang)


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

Branch: refs/heads/trunk
Commit: 43704d451dc8186b829502e4e9e351af37a0ece4
Parents: d6b0db0
Author: Xi Wang <xiwang@apache.org>
Authored: Thu Oct 30 11:03:33 2014 -0700
Committer: Xi Wang <xiwang@apache.org>
Committed: Thu Nov 13 16:05:15 2014 -0800

----------------------------------------------------------------------
 .../app/assets/data/alerts/alertGroup.json      |  82 +++
 .../app/assets/data/alerts/alertGroups.json     | 167 +++++
 ambari-web/app/config.js                        |   1 +
 ambari-web/app/controllers.js                   |   1 +
 .../controllers/global/cluster_controller.js    |   3 +
 .../app/controllers/global/update_controller.js |  11 +
 .../alert_definitions_actions_controller.js     |  85 ++-
 .../alerts/manage_alert_groups_controller.js    | 619 +++++++++++++++++++
 ambari-web/app/mappers.js                       |   1 +
 ambari-web/app/mappers/alert_groups_mapper.js   |  44 ++
 ambari-web/app/messages.js                      |  25 +
 ambari-web/app/models.js                        |   1 +
 ambari-web/app/models/alert_group.js            | 107 ++++
 .../alerts/add_definition_to_group_popup.hbs    | 139 +++++
 .../main/alerts/create_new_alert_group.hbs      |  33 +
 .../main/alerts/manage_alert_groups_popup.hbs   |  94 +++
 ambari-web/app/utils/ajax/ajax.js               |  55 +-
 ambari-web/app/utils/validator.js               |  10 +
 ambari-web/app/views.js                         |   2 +
 .../main/alerts/manage_alert_groups_view.js     | 105 ++++
 20 files changed, 1583 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/assets/data/alerts/alertGroup.json
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/data/alerts/alertGroup.json b/ambari-web/app/assets/data/alerts/alertGroup.json
new file mode 100644
index 0000000..a309fd3
--- /dev/null
+++ b/ambari-web/app/assets/data/alerts/alertGroup.json
@@ -0,0 +1,82 @@
+{
+  "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/16",
+  "AlertGroup" : {
+    "cluster_name" : "c789",
+    "default" : false,
+    "definitions" : [
+      {
+        "name" : "mapreduce_history_server_webui",
+        "label" : "History Server Web UI",
+        "service_name" : "MAPREDUCE2",
+        "component_name" : "HISTORYSERVER",
+        "id" : 1
+      },
+      {
+        "name" : "mapreduce_history_server_process",
+        "label" : "History Server Process",
+        "service_name" : "MAPREDUCE2",
+        "component_name" : "HISTORYSERVER",
+        "id" : 2
+      },
+      {
+        "name" : "datanode_process",
+        "label" : "DateNode Process",
+        "service_name" : "HDFS",
+        "component_name" : "DATANODE",
+        "id" : 19
+      },
+      {
+        "name" : "mapreduce_history_server_rpc_latency",
+        "label" : "History Server RPC Latency",
+        "service_name" : "MAPREDUCE2",
+        "component_name" : "HISTORYSERVER",
+        "id" : 3
+      },
+      {
+        "name" : "mapreduce_history_server_cpu",
+        "label" : "History Server CPU Utilization",
+        "service_name" : "MAPREDUCE2",
+        "component_name" : "HISTORYSERVER",
+        "id" : 4
+      },
+      {
+        "name" : "falcon_server_webui",
+        "label" : "Falcon Server Web UI",
+        "service_name" : "FALCON",
+        "component_name" : "FALCON_SERVER",
+        "id" : 5
+      },
+      {
+        "name" : "falcon_server_process",
+        "label" : "Falcon Server Process",
+        "service_name" : "FALCON",
+        "component_name" : "FALCON_SERVER",
+        "id" : 6
+      },
+      {
+        "name" : "flume_agent_status",
+        "label" : "Flume Agent Status",
+        "service_name" : "FLUME",
+        "component_name" : "FLUME_HANDLER",
+        "id" : 7
+      },
+      {
+        "name" : "datanode_storage",
+        "label" : "DataNode Storage",
+        "service_name" : "HDFS",
+        "component_name" : "DATANODE",
+        "id" : 26
+      },
+      {
+        "name" : "datanode_webui",
+        "label" : "DataNode Web UI",
+        "service_name" : "HDFS",
+        "component_name" : "DATANODE",
+        "id" : 14
+      }
+    ],
+    "id" : 16,
+    "name" : "group lalala",
+    "targets" : []
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/assets/data/alerts/alertGroups.json
----------------------------------------------------------------------
diff --git a/ambari-web/app/assets/data/alerts/alertGroups.json b/ambari-web/app/assets/data/alerts/alertGroups.json
new file mode 100644
index 0000000..20e568d
--- /dev/null
+++ b/ambari-web/app/assets/data/alerts/alertGroups.json
@@ -0,0 +1,167 @@
+{
+  "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups?fields=*",
+  "items" : [
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/1",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 1,
+        "name" : "MAPREDUCE2"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/2",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 2,
+        "name" : "PIG"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/3",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 3,
+        "name" : "FALCON"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/4",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 4,
+        "name" : "FLUME"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/5",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 5,
+        "name" : "NAGIOS"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/6",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 6,
+        "name" : "HIVE"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/7",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 7,
+        "name" : "ZOOKEEPER"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/8",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 8,
+        "name" : "HDFS"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/9",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 9,
+        "name" : "OOZIE"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/10",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 10,
+        "name" : "SQOOP"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/11",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 11,
+        "name" : "YARN"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/12",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 12,
+        "name" : "TEZ"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/13",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 13,
+        "name" : "HBASE"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/14",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 14,
+        "name" : "GANGLIA"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/15",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : true,
+        "id" : 15,
+        "name" : "STORM"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/16",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : false,
+        "id" : 16,
+        "name" : "group lalala"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/17",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : false,
+        "id" : 17,
+        "name" : "group test"
+      }
+    },
+    {
+      "href" : "http://c6407.ambari.apache.org:8080/api/v1/clusters/c789/alert_groups/18",
+      "AlertGroup" : {
+        "cluster_name" : "c789",
+        "default" : false,
+        "id" : 18,
+        "name" : "group def empty"
+      }
+    }
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/config.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/config.js b/ambari-web/app/config.js
index 27ab995..e6f08f2 100644
--- a/ambari-web/app/config.js
+++ b/ambari-web/app/config.js
@@ -37,6 +37,7 @@ App.componentsUpdateInterval = 6000;
 App.contentUpdateInterval = 15000;
 App.alertDefinitionsUpdateInterval = 10000;
 App.alertInstancesUpdateInterval = 10000;
+App.alertGroupsUpdateInterval = 10000;
 App.maxRunsForAppBrowser = 500;
 App.pageReloadTime=3600000;
 App.singleNodeInstall = false;

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/controllers.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers.js b/ambari-web/app/controllers.js
index 1b8a96c..687c300 100644
--- a/ambari-web/app/controllers.js
+++ b/ambari-web/app/controllers.js
@@ -72,6 +72,7 @@ require('controllers/main/alert_definitions_controller');
 require('controllers/main/alerts/alert_definitions_actions_controller');
 require('controllers/main/alerts/definition_details_controller');
 require('controllers/main/alerts/alert_instances_controller');
+require('controllers/main/alerts/manage_alert_groups_controller');
 require('controllers/main/service');
 require('controllers/main/service/item');
 require('controllers/main/service/info/summary');

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/controllers/global/cluster_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/global/cluster_controller.js b/ambari-web/app/controllers/global/cluster_controller.js
index 319ee95..f72d0cd 100644
--- a/ambari-web/app/controllers/global/cluster_controller.js
+++ b/ambari-web/app/controllers/global/cluster_controller.js
@@ -350,6 +350,9 @@ App.ClusterController = Em.Controller.extend({
               self.updateLoadStatus('alertDefinitions');
             }
           });
+          updater.updateAlertGroups(function () {
+            self.updateLoadStatus('alertGroups');
+          });
         });
       });
       self.loadRootService().done(function (data) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/controllers/global/update_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/global/update_controller.js b/ambari-web/app/controllers/global/update_controller.js
index 1f1609f..00fc486 100644
--- a/ambari-web/app/controllers/global/update_controller.js
+++ b/ambari-web/app/controllers/global/update_controller.js
@@ -131,6 +131,7 @@ App.UpdateController = Em.Controller.extend({
       App.updater.run(this, 'updateAlertDefinitions', 'isWorking', App.alertDefinitionsUpdateInterval);
       if (App.get('supports.alerts')) {
         App.updater.run(this, 'updateAlertDefinitionSummary', 'isWorking', App.alertDefinitionsUpdateInterval);
+        App.updater.run(this, 'updateAlertGroups', 'isWorking', App.alertGroupsUpdateInterval);
       }
     }
   }.observes('isWorking'),
@@ -466,6 +467,16 @@ App.UpdateController = Em.Controller.extend({
     App.HttpClient.get(url, App.alertDefinitionSummaryMapper, {
       complete: callback
     });
+  },
+
+  updateAlertGroups: function (callback) {
+    var testUrl = '/data/alerts/alertGroups.json';
+    var realUrl = '/alert_groups?fields=*';
+    var url = this.getUrl(testUrl, realUrl);
+
+    App.HttpClient.get(url, App.alertGroupsMapper, {
+      complete: callback
+    });
   }
 
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/controllers/main/alerts/alert_definitions_actions_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/alerts/alert_definitions_actions_controller.js b/ambari-web/app/controllers/main/alerts/alert_definitions_actions_controller.js
index e6a5c33..e74e612 100644
--- a/ambari-web/app/controllers/main/alerts/alert_definitions_actions_controller.js
+++ b/ambari-web/app/controllers/main/alerts/alert_definitions_actions_controller.js
@@ -64,7 +64,90 @@ App.MainAlertDefinitionActionsController = Em.ArrayController.extend({
 
   createNewAlertDefinition: Em.K,
 
-  manageAlertGroups: Em.K,
+  /**
+   *  handler when clicking on "Manage Alert Groups", a popup will show up
+   */
+  manageAlertGroups: function () {
+    return App.ModalPopup.show({
+      header: Em.I18n.t('alerts.actions.manage_alert_groups_popup.header'),
+      bodyClass: App.MainAlertsManageAlertGroupView.extend({
+        controllerBinding: 'App.router.manageAlertGroupsController'
+      }),
+      classNames: ['sixty-percent-width-modal', 'manage-configuration-group-popup'],
+      primary: Em.I18n.t('common.save'),
+      onPrimary: function () {
+        var modifiedAlertGroups = this.get('subViewController.defsModifiedAlertGroups');
+        // Save modified Alert-groups
+        console.log("manageAlertGroups(): Saving modified Alert groups: ", modifiedAlertGroups);
+        var self = this;
+        var errors = [];
+        var deleteQueriesCounter = modifiedAlertGroups.toDelete.length;
+        var createQueriesCounter = modifiedAlertGroups.toCreate.length;
+        var deleteQueriesRun = false;
+        var createQueriesRun = false;
+        var runNextQuery = function () {
+          if (!deleteQueriesRun && deleteQueriesCounter > 0) {
+            deleteQueriesRun = true;
+            modifiedAlertGroups.toDelete.forEach(function (group) {
+              self.get('subViewController').removeAlertGroup(group, finishFunction, finishFunction);
+            }, this);
+          } else if (!createQueriesRun && deleteQueriesCounter < 1) {
+            createQueriesRun = true;
+            modifiedAlertGroups.toSet.forEach(function (group) {
+              self.get('subViewController').updateAlertGroup(group, finishFunction, finishFunction);
+            }, this);
+            modifiedAlertGroups.toCreate.forEach(function (group) {
+              self.get('subViewController').postNewAlertGroup(group, finishFunction);
+            }, this);
+          }
+        };
+        var finishFunction = function (xhr, text, errorThrown) {
+          if (xhr && errorThrown) {
+            var error = xhr.status + "(" + errorThrown + ") ";
+            try {
+              var json = $.parseJSON(xhr.responseText);
+              error += json.message;
+            } catch (err) {
+            }
+            console.error('Error updating Alert Group:', error);
+            errors.push(error);
+          }
+          if (createQueriesRun) {
+            createQueriesCounter--;
+          } else {
+            deleteQueriesCounter--;
+          }
+          if (deleteQueriesCounter + createQueriesCounter < 1) {
+            if (errors.length > 0) {
+              console.log(errors);
+              self.get('subViewController').set('errorMessage', errors.join(". "));
+            } else {
+              self.hide();
+            }
+          } else {
+            runNextQuery();
+          }
+        };
+        runNextQuery();
+      },
+      onSecondary: function () {
+        this.hide();
+      },
+      onClose: function () {
+        this.hide();
+      },
+      subViewController: function () {
+        return App.router.get('manageAlertGroupsController');
+      }.property('App.router.manageAlertGroupsController'),
+      updateButtons: function () {
+        var modified = this.get('subViewController.isDefsModified');
+        this.set('disablePrimary', !modified);
+      }.observes('subViewController.isDefsModified'),
+      secondary: Em.I18n.t('common.cancel'),
+      didInsertElement: Em.K
+    });
+  },
+
 
   manageNotifications: Em.K
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/controllers/main/alerts/manage_alert_groups_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/alerts/manage_alert_groups_controller.js b/ambari-web/app/controllers/main/alerts/manage_alert_groups_controller.js
new file mode 100644
index 0000000..7833b5c
--- /dev/null
+++ b/ambari-web/app/controllers/main/alerts/manage_alert_groups_controller.js
@@ -0,0 +1,619 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+var App = require('app');
+var validator = require('utils/validator');
+var numberUtils = require('utils/number_utils');
+
+App.ManageAlertGroupsController = Em.Controller.extend({
+  name: 'manageAlertGroupsController',
+
+  isLoaded: false,
+
+  alertGroups: [],
+
+  originalAlertGroups: [],
+
+  selectedAlertGroup: null,
+
+  selectedDefinitions: [],
+
+  alertDefinitions: [],
+
+  // load alert definitions for all services
+  loadAlertDefinitions: function () {
+    App.ajax.send({
+      name: 'alerts.load_all_alert_definitions',
+      sender: this,
+      success: 'onLoadAlertDefinitionsSuccess',
+      error: 'onLoadAlertDefinitionsError'
+    });
+  },
+
+  onLoadAlertDefinitionsSuccess: function (data) {
+    var alertDefinitions = [];
+    if (data && data.items) {
+      data.items.forEach( function(def) {
+        if (def && def.AlertDefinition) {
+          alertDefinitions.pushObject (Em.Object.create({
+            name: def.AlertDefinition.name,
+            serviceName: def.AlertDefinition.service_name,
+            serviceNameDisplay:function() {
+              return App.format.role(this.get('serviceName'));
+            }.property('serviceName'),
+            componentName: def.AlertDefinition.component_name,
+            componentNameDisplay: function() {
+              return App.format.role(this.get('componentName'));
+            }.property('componentName'),
+            label: def.AlertDefinition.label,
+            id: def.AlertDefinition.id
+          }));
+        }
+      });
+    }
+    this.set('alertDefinitions', alertDefinitions);
+  },
+
+  onLoadAlertDefinitionsError: function () {
+    console.error('Unable to load all alert definitions.');
+  },
+
+  loadAlertGroups: function () {
+    this.set('isLoaded', false);
+    this.set('alertGroups.length', 0);
+    this.set('originalAlertGroups.length', 0);
+    App.ajax.send({
+      name: 'alerts.load_alert_groups',
+      sender: this,
+      success: 'onLoadAlertGroupsSuccess',
+      error: 'onLoadAlertGroupsError'
+    });
+  },
+
+  onLoadAlertGroupsSuccess: function (data) {
+    var self = this;
+    if (data && data.items) {
+      this.set('alertGroupsCount', data.items.length);
+      data.items.forEach(function(alert_group) {
+        App.ajax.send({
+          name: 'alerts.load_an_alert_group',
+          sender: self,
+          data: {
+            "group_id": alert_group.AlertGroup.id
+          },
+          success: 'onLoadAlertGroupSuccess',
+          error: 'onLoadAlertGroupError'
+        });
+      }, this);
+    }
+  },
+
+  onLoadAlertGroupSuccess: function (data) {
+    var alertGroups = this.get('alertGroups');
+    if (data && data.AlertGroup) {
+      alertGroups.pushObject ( App.AlertGroupComplex.create({
+        name: data.AlertGroup.name,
+        default: data.AlertGroup.default,
+        id: data.AlertGroup.id,
+        definitions: data.AlertGroup.definitions,
+        targets: data.AlertGroup.targets
+      }));
+    }
+    if (this.get('alertGroupsCount') == alertGroups.length) {
+      this.set('isLoaded', true);
+      this.set('originalAlertGroups', this.copyAlertGroups(alertGroups));
+    }
+  },
+
+
+  onLoadAlertGroupsError: function () {
+    console.error('Unable to load all alert groups.');
+  },
+  onLoadAlertGroupError: function () {
+    console.error('Unable to load an alert group.');
+  },
+
+  resortAlertGroup: function() {
+    var alertGroups = Ember.copy(this.get('alertGroups'));
+    if(alertGroups.length < 2){
+      return;
+    }
+    var defaultGroups = alertGroups.filterProperty('default');
+    defaultGroups.forEach( function(defaultGroup) {
+      alertGroups.removeObject(defaultGroup);
+    });
+    var sorted = defaultGroups.concat(alertGroups.sortProperty('name'));
+   // var sorted = alertGroups.sortProperty('name');
+
+    this.removeObserver('alertGroups.@each.name', this, 'resortAlertGroup');
+    this.set('alertGroups', sorted);
+    this.addObserver('alertGroups.@each.name', this, 'resortAlertGroup');
+  }.observes('alertGroups.@each.name'),
+
+  /**
+   * remove definitions from group
+   */
+  deleteDefinitions: function () {
+    if (this.get('isDeleteDefinitionsDisabled')) {
+      return;
+    }
+    var groupDefinitions = this.get('selectedAlertGroup.definitions');
+    this.get('selectedDefinitions').slice().forEach(function (defObj) {
+      groupDefinitions.removeObject(defObj);
+    }, this);
+    this.set('selectedDefinitions', []);
+  },
+
+  isDeleteDefinitionsDisabled: function () {
+    var selectedGroup = this.get('selectedAlertGroup');
+    if (selectedGroup) {
+      return selectedGroup.default || this.get('selectedDefinitions').length === 0;
+    }
+    return true;
+  }.property('selectedAlertGroup', 'selectedAlertGroup.definitions.length', 'selectedDefinitions.length'),
+
+  /**
+   * add alert definitions to a group
+   */
+  addDefinitions: function () {
+    if (this.get('selectedAlertGroup.isAddDefinitionsDisabled')){
+      return false;
+    }
+    var availableDefinitions = this.get('selectedAlertGroup.availableDefinitions');
+    var popupDescription = {
+      header: Em.I18n.t('alerts.actions.manage_alert_groups_popup.selectDefsDialog.title'),
+      dialogMessage: Em.I18n.t('alerts.actions.manage_alert_groups_popup.selectDefsDialog.message').format(this.get('selectedAlertGroup.displayName'))
+    };
+    var validComponents = App.StackServiceComponent.find().map(function (component) {
+      return Em.Object.create({
+        componentName: component.get('componentName'),
+        displayName: App.format.role(component.get('componentName')),
+        selected: false
+      });
+    });
+    var validServices = App.Service.find().map(function (service) {
+      return Em.Object.create({
+        serviceName: service.get('serviceName'),
+        displayName: App.format.role(service.get('serviceName')),
+        selected: false
+      });
+    });
+    this.launchDefsSelectionDialog (availableDefinitions, [], validServices, validComponents, this.addDefinitionsCallback.bind(this), popupDescription);
+  },
+
+  /**
+   * lauch a table view of all availavle definitions to choose
+   */
+  launchDefsSelectionDialog : function(initialDefs, selectedDefs, validServices, validComponents, callback, popupDescription) {
+
+    App.ModalPopup.show({
+      classNames: [ 'sixty-percent-width-modal' ],
+      header: popupDescription.header,
+      dialogMessage: popupDescription.dialogMessage,
+      warningMessage: null,
+      availableDefs: [],
+      onPrimary: function () {
+        this.set('warningMessage', null);
+        var arrayOfSelectedDefs = this.get('availableDefs').filterProperty('selected', true);
+        if (arrayOfSelectedDefs.length < 1) {
+          this.set('warningMessage', Em.I18n.t('alerts.actions.manage_alert_groups_popup.selectDefsDialog.message.warning'));
+          return;
+        }
+        callback(arrayOfSelectedDefs);
+        console.debug('(new-selectedDefs)=', arrayOfSelectedDefs);
+        this.hide();
+      },
+      disablePrimary: function () {
+        return !this.get('isLoaded');
+      }.property('isLoaded'),
+      onSecondary: function () {
+        callback(null);
+        this.hide();
+      },
+      bodyClass: App.TableView.extend({
+        templateName: require('templates/main/alerts/add_definition_to_group_popup'),
+        controllerBinding: 'App.router.manageAlertGroupsController',
+        isPaginate: true,
+        filteredContent: function() {
+          return this.get('parentView.availableDefs').filterProperty('filtered') || [];
+        }.property('parentView.availableDefs.@each.filtered'),
+
+        showOnlySelectedDefs: false,
+        filterComponents: validComponents,
+        filterServices: validServices,
+        filterComponent: null,
+        filterService: null,
+        isDisabled: function () {
+          return !this.get('parentView.isLoaded');
+        }.property('parentView.isLoaded'),
+        didInsertElement: function(){
+          initialDefs.setEach('filtered', true);
+          this.set('parentView.availableDefs', initialDefs);
+          this.set('parentView.isLoaded', true);
+        },
+        filterDefs: function () {
+          var showOnlySelectedDefs = this.get('showOnlySelectedDefs');
+          var filterComponent = this.get('filterComponent');
+          var filterService = this.get('filterService');
+          this.get('parentView.availableDefs').forEach(function (defObj) {
+            var componentOnObj = true;
+            var serviceOnObj = true;
+            if (filterComponent) {
+              componentOnObj = (defObj.componentName == filterComponent.get('componentName'));
+            }
+            if (defObj.serviceName && filterService) {
+              serviceOnObj = (defObj.serviceName == filterService.get('serviceName'));
+            }
+            if (showOnlySelectedDefs) {
+              defObj.set('filtered', componentOnObj && serviceOnObj && defObj.get('selected'));
+            } else {
+              defObj.set('filtered', componentOnObj && serviceOnObj);
+            }
+          }, this);
+          this.set('startIndex', 1);
+        }.observes('parentView.availableDefs', 'filterService', 'filterService.serviceName', 'filterComponent', 'filterComponent.componentName', 'showOnlySelectedDefs'),
+        defSelectMessage: function () {
+          var defs = this.get('parentView.availableDefs');
+          var selectedDefs = defs.filterProperty('selected', true);
+          return this.t('alerts.actions.manage_alert_groups_popup.selectDefsDialog.selectedDefsLink').format(selectedDefs.get('length'), defs.get('length'));
+        }.property('parentView.availableDefs.@each.selected'),
+
+        selectFilterComponent: function (event) {
+          if (event != null && event.context != null && event.context.componentName != null) {
+            var currentFilter = this.get('filterComponent');
+            if (currentFilter != null) {
+              currentFilter.set('selected', false);
+            }
+            if (currentFilter != null && currentFilter.componentName === event.context.componentName) {
+              // selecting the same filter deselects it.
+              this.set('filterComponent', null);
+            } else {
+              this.set('filterComponent', event.context);
+              event.context.set('selected', true);
+            }
+          }
+        },
+        selectFilterService: function (event) {
+          if (event != null && event.context != null && event.context.serviceName != null) {
+            var currentFilter = this.get('filterService');
+            if (currentFilter != null) {
+              currentFilter.set('selected', false);
+            }
+            if (currentFilter != null && currentFilter.serviceName === event.context.serviceName) {
+              // selecting the same filter deselects it.
+              this.set('filterService', null);
+            } else {
+              this.set('filterService', event.context);
+              event.context.set('selected', true);
+            }
+          }
+        },
+        allDefsSelected: false,
+        toggleSelectAllDefs: function (event) {
+          this.get('parentView.availableDefs').filterProperty('filtered').setEach('selected', this.get('allDefsSelected'));
+        }.observes('allDefsSelected'),
+        toggleShowSelectedDefs: function () {
+          var filter1 = this.get('filterComponent');
+          if (filter1 != null) {
+            filter1.set('selected', false);
+          }
+          var filter2 = this.get('filterService');
+          if (filter2 != null) {
+            filter2.set('selected', false);
+          }
+          this.set('filterComponent', null);
+          this.set('filterService', null);
+          this.set('showOnlySelectedDefs', !this.get('showOnlySelectedDefs'));
+        }
+      })
+    });
+  },
+
+  /**
+   * add alert definitions callback
+   */
+  addDefinitionsCallback: function (selectedDefs) {
+    var group = this.get('selectedAlertGroup');
+    if (selectedDefs) {
+      var alertGroupDefs = group.get('definitions');
+      selectedDefs.forEach(function (defObj) {
+        alertGroupDefs.pushObject(defObj);
+      }, this);
+    }
+  },
+
+  /**
+   * observes if any group changed including: group name, newly created group, deleted group, group with definitions changed
+   */
+  defsModifiedAlertGroups: function () {
+    if (!this.get('isLoaded')) {
+      return false;
+    }
+    var groupsToDelete = [];
+    var groupsToSet = [];
+    var groupsToCreate = [];
+    var groups = this.get('alertGroups'); //current alert groups
+    var originalGroups = this.get('originalAlertGroups'); // original alert groups
+    // remove default group
+    originalGroups = originalGroups.filterProperty('default', false);
+    var originalGroupsIds = originalGroups.mapProperty('id');
+    groups.forEach(function (group) {
+      if (!group.get('default')) {
+        var originalGroup = originalGroups.findProperty('id', group.get('id'));
+        if (originalGroup) {
+          if (!(JSON.stringify(group.get('definitions').slice().sort()) === JSON.stringify(originalGroup.get('definitions').slice().sort()))
+           || !(JSON.stringify(group.get('targets').slice().sort()) === JSON.stringify(originalGroup.get('targets').slice().sort()))) {
+            groupsToSet.push(group.set('id', originalGroup.get('id')));
+          } else if (group.get('name') !== originalGroup.get('name') ) {
+            // should update name
+            groupsToSet.push(group.set('id', originalGroup.get('id')));
+          }
+          originalGroupsIds = originalGroupsIds.without(group.get('id'));
+        } else {
+          groupsToCreate.push(group);
+        }
+      }
+    });
+    originalGroupsIds.forEach(function (id) {
+      groupsToDelete.push(originalGroups.findProperty('id', id));
+    }, this);
+    return {
+      toDelete: groupsToDelete,
+      toSet: groupsToSet,
+      toCreate: groupsToCreate
+    };
+  }.property('selectedAlertGroup.definitions.@each', 'selectedAlertGroup.definitions.length', 'alertGroups', 'isLoaded'),
+
+  isDefsModified: function () {
+    var modifiedGroups = this.get('defsModifiedAlertGroups');
+    if (!this.get('isLoaded')) {
+      return false;
+    }
+    return !!(modifiedGroups.toSet.length || modifiedGroups.toCreate.length || modifiedGroups.toDelete.length);
+  }.property('defsModifiedAlertGroups'),
+
+  /**
+   * copy alert groups for backup, to compare with current alert groups, so will know if some groups changed/added/deleted
+   * @param originGroups
+   * @return {Array}
+   */
+  copyAlertGroups: function (originGroups) {
+    var alertGroups = [];
+    originGroups.forEach(function (alertGroup) {
+      var copiedGroup =  App.AlertGroupComplex.create($.extend(true, {}, alertGroup));
+      alertGroups.pushObject(copiedGroup);
+    });
+    return alertGroups;
+  },
+
+  /**
+   * ==============on API side: following are four functions to an alert group: create, delete, update definitions/targets/label of a group ===========
+   */
+  /**
+   * Create a new alert group
+   * @param newAlertGroupData
+   * @param callback    Callback function for Success or Error handling
+   * @return  Returns the created alert group
+   */
+  postNewAlertGroup: function (newAlertGroupData, callback) {
+    // create a new group with name , definition and target
+    var data = {
+      'name': newAlertGroupData.get('name')
+    };
+    if (newAlertGroupData.get('definitions').length > 0) {
+      data.definitions = newAlertGroupData.get('definitions').mapProperty('id');
+    }
+    if (newAlertGroupData.get('targets').length > 0) {
+      data.targets = newAlertGroupData.get('targets').mapProperty('id');
+    }
+    var sendData = {
+      name: 'alert_groups.create',
+      data: data,
+      success: 'successFunction',
+      error: 'errorFunction',
+      successFunction: function () {
+        if (callback) {
+          callback();
+        }
+      },
+      errorFunction: function (xhr, text, errorThrown) {
+        if (callback) {
+          callback(xhr, text, errorThrown);
+        }
+        console.error('Error in creating new Alert Group');
+      }
+    };
+    sendData.sender = sendData;
+    App.ajax.send(sendData);
+    return newAlertGroupData;
+  },
+
+  /**
+   * PUTs the new alert group information on the server.
+   * Changes possible here are the name, definitions, targets
+   *
+   * @param {App.ConfigGroup} alertGroup
+   * @param {Function} successCallback
+   * @param {Function} errorCallback
+   */
+  updateAlertGroup: function (alertGroup, successCallback, errorCallback) {
+    var sendData = {
+      name: 'alert_groups.update',
+      data: {
+        "group_id": alertGroup.id,
+        'name': alertGroup.get('name'),
+        'definitions': alertGroup.get('definitions').mapProperty('id'),
+        'targets': alertGroup.get('targets').mapProperty('id')
+      },
+      success: 'successFunction',
+      error: 'errorFunction',
+      successFunction: function () {
+        if (successCallback) {
+          successCallback();
+        }
+      },
+      errorFunction: function (xhr, text, errorThrown) {
+        if (errorCallback) {
+          errorCallback(xhr, text, errorThrown);
+        }
+      }
+    };
+    sendData.sender = sendData;
+    App.ajax.send(sendData);
+  },
+
+  removeAlertGroup: function (alertGroup, successCallback, errorCallback) {
+    var sendData = {
+      name: 'alert_groups.delete',
+      data: {
+        "group_id": alertGroup.id
+      },
+      success: 'successFunction',
+      error: 'errorFunction',
+      successFunction: function () {
+        if (successCallback) {
+          successCallback();
+        }
+      },
+      errorFunction: function (xhr, text, errorThrown) {
+        if (errorCallback) {
+          errorCallback(xhr, text, errorThrown);
+        }
+      }
+    };
+    sendData.sender = sendData;
+    App.ajax.send(sendData);
+  },
+
+  /**
+   *  =============on UI side: following are four operations to an alert group: add, remove, rename and duplicate==================
+   */
+
+  /**
+   * confirm delete alert group
+   */
+  confirmDelete : function () {
+    var self = this;
+    App.showConfirmationPopup(function() {
+      self.deleteAlertGroup();
+    });
+  },
+
+  /**
+   * delete selected alert group
+   */
+  deleteAlertGroup: function () {
+    var selectedAlertGroup = this.get('selectedAlertGroup');
+    if (this.get('isDeleteAlertDisabled')) {
+      return;
+    }
+    this.get('alertGroups').removeObject(selectedAlertGroup);
+    this.set('selectedAlertGroup', this.get('alertGroups')[0]);
+  },
+
+  /**
+   * rename non-default alert group
+   */
+  renameAlertGroup: function () {
+    if(this.get('selectedAlertGroup.default')) {
+      return;
+    }
+    var self = this;
+    this.renameGroupPopup = App.ModalPopup.show({
+      header: Em.I18n.t('alerts.actions.manage_alert_groups_popup.renameButton'),
+      bodyClass: Ember.View.extend({
+        templateName: require('templates/main/alerts/create_new_alert_group')
+      }),
+      alertGroupName: self.get('selectedAlertGroup.name'),
+      warningMessage: null,
+      validate: function () {
+        var warningMessage = '';
+        var originalGroup = self.get('selectedAlertGroup');
+        var groupName = this.get('alertGroupName').trim();
+
+        if (originalGroup.get('name').trim() === groupName) {
+          warningMessage = Em.I18n.t("alerts.actions.manage_alert_groups_popup.addGroup.exist");
+        } else {
+          if (self.get('alertGroups').mapProperty('displayName').contains(groupName)) {
+            warningMessage = Em.I18n.t("alerts.actions.manage_alert_groups_popup.addGroup.exist");
+          }
+          else if (groupName && !validator.isValidAlertGroupName(groupName)) {
+            warningMessage = Em.I18n.t("form.validator.alertGroupName");
+          }
+        }
+        this.set('warningMessage', warningMessage);
+      }.observes('alertGroupName'),
+      disablePrimary: function () {
+        return !(this.get('alertGroupName').trim().length > 0 && (this.get('warningMessage') !== null && !this.get('warningMessage')));
+      }.property('warningMessage', 'alertGroupName'),
+      onPrimary: function () {
+        self.set('selectedAlertGroup.name', this.get('alertGroupName'));
+        this.hide();
+      }
+    });
+  },
+
+  /**
+   * create new alert group
+   */
+  addAlertGroup: function (duplicated) {
+    duplicated = (duplicated === true);
+    var self = this;
+    this.addGroupPopup = App.ModalPopup.show({
+      header: Em.I18n.t('alerts.actions.manage_alert_groups_popup.addButton'),
+      bodyClass: Ember.View.extend({
+        templateName: require('templates/main/alerts/create_new_alert_group')
+      }),
+      alertGroupName: duplicated ? self.get('selectedAlertGroup.name') + ' Copy' : "",
+      warningMessage: '',
+      didInsertElement: function(){
+        this.validate();
+        this.$('input').focus();
+      },
+      validate: function () {
+        var warningMessage = '';
+        var groupName = this.get('alertGroupName').trim();
+        if (self.get('alertGroups').mapProperty('displayName').contains(groupName)) {
+          warningMessage = Em.I18n.t("alerts.actions.manage_alert_groups_popup.addGroup.exist");
+        }
+        else if (groupName && !validator.isValidAlertGroupName(groupName)) {
+          warningMessage = Em.I18n.t("form.validator.alertGroupName");
+        }
+        this.set('warningMessage', warningMessage);
+      }.observes('alertGroupName'),
+      disablePrimary: function () {
+        return !(this.get('alertGroupName').trim().length > 0 && !this.get('warningMessage'));
+      }.property('warningMessage', 'alertGroupName'),
+      onPrimary: function () {
+        var newAlertGroup = App.AlertGroupComplex.create({
+          name: this.get('alertGroupName').trim(),
+          definitions: [],
+          targets: []
+        })    ;
+        self.get('alertGroups').pushObject(newAlertGroup);
+        this.hide();
+      }
+    });
+  },
+
+  duplicateAlertGroup: function() {
+    this.addAlertGroup(true);
+  }
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/mappers.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers.js b/ambari-web/app/mappers.js
index 9aec70f..daa2cf6 100644
--- a/ambari-web/app/mappers.js
+++ b/ambari-web/app/mappers.js
@@ -37,4 +37,5 @@ require('mappers/service_config_version_mapper');
 require('mappers/alert_definitions_mapper');
 require('mappers/alert_definition_summary_mapper');
 require('mappers/alert_instances_mapper');
+require('mappers/alert_groups_mapper');
 require('mappers/root_service_mapper');

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/mappers/alert_groups_mapper.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers/alert_groups_mapper.js b/ambari-web/app/mappers/alert_groups_mapper.js
new file mode 100644
index 0000000..255039c
--- /dev/null
+++ b/ambari-web/app/mappers/alert_groups_mapper.js
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+var App = require('app');
+
+var stringUtils = require('utils/string_utils');
+
+App.alertGroupsMapper = App.QuickDataMapper.create({
+
+  model: App.AlertGroup,
+
+  config: {
+    id: 'AlertGroup.id',
+    name: 'AlertGroup.name',
+    default: 'AlertGroup.default',
+    definitions: 'AlertGroup.definitions',
+    targets: 'AlertGroup.targets'
+  },
+
+  map: function (json) {
+    if (json && json.items) {
+      var alertGroups = [];
+      var self = this;
+      json.items.forEach (function(item) {
+       alertGroups.push(self.parseIt(item, self.get('config')));
+      }, this);
+      App.store.loadMany(this.get('model'), alertGroups);
+    }
+  }
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index e8b97d0..76dc25f 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -234,6 +234,7 @@ Em.I18n.translations = {
   'common.configs': "Configs",
   'common.unknown': "Unknown",
   'common.install': "Install",
+  'common.alertDefinition': "Alert Definition",
 
   'passiveState.turnOn':'Turn On Maintenance Mode',
   'passiveState.turnOff':'Turn Off Maintenance Mode',
@@ -831,6 +832,7 @@ Em.I18n.translations = {
   'form.validator.invalidIp':'Please enter valid ip address',
   'form.validator.configKey':'Invalid Key. Only alphanumerics, hyphens, underscores, asterisks and periods are allowed.',
   'form.validator.configGroupName':'Invalid Group Name. Only alphanumerics, hyphens, spaces and underscores are allowed.',
+  'form.validator.alertGroupName':'Invalid Alert Group Name. Only alphanumerics, hyphens, spaces and underscores are allowed.',
   'form.validator.configKey.specific':'"{0}" is invalid Key. Only alphanumerics, hyphens, underscores, asterisks and periods are allowed.',
 
   'alerts.actions.create': 'Create Alert',
@@ -1690,6 +1692,29 @@ Em.I18n.translations = {
 
   'services.hbase.master.error':'None of the HBase masters is active',
 
+  'alerts.actions.manage_alert_groups_popup.header':'Manage Alert Groups',
+  'alerts.actions.manage_alert_groups_popup.notice':'You can manage alert groups for each service in this dialog. View the list of alert groups and the alert definitions configured in them. ' +
+    'You can also add/remove alert definitions, and pick notification for that alert group.',
+  'alerts.actions.manage_alert_groups_popup.rename':'Rename',
+  'alerts.actions.manage_alert_groups_popup.duplicate':'Duplicate',
+  'alerts.actions.manage_alert_groups_popup.group_name_lable':'Name',
+  'alerts.actions.manage_alert_groups_popup.group_desc_lable':'Description',
+  'alerts.actions.manage_alert_groups_popup.notifications':'Notifications',
+  'alerts.actions.manage_alert_groups_popup.addButton':'Create new Alert Group',
+  'alerts.actions.manage_alert_groups_popup.addGroup.exist': 'Alert Group with given name already exists',
+  'alerts.actions.manage_alert_groups_popup.removeButton':'Delete Alert Group',
+  'alerts.actions.manage_alert_groups_popup.renameButton':'Rename Alert Group',
+  'alerts.actions.manage_alert_groups_popup.duplicateButton':'Duplicate Alert Group',
+  'alerts.actions.manage_alert_groups_popup.addDefinition':'Add alert definitions to selected Alert Group',
+  'alerts.actions.manage_alert_groups_popup.addDefinitionDisabled':'There are no available alert definitions to add',
+  'alerts.actions.manage_alert_groups_popup.addDefinitionToDefault': 'You cannot add alert definition to a default group',
+  'alerts.actions.manage_alert_groups_popup.removeDefinition':'Remove alert definitions from selected Alert Group',
+  'alerts.actions.manage_alert_groups_popup.selectDefsDialog.title': 'Select Alert Group\'s Alert Definitions',
+  'alerts.actions.manage_alert_groups_popup.selectDefsDialog.message': 'Select alert definitions to be added to this "{0}" Alert Group.',
+  'alerts.actions.manage_alert_groups_popup.selectDefsDialog.filter.placeHolder': 'All',
+  'alerts.actions.manage_alert_groups_popup.selectDefsDialog.selectedDefsLink': '{0} out of {1} alert definitions selected',
+  'alerts.actions.manage_alert_groups_popup.selectDefsDialog.message.warning': 'At least one alert definition needs to be selected.',
+
   'hosts.host.add':'Add New Hosts',
   'hosts.table.noHosts':'No hosts to display',
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/models.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models.js b/ambari-web/app/models.js
index 1b9e9be..0552ef2 100644
--- a/ambari-web/app/models.js
+++ b/ambari-web/app/models.js
@@ -47,6 +47,7 @@ require('models/alert');
 require('models/alert_definition');
 require('models/alert_instance');
 require('models/alert_notification');
+require('models/alert_group');
 require('models/user');
 require('models/host');
 require('models/rack');

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/models/alert_group.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/alert_group.js b/ambari-web/app/models/alert_group.js
new file mode 100644
index 0000000..faa46e8
--- /dev/null
+++ b/ambari-web/app/models/alert_group.js
@@ -0,0 +1,107 @@
+/**
+ * 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');
+
+/**
+ * Represents an alert-group on the cluster.
+ * A alert group is a collection of alert definitions
+ *
+ * Alert group hierarchy is at 2 levels. For
+ * each service there is a 'Default' alert group
+ * containing all definitions , this group is read-only
+ *
+ * User can create new alert group containing alert definitions from
+ * any service.
+ */
+App.AlertGroup = DS.Model.extend({
+  id: null,
+  name: null,
+  description: null,
+  default: null,
+  definitions: [],
+  targets: [],
+
+  displayName: function () {
+    var name = this.get('name');
+    if (name && name.length > App.config.CONFIG_GROUP_NAME_MAX_LENGTH) {
+      var middle = Math.floor(App.config.CONFIG_GROUP_NAME_MAX_LENGTH / 2);
+      name = name.substring(0, middle) + "..." + name.substring(name.length-middle);
+    }
+    return this.get('default') ? (name + ' Default') : name;
+  }.property('name', 'default'),
+
+  displayNameDefinitions: function () {
+    return this.get('displayName') + ' (' + this.get('definitions.length') + ')';
+  }.property('displayName', 'definitions.length')
+});
+App.AlertGroup.FIXTURES = [];
+
+App.AlertGroupComplex = Ember.Object.extend({
+  id: null,
+  name: null,
+  description: null,
+  default: null,
+  definitions: [],
+  targets: [],
+
+  /**
+   * all alert definitions that belong to all services
+   */
+  alertDefinitionsBinding: 'App.router.manageAlertGroupsController.alertDefinitions',
+
+  displayName: function () {
+    var name = this.get('name');
+    if (name && name.length > App.config.CONFIG_GROUP_NAME_MAX_LENGTH) {
+      var middle = Math.floor(App.config.CONFIG_GROUP_NAME_MAX_LENGTH / 2);
+      name = name.substring(0, middle) + "..." + name.substring(name.length-middle);
+    }
+    return this.get('default') ? (name + ' Default') : name;
+  }.property('name', 'default'),
+
+  displayNameDefinitions: function () {
+    return this.get('displayName') + ' (' + this.get('definitions.length') + ')';
+  }.property('displayName', 'definitions.length'),
+
+  /**
+   * Provides alert definitions which are available for inclusion in
+   * non-default alert groups.
+   */
+  availableDefinitions: function () {
+    if (this.get('default')) return [];
+    var usedDefinitionsMap = {};
+    var availableDefinitions = [];
+    var sharedDefinitions = this.get('alertDefinitions');
+
+    this.get('definitions').forEach(function (def) {
+      usedDefinitionsMap[def.name] = true;
+    });
+    sharedDefinitions.forEach(function (shared_def) {
+      if (!usedDefinitionsMap[shared_def.get('name')]) {
+        availableDefinitions.pushObject(shared_def);
+      }
+    });
+    return availableDefinitions;
+  }.property('alertDefinitions', 'definitions.@each', 'definitions.length'),
+
+  isAddDefinitionsDisabled: function () {
+    return (this.get('default') || this.get('availableDefinitions.length') === 0);
+  }.property('availableDefinitions.length')
+});
+
+

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/templates/main/alerts/add_definition_to_group_popup.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/alerts/add_definition_to_group_popup.hbs b/ambari-web/app/templates/main/alerts/add_definition_to_group_popup.hbs
new file mode 100644
index 0000000..a6a28db
--- /dev/null
+++ b/ambari-web/app/templates/main/alerts/add_definition_to_group_popup.hbs
@@ -0,0 +1,139 @@
+{{!
+* 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.
+}}
+
+
+<form class="form-horizontal mbm" autocomplete="off">
+  <div class="override-controls">
+    <div class="alert alert-info">{{view.parentView.dialogMessage}}</div>
+  {{#if view.parentView.warningMessage}}
+    <div class="text-warning">
+      {{view.parentView.warningMessage}}
+    </div>
+  {{/if}}
+    <table style="width: 100%;">
+      <tr>
+        <td>
+            <a href="#" {{action toggleShowSelectedDefs target="view" }}>{{view.defSelectMessage}}</a>
+          {{#if view.showOnlySelectedDefs}}
+             <i class='icon-ok-sign'></i>
+          {{/if}}
+        </td>
+        <td width="20%">
+          <div class="row">
+            <div class="span2" id="filter-dropdown-div">
+              <!-- services drop-down -->
+              <div class="btn-group">
+                <button class="btn dropdown-toggle" data-toggle="dropdown" href="#" {{bindAttr disabled="view.isDisabled"}}>
+                  {{t common.service}}
+                    <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu">
+                    <!-- dropdown menu links -->
+                  {{#each service in view.filterServices}}
+                    <li>
+                      <a href="#" {{action selectFilterService service target="view"}}>
+                        {{#if service.selected}}
+                            <i class='icon-ok-sign'></i>
+                        {{else}}
+                            <i class='icon-placeholder'></i>
+                        {{/if}}
+                        {{service.displayName}}
+                      </a>
+                    </li>
+                  {{/each}}
+                </ul>
+              </div>
+            </div>
+            <div class="span2" id="component-dropdown-div">
+              <!-- definition-components drop-down -->
+              <div class="btn-group">
+                <button class="btn dropdown-toggle" data-toggle="dropdown" href="#" {{bindAttr disabled="view.isDisabled"}}>
+                  {{t common.component}}
+                    <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu">
+                    <!-- dropdown menu links -->
+                  {{#each component in view.filterComponents}}
+                    <li>
+                      <a href="#" {{action selectFilterComponent component target="view"}}>
+                        {{#if component.selected}}
+                            <i class='icon-ok-sign'></i>
+                        {{else}}
+                            <i class='icon-placeholder'></i>
+                        {{/if}}
+                        {{component.displayName}}
+                      </a>
+                    </li>
+                  {{/each}}
+                </ul>
+              </div>
+            </div>
+          </div>
+        </td>
+      </tr>
+    </table>
+    <table class="table table-striped hosts-table">
+      <thead>
+        <tr class="success">
+          <th width="10%">
+            {{view Ember.Checkbox checkedBinding="view.allDefsSelected" disabledBinding="view.isDisabled"}}
+          </th>
+          <th width="35%">{{t common.alertDefinition}}</th>
+          <th width="25%">{{t common.service}}</th>
+          <th width="30%">{{t common.component}}</th>
+        </tr>
+    </thead>
+    </table>
+    <div class="hosts-table-container">
+      <table class="table table-striped hosts-table">
+        {{#each entry in view.pageContent}}
+          <tr {{bindAttr class="entry.filtered::hidden"}}>
+            <td width="10%">
+              {{view Ember.Checkbox checkedBinding="entry.selected"}}
+            </td>
+            <td width="35%">
+              {{entry.label}}
+            </td>
+            <td>
+              {{entry.serviceNameDisplay}}
+            </td>
+            <td>
+              {{entry.componentNameDisplay}}
+            </td>
+          </tr>
+        {{/each}}
+      </table>
+    </div>
+</div>
+</form>
+
+{{#if view.isPaginate}}
+  <div class="page-bar pull-right no-borders">
+    <div class="items-on-page">
+        <label>{{t common.show}}: {{view view.rowsPerPageSelectView selectionBinding="view.displayLength"}}</label>
+    </div>
+    <div class="info">{{view.paginationInfo}}</div>
+    <div class="paging_two_button">
+      {{view view.paginationFirst}}
+      {{view view.paginationLeft}}
+      {{view view.paginationRight}}
+      {{view view.paginationLast}}
+    </div>
+  </div>
+{{/if}}
+

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/templates/main/alerts/create_new_alert_group.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/alerts/create_new_alert_group.hbs b/ambari-web/app/templates/main/alerts/create_new_alert_group.hbs
new file mode 100644
index 0000000..07dce96
--- /dev/null
+++ b/ambari-web/app/templates/main/alerts/create_new_alert_group.hbs
@@ -0,0 +1,33 @@
+{{!
+* 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="new-config-group-div">
+    <div class="text-warning">
+      {{#if view.parentView.warningMessage}}
+        {{view.parentView.warningMessage}}
+      {{else}}
+          &nbsp;
+      {{/if}}
+    </div>
+    <table>
+        <tr>
+            <td>{{t services.service.config_groups_popup.group_name_lable }}:</td>
+            <td>{{view Ember.TextField maxlength="255" valueBinding="alertGroupName"}}</td>
+        </tr>
+    </table>
+</div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/templates/main/alerts/manage_alert_groups_popup.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/alerts/manage_alert_groups_popup.hbs b/ambari-web/app/templates/main/alerts/manage_alert_groups_popup.hbs
new file mode 100644
index 0000000..3e47b3c
--- /dev/null
+++ b/ambari-web/app/templates/main/alerts/manage_alert_groups_popup.hbs
@@ -0,0 +1,94 @@
+{{!
+* 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="alert alert-info margin-bottom-5">{{t alerts.actions.manage_alert_groups_popup.notice}}</div>
+{{#if controller.isLoaded}}
+  <div class="row-fluid  manage-configuration-group-content">
+    <div class="span12">
+      <div class="row-fluid">
+        <div class="span4">
+          <span>&nbsp;</span>
+          {{view Em.Select
+          contentBinding="alertGroups"
+          optionLabelPath="content.displayNameDefinitions"
+          selectionBinding="view.selectedAlertGroup"
+          multiple="multiple"
+          class="group-select"
+          }}
+          <div class="btn-toolbar pull-right">
+            <button rel="button-info" class="btn" {{bindAttr data-original-title="view.addButtonTooltip"}}
+              {{action addAlertGroup target="controller"}}><i class="icon-plus"></i></button>
+            <button rel="button-info" class="btn" {{bindAttr data-original-title="view.removeButtonTooltip" disabled="view.isRemoveButtonDisabled"}}
+              {{action confirmDelete target="controller"}}><i class="icon-minus"></i></button>
+            <div class="btn-group">
+              <button class="btn dropdown-toggle" data-toggle="dropdown">
+                <i class="icon-cog"></i>&nbsp;<span class="caret"></span>
+              </button>
+              <ul class="dropdown-menu">
+                <li {{bindAttr class="view.isRenameButtonDisabled:disabled"}}>
+                  <a href="" rel="button-info-dropdown" {{bindAttr data-original-title="view.renameButtonTooltip"}} {{action renameAlertGroup target="controller"}}>{{t alerts.actions.manage_alert_groups_popup.rename}}</a>
+                </li>
+                <li {{bindAttr class="view.isDuplicateButtonDisabled:disabled"}}>
+                  <a href="" rel="button-info-dropdown" {{bindAttr data-original-title="view.duplicateButtonTooltip"}} {{action duplicateAlertGroup target="controller"}}>{{t alerts.actions.manage_alert_groups_popup.duplicate}}</a>
+                </li>
+              </ul>
+            </div>
+          </div>
+        </div>
+          <div class="span8">
+            <span>&nbsp;</span>
+            <div class="row-fluid">
+              <div class="span12 pull-right">
+                {{view Em.Select
+                contentBinding="selectedAlertGroup.definitions"
+                optionLabelPath="content.label"
+                multiple="multiple"
+                class="group-select"
+                selectionBinding="selectedDefinitions"
+                }}
+              </div>
+              <div class="button-group pull-right">
+                  <a rel="button-info" {{bindAttr data-original-title="view.addDefinitionTooltip" class=":btn selectedAlertGroup.isAddDefinitionsDisabled:disabled"}} {{action addDefinitions target="controller"}} ><i class="icon-plus"></i></a>
+                  <a rel="button-info" {{bindAttr data-original-title="view.removeDefinitionTooltip" class=":btn isDeleteDefinitionsDisabled:disabled"}} {{action deleteDefinitions target="controller"}} ><i class="icon-minus"></i></a>
+              </div>
+            </div>
+            <div class="row-fluid">
+              <div class="span2">{{t alerts.actions.manage_alert_groups_popup.notifications}}</div>
+              <div class="span10">
+                {{#each target in selectedAlertGroup.targets}}
+                  {{target.name}}&nbsp;&nbsp;
+                {{/each}}
+              </div>
+            </div>
+
+          </div>
+          <div class="clearfix"></div>
+          <div class="row-fluid">
+            <div class="span12 text-error" id="manage-config-group-error-div">
+              {{#if view.errorMessage}}
+                {{errorMessage}}
+              {{else}}
+                 &nbsp;
+              {{/if}}
+            </div>
+          </div>
+      </div>
+    </div>
+  </div>
+{{else}}
+  <div class="spinner"></div>
+{{/if}}

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/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 bcaecd3..0f67b9b 100644
--- a/ambari-web/app/utils/ajax/ajax.js
+++ b/ambari-web/app/utils/ajax/ajax.js
@@ -305,7 +305,60 @@ var urls = {
   },
   'alerts.load_alert_notification': {
     'real': '/alert_targets?fields=*',
-    'mock': 'data/alerts/alert_notifications.json'
+    'mock': 'data/alerts/alertNotifications.json'
+  },
+  'alerts.load_alert_groups': {
+    'real': '/clusters/{clusterName}/alert_groups?fields=*',
+    'mock': 'data/alerts/alertGroups.json'
+  },
+  'alerts.load_an_alert_group': {
+    'real': '/clusters/{clusterName}/alert_groups/{group_id}',
+    'mock': 'data/alerts/alertGroup.json'
+  },
+  'alert_groups.create': {
+    'real': '/clusters/{clusterName}/alert_groups',
+    'mock': '',
+    'format': function (data) {
+      return {
+        type: 'POST',
+        data: JSON.stringify({
+          "AlertGroup": {
+            "name": data.name,
+            "definitions": data.definitions,
+            "targets": data.targets
+          }
+        })
+      };
+    }
+  },
+  'alert_groups.update': {
+    'real': '/clusters/{clusterName}/alert_groups/{group_id}',
+    'mock': '',
+    'format': function (data) {
+      return {
+        type: 'PUT',
+        data: JSON.stringify({
+          "AlertGroup": {
+            "name": data.name,
+            "definitions": data.definitions,
+            "targets": data.targets
+          }
+        })
+      };
+    }
+  },
+  'alert_groups.delete': {
+    'real': '/clusters/{clusterName}/alert_groups/{group_id}',
+    'mock': '',
+    'format': function (data) {
+      return {
+        type: 'DELETE'
+      };
+    }
+  },
+  'alerts.load_all_alert_definitions': {
+    'real': '/clusters/{clusterName}/alert_definitions?fields=*',
+    'mock': 'data/alerts/alertDefinitions.json'
   },
   'alerts.instances': {
     'real': '/clusters/{clusterName}/alerts?fields=*',

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/utils/validator.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/validator.js b/ambari-web/app/utils/validator.js
index fb51d4b..3fcdfb5 100644
--- a/ambari-web/app/utils/validator.js
+++ b/ambari-web/app/utils/validator.js
@@ -156,6 +156,16 @@ module.exports = {
     return configKeyRegex.test(value);
   },
 
+  /**
+   * validate alert group name
+   * @param value
+   * @return {Boolean}
+   */
+  isValidAlertGroupName: function(value) {
+    var configKeyRegex = /^[\s0-9a-z_\-]+$/i;
+    return configKeyRegex.test(value);
+  },
+
   empty:function (e) {
     switch (e) {
       case "":

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/views.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js
index 0009cfa..16bea99 100644
--- a/ambari-web/app/views.js
+++ b/ambari-web/app/views.js
@@ -44,6 +44,8 @@ require('views/main/menu');
 require('views/main/alert_definitions_view');
 require('views/main/alerts/definition_details_view');
 require('views/main/alerts/alert_definitions_actions_view');
+require('views/main/alerts');
+require('views/main/alerts/manage_alert_groups_view');
 require('views/main/charts');
 require('views/main/views/details');
 require('views/main/host');

http://git-wip-us.apache.org/repos/asf/ambari/blob/43704d45/ambari-web/app/views/main/alerts/manage_alert_groups_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/alerts/manage_alert_groups_view.js b/ambari-web/app/views/main/alerts/manage_alert_groups_view.js
new file mode 100644
index 0000000..61448f8
--- /dev/null
+++ b/ambari-web/app/views/main/alerts/manage_alert_groups_view.js
@@ -0,0 +1,105 @@
+/**
+ * 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');
+
+App.MainAlertsManageAlertGroupView = Em.View.extend({
+
+  templateName: require('templates/main/alerts/manage_alert_groups_popup'),
+
+  selectedAlertGroup: null,
+
+  isRemoveButtonDisabled: true,
+
+  isRenameButtonDisabled: true,
+
+  isDuplicateButtonDisabled: true,
+
+  buttonObserver: function () {
+    var selectedAlertGroup = this.get('controller.selectedAlertGroup');
+    if(selectedAlertGroup && selectedAlertGroup.default){
+      this.set('isRemoveButtonDisabled', true);
+      this.set('isRenameButtonDisabled', true);
+      this.set('isDuplicateButtonDisabled', false);
+    }else{
+      this.set('isRemoveButtonDisabled', false);
+      this.set('isRenameButtonDisabled', false);
+      this.set('isDuplicateButtonDisabled', false);
+    }
+  }.observes('controller.selectedAlertGroup'),
+
+  onGroupSelect: function () {
+    var selectedAlertGroup = this.get('selectedAlertGroup');
+    // to unable user select more than one alert group at a time
+    if (selectedAlertGroup && selectedAlertGroup.length) {
+      this.set('controller.selectedAlertGroup', selectedAlertGroup[selectedAlertGroup.length - 1]);
+    }
+    if (selectedAlertGroup && selectedAlertGroup.length > 1) {
+      this.set('selectedConfigGroup', selectedAlertGroup[selectedAlertGroup.length - 1]);
+    }
+    this.set('controller.selectedDefinitions', []);
+  }.observes('selectedAlertGroup'),
+
+  onLoad: function () {
+    if (this.get('controller.isLoaded')) {
+      this.set('selectedAlertGroup', this.get('controller.alertGroups')[0])
+    }
+  }.observes('controller.isLoaded', 'controller.alertGroups'),
+
+  willInsertElement: function() {
+    this.get('controller').loadAlertGroups();
+    this.get('controller').loadAlertDefinitions();
+  },
+
+  didInsertElement: function () {
+
+    this.onLoad();
+    App.tooltip($("[rel='button-info']"));
+    App.tooltip($("[rel='button-info-dropdown']"), {placement: 'left'});
+  },
+
+  addButtonTooltip: function () {
+    return  Em.I18n.t('alerts.actions.manage_alert_groups_popup.addButton');
+  }.property(),
+  removeButtonTooltip: function () {
+    return  Em.I18n.t('alerts.actions.manage_alert_groups_popup.removeButton');
+  }.property(),
+  renameButtonTooltip: function () {
+    return  Em.I18n.t('alerts.actions.manage_alert_groups_popup.renameButton');
+  }.property(),
+  duplicateButtonTooltip: function () {
+    return  Em.I18n.t('alerts.actions.manage_alert_groups_popup.duplicateButton');
+  }.property(),
+  addDefinitionTooltip: function () {
+    if (this.get('controller.selectedAlertGroup.default')) {
+      return Em.I18n.t('alerts.actions.manage_alert_groups_popup.addDefinitionToDefault');
+    } else if (this.get('controller.selectedAlertGroup.isAddDefinitionsDisabled')) {
+      return Em.I18n.t('alerts.actions.manage_alert_groups_popup.addDefinitionDisabled');
+    } else {
+      return  Em.I18n.t('alerts.actions.manage_alert_groups_popup.addDefinition');
+    }
+  }.property('controller.selectedConfigGroup.isDefault', 'controller.selectedConfigGroup.isAddHostsDisabled'),
+  removeDefinitionTooltip: function () {
+    return  Em.I18n.t('alerts.actions.manage_alert_groups_popup.removeDefinition');
+  }.property(),
+
+  errorMessage: function () {
+    return  this.get('controller.errorMessage');
+  }.property('controller.errorMessage')
+
+});


Mime
View raw message