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/branch-dev-patch-upgrade Commit: 6d9e05995f6815a600021e0e84f3f29518989b36 Parents: 46f6030 Author: Oleg Nechiporenko <onechipore...@apache.org> Authored: Wed Feb 3 16:02:23 2016 +0200 Committer: Oleg Nechiporenko <onechipore...@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> + </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 () {