Repository: ambari Updated Branches: refs/heads/trunk 65f0ff68f -> b88ede8df
AMBARI-15302 Service deletion should recommend and change configs before deleting service.(ababiichuk) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/b88ede8d Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/b88ede8d Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/b88ede8d Branch: refs/heads/trunk Commit: b88ede8dfef4fb6595ac7fe122a09eee7dba5d17 Parents: 65f0ff6 Author: ababiichuk <ababiic...@hortonworks.com> Authored: Fri Mar 4 17:49:04 2016 +0200 Committer: ababiichuk <ababiic...@hortonworks.com> Committed: Mon Mar 7 11:49:20 2016 +0200 ---------------------------------------------------------------------- ambari-web/app/controllers/main/service/item.js | 214 ++++++++++++++++--- .../app/controllers/wizard/step7_controller.js | 1 + ambari-web/app/messages.js | 2 + .../app/mixins/common/configs/configs_saver.js | 12 +- ambari-web/app/utils/blueprint.js | 22 ++ .../configs/widgets/plain_config_text_field.js | 6 +- .../widgets/textfield_config_widget_view.js | 3 +- .../test/controllers/main/service/item_test.js | 37 +++- 8 files changed, 242 insertions(+), 55 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/app/controllers/main/service/item.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/controllers/main/service/item.js b/ambari-web/app/controllers/main/service/item.js index 92d4d79..eda0485 100644 --- a/ambari-web/app/controllers/main/service/item.js +++ b/ambari-web/app/controllers/main/service/item.js @@ -18,8 +18,9 @@ var App = require('app'); var batchUtils = require('utils/batch_scheduled_requests'); +var blueprintUtils = require('utils/blueprint'); -App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDownload, App.InstallComponent, { +App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDownload, App.InstallComponent, App.ConfigsSaverMixin, App.EnhancedConfigsMixin, { name: 'mainServiceItemController', /** @@ -106,20 +107,85 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow }.property('content.serviceName'), /** - * Load all config tags for loading configs + * Returns interdependent services + * + * @returns {string[]} */ - loadConfigs: function(){ - if (this.get('serviceConfigsMap')[this.get('content.serviceName')]) { - this.set('isServiceConfigsLoaded', false); - App.ajax.send({ - name: 'config.tags', - sender: this, - success: 'onLoadConfigsTags', - error: 'onTaskError' + interDependentServices: function() { + var serviceName = this.get('content.serviceName'), interDependentServices = []; + App.StackService.find(serviceName).get('requiredServices').forEach(function(requiredService) { + if (App.StackService.find(requiredService).get('requiredServices').contains(serviceName)) { + interDependentServices.push(requiredService); + } + }); + return interDependentServices; + }.property('content.serviceName'), + + /** + * collection of serviceConfigs + * + * @type {Object[]} + */ + stepConfigs: [], + + /** + * List of service names that have configs dependent on current service configs + * + * @type {String[]} + */ + dependentServiceNames: function() { + return App.StackService.find(this.get('content.serviceName')).get('dependentServiceNames'); + }.property('content.serviceName'), + + /** + * List of service names that could be deleted + * Common case when there is only current service should be removed + * But for some services there is <code>interDependentServices<code> services + * Like 'YARN' depends on 'MAPREDUCE2' and 'MAPREDUCE2' depends on 'YARN' + * So these services can be removed only together + * + * @type {String[]} + */ + serviceNamesToDelete: function() { + return [this.get('content.serviceName')].concat(this.get('interDependentServices')); + }.property('content.serviceName'), + + /** + * List of config types that should be loaded + * Includes + * 1. Dependent services config-types + * 2. Some special cases from <code>serviceConfigsMap<code> + * 3. 'cluster-env' + * + * @type {String[]} + */ + sitesToLoad: function() { + var services = this.get('dependentServiceNames'), configTypeList = []; + if (services.length) { + var configTypeList = App.StackService.find().filter(function(s) { + return services.contains(s.get('serviceName')); + }).mapProperty('configTypeList').reduce(function(p, v) { + return p.concat(v); }); - } else { - this.set('isServiceConfigsLoaded', true); } + if (this.get('serviceConfigsMap')[this.get('content.serviceName')]) { + configTypeList = configTypeList.concat(this.get('serviceConfigsMap')[this.get('content.serviceName')]); + } + configTypeList.push('cluster-env'); + return configTypeList.uniq(); + }.property('content.serviceName'), + + /** + * Load all config tags for loading configs + */ + loadConfigs: function(){ + this.set('isServiceConfigsLoaded', false); + App.ajax.send({ + name: 'config.tags', + sender: this, + success: 'onLoadConfigsTags', + error: 'onTaskError' + }); }, /** @@ -128,7 +194,7 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow */ onLoadConfigsTags: function (data) { var self = this; - var sitesToLoad = this.get('serviceConfigsMap')[this.get('content.serviceName')]; + var sitesToLoad = this.get('sitesToLoad'), allConfigs = []; var loadedSites = data.Clusters.desired_configs; var siteTagsToLoad = []; for (var site in loadedSites) { @@ -142,7 +208,17 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow App.router.get('configurationController').getConfigsByTags(siteTagsToLoad).done(function (configs) { configs.forEach(function (site) { self.get('configs')[site.type] = site.properties; + allConfigs = allConfigs.concat(App.config.getConfigsFromJSON(site, true)); + }); + + self.get('dependentServiceNames').forEach(function(serviceName) { + var configTypes = App.StackService.find(serviceName).get('configTypeList'); + var configsByService = allConfigs.filter(function (c) { + return configTypes.contains(App.config.getConfigTagFromFileName(c.get('filename'))); + }); + self.get('stepConfigs').pushObject(App.config.createServiceConfig(serviceName, [], configsByService)); }); + self.set('isServiceConfigsLoaded', true); }); }, @@ -971,22 +1047,6 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow }, /** - * Returns interdependent services - * - * @param serviceName - * @returns {string[]} - */ - interDependentServices: function(serviceName) { - var interDependentServices = []; - App.StackService.find(serviceName).get('requiredServices').forEach(function(requiredService) { - if (App.StackService.find(requiredService).get('requiredServices').contains(serviceName)) { - interDependentServices.push(requiredService); - } - }); - return interDependentServices; - }, - - /** * find dependent services * @param {string[]} serviceNamesToDelete * @returns {Array} @@ -1035,8 +1095,8 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow */ deleteService: function(serviceName) { var self = this, - interDependentServices = this.interDependentServices(serviceName), - serviceNamesToDelete = interDependentServices.concat(serviceName), + interDependentServices = this.get('interDependentServices'), + serviceNamesToDelete = this.get('serviceNamesToDelete'), dependentServices = this.findDependentServices(serviceNamesToDelete), displayName = App.format.role(serviceName), popupHeader = Em.I18n.t('services.service.delete.popup.header'), @@ -1168,6 +1228,96 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow }, /** + * All host names + * This property required for request for recommendations + * + * @type {String[]} + * @override + */ + hostNames: Em.computed.alias('App.allHostNames'), + + /** + * Recommendation object + * This property required for request for recommendations + * + * @type {Object} + * @override + */ + hostGroups: function() { + var hostGroup = blueprintUtils.generateHostGroups(App.get('allHostNames')); + return blueprintUtils.removeDeletedComponents(hostGroup, [this.get('serviceNamesToDelete')]); + }.property('serviceNamesToDelete', 'App.allHostNames'), + + /** + * List of services without removed + * This property required for request for recommendations + * + * @type {String[]} + * @override + */ + serviceNames: function() { + return App.Service.find().filter(function(s) { + return !this.get('serviceNamesToDelete').contains(s.get('serviceName')); + }, this).mapProperty('serviceName'); + }.property('serviceNamesToDelete'), + + /** + * This property required for request for recommendations + * + * @return {Boolean} + * @override + */ + isConfigHasInitialState: function() { return false; }, + + /** + * Describes condition when recommendation should be applied + * For removing services it's always true + * This property required for request for recommendations + * + * @return {Boolean} + * @override + */ + allowUpdateProperty: function() { return true; }, + + /** + * Just config version note + * + * @type {String} + */ + serviceConfigVersionNote: function() { + var services = this.get('serviceNamesToDelete').join(','); + if (this.get('serviceNamesToDelete.length') === 1) { + return Em.I18n.t('services.service.delete.configVersionNote').format(services); + } + return Em.I18n.t('services.service.delete.configVersionNote.plural').format(services); + }.property('serviceNamesToDelete'), + + /** + * Method ot save configs after service have been removed + * @override + */ + saveConfigs: function() { + var data = []; + this.get('stepConfigs').forEach(function(stepConfig) { + var serviceConfig = this.getServiceConfigToSave(stepConfig.get('serviceName'), stepConfig.get('configs')); + + if (serviceConfig) { + data.push(serviceConfig); + } + }, this); + + if (Em.isArray(data) && data.length) { + this.putChangedConfigurations(data, 'onSaveConfigs'); + } else { + this.onSaveConfigs(); + } + }, + + onSaveConfigs: function() { + window.location.reload(); + }, + + /** * Ajax call to delete service * @param {string[]} serviceNames * @returns {$.ajax} @@ -1193,7 +1343,7 @@ App.MainServiceItemController = Em.Controller.extend(App.SupportClientConfigsDow if (params.servicesToDeleteNext) { this.deleteServiceCall(params.servicesToDeleteNext); } else { - window.location.reload(); + this.loadConfigRecommendations(null, this.saveConfigs.bind(this)); } }, http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/app/controllers/wizard/step7_controller.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/controllers/wizard/step7_controller.js b/ambari-web/app/controllers/wizard/step7_controller.js index 5557d69..06488b1 100644 --- a/ambari-web/app/controllers/wizard/step7_controller.js +++ b/ambari-web/app/controllers/wizard/step7_controller.js @@ -1327,6 +1327,7 @@ App.WizardStep7Controller = Em.Controller.extend(App.ServerValidatorMixin, App.E * @override */ allowUpdateProperty: function(parentProperties, name, fileName) { + if (name.contains('proxyuser')) return true; if (['installerController'].contains(this.get('wizardController.name')) || !!(parentProperties && parentProperties.length)) { return true; } else if (['addServiceController'].contains(this.get('wizardController.name'))) { http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/app/messages.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js index 7aa38e4..a3a4d1d 100644 --- a/ambari-web/app/messages.js +++ b/ambari-web/app/messages.js @@ -1714,6 +1714,8 @@ Em.I18n.translations = { 'services.service.actions.serviceActions':'Service Actions', 'services.service.delete.popup.header': 'Delete Service', + 'services.service.delete.configVersionNote': 'Update configs after {0} has been removed', + 'services.service.delete.configVersionNote.plural': 'Update configs after {0} have been removed', 'services.service.delete.lastService.popup.body': 'The <b>{0}</b> service can\'t be deleted, at least one service must be installed.', 'services.service.delete.popup.dependentServices': 'Prior to deleting <b>{0}</b>, you must delete the following dependent services:', 'services.service.delete.popup.mustBeStopped': 'Prior to deleting <b>{0}</b>, you must stop the service.', http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/app/mixins/common/configs/configs_saver.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/mixins/common/configs/configs_saver.js b/ambari-web/app/mixins/common/configs/configs_saver.js index 23f69c2..4f1952c 100644 --- a/ambari-web/app/mixins/common/configs/configs_saver.js +++ b/ambari-web/app/mixins/common/configs/configs_saver.js @@ -135,7 +135,7 @@ App.ConfigsSaverMixin = Em.Mixin.create({ }, this); if (Em.isArray(data) && data.length) { - this.putChangedConfigurations(data, true); + this.putChangedConfigurations(data, 'doPUTClusterConfigurationSiteSuccessCallback'); } else { this.onDoPUTClusterConfigurations(); } @@ -151,7 +151,7 @@ App.ConfigsSaverMixin = Em.Mixin.create({ var configsToSave = this.getServiceConfigToSave(serviceName, configs); if (configsToSave) { - this.putChangedConfigurations([configsToSave], false); + this.putChangedConfigurations([configsToSave]); } } else { @@ -573,11 +573,11 @@ App.ConfigsSaverMixin = Em.Mixin.create({ * Saves configuration of set of sites. The provided data * contains the site name and tag to be used. * @param {Object[]} services - * @param {boolean} showPopup + * @param {String} [successCallback] * @return {$.ajax} * @method putChangedConfigurations */ - putChangedConfigurations: function (services, showPopup) { + putChangedConfigurations: function (services, successCallback) { var ajaxData = { name: 'common.across.services.configurations', sender: this, @@ -586,8 +586,8 @@ App.ConfigsSaverMixin = Em.Mixin.create({ }, error: 'doPUTClusterConfigurationSiteErrorCallback' }; - if (showPopup) { - ajaxData.success = 'doPUTClusterConfigurationSiteSuccessCallback' + if (successCallback) { + ajaxData.success = successCallback; } return App.ajax.send(ajaxData); }, http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/app/utils/blueprint.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/blueprint.js b/ambari-web/app/utils/blueprint.js index dd9555a..30e69af 100644 --- a/ambari-web/app/utils/blueprint.js +++ b/ambari-web/app/utils/blueprint.js @@ -396,6 +396,28 @@ module.exports = { }, /** + * Clean up host groups from components that should be removed + * + * @param hostGroups + * @param serviceNames + */ + removeDeletedComponents: function(hostGroups, serviceNames) { + var components = []; + App.StackService.find().forEach(function(s) { + if (serviceNames.contains(s.get('serviceName'))) { + components = components.concat(s.get('serviceComponents').mapProperty('componentName')); + } + }); + + hostGroups.blueprint.host_groups.forEach(function(hg) { + hg.components = hg.components.filter(function(c) { + return !components.contains(c.name); + }) + }); + return hostGroups; + }, + + /** * collect all component names that are present on hosts * @returns {object} */ http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/app/views/common/configs/widgets/plain_config_text_field.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/configs/widgets/plain_config_text_field.js b/ambari-web/app/views/common/configs/widgets/plain_config_text_field.js index 25b9994..1403acc 100644 --- a/ambari-web/app/views/common/configs/widgets/plain_config_text_field.js +++ b/ambari-web/app/views/common/configs/widgets/plain_config_text_field.js @@ -24,7 +24,7 @@ var App = require('app'); require('views/common/controls_view'); -App.PlainConfigTextField = Ember.View.extend(App.SupportsDependentConfigs, App.WidgetPopoverSupport, { +App.PlainConfigTextField = Ember.View.extend(App.SupportsDependentConfigs, App.WidgetPopoverSupport, App.ValueObserver, { templateName: require('templates/common/configs/widgets/plain_config_text_field'), valueBinding: 'config.value', classNames: ['widget-config-plain-text-field'], @@ -52,10 +52,6 @@ App.PlainConfigTextField = Ember.View.extend(App.SupportsDependentConfigs, App.W return unit; }.property('unit'), - focusOut: function () { - this.sendRequestRorDependentConfigs(this.get('config')); - }, - insertNewline: function() { this.get('parentView').trigger('toggleWidgetView'); }, http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/app/views/common/configs/widgets/textfield_config_widget_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/configs/widgets/textfield_config_widget_view.js b/ambari-web/app/views/common/configs/widgets/textfield_config_widget_view.js index 48a1321..bffaaf8 100644 --- a/ambari-web/app/views/common/configs/widgets/textfield_config_widget_view.js +++ b/ambari-web/app/views/common/configs/widgets/textfield_config_widget_view.js @@ -34,8 +34,7 @@ App.TextFieldConfigWidgetView = App.ConfigWidgetView.extend({ configView: App.ServiceConfigTextField.extend({ isPopoverEnabled: 'false', textFieldClassName: 'span12', - serviceConfigBinding: 'parentView.config', - focusIn: function() {} + serviceConfigBinding: 'parentView.config' }), didInsertElement: function() { http://git-wip-us.apache.org/repos/asf/ambari/blob/b88ede8d/ambari-web/test/controllers/main/service/item_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/controllers/main/service/item_test.js b/ambari-web/test/controllers/main/service/item_test.js index 8fb772a..f2fcd2d 100644 --- a/ambari-web/test/controllers/main/service/item_test.js +++ b/ambari-web/test/controllers/main/service/item_test.js @@ -1262,16 +1262,18 @@ describe('App.MainServiceItemController', function () { sinon.stub(mainServiceItemController, 'servicesDisplayNames', function(servicesDisplayNames) { return servicesDisplayNames; }); - sinon.stub(mainServiceItemController, 'interDependentServices').returns([]); this.allowUninstallServices = sinon.stub(mainServiceItemController, 'allowUninstallServices'); this.mockService = sinon.stub(App.Service, 'find'); sinon.stub(App, 'showConfirmationPopup'); sinon.stub(App.ModalPopup, 'show'); sinon.stub(App.format, 'role', function(name) {return name}); + + mainServiceItemController.reopen({ + interDependentServices: [] + }) }); afterEach(function() { mainServiceItemController.allowUninstallServices.restore(); - mainServiceItemController.interDependentServices.restore(); mainServiceItemController.servicesDisplayNames.restore(); this.mockDependentServices.restore(); this.mockService.restore(); @@ -1372,16 +1374,27 @@ describe('App.MainServiceItemController', function () { sinon.stub(App.StackService, 'find', function (serviceName) { return stackSerivceModel[serviceName]; }); - mainServiceItemController = App.MainServiceItemController.create({}); + mainServiceItemController = App.MainServiceItemController.create({ + content: {} + }); }); afterEach(function() { App.StackService.find.restore(); }); - it('get interdependent services', function() { - expect(mainServiceItemController.interDependentServices('YARN')).to.eql(['MAPREDUCE2']); - expect(mainServiceItemController.interDependentServices('MAPREDUCE2')).to.eql(['YARN']); + it('get interdependent services for YARN', function() { + mainServiceItemController.set('content', Em.Object.create({ + serviceName: 'YARN' + })); + expect(mainServiceItemController.get('interDependentServices')).to.eql(['MAPREDUCE2']); + }); + + it('get interdependent services for MAPREDUCE2', function() { + mainServiceItemController.set('content', Em.Object.create({ + serviceName: 'MAPREDUCE2' + })); + expect(mainServiceItemController.get('interDependentServices')).to.eql(['YARN']); }); }); @@ -1409,23 +1422,27 @@ describe('App.MainServiceItemController', function () { beforeEach(function() { mainServiceItemController = App.MainServiceItemController.create({}); - sinon.stub(window.location, 'reload'); + sinon.spy(mainServiceItemController, 'loadConfigRecommendations'); sinon.spy(mainServiceItemController, 'deleteServiceCall'); + mainServiceItemController.reopen({ + interDependentServices: [] + }) }); afterEach(function() { - window.location.reload.restore(); + mainServiceItemController.loadConfigRecommendations.restore(); + mainServiceItemController.deleteServiceCall.restore(); }); it("window.location.reload should be called", function() { mainServiceItemController.deleteServiceCallSuccessCallback([], null, {}); expect(mainServiceItemController.deleteServiceCall.called).to.be.false; - expect(window.location.reload.calledOnce).to.be.true; + expect(mainServiceItemController.loadConfigRecommendations.calledOnce).to.be.true; }); it("deleteServiceCall should be called", function() { mainServiceItemController.deleteServiceCallSuccessCallback([], null, {servicesToDeleteNext: true}); expect(mainServiceItemController.deleteServiceCall.calledOnce).to.be.true; - expect(window.location.reload.called).to.be.false; + expect(mainServiceItemController.loadConfigRecommendations.called).to.be.false; }); });