You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by on...@apache.org on 2016/02/03 17:14:29 UTC

ambari git commit: AMBARI-14898. Alerts: Ability to customize props and thresholds on SCRIPT alerts via Ambari Web UI (onechiporenko)

Repository: ambari
Updated Branches:
  refs/heads/trunk 46f6030b0 -> 6d9e05995


AMBARI-14898. Alerts: Ability to customize props and thresholds on SCRIPT alerts via Ambari Web UI (onechiporenko)


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

Branch: refs/heads/trunk
Commit: 6d9e05995f6815a600021e0e84f3f29518989b36
Parents: 46f6030
Author: Oleg Nechiporenko <on...@apache.org>
Authored: Wed Feb 3 16:02:23 2016 +0200
Committer: Oleg Nechiporenko <on...@apache.org>
Committed: Wed Feb 3 18:12:10 2016 +0200

----------------------------------------------------------------------
 .../alerts/definition_configs_controller.js     |  29 ++++++
 .../app/mappers/alert_definitions_mapper.js     |  36 ++++---
 ambari-web/app/models/alerts/alert_config.js    |  62 +++++++++++-
 .../app/models/alerts/alert_definition.js       |   4 +-
 ambari-web/app/styles/alerts.less               |   4 +
 .../alerts/configs/alert_config_parameter.hbs   |  33 ++++++
 .../main/alerts/definition_configs_view.js      |  10 ++
 .../definitions_configs_controller_test.js      |  44 +++++++-
 .../mappers/alert_definitions_mapper_test.js    |  45 ++++++++-
 .../test/models/alerts/alert_config_test.js     | 100 +++++++++++++++++++
 10 files changed, 341 insertions(+), 26 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/alerts/definition_configs_controller.js b/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
index 1b66f60..3fd5510 100644
--- a/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
+++ b/ambari-web/app/controllers/main/alerts/definition_configs_controller.js
@@ -320,6 +320,23 @@ App.MainAlertDefinitionConfigsController = Em.Controller.extend({
       })
     ]);
 
+    var mixins = {
+      STRING: App.AlertConfigProperties.Parameters.StringMixin,
+      NUMERIC: App.AlertConfigProperties.Parameters.NumericMixin,
+      PERCENT: App.AlertConfigProperties.Parameters.PercentageMixin
+    };
+    alertDefinition.get('parameters').forEach(function (parameter) {
+      var mixin = mixins[parameter.get('type')] || {}; // validation depends on parameter-type
+      result.push(App.AlertConfigProperties.Parameter.create(mixin, {
+        value: isWizard ? '' : parameter.get('value'),
+        apiProperty: parameter.get('name'),
+        label: isWizard ? '' : parameter.get('displayName'),
+        threshold: isWizard ? '' : parameter.get('threshold'),
+        units: isWizard ? '' : parameter.get('units'),
+        type: isWizard ? '' : parameter.get('type'),
+      }));
+    });
+
     return result;
   },
 
@@ -478,6 +495,9 @@ App.MainAlertDefinitionConfigsController = Em.Controller.extend({
   getPropertiesToUpdate: function (onlyChanged) {
     var propertiesToUpdate = {};
     var configs = onlyChanged ? this.get('configs').filterProperty('wasChanged') : this.get('configs');
+    configs = configs.filter(function (c) {
+      return c.get('name') !== 'parameter';
+    });
     configs.forEach(function (property) {
       var apiProperties = property.get('apiProperty');
       var apiFormattedValues = property.get('apiFormattedValue');
@@ -521,6 +541,15 @@ App.MainAlertDefinitionConfigsController = Em.Controller.extend({
       }, this);
     }, this);
 
+    // `source.parameters` is an array and should be updated separately from other configs
+    if (this.get('content.parameters.length')) {
+      propertiesToUpdate['AlertDefinition/source/parameters'] = this.get('content.rawSourceData.parameters');
+      var parameterConfigs = this.get('configs').filterProperty('name', 'parameter');
+      parameterConfigs.forEach(function (parameter) {
+        propertiesToUpdate['AlertDefinition/source/parameters'].findProperty('name', parameter.get('apiProperty')).value = parameter.get('apiFormattedValue');
+      });
+    }
+
     return propertiesToUpdate;
   },
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/app/mappers/alert_definitions_mapper.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/mappers/alert_definitions_mapper.js b/ambari-web/app/mappers/alert_definitions_mapper.js
index b027d67..7aae518 100644
--- a/ambari-web/app/mappers/alert_definitions_mapper.js
+++ b/ambari-web/app/mappers/alert_definitions_mapper.js
@@ -17,8 +17,6 @@
 
 var App = require('app');
 
-var stringUtils = require('utils/string_utils');
-
 App.alertDefinitionsMapper = App.QuickDataMapper.create({
 
   model: App.AlertDefinition,
@@ -44,7 +42,7 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
     reporting: {
       item: 'id'
     },
-    parameters_key: 'reporting',
+    parameters_key: 'parameters',
     parameters_type: 'array',
     parameters: {
       item: 'id'
@@ -76,21 +74,11 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
     connection_timeout: 'AlertDefinition.source.uri.connection_timeout'
   },
 
-  parameterConfig: {
-    id: 'AlertDefinition.source.parameters.id',
-    name: 'AlertDefinition.source.parameters.name',
-    display_name: 'AlertDefinition.source.parameters.display_name',
-    units: 'AlertDefinition.source.parameters.units',
-    value: 'AlertDefinition.source.parameters.value',
-    description: 'AlertDefinition.source.parameters.description',
-    type: 'AlertDefinition.source.parameters.type',
-    threshold: 'AlertDefinition.source.parameters.threshold'
-  },
-
   map: function (json) {
     console.time('App.alertDefinitionsMapper execution time');
     if (json && json.items) {
       var self = this,
+          parameters = [],
           alertDefinitions = [],
           alertReportDefinitions = [],
           alertMetricsSourceDefinitions = [],
@@ -123,8 +111,27 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
           }
         }
 
+        var convertedParameters = [];
+        var sourceParameters = item.AlertDefinition.source.parameters;
+        if (Array.isArray(sourceParameters)) {
+          sourceParameters.forEach(function (parameter) {
+            convertedParameters.push({
+              id: item.AlertDefinition.id + parameter.name,
+              name: parameter.name,
+              display_name: parameter.display_name,
+              units: parameter.units,
+              value: parameter.value,
+              description: parameter.description,
+              type: parameter.type,
+              threshold: parameter.threshold
+            });
+          });
+        }
+
         alertReportDefinitions = alertReportDefinitions.concat(convertedReportDefinitions);
+        parameters = parameters.concat(convertedParameters);
         item.reporting = convertedReportDefinitions;
+        item.parameters = convertedParameters;
 
         rawSourceData[item.AlertDefinition.id] = item.AlertDefinition.source;
         item.AlertDefinition.description = item.AlertDefinition.description || '';
@@ -207,6 +214,7 @@ App.alertDefinitionsMapper = App.QuickDataMapper.create({
 
       // load all mapped data to model
       App.store.loadMany(this.get('reportModel'), alertReportDefinitions);
+      App.store.loadMany(this.get('parameterModel'), parameters);
       App.store.loadMany(this.get('metricsSourceModel'), alertMetricsSourceDefinitions);
       this.setMetricsSourcePropertyLists(this.get('metricsSourceModel'), alertMetricsSourceDefinitions);
       App.store.loadMany(this.get('metricsUriModel'), alertMetricsUriDefinitions);

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/app/models/alerts/alert_config.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/alerts/alert_config.js b/ambari-web/app/models/alerts/alert_config.js
index 4ef3edd..a9a8154 100644
--- a/ambari-web/app/models/alerts/alert_config.js
+++ b/ambari-web/app/models/alerts/alert_config.js
@@ -139,6 +139,8 @@ App.AlertConfigProperty = Ember.Object.extend({
         return App.AlertConfigThresholdView;
       case 'radioButton':
         return App.AlertConfigRadioButtonView;
+      case 'parameter':
+        return App.AlertConfigParameterView;
       default:
     }
   }.property('displayType'),
@@ -331,9 +333,7 @@ App.AlertConfigProperties = {
      * Custom css-class for different badges
      * type {string}
      */
-    badgeCssClass: function () {
-      return 'alert-state-' + this.get('badge');
-    }.property('badge'),
+    badgeCssClass: Em.computed.format('alert-state-{0}', 'badge'),
 
     /**
      * Determines if <code>value</code> or <code>text</code> were changed
@@ -476,10 +476,66 @@ App.AlertConfigProperties = {
     displayType: 'textArea',
     classNames: 'alert-config-text-area',
     apiProperty: Em.computed.ifThenElse('isJMXMetric', 'source.jmx.value', 'source.ganglia.value')
+  }),
+
+  Parameter: App.AlertConfigProperty.extend({
+
+    name: 'parameter',
+
+    displayType: 'parameter',
+
+    badge: Em.computed.alias('threshold'),
+
+    thresholdNotExists: Em.computed.empty('threshold'),
+
+    /**
+     * Custom css-class for different badges
+     * type {string}
+     */
+    badgeCssClass: Em.computed.format('alert-state-{0}', 'badge'),
+
   })
 
 };
+App.AlertConfigProperties.Parameters = {
 
+  StringMixin: Em.Mixin.create({
+    isValid: function () {
+      var value = this.get('value');
+      return String(value).trim() !== '';
+    }.property('value')
+  }),
+  NumericMixin: Em.Mixin.create({
+    isValid: function () {
+      var value = this.get('value');
+      if (!value) {
+        return false;
+      }
+      value = ('' + value).trim();
+      if (!numericUtils.isPositiveNumber(value)) {
+        return false;
+      }
+      value = parseFloat(value);
+      return !isNaN(value);
+    }.property('value')
+  }),
+  PercentageMixin: Em.Mixin.create({
+    isValid: function () {
+      var value = this.get('value');
+      if (!value) {
+        return false;
+      }
+      if (!validator.isValidFloat(value) || !numericUtils.isPositiveNumber(value)) {
+        return false;
+      }
+      value = String(value).trim();
+      value = parseFloat(value);
+
+      return !isNaN(value) && value > 0 && value <= 100;
+    }.property('value')
+  })
+
+};
 App.AlertConfigProperties.Thresholds = {
 
   OkThreshold: App.AlertConfigProperties.Threshold.extend({

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/app/models/alerts/alert_definition.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/alerts/alert_definition.js b/ambari-web/app/models/alerts/alert_definition.js
index 3f59e86..e91bd4f 100644
--- a/ambari-web/app/models/alerts/alert_definition.js
+++ b/ambari-web/app/models/alerts/alert_definition.js
@@ -315,8 +315,8 @@ App.AlertDefinition.reopenClass({
 App.AlertDefinitionParameter = DS.Model.extend({
   name: DS.attr('string'),
   displayName: DS.attr('string'),
-  unit: DS.attr('string'),
-  value: DS.attr('number'),
+  units: DS.attr('string'),
+  value: DS.attr('string'),
   description: DS.attr('string'),
   type: DS.attr('string'),
   threshold: DS.attr('string')

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/app/styles/alerts.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/alerts.less b/ambari-web/app/styles/alerts.less
index 2eabbe2..1063ecf 100644
--- a/ambari-web/app/styles/alerts.less
+++ b/ambari-web/app/styles/alerts.less
@@ -300,6 +300,10 @@
     width: 170px;
   }
 
+  .stuck-left {
+    margin-left: 0!important;
+  }
+
   .controls.shifted {
     margin-left: 190px;
   }

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/app/templates/main/alerts/configs/alert_config_parameter.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/main/alerts/configs/alert_config_parameter.hbs b/ambari-web/app/templates/main/alerts/configs/alert_config_parameter.hbs
new file mode 100644
index 0000000..fffa7bd
--- /dev/null
+++ b/ambari-web/app/templates/main/alerts/configs/alert_config_parameter.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>
+  {{#if view.property.threshold}}
+    <div class="span2 badge-container">
+      <span {{bindAttr class="view.property.badgeCssClass :alert-parameter-badge :alert-state-single-host view.property.threshold:label"}}>
+        {{view.property.badge}}
+        </span>&nbsp;
+    </div>
+  {{/if}}
+  <div {{bindAttr class="view.bigInput:span12:span3 view.property.units:input-append view.property.thresholdNotExists:stuck-left"}}>
+    {{view Em.TextField valueBinding="view.property.value" disabledBinding="view.property.isDisabled" class ="view.bigInput:span12:span7"}}
+    {{#if view.property.units}}
+      <span class="add-on">{{view.property.units}}</span>
+    {{/if}}
+  </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/app/views/main/alerts/definition_configs_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/main/alerts/definition_configs_view.js b/ambari-web/app/views/main/alerts/definition_configs_view.js
index d909367..00e26d4 100644
--- a/ambari-web/app/views/main/alerts/definition_configs_view.js
+++ b/ambari-web/app/views/main/alerts/definition_configs_view.js
@@ -93,3 +93,13 @@ App.AlertConfigRadioButtonView = Em.Checkbox.extend({
 
   classNameBindings: ['property.classNames']
 });
+
+App.AlertConfigParameterView = Em.View.extend({
+
+  templateName: require('templates/main/alerts/configs/alert_config_parameter'),
+
+  bigInput: Em.computed.equal('property.type', 'STRING'),
+
+  classNameBindings: ['property.classNames', 'parentView.basicClass']
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js b/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
index ae25d2d..4061f35 100644
--- a/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
+++ b/ambari-web/test/controllers/main/alerts/definitions_configs_controller_test.js
@@ -249,6 +249,10 @@ describe('App.MainAlertDefinitionConfigsController', function () {
         scope: 'HOST',
         description: 'alertDefinitionDescription',
         interval: 60,
+        parameters: [
+          Em.Object.create({}),
+          Em.Object.create({}),
+        ],
         reporting: [
           Em.Object.create({
             type: 'warning',
@@ -270,13 +274,13 @@ describe('App.MainAlertDefinitionConfigsController', function () {
     it('isWizard = true', function () {
       controller.set('isWizard', true);
       var result = controller.renderScriptConfigs();
-      expect(result.length).to.equal(8);
+      expect(result.length).to.equal(10);
     });
 
     it('isWizard = false', function () {
       controller.set('isWizard', false);
       var result = controller.renderScriptConfigs();
-      expect(result.length).to.equal(2);
+      expect(result.length).to.equal(4);
     });
 
   });
@@ -477,6 +481,42 @@ describe('App.MainAlertDefinitionConfigsController', function () {
         expect(result).to.eql(testCase.result);
       });
     });
+
+    describe('`source/parameters` for SCRIPT configs', function () {
+
+      beforeEach(function () {
+        controller.set('content', Em.Object.create({
+          parameters: [
+            Em.Object.create({name: 'p1', value: 'v1'}),
+            Em.Object.create({name: 'p2', value: 'v2'}),
+            Em.Object.create({name: 'p3', value: 'v3'}),
+            Em.Object.create({name: 'p4', value: 'v4'})
+          ],
+          rawSourceData: {
+            parameters: [
+              {name: 'p1', value: 'v1'},
+              {name: 'p2', value: 'v2'},
+              {name: 'p3', value: 'v3'},
+              {name: 'p4', value: 'v4'}
+            ]
+          }
+        }));
+        controller.set('configs', [
+          Em.Object.create({apiProperty:'p1', apiFormattedValue: 'v11', wasChanged: true, name: 'parameter'}),
+          Em.Object.create({apiProperty:'p2', apiFormattedValue: 'v21', wasChanged: true, name: 'parameter'}),
+          Em.Object.create({apiProperty:'p3', apiFormattedValue: 'v31', wasChanged: true, name: 'parameter'}),
+          Em.Object.create({apiProperty:'p4', apiFormattedValue: 'v41', wasChanged: true, name: 'parameter'})
+        ]);
+        this.result = controller.getPropertiesToUpdate();
+      });
+
+      it('should update parameters', function () {
+        expect(this.result['AlertDefinition/source/parameters']).to.have.property('length').equal(4);
+        expect(this.result['AlertDefinition/source/parameters'].mapProperty('value')).to.be.eql(['v11', 'v21', 'v31', 'v41']);
+      });
+
+    });
+
   });
 
   describe('#changeType()', function () {

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/test/mappers/alert_definitions_mapper_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mappers/alert_definitions_mapper_test.js b/ambari-web/test/mappers/alert_definitions_mapper_test.js
index 6626187..564bf1d 100644
--- a/ambari-web/test/mappers/alert_definitions_mapper_test.js
+++ b/ambari-web/test/mappers/alert_definitions_mapper_test.js
@@ -21,7 +21,8 @@ require('mappers/alert_definitions_mapper');
 var testHelpers = require('test/helpers');
 
 describe('App.alertDefinitionsMapper', function () {
-  describe.skip('#map', function () {
+  /*eslint-disable mocha-cleanup/asserts-limit */
+  describe('#map', function () {
 
     var json = {
       items: [
@@ -148,6 +149,17 @@ describe('App.alertDefinitionsMapper', function () {
             "scope" : "HOST",
             "service_name" : "YARN",
             "source" : {
+              "parameters" : [
+                {
+                  "name" : "connection.timeout",
+                  "display_name" : "Connection Timeout",
+                  "units" : "seconds",
+                  "value" : 5.0,
+                  "description" : "The maximum time before this alert is considered to be CRITICAL",
+                  "type" : "NUMERIC",
+                  "threshold" : "CRITICAL"
+                }
+              ],
               "path" : "HDP/2.0.6/services/YARN/package/files/alert_nodemanager_health.py",
               "type" : "SCRIPT"
             }
@@ -187,7 +199,7 @@ describe('App.alertDefinitionsMapper', function () {
 
       App.alertDefinitionsMapper.setProperties({
         'model': {},
-
+        'parameterModel': {},
         'reportModel': {},
         'metricsSourceModel': {},
         'metricsUriModel': {}
@@ -352,7 +364,7 @@ describe('App.alertDefinitionsMapper', function () {
 
     });
 
-    it('should parse SCRIPT alertDefinitions', function () {
+    describe('should parse SCRIPT alertDefinitions', function () {
 
       var data = {items: [json.items[3]]},
         expected = [
@@ -370,9 +382,29 @@ describe('App.alertDefinitionsMapper', function () {
             "location":"HDP/2.0.6/services/YARN/package/files/alert_nodemanager_health.py"
           }
         ];
-      App.alertDefinitionsMapper.map(data);
 
-      testHelpers.nestedExpect(expected, App.alertDefinitionsMapper.get('model.content'));
+      var expectedParameters = [{
+        "id": "4connection.timeout",
+        "name": "connection.timeout",
+        "display_name": "Connection Timeout",
+        "units": "seconds",
+        "value": 5,
+        "description": "The maximum time before this alert is considered to be CRITICAL",
+        "type": "NUMERIC",
+        "threshold": "CRITICAL"
+      }];
+
+      beforeEach(function () {
+        App.alertDefinitionsMapper.map(data);
+      });
+
+      it('should map definition', function () {
+        testHelpers.nestedExpect(expected, App.alertDefinitionsMapper.get('model.content'));
+      });
+
+      it('should map parameters', function () {
+        testHelpers.nestedExpect(expectedParameters, App.alertDefinitionsMapper.get('parameterModel.content'));
+      });
 
     });
 
@@ -401,6 +433,7 @@ describe('App.alertDefinitionsMapper', function () {
 
     });
 
+    /*eslint-disable mocha-cleanup/complexity-it */
     it('should set groups from App.cache.previousAlertGroupsMap', function () {
 
       App.cache.previousAlertGroupsMap = {
@@ -421,6 +454,7 @@ describe('App.alertDefinitionsMapper', function () {
 
 
     });
+    /*eslint-enable mocha-cleanup/complexity-it */
 
     describe('should delete not existing definitions', function () {
 
@@ -450,5 +484,6 @@ describe('App.alertDefinitionsMapper', function () {
     });
 
   });
+  /*eslint-enable mocha-cleanup/asserts-limit */
 
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d9e0599/ambari-web/test/models/alerts/alert_config_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/alerts/alert_config_test.js b/ambari-web/test/models/alerts/alert_config_test.js
index 236fcde..4b788f8 100644
--- a/ambari-web/test/models/alerts/alert_config_test.js
+++ b/ambari-web/test/models/alerts/alert_config_test.js
@@ -24,6 +24,106 @@ var model;
 
 describe('App.AlertConfigProperties', function () {
 
+  describe('Parameter', function () {
+
+    function getModel() {
+      return App.AlertConfigProperties.Parameter.create();
+    }
+
+    App.TestAliases.testAsComputedAlias(getModel(), 'badge', 'threshold');
+
+  });
+
+  describe('App.AlertConfigProperties.Parameters', function () {
+
+    describe('StringMixin', function () {
+
+      var obj;
+
+      beforeEach(function () {
+        obj = App.AlertConfigProperties.Parameter.create(App.AlertConfigProperties.Parameters.StringMixin, {});
+      });
+
+      describe('#isValid', function () {
+        Em.A([
+          {value: '', expected: false},
+          {value: '\t', expected: false},
+          {value: '    ', expected: false},
+          {value: '\n', expected: false},
+          {value: '\r', expected: false},
+          {value: 'some not empty string', expected: true}
+        ]).forEach(function (test) {
+          it('value: ' + JSON.stringify(test.value) + ' ;result - ' + test.expected, function () {
+            obj.set('value', test.value);
+            expect(obj.get('isValid')).to.be.equal(test.expected);
+          });
+        });
+      });
+
+    });
+
+    describe('NumericMixin', function () {
+
+      var obj;
+
+      beforeEach(function () {
+        obj = App.AlertConfigProperties.Parameter.create(App.AlertConfigProperties.Parameters.NumericMixin, {});
+      });
+
+      describe('#isValid', function () {
+        Em.A([
+          {value: '', expected: false},
+          {value: 'abc', expected: false},
+          {value: 'g1', expected: false},
+          {value: '1g', expected: false},
+          {value: '123', expected: true},
+          {value: '123.8', expected: true},
+          {value: 123, expected: true},
+          {value: 123.8, expected: true},
+        ]).forEach(function (test) {
+          it('value: ' + JSON.stringify(test.value) + ' ;result - ' + test.expected, function () {
+            obj.set('value', test.value);
+            expect(obj.get('isValid')).to.be.equal(test.expected);
+          });
+        });
+      });
+
+    });
+
+    describe('PercentageMixin', function () {
+
+      var obj;
+
+      beforeEach(function () {
+        obj = App.AlertConfigProperties.Parameter.create(App.AlertConfigProperties.Parameters.PercentageMixin, {});
+      });
+
+      describe('#isValid', function () {
+        Em.A([
+          {value: '', expected: false},
+          {value: 'abc', expected: false},
+          {value: 'g1', expected: false},
+          {value: '1g', expected: false},
+          {value: '123', expected: false},
+          {value: '23', expected: true},
+          {value: '123.8', expected: false},
+          {value: '5.8', expected: true},
+          {value: 123, expected: false},
+          {value: 23, expected: true},
+          {value: 123.8, expected: false},
+          {value: 5.8, expected: true}
+        ]).forEach(function (test) {
+          it('value: ' + JSON.stringify(test.value) + ' ;result - ' + test.expected, function () {
+            obj.set('value', test.value);
+            expect(obj.get('isValid')).to.be.equal(test.expected);
+          });
+        });
+      });
+
+    });
+
+  });
+
   describe('Threshold', function () {
 
     beforeEach(function () {