Browse Source

AMBARI-10156 Editing configs with depended-by configs should trigger call to /recommendations. (ababiichuk)

aBabiichuk 10 years ago
parent
commit
b17747ec50

+ 87 - 0
ambari-web/app/assets/data/configurations/recommendations/configuration_dependencies.json

@@ -0,0 +1,87 @@
+{
+  "resources": [
+    {
+      "href": "http://c6405:8080/api/v1/stacks/HDP/versions/2.2/recommendations/5",
+      "hosts": [
+        "c6406.ambari.apache.org",
+        "c6405.ambari.apache.org"
+      ],
+      "services": [
+        "HDFS",
+        "MAPREDUCE2",
+        "ZOOKEEPER",
+        "YARN"
+      ],
+      "Recommendation": {
+        "id": 5
+      },
+      "Versions": {
+        "stack_name": "HDP",
+        "stack_version": "2.2"
+      },
+      "recommendations": {
+        "blueprint": {
+          "host_groups": [
+            {
+              "components": [
+                {
+                  "name": "DATANODE"
+                }
+              ],
+              "name": "host-group-1"
+            },
+            {
+              "components": [
+                {
+                  "name": "DATANODE"
+                }
+              ],
+              "name": "host-group-2"
+            }
+          ],
+          "configurations": {
+            "mapred-site": {
+              "properties": {
+                "mapreduce.map.java.opts": "-Xmx546m1",
+                "mapreduce.map.memory.mb": "6821",
+                "mapreduce.reduce.java.opts": "-Xmx546m1",
+                "mapreduce.reduce.memory.mb": "6821",
+                "mapreduce.task.io.sort.mb": "2731",
+                "yarn.app.mapreduce.am.command-opts": "-Xmx546m -Dhdp.version=${hdp.version}1",
+                "yarn.app.mapreduce.am.resource.mb": "6821"
+              }
+            },
+            "yarn-site": {
+              "properties": {
+                "yarn.nodemanager.resource.cpu-vcores": "11",
+                "yarn.nodemanager.resource.memory-mb": "2048",
+                "yarn.scheduler.maximum-allocation-mb": "20481",
+                "yarn.scheduler.minimum-allocation-mb": "6821"
+              }
+            }
+          }
+        },
+        "blueprint_cluster_binding": {
+          "host_groups": [
+            {
+              "hosts": [
+                {
+                  "name": "c6405.ambari.apache.org"
+                }
+              ],
+              "name": "host-group-1"
+            },
+            {
+              "hosts": [
+                {
+                  "name": "c6406.ambari.apache.org"
+                }
+              ],
+              "name": "host-group-2"
+            }
+          ]
+        }
+      }
+    }
+  ]
+}

+ 16 - 0
ambari-web/app/assets/data/stacks/HDP-2.2/configurations.json

@@ -7725,6 +7725,12 @@
             "property_description" : "By default, Tez uses the java options from map tasks. Use this property to override that value.",
             "property_name" : "hive.tez.container.size",
             "property_type" : [ ],
+            "property_depended_by": [
+              {
+                "property_type": "hdfs-site",
+                "property_name": "dfs.replication.max"
+              }
+            ],
             "property_value" : "682",
             "service_name" : "HIVE",
             "stack_name" : "HDP",
@@ -16759,6 +16765,16 @@
             "property_description" : "\n      The minimum allocation for every container request at the RM,\n      in MBs. Memory requests lower than this won't take effect,\n      and the specified value will get allocated at minimum.\n    ",
             "property_name" : "yarn.scheduler.minimum-allocation-mb",
             "property_type" : [ ],
+            "property_depended_by": [
+              {
+                "property_type": "hive-site",
+                "property_name": "hive.tez.container.size"
+              },
+              {
+                "property_type": "mapred-site",
+                "property_name": "mapreduce.map.memory.mb"
+              }
+            ],
             "property_value" : "512",
             "service_name" : "YARN",
             "stack_name" : "HDP",

+ 1 - 1
ambari-web/app/controllers/global/cluster_controller.js

@@ -280,7 +280,7 @@ App.ClusterController = Em.Controller.extend({
         });
 
         updater.updateServiceMetric(function () {
-
+          App.config.loadConfigsFromStack(App.Service.find().mapProperty('serviceName'));
           updater.updateComponentConfig(function () {
             self.updateLoadStatus('componentConfigs');
           });

+ 1 - 1
ambari-web/app/controllers/global/configuration_controller.js

@@ -84,7 +84,7 @@ App.ConfigurationController = Em.Controller.extend({
       jqXhr.done(function (data) {
         configTags = data.Clusters.desired_configs;
         tags.forEach(function (_tag) {
-          if (_tag.siteName && !_tag.tagName) {
+          if (_tag.siteName && configTags[_tag.siteName] && !_tag.tagName) {
             _tag.tagName = configTags[_tag.siteName].tag;
           }
         }, self);

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

@@ -64,6 +64,14 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
     return App.ServiceConfigVersion.find(this.get('content.serviceName') + "_" + this.get('selectedVersion')).get('isCurrent');
   }.property('selectedVersion', 'content.serviceName', 'dataIsLoaded'),
 
+  /**
+   * @type {[Object]} that will contain items like
+   *{
+   *  "type": "yarn-site",
+   *  "name": "yarn.nodemanager.resource.memory-mb"
+   *}
+   */
+  changedConfigWithDependencies: [],
   /**
    * @type {boolean}
    */
@@ -101,12 +109,6 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
    * }
    */
   loadedClusterSiteToTagMap: {},
-  /**
-   * Holds the actual base service-config server data uploaded.
-   * This is used by the host-override mechanism to update host
-   * specific values.
-   */
-  savedSiteNameToServerServiceConfigDataMap: {},
 
   isSubmitDisabled: function () {
     return (!(this.get('stepConfigs').everyProperty('errorCount', 0)) || this.get('isApplyingChanges'));
@@ -224,7 +226,6 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
     this.get('uiConfigs').clear();
     this.set('loadedGroupToOverrideSiteToTagMap', {});
     this.set('serviceConfigVersionNote', '');
-    this.set('savedSiteNameToServerServiceConfigDataMap', {});
     if (this.get('serviceConfigTags')) {
       this.set('serviceConfigTags', null);
     }
@@ -321,6 +322,73 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
     }));
   },
 
+  /**
+   * this method defines dependent file names for configs
+   * and load them to model
+   * @method loadDependentConfigs
+   */
+  loadDependentConfigs: function() {
+    /**
+     * filter out configs for current service with
+     * <code>propertyDependedBy<code>
+     * @type {Array}
+     */
+    var serviceStackProperties = App.StackConfigProperty.find().filter(function(stackProperty) {
+      return stackProperty.get('serviceName') === this.get('content.serviceName') && stackProperty.get('propertyDependedBy.length') > 0
+    }, this);
+
+    /**
+     * defines what fileNames should UI load
+     */
+    serviceStackProperties.forEach(function(serviceStackProperty) {
+      this.calculateDependentFileNames(serviceStackProperty);
+    }, this);
+
+    var serviceConfigsToLoad = this.getServiceNamesForConfigs();
+
+    /**
+     * load serviceConfigVersion
+     * by serviceName that has dependent properties
+     */
+    if (serviceConfigsToLoad.length > 0) {
+      App.config.loadConfigCurrentVersions(serviceConfigsToLoad);
+    }
+  },
+
+  /**
+   * get required fileNames that has dependencies
+   * @returns {string[]}
+   */
+  getServiceNamesForConfigs: function() {
+    return App.StackService.find().filter(function(s) {
+      for (var i = 0; i < this.get('dependentFileNames.length'); i++) {
+        if (Object.keys(s.get('configTypes')).contains(App.config.getConfigTagFromFileName(this.get('dependentFileNames')[i])))
+          return true;
+      }
+      return false;
+    }, this).mapProperty('serviceName').concat(this.get('content.serviceName'));
+  },
+
+  /**
+   * dependent file names for configs
+   */
+  dependentFileNames: [],
+
+  /**
+   * defines file names for configs
+   * @param {App.StackConfigProperty} stackProperty
+   */
+  calculateDependentFileNames: function(stackProperty) {
+    if (stackProperty.get('propertyDependedBy.length') > 0) {
+      stackProperty.get('propertyDependedBy').forEach(function(dependent) {
+        if (!this.get('dependentFileNames').contains(dependent.type)) {
+          this.get('dependentFileNames').push(dependent.type);
+        }
+        this.calculateDependentFileNames(App.StackConfigProperty.find(dependent.name + "_" + dependent.type));
+      }, this);
+    }
+  },
+
   /**
    * get service config versions of current service
    */
@@ -917,6 +985,7 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
     this.set('versionLoaded', true);
     this.set('hash', this.getHash());
     this.set('isInit', false);
+    this.loadDependentConfigs();
   },
 
   /**
@@ -1172,6 +1241,63 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
     return dirChanged;
   },
 
+  /**
+   *
+   * @param modifiedProperties
+   * @param configs
+   * @returns {Array}
+   */
+  generateModifiedConfigs: function(modifiedProperties, configs) {
+
+    var modifiedConfigs = modifiedProperties
+      // get file names and add file names that was modified, for example after property removing
+      .mapProperty('filename').concat(this.get('modifiedFileNames')).uniq()
+      // get configs by filename
+      .map(function(fileName) {
+        return configs.filterProperty('filename', fileName);
+      });
+
+    /**
+     * make same operations to find what configs need to be saved
+     * for configs from model
+     * @type {Ember.Enumerable}
+     */
+    var modifiedFileNames = App.ConfigProperty.find().filter(function(cp) {
+      return cp.get('isNotDefaultValue') && this.get('dependentFileNames').contains(cp.get('fileName'));
+    }, this).mapProperty('fileName').uniq();
+
+    /**
+     * create default ServiceConfigProperty objects from model
+     * and pushing properties that need to be saved to modified configs
+     * @type {Array}
+     */
+    App.ConfigProperty.find().filter(function(cp) {
+      return modifiedFileNames.contains(cp.get('fileName'));
+    }).forEach(function(configFromModel) {
+      var configData = {
+        name: configFromModel.get('name'),
+        displayName: configFromModel.get('stackConfigProperty.displayName'),
+        serviceName: configFromModel.get('stackConfigProperty.serviceName'),
+        value: configFromModel.get('value'),
+        defaultValue: configFromModel.get('defaultValue'),
+        filename: App.config.getOriginalFileName(configFromModel.get('fileName')),
+        isFinal: configFromModel.get('isFinal')
+      };
+      if (configFromModel.get('stackConfigProperty.serviceName') === this.get('content.serviceName')) {
+        var confObject = modifiedConfigs.findProperty('name', configFromModel.get('name'));
+        if (confObject && configFromModel.get('allowSave')) {
+          confObject.set('value', configFromModel.get('value'));
+        } else {
+          modifiedConfigs.push(App.ServiceConfigProperty.create(configData));
+        }
+      } else {
+        modifiedConfigs.push(App.ServiceConfigProperty.create(configData));
+      }
+    }, this);
+
+    return modifiedConfigs;
+  },
+
   /**
    * Save changed configs and config groups
    */
@@ -1184,28 +1310,62 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
       if (this.get('content.serviceName') === 'YARN') {
         configs = App.config.textareaIntoFileConfigs(configs, 'capacity-scheduler.xml');
       }
-      var modifiedConfigs = configs
-        // get only modified and created configs
-        .filter(function(config) { return config.get('isNotDefaultValue') || config.get('isNotSaved'); })
-        // get file names and add file names that was modified, for example after property removing
-        .mapProperty('filename').concat(this.get('modifiedFileNames')).uniq()
-        // get configs by filename
-        .map(function(fileName) {
-          return configs.filterProperty('filename', fileName);
-        });
-      if (!!modifiedConfigs.length) {
-        // concatenate results
-        modifiedConfigs = modifiedConfigs.reduce(function(current, prev) { return current.concat(prev); });
-      }
 
-      // save modified original configs that have no group
-      this.saveSiteConfigs(modifiedConfigs.filter(function(config) { return !config.get('group'); }));
+      /**
+       * generates list of properties that was chabged
+       * @type {Array}
+       */
+      var modifiedProperties = configs
+        // get only modified and created configs
+        .filter(function(config) { return config.get('isNotDefaultValue') || config.get('isNotSaved'); });
 
       /**
-       * First we put cluster configurations, which automatically creates /configurations
-       * resources. Next we update host level overrides.
+       * check if some of properties that was changed has dependencies;
        */
-      this.doPUTClusterConfigurations();
+      modifiedProperties.forEach(function(p) {
+        /**
+         * step configs don't have <code>propertyDependedBy<code> property
+         * so we need to look in <code>stackConfigProperty<code>;
+         * use <code>App.ConfigProperty<code> that has link to <code>stackConfigProperty<code>
+         */
+        var cfgFromModel = App.ConfigProperty.find().find(function(cp) {
+          return cp.get('name') === p.get('name') && cp.get('fileName') === p.get('filename');
+        }, this);
+
+        if (cfgFromModel) {
+
+          cfgFromModel.set('defaultValue', p.get('value'));
+          /**
+           * generates <code>changedConfigWithDependencies<code>
+           * this array will be send for recommendations as <code>changed_configurations<code>
+           */
+          if (cfgFromModel.get('stackConfigProperty.propertyDependedBy.length') > 0) {
+            this.get('changedConfigWithDependencies').push({
+              "type": cfgFromModel.get('fileName'),
+              "name": cfgFromModel.get("name")
+            });
+          }
+        }
+      }, this);
+
+      this.getRecommendationsForDependencies(this.get('changedConfigWithDependencies')).done(function() {
+
+        var modifiedConfigs = self.generateModifiedConfigs(modifiedProperties, configs);
+
+        if (!!modifiedConfigs.length) {
+          // concatenate results
+          modifiedConfigs = modifiedConfigs.reduce(function(current, prev) { return current.concat(prev); });
+        }
+        // save modified original configs that have no group
+        self.saveSiteConfigs(modifiedConfigs.filter(function(config) { return !config.get('group'); }));
+
+        /**
+         * First we put cluster configurations, which automatically creates /configurations
+         * resources. Next we update host level overrides.
+         */
+        self.doPUTClusterConfigurations();
+      });
+
     } else {
       var overridenConfigs = [];
       var groupHosts = [];
@@ -1866,6 +2026,12 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
   doPUTClusterConfigurations: function () {
     this.set('saveConfigsFlag', true);
     var serviceConfigTags = this.get('serviceConfigTags');
+    /**
+     * adding config tags for dependentConfigs
+     */
+    for (var i = 0; i < this.get('dependentFileNames.length'); i++) {
+      serviceConfigTags.pushObject({siteName: this.get('dependentFileNames')[i]});
+    }
     this.setNewTagNames(serviceConfigTags);
     var siteNameToServerDataMap = {};
     var configsToSave = [];
@@ -1882,7 +2048,6 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
     } else {
       this.onDoPUTClusterConfigurations();
     }
-    this.set("savedSiteNameToServerServiceConfigDataMap", siteNameToServerDataMap);
   },
 
   /**
@@ -1904,7 +2069,7 @@ App.MainServiceInfoConfigsController = Em.Controller.extend(App.ServerValidatorM
         }
         break;
       default:
-        var filename = (App.config.get('filenameExceptions').contains(siteName)) ? siteName : siteName + '.xml';
+        var filename = App.config.getOriginalFileName(siteName);
         if (filename === 'mapred-queue-acls.xml') {
           return null;
         }

+ 19 - 0
ambari-web/app/mappers/configs/config_versions_mapper.js

@@ -40,6 +40,7 @@ App.configVersionsMapper = App.QuickDataMapper.create({
     var configVersions = [];
     var itemIds = {};
     var serviceToHostMap = {};
+    var requestedProperties = [];
 
     if (json && json.items) {
       json.items.forEach(function (item, index) {
@@ -71,6 +72,8 @@ App.configVersionsMapper = App.QuickDataMapper.create({
 
               var property = {
                 id: key  + '_' + type + '_' + item.service_config_version,
+                name: key,
+                file_name: App.config.getOriginalFileName(type),
                 config_version_id: parsedItem.id,
                 stack_config_property_id: key  + '_' + type
               };
@@ -78,6 +81,7 @@ App.configVersionsMapper = App.QuickDataMapper.create({
               property.is_final = property.default_is_final = !!item.properties_attributes && item.properties_attributes.final[key] === "true";
 
               properties.push(property);
+              requestedProperties.push(property.id);
             }
           }, this);
         }
@@ -117,6 +121,21 @@ App.configVersionsMapper = App.QuickDataMapper.create({
       });
       App.store.commit();
       App.store.loadMany(this.get('model'), configVersions);
+
+      this.deleteUnusedProperties(requestedProperties);
     }
+  },
+
+
+  /**
+   * delete unused Properties
+   * @param requestedProperties
+   */
+  deleteUnusedProperties: function(requestedProperties) {
+    App.ConfigProperty.find().filterProperty('id').forEach(function(p) {
+      if (!requestedProperties.contains(p.get('id'))) {
+        this.deleteRecord(p);
+      }
+    }, this);
   }
 });

+ 7 - 0
ambari-web/app/messages.js

@@ -249,6 +249,7 @@ Em.I18n.translations = {
   'common.dismiss': "Dismiss",
   'common.stdout': "stdout",
   'common.stderr': "stderr",
+  'common.fileName': 'File name',
 
   'models.alert_instance.tiggered.verbose': "Occured on {0} <br> Checked on {1}",
   'models.alert_definition.triggered.verbose': "Occured on {0}",
@@ -332,6 +333,12 @@ Em.I18n.translations = {
   'popup.invalid.KDC.admin.principal': 'Admin principal',
   'popup.invalid.KDC.admin.password': 'Admin password',
 
+  'popup.dependent.configs.header': 'Dependent Properties',
+  'popup.dependent.configs.title': 'Properties that was changed has dependent properties. It\'s recommended to update these properties!',
+  'popup.dependent.configs.table.saveProperty': 'Save property',
+  'popup.dependent.configs.table.currentValue': 'Current value',
+  'popup.dependent.configs.table.recommendedValue': 'Recommended value',
+
   'login.header':'Sign in',
   'login.username':'Username',
   'login.loginButton':'Sign in',

+ 80 - 5
ambari-web/app/mixins/common/serverValidator.js

@@ -118,19 +118,94 @@ App.ServerValidatorMixin = Em.Mixin.create({
       return $.Deferred().resolve();
     }
     return App.ajax.send({
-      'name': 'wizard.step7.loadrecommendations.configs',
+      'name': 'config.recommendations',
       'sender': this,
       'data': {
         stackVersionUrl: App.get('stackVersionURL'),
-        hosts: this.get('hostNames'),
-        services: this.get('serviceNames'),
-        recommendations: this.get('hostGroups')
+        dataToSend: {
+          recommend: 'configurations',
+          hosts: this.get('hostNames'),
+          services: this.get('serviceNames'),
+          recommendations: this.get('hostGroups')
+        }
       },
       'success': 'loadRecommendationsSuccess',
       'error': 'loadRecommendationsError'
     });
   },
 
+  /**
+   *
+   * @param changedConfigs
+   */
+  getRecommendationsForDependencies: function(changedConfigs) {
+    var dfd = $.Deferred();
+    if (Em.isArray(changedConfigs) && changedConfigs.length > 0) {
+      var recommendations = this.get('hostGroups');
+      recommendations.blueprint.configurations = blueprintUtils.buildConfigsJSON(this.get('services'), this.get('stepConfigs'));
+
+      var dataToSend = {
+        recommend: 'configurations',
+        hosts: this.get('hostNames'),
+        services: this.get('serviceNames'),
+        recommendations: recommendations
+      };
+      if (App.get('supports.enhancedConfigs')) {
+        dataToSend.recommend = 'configuration-dependencies';
+        dataToSend.changed_configurations = changedConfigs;
+      }
+      App.ajax.send({
+        name: 'config.recommendations',
+        sender: this,
+        data: {
+          stackVersionUrl: App.get('stackVersionURL'),
+          dataToSend: dataToSend,
+          dfd: dfd
+        },
+        success: 'dependenciesSuccess',
+        error: 'dependenciesError'
+      });
+    } else {
+      dfd.resolve();
+    }
+    return dfd.promise();
+  },
+
+  /**
+   *
+   * @param data
+   * @param opt
+   * @param params
+   */
+  dependenciesSuccess: function(data, opt, params) {
+    Em.assert('invalid data', data && data.resources[0] && Em.get(data.resources[0], 'recommendations.blueprint.configurations'));
+    var configs = data.resources[0].recommendations.blueprint.configurations;
+    var currentProperties = App.ConfigProperty.find().filterProperty('configVersion.isCurrent').filterProperty('configVersion.groupId', -1);
+    for (var key in configs) {
+      for (var propertyName in configs[key].properties) {
+        var property = currentProperties.findProperty('name', propertyName)
+        if (property) {
+          property.set('value', configs[key].properties[propertyName]);
+        }
+      }
+    }
+    var configsToShow = currentProperties.filterProperty('isNotDefaultValue');
+    App.showDependentConfigsPopup(configsToShow, params.dfd);
+  },
+
+  /**
+   *
+   * @param jqXHR
+   * @param ajaxOptions
+   * @param error
+   * @param opt
+   * @param params
+   */
+  dependenciesError: function(jqXHR, ajaxOptions, error, opt, params) {
+    App.ajax.defaultErrorHandler(jqXHR, opt.url, opt.method, jqXHR.status);
+    params.dfd.reject();
+  },
+
   /**
    * @method loadRecommendationsSuccess
    * success callback after loading recommendations
@@ -280,7 +355,7 @@ App.ServerValidatorMixin = Em.Mixin.create({
         },
         bodyClass: Em.View.extend({
           controller: self,
-          templateName: require('templates/common/configs/config_recommendation_popup')
+          templateName: require('templates/common/modal_popups/config_recommendation_popup')
         })
       });
     } else {

+ 17 - 0
ambari-web/app/models/configs/config_property.js

@@ -25,6 +25,17 @@ App.ConfigProperty = DS.Model.extend({
    */
   id: DS.attr('string'),
 
+  /**
+   * config property name
+   * @property {string}
+   */
+  name: DS.attr('string'),
+
+  /**
+   * config property name
+   * @property {string}
+   */
+  fileName: DS.attr('string'),
   /**
    * value of property
    * by default is same as <code>defaultValue<code>
@@ -108,6 +119,12 @@ App.ConfigProperty = DS.Model.extend({
    */
   isHiddenByFilter: DS.attr('boolean', {defaultValue: false}),
 
+  /**
+   * properties with this flag set to false will not be saved
+   * @property {boolean}
+   */
+  allowSave: DS.attr('boolean', {defaultValue: true}),
+
   /**
    * Don't show "Undo" for hosts on Installer Step7
    * if value is true

+ 0 - 0
ambari-web/app/templates/common/configs/config_recommendation_popup.hbs → ambari-web/app/templates/common/modal_popups/config_recommendation_popup.hbs


+ 29 - 0
ambari-web/app/templates/common/modal_popups/dependent_configs_list.hbs

@@ -0,0 +1,29 @@
+<div class="alert alert-warning">
+  {{t popup.dependent.configs.title}}
+</div>
+<div id="config-validation-warnings" class="limited-height-2">
+  <table class="table no-borders">
+    <thead>
+    <tr>
+      <th>{{t popup.dependent.configs.table.saveProperty}}</th>
+      <th>{{t common.property}}</th>
+      <th>{{t common.fileName}}</th>
+      <th>{{t common.service}}</th>
+      <th>{{t popup.dependent.configs.table.currentValue}}</th>
+      <th>{{t popup.dependent.configs.table.recommendedValue}}</th>
+    </tr>
+    </thead>
+    <tbody>
+    {{#each config in view.parentView.configs}}
+      <tr>
+        <td>{{view Em.Checkbox checkedBinding="config.allowSave"}}</td>
+        <td>{{config.name}}</td>
+        <td>{{config.fileName}}</td>
+        <td>{{config.stackConfigProperty.serviceName}}</td>
+        <td>{{config.defaultValue}}</td>
+        <td>{{config.value}}</td>
+      </tr>
+    {{/each}}
+    </tbody>
+  </table>
+</div>

+ 9 - 9
ambari-web/app/utils/ajax/ajax.js

@@ -630,7 +630,7 @@ var urls = {
   },
 
   'configs.stack_configs.load.services': {
-    'real': '{stackVersionUrl}/services?StackServices/service_name.in({serviceList})?fields=configurations/*,StackServices/config_types/*',
+    'real': '{stackVersionUrl}/services?StackServices/service_name.in({serviceList})&fields=configurations/*,StackServices/config_types/*',
     'mock': '/data/stacks/HDP-2.2/configurations.json'
   },
 
@@ -661,6 +661,10 @@ var urls = {
     'mock': '/data/configurations/config_versions.json'
   },
 
+  'configs.config_versions.load.current_versions': {
+    'real': '/clusters/{clusterName}/configurations/service_config_versions?service_name.in({serviceNames})&group_id=-1&is_current=true&fields=*',
+    'mock': '/data/configurations/config_versions.json'
+  },
 
 
   'service.load_config_groups': {
@@ -1804,18 +1808,14 @@ var urls = {
 
 
   // TODO: merge with wizard.loadrecommendations query
-  'wizard.step7.loadrecommendations.configs': {
+  'config.recommendations': {
     'real': '{stackVersionUrl}/recommendations',
-    'mock': '/data/stacks/HDP-2.1/recommendations_configs.json',
+    'mock': '/data/configurations/recommendations/configuration_dependencies.json',
+    //'mock': '/data/stacks/HDP-2.1/recommendations_configs.json',
     'type': 'POST',
     'format': function (data) {
       return {
-        data: JSON.stringify({
-          hosts: data.hosts,
-          services: data.services,
-          recommendations: data.recommendations,
-          recommend: "configurations"
-        })
+        data: JSON.stringify(data.dataToSend)
       }
     }
   },

+ 17 - 1
ambari-web/app/utils/config.js

@@ -1336,7 +1336,7 @@ App.config = Em.Object.create({
   textareaIntoFileConfigs: function (configs, filename) {
     var complexConfigName = this.get('complexConfigsTemplate').findProperty('filename', filename).name;
     var configsTextarea = configs.findProperty('name', complexConfigName);
-    if (configsTextarea) {
+    if (configsTextarea && !App.get('testMode')) {
       var properties = configsTextarea.get('value').split('\n');
 
       properties.forEach(function (_property) {
@@ -1910,6 +1910,22 @@ App.config = Em.Object.create({
     return App.ajax.send($.extend({sender: this, success: 'saveConfigVersionsToModel'}, info));
   },
 
+  /**
+   *
+   * @param serviceNames
+   * @returns {$.ajax}
+   */
+  loadConfigCurrentVersions: function (serviceNames) {
+    Em.assert('serviceNames should be not empty array', Em.isArray(serviceNames) && serviceNames.length > 0);
+    return App.ajax.send({
+      name: 'configs.config_versions.load.current_versions',
+      sender: this,
+      data: {
+        serviceNames: serviceNames.join(",")
+      },
+      success: 'saveConfigVersionsToModel'
+    });
+  },
   /**
    * generate ajax info
    * @param {string} [serviceName=null]

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

@@ -33,6 +33,7 @@ require('views/common/modal_popups/prompt_popup');
 require('views/common/modal_popups/reload_popup');
 require('views/common/modal_popups/cluster_check_popup');
 require('views/common/modal_popups/invalid_KDC_popup');
+require('views/common/modal_popups/dependent_configs_list_popup');
 require('views/common/editable_list');
 require('views/common/rolling_restart_view');
 require('views/common/select_custom_date_view');

+ 66 - 0
ambari-web/app/views/common/modal_popups/dependent_configs_list_popup.js

@@ -0,0 +1,66 @@
+/**
+ * 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');
+
+/**
+ * Show confirmation popup
+ * @param {[Object]} configs
+ * we use this parameter to defer saving configs before we make some decisions.
+ * @param {$.Deferred} dfd
+ * @return {App.ModalPopup}
+ */
+App.showDependentConfigsPopup = function (configs, dfd) {
+  if (!configs || configs.length === 0) {
+    dfd.resolve();
+  }
+  return App.ModalPopup.show({
+    encodeBody: false,
+    primary: Em.I18n.t('common.save'),
+    secondary: Em.I18n.t('common.cancel'),
+    third: Em.I18n.t('common.discard'),
+    header: Em.I18n.t('popup.dependent.configs.header'),
+    classNames: ['full-width-modal'],
+    configs: configs,
+    bodyClass: Em.View.extend({
+      templateName: require('templates/common/modal_popups/dependent_configs_list')
+    }),
+    onPrimary: function () {
+      this.hide();
+      configs.filterProperty('allowSave', false).forEach(function(c) {
+        c.set('value', c.get('defaultValue'));
+      });
+      dfd.resolve();
+    },
+    onSecondary: function () {
+      App.get('router.mainServiceInfoConfigsController').set('isApplyingChanges', false);
+      dfd.reject();
+      this.hide();
+    },
+    onThird: function () {
+      App.get('router.mainServiceInfoConfigsController').set('isApplyingChanges', false);
+      App.get('router.mainServiceInfoConfigsController').set('preSelectedConfigVersion', null);
+      App.get('router.mainServiceInfoConfigsController').onConfigGroupChange();
+      dfd.reject();
+      this.hide();
+    },
+    onClose:  function () {
+      this.onSecondary();
+    }
+  });
+};

+ 44 - 0
ambari-web/test/controllers/main/service/info/config_test.js

@@ -1304,4 +1304,48 @@ describe("App.MainServiceInfoConfigsController", function () {
     });
   });
 
+
+  describe('#calculateDependentFileNames()', function() {
+
+    beforeEach(function() {
+      mainServiceInfoConfigsController.set('dependentFileNames', []);
+      App.resetDsStoreTypeMap(App.StackConfigProperty);
+      App.StackConfigProperty.createRecord({id: 'name1_site1', propertyDependedBy: [{
+        type: 'site2',
+        name: 'name2'
+      }]});
+      App.StackConfigProperty.createRecord({id: 'name2_site2', propertyDependedBy: [{
+          type: 'site3',
+          name: 'name3'
+        },
+        {
+          type: 'site4',
+          name: 'name4'
+        }]
+      });
+      App.StackConfigProperty.createRecord({id: 'name3_site3', propertyDependedBy: []});
+      App.StackConfigProperty.createRecord({id: 'name4_site4', propertyDependedBy: []});
+    });
+
+    it('adds all file names that need to be loaded to dependentFileNames', function() {
+      mainServiceInfoConfigsController.calculateDependentFileNames(App.StackConfigProperty.find('name1_site1'));
+      expect(mainServiceInfoConfigsController.get('dependentFileNames').toArray()).to.eql(["site2", "site3", "site4"]);
+    });
+  });
+
+  describe('#getServiceNamesForConfigs()', function() {
+    it('returns serviceNames which configs need to be loaded', function() {
+      mainServiceInfoConfigsController.set('content', {});
+      mainServiceInfoConfigsController.set('content.serviceName', 'currentService');
+      mainServiceInfoConfigsController.set('dependentFileNames', ['site1', 'site2']);
+      App.resetDsStoreTypeMap(App.StackService);
+      App.StackService.createRecord({id: 'service1', serviceName: 'service1', configTypes: {'site1':'site1'}});
+      App.StackService.createRecord({id: 'service2', serviceName: 'service2', configTypes: {'site1':'site1', 'site2': 'site2'}});
+      App.StackService.createRecord({id: 'service3', serviceName: 'service3', configTypes: {'site3':'site3'}});
+      expect(mainServiceInfoConfigsController.getServiceNamesForConfigs()).to.eql(['service1', 'service2', 'currentService']);
+    });
+
+
+  });
+
 });