浏览代码

AMBARI-14563 Cover with unit tests recommendations flow 1. (ababiichuk)

ababiichuk 9 年之前
父节点
当前提交
f5b87e008c

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

@@ -136,6 +136,7 @@ var files = [
   'test/mappers/configs/service_config_version_mapper_test',
   'test/mappers/configs/themes_mapper_test',
   'test/mixins/common/configs/enhanced_configs_test',
+  'test/mixins/common/configs/config_recommendations_test',
   'test/mixins/common/configs/configs_saver_test',
   'test/mixins/common/configs/toggle_isrequired_test',
   'test/mixins/common/chart/storm_linear_time_test',

+ 1 - 1
ambari-web/app/controllers/main/service/info/configs.js

@@ -494,7 +494,7 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ConfigsLoader, A
     } else {
       App.config.removeRangerConfigs(this.get('stepConfigs'));
     }
-    this.getRecommendationsForDependencies(null, this._onLoadComplete.bind(this));
+    this.loadConfigRecommendations(null, this._onLoadComplete.bind(this));
     App.loadTimer.finish('Service Configs Page');
   },
 

+ 1 - 1
ambari-web/app/controllers/wizard/step7_controller.js

@@ -721,7 +721,7 @@ App.WizardStep7Controller = Em.Controller.extend(App.ServerValidatorMixin, App.E
     if (rangerService && !rangerService.get('isInstalled') && !rangerService.get('isSelected')) {
       App.config.removeRangerConfigs(self.get('stepConfigs'));
     }
-    this.loadServerSideConfigsRecommendations().always(this.completeConfigLoading.bind(this));
+    this.loadConfigRecommendations(null, this.completeConfigLoading.bind(this));
   },
 
   completeConfigLoading: function() {

+ 4 - 3
ambari-web/app/mixins/common/configs/config_recommendation_parser.js

@@ -99,12 +99,13 @@ App.ConfigRecommendationParser = Em.Mixin.create(App.ConfigRecommendations, {
 	 */
 	addByRecommendations: function (recommendationObject, parentProperties) {
 		for (var site in recommendationObject) {
-			if (Object.keys(recommendationObject[site].properties).length) {
+			var properties = recommendationObject[site].properties;
+			if (properties && Object.keys(properties).length) {
 				var stepConfig = App.config.getStepConfigForProperty(this.get('stepConfigs'), site), configs = [];
 				if (stepConfig) {
-					for (var propertyName in recommendationObject[site].properties) {
+					for (var propertyName in properties) {
 						if (this.allowUpdateProperty(parentProperties, propertyName, site)) {
-							this._addConfigByRecommendation(configs, propertyName, site, recommendationObject[site].properties[propertyName], parentProperties);
+							this._addConfigByRecommendation(configs, propertyName, site, properties[propertyName], parentProperties);
 						}
 					}
 					var mergedConfigs = configs.concat(stepConfig.get('configs'));

+ 35 - 24
ambari-web/app/mixins/common/configs/config_recommendations.js

@@ -53,17 +53,32 @@ App.ConfigRecommendations = Em.Mixin.create({
    * @param {string} configGroupName
    * @param {string} recommendedValue
    * @param {string} initialValue
-   * @param {Object[]}parentProperties
+   * @param {Object[]} parentProperties
    * @returns {recommendation}
    */
   applyRecommendation: function (name, fileName, configGroupName, recommendedValue, initialValue, parentProperties) {
-    var parentPropertiesNames = parentProperties ? parentProperties.map(function (p) {
+    try {
+      var parentPropertyIds = this.formatParentProperties(parentProperties);
+      var recommendation = this.getRecommendation(name, fileName, configGroupName);
+      if (recommendation) {
+        return this.updateRecommendation(recommendation, recommendedValue, parentPropertyIds);
+      }
+      return this.addRecommendation(name, fileName, configGroupName, recommendedValue, initialValue, parentPropertyIds);
+    } catch(e) {
+      console.error(e.message);
+    }
+  },
+
+  /**
+   * Format objects like {name: {String}, type: {String}} to config Id
+   *
+   * @param parentProperties
+   * @returns {*}
+   */
+  formatParentProperties: function(parentProperties) {
+    return Em.isArray(parentProperties) ? parentProperties.map(function (p) {
       return App.config.configId(p.name, p.type);
     }) : [];
-    var updated = this.updateRecommendation(name, fileName, configGroupName, recommendedValue, parentPropertiesNames);
-    if (!updated)
-      var added = this.addRecommendation(name, fileName, configGroupName, recommendedValue, initialValue, parentPropertiesNames);
-    return updated || added;
   },
 
   /**
@@ -74,10 +89,10 @@ App.ConfigRecommendations = Em.Mixin.create({
    * @param {string} configGroupName
    * @param {string} recommendedValue
    * @param {string} initialValue
-   * @param {string[]} parentPropertiesNames
+   * @param {string[]} parentPropertyIds
    * @returns {recommendation}
    */
-  addRecommendation: function (name, fileName, configGroupName, recommendedValue, initialValue, parentPropertiesNames) {
+  addRecommendation: function (name, fileName, configGroupName, recommendedValue, initialValue, parentPropertyIds) {
     Em.assert('name and fileName should be defined', name && fileName);
     var site = App.config.getConfigTagFromFileName(fileName);
     var service = App.config.get('serviceByConfigTypeMap')[site];
@@ -92,7 +107,7 @@ App.ConfigRecommendations = Em.Mixin.create({
 
       configGroup: configGroupName || "Default",
       initialValue: initialValue,
-      parentConfigs: parentPropertiesNames || [],
+      parentConfigs: parentPropertyIds || [],
       serviceName: service.get('serviceName'),
       allowChangeGroup: false,//TODO groupName!= "Default" && (service.get('serviceName') != this.get('selectedService.serviceName'))
       //TODO&& (App.ServiceConfigGroup.find().filterProperty('serviceName', service.get('serviceName')).length > 1), //TODO
@@ -121,28 +136,24 @@ App.ConfigRecommendations = Em.Mixin.create({
    * @param {recommendation} recommendation
    */
   removeRecommendationObject: function (recommendation) {
-    if (recommendation)
-      this.get('recommendations').removeObject(recommendation);
+    Em.assert('recommendation should be defined object', recommendation && typeof recommendation === 'object');
+    this.get('recommendations').removeObject(recommendation);
   },
 
   /**
    * Update recommended object
    *
-   * @param name
-   * @param fileName
-   * @param configGroupName
+   * @param recommendation
    * @param recommendedValue
-   * @param parentPropertiesNames
+   * @param parentPropertyIds
    * @returns {*|recommendation|null}
    */
-  updateRecommendation: function (name, fileName, configGroupName, recommendedValue, parentPropertiesNames) {
-    var recommendation = this.getRecommendation(name, fileName, configGroupName);
-    if (recommendation) {
-      Em.set(recommendation, 'recommendedValue', recommendedValue);
-      if (parentPropertiesNames && parentPropertiesNames.length) {
-        var mergedProperties = parentPropertiesNames.concat(Em.get(recommendation, 'parentPropertiesNames'));
-        Em.set(recommendation, 'parentPropertiesNames', mergedProperties);
-      }
+  updateRecommendation: function (recommendation, recommendedValue, parentPropertyIds) {
+    Em.assert('recommendation should be defined object', recommendation && typeof recommendation === 'object');
+    Em.set(recommendation, 'recommendedValue', recommendedValue);
+    if (parentPropertyIds && parentPropertyIds.length) {
+      var mergedProperties = parentPropertyIds.concat(Em.get(recommendation, 'parentConfigs') || []).uniq();
+      Em.set(recommendation, 'parentConfigs', mergedProperties);
     }
     return recommendation;
   },
@@ -178,7 +189,7 @@ App.ConfigRecommendations = Em.Mixin.create({
       return dcv.propertyName === name
         && dcv.propertyFileName === App.config.getConfigTagFromFileName(fileName)
         && dcv.configGroup === (configGroupName || "Default");
-    });
+    }) || null;
   },
 
   /**

+ 26 - 44
ambari-web/app/mixins/common/configs/enhanced_configs.js

@@ -35,6 +35,13 @@ App.EnhancedConfigsMixin = Em.Mixin.create(App.ConfigWithOverrideRecommendationP
    */
   forceUpdateBoundaries: false,
 
+  /**
+   * object with loaded recommendations
+   *
+   * @type {Object}
+   */
+  recommendationsConfigs: null,
+
   /**
    * flag is true when Ambari changes some of the dependent properties
    * @type {boolean}
@@ -142,32 +149,29 @@ App.EnhancedConfigsMixin = Em.Mixin.create(App.ConfigWithOverrideRecommendationP
    * @param {Function} onComplete
    * @returns {$.ajax|null}
    */
-  getRecommendationsForDependencies: function(changedConfigs, onComplete) {
-    if ((Em.isArray(changedConfigs) && changedConfigs.length > 0) || Em.isNone(this.get('recommendationsConfigs'))) {
+  loadConfigRecommendations: function(changedConfigs, onComplete) {
+    var updateDependencies = Em.isArray(changedConfigs) && changedConfigs.length > 0;
+    if (updateDependencies || Em.isNone(this.get('recommendationsConfigs'))) {
       var configGroup = this.get('selectedConfigGroup');
       var recommendations = this.get('hostGroups');
-      delete recommendations.config_groups;
 
       var dataToSend = {
-        recommend: 'configurations',
+        recommend: updateDependencies ? 'configuration-dependencies' : 'configurations',
         hosts: this.get('hostNames'),
         services: this.get('serviceNames')
       };
+      if (updateDependencies) {
+        dataToSend.changed_configurations = changedConfigs;
+      }
 
-      var clearConfigsOnAddService = configGroup.get('isDefault') && this.isConfigHasInitialState();
-      if (clearConfigsOnAddService && !Em.isNone(this.get('initialConfigValues'))) {
-        recommendations.blueprint.configurations = this.get('initialConfigValues');
+      recommendations.blueprint.configurations = blueprintUtils.buildConfigsJSON(this.get('stepConfigs'));
+
+      if (configGroup && !configGroup.get('isDefault') && configGroup.get('hosts.length') > 0) {
+        recommendations.config_groups = [this.buildConfigGroupJSON(this.get('selectedService.configs'), configGroup)];
       } else {
-        recommendations.blueprint.configurations = blueprintUtils.buildConfigsJSON(this.get('services'), this.get('stepConfigs'));
-        if (changedConfigs) {
-          dataToSend.recommend = 'configuration-dependencies';
-          dataToSend.changed_configurations = changedConfigs;
-        }
-      }
-      if (!configGroup.get('isDefault') && configGroup.get('hosts.length') > 0) {
-        var configGroups = this.buildConfigGroupJSON(this.get('selectedService.configs'), configGroup);
-        recommendations.config_groups = [configGroups];
+        delete recommendations.config_groups;
       }
+
       dataToSend.recommendations = recommendations;
       var self = this;
       return App.ajax.send({
@@ -175,8 +179,7 @@ App.EnhancedConfigsMixin = Em.Mixin.create(App.ConfigWithOverrideRecommendationP
         sender: this,
         data: {
           stackVersionUrl: App.get('stackVersionURL'),
-          dataToSend: dataToSend,
-          clearConfigsOnAddService: clearConfigsOnAddService
+          dataToSend: dataToSend
         },
         success: 'loadRecommendationsSuccess',
         error: 'loadRecommendationsError',
@@ -202,7 +205,8 @@ App.EnhancedConfigsMixin = Em.Mixin.create(App.ConfigWithOverrideRecommendationP
    * @returns {boolean}
    */
   isConfigHasInitialState: function() {
-    return !this.get('stepConfigs').filter(function(stepConfig) {
+    return this.get('selectedConfigGroup.isDefault') && !Em.isNone(this.get('recommendationsConfigs'))
+      && !this.get('stepConfigs').filter(function(stepConfig) {
       return stepConfig.get('changedConfigProperties').filter(function(c) {
         return !this.get('changedProperties').map(function(changed) {
           return App.config.configId(changed.propertyName, changed.propertyFileName);
@@ -211,28 +215,6 @@ App.EnhancedConfigsMixin = Em.Mixin.create(App.ConfigWithOverrideRecommendationP
     }, this).length;
   },
 
-
-  /**
-   * Set all config values to their default (initialValue)
-   */
-  clearConfigValues: function() {
-    this.get('stepConfigs').forEach(function(stepConfig) {
-      stepConfig.get('changedConfigProperties').forEach(function(c) {
-        var recommendedProperty = this.getRecommendation(c.get('name'), c.get('filename'), c.get('group.name'));
-        if (recommendedProperty) {
-          var initialValue = recommendedProperty.initialValue;
-          if (Em.isNone(initialValue)) {
-            stepConfig.get('configs').removeObject(c);
-          } else {
-            c.set('initialValue', initialValue);
-            c.set('value', initialValue);
-          }
-          this.removeRecommendationObject(recommendedProperty);
-        }
-      }, this)
-    }, this);
-  },
-
   /**
    * generates JSON with config group info to send it for recommendations
    * @param configs
@@ -270,11 +252,11 @@ App.EnhancedConfigsMixin = Em.Mixin.create(App.ConfigWithOverrideRecommendationP
    */
   loadRecommendationsSuccess: function (data, opt, params) {
     this._saveRecommendedValues(data, params.dataToSend.changed_configurations);
-    this.set("recommendationsConfigs", Em.get(data.resources[0] , "recommendations.blueprint.configurations"));
-    if (params.clearConfigsOnAddService) {
-      this.clearConfigValues();
+    if (this.isConfigHasInitialState()) {
+      this.undoRedoRecommended(this.get('recommendations'), false);
       this.clearAllRecommendations();
     }
+    this.set("recommendationsConfigs", Em.get(data, "resources.0.recommendations.blueprint.configurations"));
   },
 
   loadRecommendationsError: Em.K,

+ 3 - 78
ambari-web/app/mixins/common/serverValidator.js

@@ -87,18 +87,6 @@ App.ServerValidatorMixin = Em.Mixin.create({
     return this.get('isWizard') ? this.get('allSelectedServiceNames') : App.Service.find().mapProperty('serviceName');
   }.property('isWizard', 'allSelectedServiceNames'),
 
-  /**
-   * by default loads data from model otherwise must be overridden as computed property
-   * filter services that support server validation and concat with misc configs if Installer or current service
-   * @type {Array} - of objects (services)
-   */
-  services: function() {
-    var stackServices = App.StackService.find().filter(function(s) {
-      return this.get('serviceNames').contains(s.get('serviceName'));
-    }, this);
-    return this.get('isWizard') ? stackServices.concat(require("data/service_configs")) : stackServices;
-  }.property('serviceNames'),
-
   /**
    * by default loads data from model otherwise must be overridden as computed property
    * can be used for service|host configs pages
@@ -114,59 +102,6 @@ App.ServerValidatorMixin = Em.Mixin.create({
    */
   stepConfigs: null,
 
-  /**
-   * @method loadServerSideConfigsRecommendations
-   * load recommendations from server
-   * (used only during install)
-   * @returns {*}
-   */
-  loadServerSideConfigsRecommendations: function() {
-    /**
-     * if extended controller doesn't support recommendations or recommendations has been already loaded
-     * ignore this call but keep promise chain
-     */
-    if (!this.get('isControllerSupportsEnhancedConfigs') || !Em.isNone(this.get('recommendationsConfigs'))) {
-      return $.Deferred().resolve().promise();
-    }
-    var recommendations = this.get('hostGroups');
-    // send user's input based on stored configurations
-    recommendations.blueprint.configurations = blueprintUtils.buildConfigsJSON(this.get('services'), this.get('stepConfigs'));
-
-    // include cluster-env site to recommendations call
-    var miscService = this.get('services').findProperty('serviceName', 'MISC');
-    if (miscService) {
-      var miscConfigs = blueprintUtils.buildConfigsJSON([miscService], [this.get('stepConfigs').findProperty('serviceName', 'MISC')]);
-      var clusterEnv = App.permit(miscConfigs, 'cluster-env');
-      if (!App.isEmptyObject(clusterEnv)) {
-        $.extend(recommendations.blueprint.configurations, clusterEnv);
-      }
-      /** add user properties from misc tabs to proper filename **/
-      this.get('stepConfigs').findProperty('serviceName', 'MISC').get('configs').forEach(function(config) {
-        var tag = App.config.getConfigTagFromFileName(config.get('filename'));
-        if (recommendations.blueprint.configurations[tag] && tag != 'cluster-env') {
-          recommendations.blueprint.configurations[tag].properties[config.get('name')] = config.get('value');
-        }
-      })
-    }
-
-    this.set('initialConfigValues', recommendations.blueprint.configurations);
-    return App.ajax.send({
-      'name': 'config.recommendations',
-      'sender': this,
-      'data': {
-        stackVersionUrl: App.get('stackVersionURL'),
-        dataToSend: {
-          recommend: 'configurations',
-          hosts: this.get('hostNames'),
-          services: this.get('serviceNames'),
-          recommendations: recommendations
-        }
-      },
-      'success': 'loadRecommendationsSuccess',
-      'error': 'loadRecommendationsError'
-    });
-  },
-
   serverSideValidation: function () {
     var deferred = $.Deferred();
     this.set('configValidationFailed', false);
@@ -197,7 +132,6 @@ App.ServerValidatorMixin = Em.Mixin.create({
   runServerSideValidation: function (deferred) {
     var self = this;
     var recommendations = this.get('hostGroups');
-    var services = this.get('services');
     var stepConfigs = this.get('stepConfigs');
 
     return this.getBlueprintConfigurations().done(function(blueprintConfigurations){
@@ -226,28 +160,19 @@ App.ServerValidatorMixin = Em.Mixin.create({
    */
   getBlueprintConfigurations: function () {
     var dfd = $.Deferred();
-    var services = this.get('services');
     var stepConfigs = this.get('stepConfigs');
-    var allConfigTypes = [];
 
-    services.forEach(function (service) {
-      allConfigTypes = allConfigTypes.concat(Em.keys(service.get('configTypes')))
-    });
     // check if we have configs from 'cluster-env', if not, then load them, as they are mandatory for validation request
-    if (!allConfigTypes.contains('cluster-env')) {
+    if (!stepConfigs.findProperty('serviceName', 'MISC')) {
       this.getClusterEnvConfigsForValidation().done(function(clusterEnvConfigs){
-        services = services.concat(Em.Object.create({
-          serviceName: 'MISC',
-          configTypes: {'cluster-env': {}}
-        }));
         stepConfigs = stepConfigs.concat(Em.Object.create({
           serviceName: 'MISC',
           configs: clusterEnvConfigs
         }));
-        dfd.resolve(blueprintUtils.buildConfigsJSON(services, stepConfigs));
+        dfd.resolve(blueprintUtils.buildConfigsJSON(stepConfigs));
       });
     } else {
-      dfd.resolve(blueprintUtils.buildConfigsJSON(services, stepConfigs));
+      dfd.resolve(blueprintUtils.buildConfigsJSON(stepConfigs));
     }
     return dfd.promise();
   },

+ 10 - 17
ambari-web/app/utils/blueprint.js

@@ -299,7 +299,6 @@ module.exports = {
   /**
    * @method buildConfigsJSON - generates JSON according to blueprint format
    * @param {Em.Array} stepConfigs - array of Ember Objects
-   * @param {Array} services
    * @returns {Object}
    * Example:
    * {
@@ -317,24 +316,18 @@ module.exports = {
    *   }
    * }
    */
-  buildConfigsJSON: function(services, stepConfigs) {
+  buildConfigsJSON: function (stepConfigs) {
     var configurations = {};
-    services.forEach(function(service) {
-      var config = stepConfigs.findProperty('serviceName', service.get('serviceName'));
-      if (config && service.get('configTypes')) {
-        Object.keys(service.get('configTypes')).forEach(function(type) {
-          if(!configurations[type]){
-            configurations[type] = {
-              properties: {}
-            }
+    stepConfigs.forEach(function (stepConfig) {
+      stepConfig.get('configs').forEach(function (config) {
+        if (config.get('isRequiredByAgent')) {
+          var type = App.config.getConfigTagFromFileName(config.get('filename'));
+          if (!configurations[type]) {
+            configurations[type] = {properties: {}}
           }
-        });
-        config.get('configs').forEach(function(property){
-          if (configurations[property.get('filename').replace('.xml','')]){
-            configurations[property.get('filename').replace('.xml','')]['properties'][property.get('name')] = property.get('value');
-          }
-        });
-      }
+          configurations[type]['properties'][config.get('name')] = config.get('value');
+        }
+      });
     });
     return configurations;
   },

+ 2 - 2
ambari-web/app/views/common/controls_view.js

@@ -89,11 +89,11 @@ App.SupportsDependentConfigs = Ember.Mixin.create({
        if ((p && Em.get(p, 'propertyDependedBy.length') > 0 || Em.get(p, 'displayType') === 'user') && config.get('oldValue') !== config.get('value')) {
          var old = config.get('oldValue');
          config.set('oldValue', config.get('value'));
-         return controller.getRecommendationsForDependencies([{
+         return controller.loadConfigRecommendations([{
            "type": type,
            "name": name,
            "old_value": Em.isNone(old) ? config.get('initialValue') : old
-         }], false, function() {
+         }], function() {
            controller.removeCurrentFromDependentList(config, saveRecommended);
          });
       } else {

+ 5 - 5
ambari-web/test/controllers/wizard/step7_test.js

@@ -1104,8 +1104,8 @@ describe('App.InstallerStep7Controller', function () {
       sinon.stub(App.config, 'fileConfigsIntoTextarea', function(configs) {
         return configs;
       });
-      sinon.stub(installerStep7Controller, 'loadServerSideConfigsRecommendations', function() {
-        return $.Deferred().resolve();
+      sinon.stub(installerStep7Controller, 'loadConfigRecommendations', function(c, callback) {
+        return callback();
       });
       sinon.stub(installerStep7Controller, 'checkHostOverrideInstaller', Em.K);
       sinon.stub(installerStep7Controller, 'selectProperService', Em.K);
@@ -1128,15 +1128,15 @@ describe('App.InstallerStep7Controller', function () {
 
     afterEach(function () {
       App.config.fileConfigsIntoTextarea.restore();
-      installerStep7Controller.loadServerSideConfigsRecommendations.restore();
+      installerStep7Controller.loadConfigRecommendations.restore();
       installerStep7Controller.checkHostOverrideInstaller.restore();
       installerStep7Controller.selectProperService.restore();
       App.router.send.restore();
       App.StackService.find.restore();
     });
 
-    it('loadServerSideConfigsRecommendations is called once' , function () {
-     expect(installerStep7Controller.loadServerSideConfigsRecommendations.calledOnce).to.equal(true);
+    it('loadConfigRecommendations is called once' , function () {
+     expect(installerStep7Controller.loadConfigRecommendations.calledOnce).to.equal(true);
     });
     it('isRecommendedLoaded is true' , function () {
      expect(installerStep7Controller.get('isRecommendedLoaded')).to.equal(true);

+ 399 - 0
ambari-web/test/mixins/common/configs/config_recommendations_test.js

@@ -0,0 +1,399 @@
+/**
+ * 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');
+
+describe('App.ConfigRecommendations', function() {
+	var mixinObject =  Em.Controller.extend(App.ConfigRecommendations, {});
+	var instanceObject = mixinObject.create({});
+
+	beforeEach(function() {
+		instanceObject.set('recommendations', []);
+	});
+
+	describe('#applyRecommendation', function() {
+    beforeEach(function() {
+      sinon.stub(instanceObject, 'formatParentProperties', function(parentProperties) { return parentProperties} );
+      sinon.stub(App.config, 'get').withArgs('serviceByConfigTypeMap').returns({
+        'pFile': Em.Object.create({serviceName: 'sName', displayName: 'sDisplayName'})
+      });
+    });
+    afterEach(function() {
+      instanceObject.formatParentProperties.restore();
+      App.config.get.restore();
+    });
+
+    it('adds new recommendation', function() {
+      var res = instanceObject.applyRecommendation('pName', 'pFile', 'pGroup', 'pRecommended', 'pInitial', ['p_id']);
+      expect(res).to.eql({
+        saveRecommended: true,
+        saveRecommendedDefault: true,
+        propertyFileName: 'pFile',
+        propertyName: 'pName',
+        isDeleted: false,
+        notDefined: false,
+        configGroup: 'pGroup',
+        initialValue: 'pInitial',
+        parentConfigs: ['p_id'],
+        serviceName: 'sName',
+        allowChangeGroup: false,
+        serviceDisplayName: 'sDisplayName',
+        recommendedValue: 'pRecommended'
+      });
+      expect(instanceObject.getRecommendation('pName', 'pFile', 'pGroup')).to.eql(res);
+    });
+
+    it('updates recommendation', function() {
+			instanceObject.set('recommendations', [{
+				saveRecommended: true,
+				saveRecommendedDefault: true,
+				propertyFileName: 'pFile',
+				propertyName: 'pName',
+				isDeleted: false,
+				notDefined: false,
+				configGroup: 'pGroup',
+				initialValue: 'pInitial',
+				parentConfigs: ['p_id'],
+				serviceName: 'sName',
+				allowChangeGroup: false,
+				serviceDisplayName: 'sDisplayName',
+				recommendedValue: 'pRecommended'
+			}]);
+      expect(instanceObject.applyRecommendation('pName', 'pFile', 'pGroup', 'pRecommended1', 'pInitial', ['p_id1'])).to.eql({
+        saveRecommended: true,
+        saveRecommendedDefault: true,
+        propertyFileName: 'pFile',
+        propertyName: 'pName',
+        isDeleted: false,
+        notDefined: false,
+        configGroup: 'pGroup',
+        initialValue: 'pInitial',
+        parentConfigs: ['p_id1', 'p_id'],
+        serviceName: 'sName',
+        allowChangeGroup: false,
+        serviceDisplayName: 'sDisplayName',
+        recommendedValue: 'pRecommended1'
+      });
+    });
+	});
+
+  describe('#formatParentProperties', function() {
+    beforeEach(function() {
+      sinon.stub(App.config, 'configId', function(a,b) { return a + b; });
+    });
+    afterEach(function() {
+      App.config.configId.restore();
+    });
+
+    it('returns empty array if nothing was passed', function() {
+      expect(instanceObject.formatParentProperties(null)).to.eql([]);
+    });
+
+    it('returns config ids array', function() {
+      expect(instanceObject.formatParentProperties([{name: "n1", type: "t1"}, {name: "n2", type: "t2"}])).to.eql(["n1t1", "n2t2"]);
+    });
+  });
+
+	describe('#addRecommendation', function() {
+		var cases = [
+			{
+				title: 'add recommendation with full info',
+				name: 'pName', file: 'pFile.xml', group: 'pGroup', recommended: 'pRecommended', initial: 'pInitial', parent: ['p_id'],
+				service: Em.Object.create({serviceName: 'sName', displayName: 'sDisplayName'}),
+				result: {
+					saveRecommended: true,
+					saveRecommendedDefault: true,
+					propertyFileName: 'pFile',
+					propertyName: 'pName',
+					isDeleted: false,
+					notDefined: false,
+					configGroup: 'pGroup',
+					initialValue: 'pInitial',
+					parentConfigs: ['p_id'],
+					serviceName: 'sName',
+					allowChangeGroup: false,
+					serviceDisplayName: 'sDisplayName',
+					recommendedValue: 'pRecommended'
+				}
+			},
+			{
+				title: 'add recommendation with min info',
+				name: 'pName', file: 'pFile.xml',
+				service: Em.Object.create({serviceName: 'sName', displayName: 'sDisplayName'}),
+				result: {
+					saveRecommended: true,
+					saveRecommendedDefault: true,
+					propertyFileName: 'pFile',
+					propertyName: 'pName',
+					isDeleted: true,
+					notDefined: true,
+					configGroup: 'Default',
+					initialValue: undefined,
+					parentConfigs: [],
+					serviceName: 'sName',
+					allowChangeGroup: false,
+					serviceDisplayName: 'sDisplayName',
+					recommendedValue: undefined
+				}
+			}
+		];
+		cases.forEach(function(c) {
+			describe('successful add recommendation', function() {
+				var recommendation;
+				beforeEach(function() {
+					instanceObject.set('recommendations', []);
+					sinon.stub(App.config, 'get').withArgs('serviceByConfigTypeMap').returns({
+						'pFile': c.service
+					});
+					recommendation = instanceObject.addRecommendation(c.name, c.file, c.group, c.recommended, c.initial, c.parent);
+				});
+
+				afterEach(function() {
+					App.config.get.restore();
+				});
+
+				it(c.title, function() {
+					expect(recommendation).to.eql(c.result);
+				});
+
+				it(c.title + ' check recommendations collection', function() {
+					expect(instanceObject.get('recommendations.0')).to.eql(c.result);
+				});
+			})
+		});
+
+		it('throw exception when name, fileName', function() {
+			expect(instanceObject.addRecommendation.bind()).to.throw(Error, 'name and fileName should be defined');
+			expect(instanceObject.addRecommendation.bind(null, 'fname')).to.throw(Error, 'name and fileName should be defined');
+			expect(instanceObject.addRecommendation.bind('name', null)).to.throw(Error, 'name and fileName should be defined');
+		});
+	});
+
+	describe('#removeRecommendationObject', function () {
+		var recommendations = [
+			{
+				propertyName: 'p1',
+				propertyFileName: 'f1'
+			},
+			{
+				propertyName: 'p2',
+				propertyFileName: 'f2'
+			}
+		];
+
+		beforeEach(function () {
+			instanceObject.set('recommendations', recommendations);
+		});
+
+		it('remove recommendation', function () {
+			instanceObject.removeRecommendationObject(recommendations[1]);
+
+			expect(instanceObject.get('recommendations.length')).to.equal(1);
+			expect(instanceObject.get('recommendations.0')).to.eql({
+				propertyName: 'p1',
+				propertyFileName: 'f1'
+			});
+		});
+
+		it('remove recommendation that is not exist (don\'t do anything)', function () {
+			instanceObject.removeRecommendationObject({propertyName: 'any', 'propertyFileName': 'aby'});
+			expect(instanceObject.get('recommendations')).to.eql(recommendations);
+		});
+
+		it('throw error if recommendation is undefined ', function () {
+			expect(instanceObject.removeRecommendationObject.bind()).to.throw(Error, 'recommendation should be defined object');
+			expect(instanceObject.removeRecommendationObject.bind(null)).to.throw(Error, 'recommendation should be defined object');
+		});
+
+		it('throw error if recommendation is not an object ', function () {
+			expect(instanceObject.removeRecommendationObject.bind('recommendation')).to.throw(Error, 'recommendation should be defined object');
+			expect(instanceObject.removeRecommendationObject.bind(['recommendation'])).to.throw(Error, 'recommendation should be defined object');
+		});
+	});
+
+	describe('#updateRecommendation', function () {
+		it('update recommended value and parent properties', function () {
+			expect(instanceObject.updateRecommendation({'recommendedValue': 'v2', parentConfigs: ['id1']}, 'v1', ['id2']))
+				.to.eql({'recommendedValue': 'v1', parentConfigs: ['id2', 'id1']});
+		});
+
+		it('update recommended value and add parent properties', function () {
+			expect(instanceObject.updateRecommendation({}, 'v1', ['id1'])).to.eql({'recommendedValue': 'v1', parentConfigs: ['id1']});
+		});
+
+		it('update recommended value', function () {
+			expect(instanceObject.updateRecommendation({}, 'v1')).to.eql({'recommendedValue': 'v1'});
+			expect(instanceObject.updateRecommendation({'recommendedValue': 'v1'}, 'v2')).to.eql({'recommendedValue': 'v2'});
+		});
+
+		it('throw error if recommendation is undefined ', function () {
+			expect(instanceObject.updateRecommendation.bind()).to.throw(Error, 'recommendation should be defined object');
+			expect(instanceObject.updateRecommendation.bind(null)).to.throw(Error, 'recommendation should be defined object');
+		});
+
+		it('throw error if recommendation is not an object ', function () {
+			expect(instanceObject.updateRecommendation.bind('recommendation')).to.throw(Error, 'recommendation should be defined object');
+			expect(instanceObject.updateRecommendation.bind(['recommendation'])).to.throw(Error, 'recommendation should be defined object');
+		});
+	});
+
+	describe('#saveRecommendation', function() {
+
+    it('skip update since values are same', function() {
+      expect(instanceObject.saveRecommendation({saveRecommended: false, saveRecommendedDefault: false}, false)).to.be.false;
+    });
+
+    it('perform update since values are different', function() {
+      expect(instanceObject.saveRecommendation({saveRecommended: false, saveRecommendedDefault: false}, true)).to.be.true;
+    });
+
+    it('updates "saveRecommended" and "saveRecommendedDefault", set "false"', function() {
+      var res = {saveRecommended: true, saveRecommendedDefault: true};
+      instanceObject.saveRecommendation(res, false);
+      expect(res.saveRecommended).to.be.false;
+      expect(res.saveRecommendedDefault).to.be.false;
+    });
+
+    it('throw error if recommendation is undefined ', function () {
+      expect(instanceObject.updateRecommendation.bind()).to.throw(Error, 'recommendation should be defined object');
+      expect(instanceObject.updateRecommendation.bind(null)).to.throw(Error, 'recommendation should be defined object');
+    });
+
+    it('throw error if recommendation is not an object ', function () {
+      expect(instanceObject.updateRecommendation.bind('recommendation')).to.throw(Error, 'recommendation should be defined object');
+      expect(instanceObject.updateRecommendation.bind(['recommendation'])).to.throw(Error, 'recommendation should be defined object');
+    });
+	});
+
+	describe('#getRecommendation', function () {
+		var recommendations = [
+			{
+				propertyName: 'p1',
+				propertyFileName: 'f1',
+				configGroup: 'Default'
+			},
+			{
+				propertyName: 'p2',
+				propertyFileName: 'f2',
+				configGroup: 'group1'
+			},
+			{
+				propertyName: 'p1',
+				propertyFileName: 'f1',
+				configGroup: 'group1'
+			}
+		];
+
+		beforeEach(function () {
+			instanceObject.set('recommendations', recommendations);
+		});
+
+		it('get recommendation for default group', function () {
+			expect(instanceObject.getRecommendation('p1', 'f1')).to.eql(recommendations[0]);
+		});
+
+		it('get recommendation for default group', function () {
+			expect(instanceObject.getRecommendation('p1', 'f1', 'group1')).to.eql(recommendations[2]);
+		});
+
+		it('get recommendation for wrong group', function () {
+			expect(instanceObject.getRecommendation('p2', 'f2', 'group2')).to.equal(null);
+		});
+
+		it('get undefined recommendation', function () {
+			expect(instanceObject.getRecommendation('some', 'amy')).to.equal(null);
+		});
+
+		it('get throw error if undefined name or fileName passed', function () {
+			expect(instanceObject.getRecommendation.bind()).to.throw(Error, 'name and fileName should be defined');
+			expect(instanceObject.getRecommendation.bind('name')).to.throw(Error, 'name and fileName should be defined');
+			expect(instanceObject.getRecommendation.bind(null, 'fileName')).to.throw(Error, 'name and fileName should be defined');
+		});
+	});
+
+	describe('#cleanUpRecommendations', function() {
+		var cases = [
+			{
+				title: 'remove recommendations with same init and recommended values',
+				recommendations: [{
+					initialValue: 'v1', recommendedValue: 'v1'
+				}, {
+						initialValue: 'v1', recommendedValue: 'v2'
+				}],
+				cleanUpRecommendations: [{
+					initialValue: 'v1', recommendedValue: 'v2'
+				}]
+			},
+			{
+				title: 'remove recommendations with null init and recommended values',
+				recommendations: [{
+					initialValue: null, recommendedValue: null
+				}, {
+					recommendedValue: null
+				}, {
+					initialValue: null
+				},{
+					initialValue: null, recommendedValue: 'v1'
+				}, {
+					initialValue: 'v1', recommendedValue: null
+				}],
+				cleanUpRecommendations: [{
+					initialValue: null, recommendedValue: 'v1'
+				}, {
+					initialValue: 'v1', recommendedValue: null
+				}
+				]
+			}
+		];
+
+		cases.forEach(function(c) {
+			describe(c.title, function() {
+				beforeEach(function() {
+					instanceObject.set('recommendations', c.recommendations);
+					instanceObject.cleanUpRecommendations()
+				});
+				it('do clean up', function() {
+					expect(instanceObject.get('recommendations')).to.eql(c.cleanUpRecommendations);
+				});
+			});
+		});
+	});
+
+	describe('#clearRecommendationsByServiceName', function () {
+		beforeEach(function () {
+			instanceObject.set('recommendations', [{serviceName: 's1'}, {serviceName: 's2'}, {serviceName: 's3'}]);
+		});
+
+		it('remove with specific service names ', function () {
+			instanceObject.clearRecommendationsByServiceName(['s2','s3']);
+			expect(instanceObject.get('recommendations')).to.eql([{serviceName: 's1'}]);
+		});
+	});
+
+	describe('#clearAllRecommendations', function () {
+		beforeEach(function () {
+			instanceObject.set('recommendations', [{anyObject: 'o1'}, {anyObject: 'o2'}]);
+		});
+
+		it('remove all recommendations', function () {
+			instanceObject.clearAllRecommendations();
+			expect(instanceObject.get('recommendations.length')).to.equal(0);
+		});
+	});
+});
+

+ 0 - 54
ambari-web/test/mixins/common/serverValidator_test.js

@@ -130,59 +130,5 @@ describe('App.ServerValidatorMixin', function() {
       });
     });
   });
-
-  describe('#loadServerSideConfigsRecommendations', function() {
-    describe('Request on recommendations for only specified controllers', function() {
-      beforeEach(function() {
-        sinon.stub(App.ajax, 'send', function(args) { return args; });
-      });
-
-      afterEach(function() {
-        App.ajax.send.restore();
-      });
-
-      [
-        {
-          controllerName: '',
-          injectEnhancedConfigsMixin: false,
-          e: false
-        },
-        {
-          controllerName: 'wizardStep7Controller',
-          injectEnhancedConfigsMixin: true,
-          e: true
-        },
-        {
-          controllerName: 'kerberosWizardStep2Controller',
-          injectEnhancedConfigsMixin: true,
-          e: false
-        }
-      ].forEach(function(test) {
-        describe('controller "name": {0} using "EnhancedConfigsMixin": {1} recommendations called: {2}'.format(test.controllerName, test.injectEnhancedConfigsMixin, test.e), function() {
-          var mixed;
-          beforeEach(function () {
-            if (test.injectEnhancedConfigsMixin) {
-              mixed = Em.Object.extend(App.EnhancedConfigsMixin, App.ServerValidatorMixin);
-            } else {
-              mixed = Em.Object.extend(App.ServerValidatorMixin);
-            }
-            // mock controller name in mixed object directly
-            mixed.create({name: test.controllerName}).loadServerSideConfigsRecommendations();
-          });
-
-          it('request is ' + (test.e ? '' : 'not') + ' sent', function () {
-            expect(App.ajax.send.calledOnce).to.be.eql(test.e);
-          });
-
-          if (test.e) {
-            it('request is valid', function () {
-              expect(App.ajax.send.args[0][0].name).to.be.eql('config.recommendations');
-            });
-          }
-
-        });
-      });
-    });
-  });
 });
 

+ 20 - 15
ambari-web/test/utils/blueprint_test.js

@@ -243,16 +243,6 @@ describe('utils/blueprint', function() {
   describe('#buildConfigsJSON', function () {
     var tests = [
       {
-        "services": [
-          Em.Object.create({
-            serviceName: "YARN",
-            configTypes: {
-              "yarn-site": {},
-              "yarn-env": {}
-            },
-            isInstalled: true
-          })
-        ],
         "stepConfigs": [
           Em.Object.create({
             serviceName: "YARN",
@@ -260,17 +250,31 @@ describe('utils/blueprint', function() {
               Em.Object.create({
                 name: "p1",
                 value: "v1",
-                filename: "yarn-site.xml"
+                filename: "yarn-site.xml",
+                isRequiredByAgent: true
               }),
               Em.Object.create({
                 name: "p2",
                 value: "v2",
-                filename: "yarn-site.xml"
+                filename: "yarn-site.xml",
+                isRequiredByAgent: true
               }),
               Em.Object.create({
                 name: "p3",
                 value: "v3",
-                filename: "yarn-env.xml"
+                filename: "yarn-env.xml",
+                isRequiredByAgent: true
+              })
+            ]
+          }),
+          Em.Object.create({
+            serviceName: "MISC",
+            configs: [
+              Em.Object.create({
+                name: "user",
+                value: "yarn",
+                filename: "yarn-env.xml",
+                isRequiredByAgent: true
               })
             ]
           })
@@ -284,7 +288,8 @@ describe('utils/blueprint', function() {
           },
           "yarn-env": {
             "properties": {
-              "p3": "v3"
+              "p3": "v3",
+              "user": "yarn"
             }
           }
         }
@@ -292,7 +297,7 @@ describe('utils/blueprint', function() {
     ];
     tests.forEach(function (test) {
       it("generate configs for request (use in validation)", function () {
-        expect(blueprintUtils.buildConfigsJSON(test.services, test.stepConfigs)).to.eql(test.configurations);
+        expect(blueprintUtils.buildConfigsJSON(test.stepConfigs)).to.eql(test.configurations);
       });
     });
   });