Bläddra i källkod

AMBARI-8349. Alerts UI: Create definition details for 5 types of definitions. (akovalenko)

Aleksandr Kovalenko 10 år sedan
förälder
incheckning
6dd73517b2

+ 3 - 0
ambari-web/app/assets/test/tests.js

@@ -45,6 +45,9 @@ var files = ['test/init_model_test',
   'test/controllers/global/configuration_controller_test',
   'test/controllers/main/app_contoller_test',
   'test/controllers/main/alert_definitions_controller_test',
+  'test/controllers/main/alerts/alert_definitions_actions_controller_test',
+  'test/controllers/main/alerts/definitions_configs_controller_test',
+  'test/controllers/main/alerts/definitions_details_controller_test',
   'test/controllers/main/alerts/alert_instances_controller_test',
   'test/controllers/main/admin/stack_and_upgrade_test',
   'test/controllers/main/admin/serviceAccounts_controller_test',

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

@@ -71,6 +71,7 @@ require('controllers/main/alerts_controller');
 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/definition_configs_controller');
 require('controllers/main/alerts/alert_instances_controller');
 require('controllers/main/alerts/manage_alert_groups_controller');
 require('controllers/main/service');

+ 314 - 0
ambari-web/app/controllers/main/alerts/definition_configs_controller.js

@@ -0,0 +1,314 @@
+/**
+ * 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.MainAlertDefinitionConfigsController = Em.Controller.extend({
+
+  name: 'mainAlertDefinitionConfigsController',
+
+  /**
+   * All configurable properties of alert definition
+   * @type {Array}
+   */
+  configs: [],
+
+  /**
+   * Define whether configs are editable
+   * binds to property populated in template
+   * @type {Boolean}
+   */
+  canEdit: true,
+
+  /**
+   * Array of displayNames of all services
+   * is used for "Service" config options
+   * @type {Array}
+   */
+  allServices: function () {
+    return App.Service.find().mapProperty('displayName');
+  }.property(),
+
+  /**
+   * Array of all aggregate-alerts names
+   * @type {Array}
+   */
+  aggregateAlertNames: function () {
+    return App.AggregateAlertDefinition.find().mapProperty('name');
+  }.property(),
+
+  /**
+   * Change options of "Component", after changing value of "Service" config
+   */
+  onServiceSelect: function () {
+    var serviceProperty = this.get('configs').findProperty('label', 'Service');
+    if (serviceProperty) {
+      var componentsProperty = this.get('configs').findProperty('label', 'Component');
+      componentsProperty.set('options', ['No component'].concat(App.HostComponent.find().filterProperty('service.displayName', serviceProperty.get('value')).mapProperty('displayName').uniq()));
+    }
+  }.observes('configs.@each.value'),
+
+  /**
+   * Render array of configs for appropriate alert definition type
+   */
+  renderConfigs: function () {
+    var alertDefinition = this.get('content');
+    var configs = [];
+    switch (alertDefinition.get('type')) {
+      case 'PORT':
+        configs = this.renderPortConfigs();
+        break;
+      case 'METRIC':
+        configs = this.renderMetricConfigs();
+        break;
+      case 'WEB':
+        configs = this.renderWebConfigs();
+        break;
+      case 'SCRIPT':
+        configs = this.renderScriptConfigs();
+        break;
+      case 'AGGREGATE':
+        configs = this.renderAggregateConfigs();
+        break;
+      default:
+        console.error('Incorrect Alert Definition Type: ', alertDefinition.get('type'));
+    }
+
+    configs.setEach('isDisabled', !this.get('canEdit'));
+
+    this.set('configs', configs);
+  },
+
+  /**
+   * Render config properties for port-type alert definition
+   * @returns {Array}
+   */
+  renderPortConfigs: function () {
+    var alertDefinition = this.get('content');
+    return [
+      App.AlertConfigProperties.AlertName.create({
+        value: alertDefinition.get('name')
+      }),
+      App.AlertConfigProperties.Service.create({
+        options: this.get('allServices'),
+        value: alertDefinition.get('service.displayName')
+      }),
+      App.AlertConfigProperties.Component.create({
+        options: this.get('allComponents'),
+        value: alertDefinition.get('componentName') ? App.format.role(alertDefinition.get('componentName')) : 'No component'
+      }),
+      App.AlertConfigProperties.Scope.create({
+        value: alertDefinition.get('scope').toLowerCase().capitalize()
+      }),
+      App.AlertConfigProperties.Description.create({
+        value: alertDefinition.get('description')
+      }),
+      App.AlertConfigProperties.Interval.create({
+        value: alertDefinition.get('interval')
+      }),
+      App.AlertConfigProperties.Thresholds.create({
+        value: alertDefinition.get('thresholds'),
+        from: alertDefinition.get('thresholds').split('-')[0],
+        to: alertDefinition.get('thresholds').split('-')[1]
+      }),
+      App.AlertConfigProperties.URI.create({
+        value: alertDefinition.get('uri')
+      }),
+      App.AlertConfigProperties.DefaultPort.create({
+        value: alertDefinition.get('defaultPort')
+      })
+    ];
+  },
+
+  /**
+   * Render config properties for metric-type alert definition
+   * @returns {Array}
+   */
+  renderMetricConfigs: function () {
+    var alertDefinition = this.get('content');
+    return [
+      App.AlertConfigProperties.AlertName.create({
+        value: alertDefinition.get('name')
+      }),
+      App.AlertConfigProperties.Service.create({
+        options: this.get('allServices'),
+        value: alertDefinition.get('service.displayName')
+      }),
+      App.AlertConfigProperties.Component.create({
+        options: this.get('allComponents'),
+        value: alertDefinition.get('componentName') ? App.format.role(alertDefinition.get('componentName')) : 'No component'
+      }),
+      App.AlertConfigProperties.Scope.create({
+        value: alertDefinition.get('scope').toLowerCase().capitalize()
+      }),
+      App.AlertConfigProperties.Description.create({
+        value: alertDefinition.get('description')
+      }),
+      App.AlertConfigProperties.Interval.create({
+        value: alertDefinition.get('interval')
+      }),
+      App.AlertConfigProperties.Thresholds.create({
+        value: alertDefinition.get('thresholds'),
+        from: alertDefinition.get('thresholds').split('-')[0],
+        to: alertDefinition.get('thresholds').split('-')[1]
+      }),
+      App.AlertConfigProperties.URIExtended.create({
+        value: JSON.stringify({
+          http: alertDefinition.get('uri.http'),
+          https: alertDefinition.get('uri.https'),
+          https_property: alertDefinition.get('uri.httpsProperty'),
+          https_property_value: alertDefinition.get('uri.httpsPropertyValue')
+        })
+      }),
+      App.AlertConfigProperties.Metrics.create({
+        value: alertDefinition.get('jmx.propertyList') ? alertDefinition.get('jmx.propertyList').join('\n') : alertDefinition.get('ganglia.propertyList').join('\n')
+      }),
+      App.AlertConfigProperties.FormatString.create({
+        value: alertDefinition.get('jmx.value') ? alertDefinition.get('jmx.value') : alertDefinition.get('ganglia.value')
+      })
+    ];
+  },
+
+  /**
+   * Render config properties for web-type alert definition
+   * @returns {Array}
+   */
+  renderWebConfigs: function () {
+    var alertDefinition = this.get('content');
+    return [
+      App.AlertConfigProperties.AlertName.create({
+        value: alertDefinition.get('name')
+      }),
+      App.AlertConfigProperties.Service.create({
+        options: this.get('allServices'),
+        value: alertDefinition.get('service.displayName')
+      }),
+      App.AlertConfigProperties.Component.create({
+        options: this.get('allComponents'),
+        value: alertDefinition.get('componentName') ? App.format.role(alertDefinition.get('componentName')) : 'No component'
+      }),
+      App.AlertConfigProperties.Scope.create({
+        value: alertDefinition.get('scope').toLowerCase().capitalize()
+      }),
+      App.AlertConfigProperties.Description.create({
+        value: alertDefinition.get('description')
+      }),
+      App.AlertConfigProperties.Interval.create({
+        value: alertDefinition.get('interval')
+      }),
+      App.AlertConfigProperties.Thresholds.create({
+        value: alertDefinition.get('thresholds'),
+        from: alertDefinition.get('thresholds').split('-')[0],
+        to: alertDefinition.get('thresholds').split('-')[1]
+      }),
+      App.AlertConfigProperties.URIExtended.create({
+        value: JSON.stringify({
+          http: alertDefinition.get('uri.http'),
+          https: alertDefinition.get('uri.https'),
+          https_property: alertDefinition.get('uri.httpsProperty'),
+          https_property_value: alertDefinition.get('uri.httpsPropertyValue')
+        })
+      })
+    ];
+  },
+
+  /**
+   * Render config properties for script-type alert definition
+   * @returns {Array}
+   */
+  renderScriptConfigs: function () {
+    var alertDefinition = this.get('content');
+    return [
+      App.AlertConfigProperties.AlertName.create({
+        value: alertDefinition.get('name')
+      }),
+      App.AlertConfigProperties.Service.create({
+        options: this.get('allServices'),
+        value: alertDefinition.get('service.displayName')
+      }),
+      App.AlertConfigProperties.Component.create({
+        options: this.get('allComponents'),
+        value: alertDefinition.get('componentName') ? App.format.role(alertDefinition.get('componentName')) : 'No component'
+      }),
+      App.AlertConfigProperties.Scope.create({
+        value: alertDefinition.get('scope').toLowerCase().capitalize()
+      }),
+      App.AlertConfigProperties.Description.create({
+        value: alertDefinition.get('description')
+      }),
+      App.AlertConfigProperties.Interval.create({
+        value: alertDefinition.get('interval')
+      }),
+      App.AlertConfigProperties.Thresholds.create({
+        value: alertDefinition.get('thresholds'),
+        from: alertDefinition.get('thresholds').split('-')[0],
+        to: alertDefinition.get('thresholds').split('-')[1]
+      }),
+      App.AlertConfigProperties.Path.create({
+        value: alertDefinition.get('location')
+      })
+    ];
+  },
+
+  /**
+   * Render config properties for aggregate-type alert definition
+   * @returns {Array}
+   */
+  renderAggregateConfigs: function () {
+    var alertDefinition = this.get('content');
+    return [
+      App.AlertConfigProperties.AlertNameSelected.create({
+        value: alertDefinition.get('name'),
+        options: this.get('aggregateAlertNames')
+      }),
+      App.AlertConfigProperties.Description.create({
+        value: alertDefinition.get('description')
+      })
+    ];
+  },
+
+  /**
+   * Edit configs button handler
+   */
+  editConfigs: function () {
+    this.get('configs').forEach(function (property) {
+      property.set('previousValue', property.get('value'));
+    });
+    this.get('configs').setEach('isDisabled', false);
+    this.set('canEdit', true);
+  },
+
+  /**
+   * Cancel edit configs button handler
+   */
+  cancelEditConfigs: function () {
+    this.get('configs').forEach(function (property) {
+      property.set('value', property.get('previousValue'));
+    });
+    this.get('configs').setEach('isDisabled', true);
+    this.set('canEdit', false);
+  },
+
+  /**
+   * Save edit configs button handler
+   */
+  saveConfigs: function () {
+    //todo: write logic for saving alert definition properties to server
+    this.get('configs').setEach('isDisabled', true);
+    this.set('canEdit', false);
+  }
+
+});

+ 117 - 1
ambari-web/app/controllers/main/alerts/definition_details_controller.js

@@ -18,5 +18,121 @@
 
 App.MainAlertDefinitionDetailsController = Em.Controller.extend({
 
-  name: 'mainAlertDefinitionDetailsController'
+  name: 'mainAlertDefinitionDetailsController',
+
+  // stores object with editing form data (label, description, thresholds)
+  editing: Em.Object.create({
+    label: Em.Object.create({
+      name: 'label',
+      isEditing: false,
+      value: '',
+      originalValue: '',
+      isError: false,
+      bindingValue: 'content.label'
+    }),
+    description: Em.Object.create({
+      name: 'description',
+      isEditing: false,
+      value: '',
+      originalValue: '',
+      isError: false,
+      bindingValue: 'content.description'
+    })
+  }),
+
+  /**
+   * Validation function to define if label field populated correctly
+   */
+  labelValidation: function () {
+    this.set('editing.label.isError', !this.get('editing.label.value').trim());
+  }.observes('editing.label.value'),
+
+  /**
+   * Validation function to define if description field populated correctly
+   */
+  descriptionValidation: function () {
+    this.set('editing.description.isError', !this.get('editing.description.value').trim());
+  }.observes('editing.description.value'),
+
+  /**
+   * Edit button handler
+   * @param event
+   */
+  edit: function (event) {
+    var element = event.context;
+    var value = this.get(element.get('bindingValue'));
+    element.set('originalValue', value);
+    element.set('value', value);
+    element.set('isEditing', true);
+  },
+
+  /**
+   * Cancel button handler
+   * @param event
+   */
+  cancelEdit: function (event) {
+    var element = event.context;
+    element.set('value', element.get('originalValue'));
+    element.set('isEditing', false);
+  },
+
+  /**
+   * Save button handler, could save label/description/thresholds of alert definition
+   * @param event
+   * @returns {$.ajax}
+   * @method saveEdit
+   */
+  saveEdit: function (event) {
+    var element = event.context;
+    this.set(element.get('bindingValue'), element.get('value'));
+    element.set('isEditing', false);
+
+    var data = Em.Object.create({});
+    var property_name = "AlertDefinition/" + element.get('name');
+    data.set(property_name,  element.get('value'));
+    var alertDefinition_id = this.get('content.id');
+    return App.ajax.send({
+      name: 'alerts.update_alert_definition',
+      sender: this,
+      data: {
+        id: alertDefinition_id,
+        data: data
+      }
+    });
+  },
+
+  /**
+   * "Delete" button handler
+   * @param event
+   */
+  deleteAlertDefinition: function (event) {
+    // todo: provide deleting of alert definition
+  },
+
+  /**
+   * "Disable / Enable" button handler
+   * @returns {$.ajax}
+   * @method toggleState
+   */
+  toggleState: function() {
+    var alertDefinition = this.get('content');
+    return App.ajax.send({
+      name: 'alerts.update_alert_definition',
+      sender: this,
+      data: {
+        id: alertDefinition.get('id'),
+        data: {
+          "AlertDefinition/enabled": !alertDefinition.get('enabled')
+        }
+      }
+    });
+  },
+
+  /**
+   * Router transition to host level alerts page
+   * @param event
+   */
+  goToHostDetails: function (event) {
+    // todo: provide transition to host level alert details
+  }
 });

+ 8 - 6
ambari-web/app/mappers/alert_definitions_mapper.js

@@ -118,17 +118,19 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
             var jmxMetric = item.AlertDefinition.source.jmx;
             var gangliaMetric = item.AlertDefinition.source.ganglia;
             if (jmxMetric) {
-              alertReportDefinitions.jmx_id = item.AlertDefinition.id + 'jmx';
+              alertDefinition.jmx_id = item.AlertDefinition.id + 'jmx';
               alertMetricsSourceDefinitions.push({
-                id: item.AlertDefinition.id + 'jmx',
-                value: jmxMetric.value
+                id: alertDefinition.jmx_id,
+                value: jmxMetric.value,
+                property_list: jmxMetric.property_list
               });
             }
             if (gangliaMetric) {
-              alertReportDefinitions.ganglia_id = item.AlertDefinition.id + 'ganglia';
+              alertDefinition.ganglia_id = item.AlertDefinition.id + 'ganglia';
               alertMetricsSourceDefinitions.push({
-                id: alertReportDefinitions.ganglia_id,
-                value: gangliaMetric.value
+                id: alertDefinition.ganglia_id,
+                value: gangliaMetric.value,
+                property_list: gangliaMetric.property_list
               });
             }
 

+ 1 - 0
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_config');
 require('models/alert_group');
 require('models/user');
 require('models/host');

+ 187 - 0
ambari-web/app/models/alert_config.js

@@ -0,0 +1,187 @@
+/**
+ * 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.AlertConfigProperty = Ember.Object.extend({
+
+  /**
+   * label to be shown for config property
+   * @type {String}
+   */
+  label: '',
+
+  /**
+   * config property value
+   * @type {*}
+   */
+  value: null,
+
+  /**
+   * property value cache to realise undo function
+   * @type {*}
+   */
+  previousValue: null,
+
+  /**
+   * define either input is disabled or enabled
+   * @type {Boolean}
+   */
+  isDisabled: false,
+
+  /**
+   * options that Select list will have
+   * @type {Array}
+   */
+  options: [],
+
+  /**
+   * input displayType
+   * one of 'textFields', 'textArea', 'select' or 'threshold'
+   * @type {String}
+   */
+  displayType: '',
+
+  /**
+   * unit to be shown with value
+   * @type {String}
+   */
+  unit: null,
+
+  /**
+   * space separated list of css class names to use
+   * @type {String}
+   */
+  classNames: '',
+
+  /**
+   * view class according to <code>displayType</code>
+   * @type {Em.View}
+   */
+  viewClass: function () {
+    var displayType = this.get('displayType');
+    switch (displayType) {
+      case 'textField':
+        return App.AlertConfigTextFieldView;
+      case 'textArea':
+        return App.AlertConfigTextAreaView;
+      case 'select':
+        return App.AlertConfigSelectView;
+      case 'threshold':
+        return App.AlertConfigThresholdView;
+      default:
+        console.error('Unable to find viewClass for displayType ', displayType);
+    }
+  }.property('displayType')
+
+});
+
+App.AlertConfigProperties = {
+
+  AlertName: App.AlertConfigProperty.extend({
+    label: 'Alert Name',
+    displayType: 'textField',
+    classNames: 'alert-text-input'
+  }),
+  AlertNameSelected: App.AlertConfigProperty.extend({
+    label: 'Alert Name',
+    displayType: 'select'
+  }),
+  Service: App.AlertConfigProperty.extend({
+    label: 'Service',
+    displayType: 'select'
+  }),
+  Component: App.AlertConfigProperty.extend({
+    label: 'Component',
+    displayType: 'select'
+  }),
+  Scope: App.AlertConfigProperty.extend({
+    label: 'Scope',
+    options: ['Any', 'Host', 'Service'],
+    displayType: 'select'
+  }),
+  Description: App.AlertConfigProperty.extend({
+    label: 'Description',
+    displayType: 'textArea',
+    classNames: 'alert-config-text-area'
+  }),
+  Interval: App.AlertConfigProperty.extend({
+    label: 'Interval',
+    displayType: 'textField',
+    unit: 'Second',
+    classNames: 'alert-interval-input'
+  }),
+  Thresholds: App.AlertConfigProperty.extend({
+    label: 'Thresholds',
+    displayType: 'threshold',
+    classNames: 'alert-thresholds-input',
+    from: '',
+    to: '',
+    value: '',
+
+    setFromTo: function () {
+      this.set('doNotChangeValue', true);
+      this.set('from', this.get('value').split('-')[0]);
+      this.set('to', this.get('value').split('-')[1]);
+      this.set('doNotChangeValue', false);
+    }.observes('value'),
+
+    setValue: function () {
+      if (!this.get('doNotChangeValue')) {
+        this.set('value', this.get('from') + '-' + this.get('to'));
+      }
+    }.observes('from', 'to'),
+
+    // flag for providing correct from, to and value recomputing
+    doNotChangeValue: false
+  }),
+  URI: App.AlertConfigProperty.extend({
+    label: 'URI',
+    displayType: 'textField',
+    classNames: 'alert-text-input'
+  }),
+  URIExtended: App.AlertConfigProperty.extend({
+    label: 'URI',
+    displayType: 'textArea',
+    classNames: 'alert-config-text-area'
+  }),
+  DefaultPort: App.AlertConfigProperty.extend({
+    label: 'Default Port',
+    displayType: 'textField',
+    classNames: 'alert-port-input'
+  }),
+  Path: App.AlertConfigProperty.extend({
+    label: 'Path',
+    displayType: 'textField',
+    classNames: 'alert-text-input'
+  }),
+  Metrics: App.AlertConfigProperty.extend({
+    label: 'JMX/Ganglia Metrics',
+    displayType: 'textArea',
+    classNames: 'alert-config-text-area'
+  }),
+  FormatString: App.AlertConfigProperty.extend({
+    label: 'Format String',
+    displayType: 'textArea',
+    classNames: 'alert-config-text-area'
+  })
+
+};
+
+
+

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

@@ -91,7 +91,7 @@ App.AlertDefinition = DS.Model.extend({
   // todo: in future be mapped from server response
   description: 'Description for the Alert Definition.',
   // todo: in future be mapped from server response
-  thresholds: 'Thresholds for the Alert Definition.'
+  thresholds: '5-10'
 });
 
 App.AlertReportDefinition = DS.Model.extend({

+ 33 - 4
ambari-web/app/styles/alerts.less

@@ -180,10 +180,12 @@
     margin-bottom: 20px;
   }
 
-  textarea {
-    width: 99%;
-    height: 100px;
-    margin-left: -5px;
+  .text-area-edit {
+    textarea {
+      width: 99%;
+      height: 100px;
+      margin-left: -5px;
+    }
   }
 
   .edit-link {
@@ -191,6 +193,33 @@
   }
 }
 
+.alert-configs {
+
+  margin-top: 20px;
+
+  .alert-config-text-area {
+    height: 100px;
+  }
+
+  .alert-text-input {
+    input {
+      width: 98%;
+    }
+  }
+
+  .alert-interval-input {
+    input {
+      width: 20%;
+    }
+  }
+
+  .alert-port-input {
+    input {
+      width: 30%;
+    }
+  }
+}
+
 #host-alerts-table {
   a {
     &.disabled {

+ 28 - 0
ambari-web/app/templates/main/alerts/configs.hbs

@@ -0,0 +1,28 @@
+{{!
+* 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 alert-configs">
+  {{#each property in controller.configs}}
+    <div class="control-group">
+      <label class="control-label">{{property.label}}</label>
+
+      <div class="controls">
+        {{view property.viewClass propertyBinding="property"}}
+      </div>
+    </div>
+  {{/each}}
+</form>

+ 23 - 0
ambari-web/app/templates/main/alerts/configs/alert_config_text_field.hbs

@@ -0,0 +1,23 @@
+{{!
+* 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 {{bindAttr class="view.property.unit:input-append"}}>
+  {{view Em.TextField valueBinding="view.property.value" disabledBinding="view.property.isDisabled"}}
+  {{#if view.property.unit}}
+    <span class="add-on">{{view.property.unit}}</span>
+  {{/if}}
+</div>

+ 24 - 0
ambari-web/app/templates/main/alerts/configs/alert_config_threshold.hbs

@@ -0,0 +1,24 @@
+{{!
+* 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.
+}}
+
+<span class="icon-ok-sign alert-state-OK"></span>
+{{view Em.TextField valueBinding="view.property.from" disabledBinding="view.property.isDisabled" class="span2"}}
+<span class="icon-warning-sign alert-state-WARNING"></span>
+{{view Em.TextField valueBinding="view.property.to" disabledBinding="view.property.isDisabled" class="span2"}}
+<span class="icon-remove-sign alert-state-CRITICAL"></span>
+

+ 24 - 30
ambari-web/app/templates/main/alerts/definition_details.hbs

@@ -20,41 +20,41 @@
   <div class="row-fluid">
     <div class="span9">
       <div class="definition-name">
-        {{#if view.editing.label.isEditing}}
-          <div {{bindAttr class="view.editing.label.isError:error :control-group"}}>
-            {{view Em.TextField valueBinding="view.editing.label.value"}}
+        {{#if controller.editing.label.isEditing}}
+          <div {{bindAttr class="controller.editing.label.isError:error :control-group"}}>
+            {{view Em.TextField valueBinding="controller.editing.label.value"}}
           </div>
           <div class="edit-buttons">
-            <button {{action cancelEdit view.editing.label target="view"}} class="btn">{{t common.cancel}}</button>
-            <button {{bindAttr disabled="view.editing.label.isError"}} {{action saveEdit view.editing.label target="view"}}
+            <button {{action cancelEdit controller.editing.label target="controller"}} class="btn">{{t common.cancel}}</button>
+            <button {{bindAttr disabled="controller.editing.label.isError"}} {{action saveEdit controller.editing.label target="controller"}}
                 class="btn btn-primary">{{t common.save}}
             </button>
           </div>
         {{else}}
           {{{controller.content.status}}} {{controller.content.label}}
         {{/if}}
-        {{#unless view.editing.label.isEditing}}
-          <a {{action edit view.editing.label target="view"}} class="edit-description-button"><i
+        {{#unless controller.editing.label.isEditing}}
+          <a {{action edit controller.editing.label target="controller"}} class="edit-description-button"><i
               class="icon-pencil"></i></a>
         {{/unless}}
       </div>
 
       <div class="definition-details-block">
         <strong>{{t common.description}}</strong>
-        {{#unless view.editing.description.isEditing}}
-          <a {{action edit view.editing.description target="view"}} class="pull-right edit-link">
+        {{#unless controller.editing.description.isEditing}}
+          <a {{action edit controller.editing.description target="controller"}} class="pull-right edit-link">
             <strong>{{t common.edit}}</strong>
           </a>
         {{/unless}}
         <hr>
-        {{#if view.editing.description.isEditing}}
-          <div {{bindAttr class="view.editing.description.isError:error :control-group"}}>
-            {{view Em.TextArea valueBinding="view.editing.description.value"}}
+        {{#if controller.editing.description.isEditing}}
+          <div {{bindAttr class="controller.editing.description.isError:error :control-group :text-area-edit"}}>
+            {{view Em.TextArea valueBinding="controller.editing.description.value"}}
           </div>
           <div class="edit-buttons">
-            <button {{action cancelEdit view.editing.description target="view"}}
+            <button {{action cancelEdit controller.editing.description target="controller"}}
                 class="btn">{{t common.cancel}}</button>
-            <button {{bindAttr disabled="view.editing.description.isError"}} {{action saveEdit view.editing.description target="view"}}
+            <button {{bindAttr disabled="controller.editing.description.isError"}} {{action saveEdit controller.editing.description target="controller"}}
                 class="btn btn-primary">{{t common.save}}
             </button>
           </div>
@@ -66,42 +66,36 @@
       </div>
       <div class="definition-details-block">
         <strong>{{t alerts.thresholds}}</strong>
-        {{#unless view.editing.thresholds.isEditing}}
-          <a {{action edit view.editing.thresholds target="view"}} class="pull-right edit-link">
+        {{#unless App.router.mainAlertDefinitionConfigsController.canEdit}}
+          <a {{action editConfigs target="App.router.mainAlertDefinitionConfigsController"}} class="pull-right edit-link">
             <strong>{{t common.edit}}</strong>
           </a>
         {{/unless}}
         <hr>
-        {{#if view.editing.thresholds.isEditing}}
-          <div {{bindAttr class="view.editing.thresholds.isError:error :control-group"}}>
-            {{view Em.TextArea valueBinding="view.editing.thresholds.value"}}
-          </div>
+        {{view App.AlertDefinitionConfigsView contentBinding="view.controller.content" canEdit=false}}
+        {{#if App.router.mainAlertDefinitionConfigsController.canEdit}}
           <div class="edit-buttons">
-            <button {{action cancelEdit view.editing.thresholds target="view"}} class="btn">{{t common.cancel}}</button>
-            <button {{bindAttr disabled="view.editing.thresholds.isError"}} {{action saveEdit view.editing.thresholds target="view"}}
+            <button {{action cancelEditConfigs target="App.router.mainAlertDefinitionConfigsController"}} class="btn">{{t common.cancel}}</button>
+            <button {{bindAttr disabled="controller.editing.thresholds.isError"}} {{action saveConfigs target="App.router.mainAlertDefinitionConfigsController"}}
                 class="btn btn-primary">{{t common.save}}
             </button>
           </div>
-        {{else}}
-          <div class="multiline-text">
-            {{controller.content.thresholds}}
-          </div>
         {{/if}}
       </div>
     </div>
     <div class="span3 right-column">
       <div class="service-name">
-        {{t common.service}}: <span class="label label-info">{{controller.content.service.serviceName}}</span>
+        {{t common.service}}: <span class="label label-info">{{controller.content.service.displayName}}</span>
       </div>
       <div>
         {{#if controller.content.enabled}}
-          <button {{action toggleState target="view"}} class="btn btn-danger"><i
+          <button {{action toggleState target="controller"}} class="btn btn-danger"><i
              class="icon-power-off"></i>&nbsp;{{t alerts.definition.details.disable}}</button>
         {{else}}
-          <button {{action toggleState target="view"}} class="btn btn-success"><i
+          <button {{action toggleState target="controller"}} class="btn btn-success"><i
              class="icon-power-off"></i>&nbsp;{{t alerts.definition.details.enable}}</button>
         {{/if}}
-        <button {{action deleteAlertDefinition target="view"}} class="btn btn-primary"><i
+        <button {{action deleteAlertDefinition target="controller"}} class="btn btn-primary"><i
             class="icon-trash"></i>&nbsp;{{t common.delete}}</button>
       </div>
       <div class="properties-list">

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

@@ -44,6 +44,7 @@ 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/definition_configs_view');
 require('views/main/alerts');
 require('views/main/alerts/manage_alert_groups_view');
 require('views/main/charts');

+ 71 - 0
ambari-web/app/views/main/alerts/definition_configs_view.js

@@ -0,0 +1,71 @@
+/**
+ * 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.AlertDefinitionConfigsView = Em.View.extend({
+
+  controllerBinding: 'App.router.mainAlertDefinitionConfigsController',
+
+  templateName: require('templates/main/alerts/configs'),
+
+  /**
+   * Define whether configs are aditable
+   * is set in template
+   * @type {Boolean}
+   */
+  canEdit: true,
+
+  /**
+   * List of classes applied to all inputs
+   * @type {String}
+   */
+  basicClass: 'span9',
+
+  init: function () {
+    this.set('controller.canEdit', this.get('canEdit'));
+    this.set('controller.content', this.get('content'));
+    this.get('controller').renderConfigs();
+    this._super();
+  }
+
+});
+
+App.AlertConfigTextFieldView = Em.View.extend({
+  templateName: require('templates/main/alerts/configs/alert_config_text_field'),
+  classNameBindings: ['property.classNames', 'parentView.basicClass']
+});
+
+App.AlertConfigTextAreaView = Em.TextArea.extend({
+  valueBinding: 'property.value',
+  disabledBinding: 'property.isDisabled',
+  classNameBindings: ['property.classNames', 'parentView.basicClass']
+});
+
+App.AlertConfigSelectView = Em.Select.extend({
+  attributeBindings: ['disabled'],
+  selectionBinding: 'property.value',
+  disabledBinding: 'property.isDisabled',
+  contentBinding: 'property.options',
+  classNameBindings: ['property.classNames', 'parentView.basicClass']
+});
+
+App.AlertConfigThresholdView = Em.View.extend({
+  templateName: require('templates/main/alerts/configs/alert_config_threshold'),
+  classNameBindings: ['property.classNames', 'parentView.basicClass']
+});

+ 1 - 132
ambari-web/app/views/main/alerts/definition_details_view.js

@@ -29,136 +29,5 @@ App.MainAlertDefinitionDetailsView = App.TableView.extend({
   content: function () {
     // todo: replace with this.get('controller.content.alerts') when this relationship will be provided
     return App.AlertInstance.find().toArray();
-  }.property(),
-
-  // stores object with editing form data (label, description, thresholds)
-  editing: Em.Object.create({
-    label: Em.Object.create({
-      name: 'label',
-      isEditing: false,
-      value: '',
-      originalValue: '',
-      isError: false,
-      bindingValue: 'controller.content.label'
-    }),
-    description: Em.Object.create({
-      name: 'description',
-      isEditing: false,
-      value: '',
-      originalValue: '',
-      isError: false,
-      bindingValue: 'controller.content.description'
-    }),
-    thresholds: Em.Object.create({
-      name: 'thresholds',
-      isEditing: false,
-      value: '',
-      originalValue: '',
-      isError: false,
-      bindingValue: 'controller.content.thresholds'
-    })
-  }),
-
-  /**
-   * Validation function to define if label field populated correctly
-   */
-  labelValidation: function () {
-    this.set('editing.label.isError', !this.get('editing.label.value').trim());
-  }.observes('editing.label.value'),
-
-  /**
-   * Validation function to define if description field populated correctly
-   */
-  descriptionValidation: function () {
-    this.set('editing.description.isError', !this.get('editing.description.value').trim());
-  }.observes('editing.description.value'),
-
-  /**
-   * Validation function to define if thresholds field populated correctly
-   */
-  thresholsdValidation: function () {
-    this.set('editing.thresholds.isError', !this.get('editing.thresholds.value').trim());
-  }.observes('editing.thresholds.value'),
-
-  /**
-   * Edit button handler
-   * @param event
-   */
-  edit: function (event) {
-    var element = event.context;
-    var value = this.get(element.get('bindingValue'));
-    element.set('originalValue', value);
-    element.set('value', value);
-    element.set('isEditing', true);
-  },
-
-  /**
-   * Cancel button handler
-   * @param event
-   */
-  cancelEdit: function (event) {
-    var element = event.context;
-    element.set('value', element.get('originalValue'));
-    element.set('isEditing', false);
-  },
-
-  /**
-   * Save button handler, could save label/description/thresholds of alert definition
-   * @param event
-   * @returns {$.ajax}
-   * @method saveEdit
-   */
-  saveEdit: function (event) {
-    var element = event.context;
-    this.set(element.get('bindingValue'), element.get('value'));
-    element.set('isEditing', false);
-
-    var data = Em.Object.create({});
-    var property_name = "AlertDefinition/" + element.get('name');
-    data.set(property_name,  element.get('value'));
-    var alertDefinition_id = this.get('controller.content.id');
-    return App.ajax.send({
-      name: 'alerts.update_alert_definition',
-      sender: this,
-      data: {
-        id: alertDefinition_id,
-        data: data
-      }
-    });
-  },
-
-  /**
-   * "Delete" button handler
-   * @param event
-   */
-  deleteAlertDefinition: function (event) {
-    // todo: provide deleting of alert definition
-  },
-
-  /**
-   * "Disable / Enable" button handler
-   * @returns {$.ajax}
-   * @method toggleState
-   */
-  toggleState: function() {
-    var alertDefinition = this.get('controller.content');
-    return App.ajax.send({
-      name: 'alerts.update_alert_definition',
-      sender: this,
-      data: {
-        id: alertDefinition.get('id'),
-        data: {
-          "AlertDefinition/enabled": !alertDefinition.get('enabled')
-        }
-      }
-    });
-  },
-
-  /**
-   * Router transition to host level alerts page
-   * @param event
-   */
-  goToHostDetails: function (event) {
-    // todo: provide transition to host level alert details
-  }
+  }.property()
 });

+ 3 - 3
ambari-web/test/controllers/main/alerts/alert_definitions_actions_controller_test.js

@@ -42,9 +42,9 @@ describe('App.MainAlertDefinitionActionsController', function () {
 
     it('should call proper methods', function () {
 
-      controller.actionHandler({action: 'createNewAlertDefinition'});
-      controller.actionHandler({action: 'manageAlertGroups'});
-      controller.actionHandler({action: 'manageNotifications'});
+      controller.actionHandler({context: {action: 'createNewAlertDefinition'}});
+      controller.actionHandler({context: {action: 'manageAlertGroups'}});
+      controller.actionHandler({context: {action: 'manageNotifications'}});
       expect(controller.createNewAlertDefinition.calledOnce).to.be.ok;
       expect(controller.manageAlertGroups.calledOnce).to.be.ok;
       expect(controller.manageNotifications.calledOnce).to.be.ok;

+ 318 - 0
ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js

@@ -0,0 +1,318 @@
+/**
+ * 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 controller;
+
+describe('App.MainAlertDefinitionConfigsController', function () {
+
+  beforeEach(function () {
+    controller = App.MainAlertDefinitionConfigsController.create({
+      allServices: ['service1', 'service2', 'service3'],
+      allComponents: ['component1', 'component2', 'component3'],
+      aggregateAlertNames: ['alertDefinitionName', 'alertDefinitionName2', 'alertDefinitionName3']
+    });
+  });
+
+  describe('#renderConfigs()', function () {
+
+    beforeEach(function () {
+      controller.set('content', Em.Object.create({}));
+      sinon.stub(controller, 'renderPortConfigs', function () {
+        return [];
+      });
+      sinon.stub(controller, 'renderMetricConfigs', function () {
+        return [];
+      });
+      sinon.stub(controller, 'renderWebConfigs', function () {
+        return [];
+      });
+      sinon.stub(controller, 'renderScriptConfigs', function () {
+        return [];
+      });
+      sinon.stub(controller, 'renderAggregateConfigs', function () {
+        return [];
+      });
+    });
+
+    afterEach(function () {
+      controller.renderPortConfigs.restore();
+      controller.renderMetricConfigs.restore();
+      controller.renderWebConfigs.restore();
+      controller.renderScriptConfigs.restore();
+      controller.renderAggregateConfigs.restore();
+    });
+
+    it('should call renderPortConfigs method', function () {
+      controller.set('content.type', 'PORT');
+      controller.renderConfigs();
+      expect(controller.renderPortConfigs.calledOnce).to.be.true;
+    });
+
+    it('should call renderMetricConfigs method', function () {
+      controller.set('content.type', 'METRIC');
+      controller.renderConfigs();
+      expect(controller.renderMetricConfigs.calledOnce).to.be.true;
+    });
+
+    it('should call renderWebConfigs method', function () {
+      controller.set('content.type', 'WEB');
+      controller.renderConfigs();
+      expect(controller.renderWebConfigs.calledOnce).to.be.true;
+    });
+
+    it('should call renderScriptConfigs method', function () {
+      controller.set('content.type', 'SCRIPT');
+      controller.renderConfigs();
+      expect(controller.renderScriptConfigs.calledOnce).to.be.true;
+    });
+
+    it('should call renderAggregateConfigs method', function () {
+      controller.set('content.type', 'AGGREGATE');
+      controller.renderConfigs();
+      expect(controller.renderAggregateConfigs.calledOnce).to.be.true;
+    });
+
+  });
+
+  describe('#renderPortConfigs()', function () {
+
+    it('should render array of configs with correct values', function () {
+
+      controller.set('content', Em.Object.create({
+        name: 'alertDefinitionName',
+        service: {displayName: 'alertDefinitionService'},
+        componentName: 'component1',
+        scope: 'HOST',
+        description: 'alertDefinitionDescription',
+        interval: 60,
+        thresholds: '10-20',
+        uri: 'alertDefinitionUri',
+        defaultPort: '777'
+      }));
+
+      var result = controller.renderPortConfigs();
+
+      expect(result.length).to.equal(9);
+      expect(result.someProperty('value', 'alertDefinitionName')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionService')).to.be.true;
+      expect(result.someProperty('value', 'Component1')).to.be.true;
+      expect(result.someProperty('value', 'Host')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionDescription')).to.be.true;
+      expect(result.someProperty('value', 60)).to.be.true;
+      expect(result.someProperty('value', '10-20')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionUri')).to.be.true;
+      expect(result.someProperty('value', '777')).to.be.true;
+    });
+
+  });
+
+  describe('#renderMetricConfigs()', function () {
+
+    it('should render array of configs with correct values', function () {
+
+      controller.set('content', Em.Object.create({
+        name: 'alertDefinitionName',
+        service: {displayName: 'alertDefinitionService'},
+        componentName: 'component1',
+        scope: 'HOST',
+        description: 'alertDefinitionDescription',
+        interval: 60,
+        thresholds: '10-20',
+        uri: {
+          "http": "{{mapred-site/mapreduce.jobhistory.webapp.address}}",
+          "https": "{{mapred-site/mapreduce.jobhistory.webapp.https.address}}",
+          "https_property": "{{mapred-site/mapreduce.jobhistory.http.policy}}",
+          "https_property_value": "HTTPS_ONLY",
+          "default_port": 0.0
+        },
+        jmx: {
+          propertyList: ['property1', 'property2'],
+          value: 'jmxValue'
+        },
+        ganglia: {
+          propertyList: null,
+          value: null
+        }
+      }));
+
+      var result = controller.renderMetricConfigs();
+
+      expect(result.length).to.equal(10);
+      expect(result.someProperty('value', 'alertDefinitionName')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionService')).to.be.true;
+      expect(result.someProperty('value', 'Component1')).to.be.true;
+      expect(result.someProperty('value', 'Host')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionDescription')).to.be.true;
+      expect(result.someProperty('value', 60)).to.be.true;
+      expect(result.someProperty('value', '10-20')).to.be.true;
+      expect(result.someProperty('value', '{\"http\":\"{{mapred-site/mapreduce.jobhistory.webapp.address}}\",\"https\":\"{{mapred-site/mapreduce.jobhistory.webapp.https.address}}\"}')).to.be.true;
+      expect(result.someProperty('value', 'property1\nproperty2')).to.be.true;
+      expect(result.someProperty('value', 'jmxValue')).to.be.true;
+    });
+
+  });
+
+  describe('#renderWebConfigs()', function () {
+
+    it('should render array of configs with correct values', function () {
+
+      controller.set('content', Em.Object.create({
+        name: 'alertDefinitionName',
+        service: {displayName: 'alertDefinitionService'},
+        componentName: 'component1',
+        scope: 'HOST',
+        description: 'alertDefinitionDescription',
+        interval: 60,
+        thresholds: '10-20',
+        uri: {
+          "http": "{{mapred-site/mapreduce.jobhistory.webapp.address}}",
+          "https": "{{mapred-site/mapreduce.jobhistory.webapp.https.address}}",
+          "https_property": "{{mapred-site/mapreduce.jobhistory.http.policy}}",
+          "https_property_value": "HTTPS_ONLY",
+          "default_port": 0.0
+        }
+      }));
+
+      var result = controller.renderWebConfigs();
+
+      expect(result.length).to.equal(8);
+      expect(result.someProperty('value', 'alertDefinitionName')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionService')).to.be.true;
+      expect(result.someProperty('value', 'Component1')).to.be.true;
+      expect(result.someProperty('value', 'Host')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionDescription')).to.be.true;
+      expect(result.someProperty('value', 60)).to.be.true;
+      expect(result.someProperty('value', '10-20')).to.be.true;
+      expect(result.someProperty('value', '{\"http\":\"{{mapred-site/mapreduce.jobhistory.webapp.address}}\",\"https\":\"{{mapred-site/mapreduce.jobhistory.webapp.https.address}}\"}')).to.be.true;
+    });
+
+  });
+
+  describe('#renderScriptConfigs()', function () {
+
+    it('should render array of configs with correct values', function () {
+
+      controller.set('content', Em.Object.create({
+        name: 'alertDefinitionName',
+        service: {displayName: 'alertDefinitionService'},
+        componentName: 'component1',
+        scope: 'HOST',
+        description: 'alertDefinitionDescription',
+        interval: 60,
+        thresholds: '10-20',
+        location: 'path to script'
+      }));
+
+      var result = controller.renderScriptConfigs();
+
+      expect(result.length).to.equal(8);
+      expect(result.someProperty('value', 'alertDefinitionName')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionService')).to.be.true;
+      expect(result.someProperty('value', 'Component1')).to.be.true;
+      expect(result.someProperty('value', 'Host')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionDescription')).to.be.true;
+      expect(result.someProperty('value', 60)).to.be.true;
+      expect(result.someProperty('value', '10-20')).to.be.true;
+      expect(result.someProperty('value', 'path to script')).to.be.true;
+    });
+
+  });
+
+  describe('#renderAggregateConfigs()', function () {
+
+    it('should render array of configs with correct values', function () {
+
+      controller.set('content', Em.Object.create({
+        name: 'alertDefinitionName',
+        description: 'alertDefinitionDescription'
+      }));
+
+      var result = controller.renderAggregateConfigs();
+
+      expect(result.length).to.equal(2);
+      expect(result.someProperty('value', 'alertDefinitionName')).to.be.true;
+      expect(result.someProperty('value', 'alertDefinitionDescription')).to.be.true;
+    });
+
+  });
+
+  describe('#editConfigs()', function () {
+
+    it('should set previousValue, isDisabled for each config and change canEdit flag', function () {
+
+      controller.set('configs', [
+        Em.Object.create({value: 'value1', previousValue: '', isDisabled: true}),
+        Em.Object.create({value: 'value2', previousValue: '', isDisabled: true}),
+        Em.Object.create({value: 'value3', previousValue: '', isDisabled: true})
+      ]);
+
+      controller.set('canEdit', false);
+
+      controller.editConfigs();
+
+      expect(controller.get('configs').mapProperty('previousValue')).to.eql(['value1', 'value2', 'value3']);
+      expect(controller.get('configs').someProperty('isDisabled', true)).to.be.false;
+      expect(controller.get('canEdit')).to.be.true;
+    });
+
+  });
+
+  describe('#cancelEditConfigs()', function () {
+
+    it('should set previousValue, isDisabled for each config and change canEdit flag', function () {
+
+      controller.set('configs', [
+        Em.Object.create({value: '', previousValue: 'value1', isDisabled: false}),
+        Em.Object.create({value: '', previousValue: 'value2', isDisabled: false}),
+        Em.Object.create({value: '', previousValue: 'value3', isDisabled: false})
+      ]);
+
+      controller.set('canEdit', true);
+
+      controller.cancelEditConfigs();
+
+      expect(controller.get('configs').mapProperty('value')).to.eql(['value1', 'value2', 'value3']);
+      expect(controller.get('configs').someProperty('isDisabled', false)).to.be.false;
+      expect(controller.get('canEdit')).to.be.false;
+    });
+
+  });
+
+  describe('#saveConfigs()', function () {
+
+    it('should set previousValue, isDisabled for each config and change canEdit flag', function () {
+
+      controller.set('configs', [
+        Em.Object.create({isDisabled: true}),
+        Em.Object.create({isDisabled: true}),
+        Em.Object.create({isDisabled: true})
+      ]);
+
+      controller.set('canEdit', true);
+
+      controller.saveConfigs();
+
+      expect(controller.get('configs').someProperty('isDisabled', false)).to.be.false;
+      expect(controller.get('canEdit')).to.be.false;
+    });
+
+  });
+
+});

+ 97 - 0
ambari-web/test/controllers/main/alerts/definitions_details_controller_test.js

@@ -0,0 +1,97 @@
+/**
+ * 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 controller;
+
+describe('App.MainAlertDefinitionDetailsController', function () {
+
+  beforeEach(function () {
+    controller = App.MainAlertDefinitionDetailsController.create({
+      content: Em.Object.create({
+        description: 'description',
+        label: 'label'
+      })
+    });
+  });
+
+  describe('#labelValidation()', function () {
+
+    it('should set editing.label.isError to true', function () {
+      controller.set('editing.label.value', ' ');
+      expect(controller.get('editing.label.isError')).to.be.true;
+    });
+
+  });
+
+  describe('#descriptionValidation()', function () {
+
+    it('should set editing.description.isError to true', function () {
+      controller.set('editing.description.value', ' ');
+      expect(controller.get('editing.description.isError')).to.be.true;
+    });
+
+  });
+
+  describe('#edit()', function () {
+
+    it('should change value of value, originalValue and isEditing properties', function () {
+      controller.set('editing.label.value', 'test');
+      controller.set('editing.label.originalValue', 'test');
+      controller.set('editing.label.isEditing', false);
+
+      controller.edit({context:controller.get('editing.label')});
+
+      expect(controller.get('editing.label.value')).to.equal('label');
+      expect(controller.get('editing.label.originalValue')).to.equal('label');
+      expect(controller.get('editing.label.isEditing')).to.be.true;
+    });
+
+  });
+
+  describe('#saveEdit()', function () {
+
+    it('should change values of content.label and isEditing properties', function () {
+      controller.set('editing.label.value', 'test');
+      controller.set('editing.label.isEditing', true);
+
+      controller.saveEdit({context:controller.get('editing.label')});
+
+      expect(controller.get('content.label')).to.equal('test');
+      expect(controller.get('editing.label.isEditing')).to.be.false;
+    });
+
+  });
+
+  describe('#toggleState()', function () {
+
+    it('should call App.ajax.send function', function () {
+
+      sinon.spy(App.ajax, 'send');
+
+      controller.toggleState();
+
+      expect(App.ajax.send.calledOnce).to.be.true;
+
+      App.ajax.send.restore();
+    });
+
+  });
+
+});