Pārlūkot izejas kodu

AMBARI-734. Initial work for adding DataNode/TaskTracker/RegionServer configuration overrides on groups of hosts in Installer Customize Services page. (yusaku)

git-svn-id: https://svn.apache.org/repos/asf/incubator/ambari/branches/AMBARI-666@1384967 13f79535-47bb-0310-9956-ffa450edef68
Yusaku Sako 12 gadi atpakaļ
vecāks
revīzija
33169c13a0

+ 4 - 0
AMBARI-666-CHANGES.txt

@@ -12,6 +12,10 @@ AMBARI-666 branch (unreleased changes)
 
   NEW FEATURES
 
+  AMBARI-734. Initial work for adding DataNode/TaskTracker/RegionServer
+  configuration overrides on groups of hosts in Installer Customize Services
+  page. (yusaku)
+
   AMBARI-736. Initial work on Cluster Management pages. (yusaku)
 
   AMBARI-733. Add Jersey Resource for BootStrapping and JAXB elements for API

+ 87 - 18
ambari-web/app/controllers/installer/step7.js

@@ -26,16 +26,23 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
 
   selectedService: null,
 
-  submit: function () {
-    // validate all fields
-    this.get('content');
-  },
+  selectedSlaveHosts: null,
+
+  isSubmitDisabled: function() {
+    return !this.everyProperty('errorCount', 0);
+  }.property('@each.errorCount'),
 
   init: function () {
     var mockData = [
       {
         serviceName: 'HDFS',
-        configCategories: [ 'General', 'NameNode', 'SNameNode', 'DataNode', 'Advanced' ],
+        configCategories: [
+          App.ServiceConfigCategory.create({ name: 'General'}),
+          App.ServiceConfigCategory.create({ name: 'NameNode'}),
+          App.ServiceConfigCategory.create({ name: 'SNameNode'}),
+          App.ServiceConfigCategory.create({ name: 'DataNode'}),
+          App.ServiceConfigCategory.create({ name: 'Advanced'})
+        ],
         configs: [
           {
             name: 'dfs.prop1',
@@ -43,18 +50,18 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
             value: '',
             defaultValue: '100',
             description: 'This is Prop1',
-            displayType: 'string',
+            displayType: 'digits',
             unit: 'MB',
-            category: 'General',
-            errorMessage: 'Prop1 validation error'
+            category: 'General'
           },
           {
             name: 'dfs.prop2',
             displayName: 'Prop2',
             value: '',
             defaultValue: '0',
-            description: 'This is Prop2',
-            displayType: 'int',
+            description: 'This is Prop2 (Optional)',
+            displayType: 'number',
+            isRequired: false,
             category: 'General'
           },
           {
@@ -64,6 +71,7 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
             defaultValue: '100',
             description: 'This is Adv Prop1',
             displayType: 'int',
+            isRequired: false,
             category: 'Advanced'
           },
           {
@@ -72,17 +80,28 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
             value: '',
             displayType: 'string',
             defaultValue: 'This is Adv Prop2',
+            isRequired: false,
             category: 'Advanced'
           },
           {
             name: 'hdfs-site.xml',
-            displayName: 'hdfs-site.xml',
+            displayName: 'Custom HDFS Configs',
             value: '',
             defaultValue: '',
-            description: 'Custom configurations that you want to put in hdfs-site.xml.<br>The text you specify here will be injected into hdfs-site.xml verbatim.',
+            description: 'If you wish to set configuration parameters not exposed through this page, you can specify them here.<br>The text you specify here will be injected into hdfs-site.xml verbatim.',
             displayType: 'custom',
+            isRequired: false,
             category: 'Advanced'
           },
+          {
+            name: 'ambari.namenode.host',
+            displayName: 'NameNode host',
+            value: 'host0001.com.com',
+            defaultValue: '',
+            description: 'The host that has been assigned to run NameNode',
+            displayType: 'masterHost',
+            category: 'NameNode'
+          },
           {
             name: 'dfs.namenode.dir',
             displayName: 'NameNode directories',
@@ -98,6 +117,15 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
             defaultValue: 'default (nn)',
             category: 'NameNode'
           },
+          {
+            name: 'ambari.snamenode.host',
+            displayName: 'SNameNode host',
+            value: 'host0002.com.com',
+            defaultValue: '',
+            description: 'The host that has been assigned to run Secondary NameNode',
+            displayType: 'masterHost',
+            category: 'SNameNode'
+          },
           {
             name: 'fs.checkpoint.dir',
             displayName: 'SNameNode directories',
@@ -113,6 +141,15 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
             defaultValue: 'default (snn)',
             category: 'SNameNode'
           },
+          {
+            name: 'ambari.datanode.hosts',
+            displayName: 'DataNode hosts',
+            value: [ 'host0003.com.com', 'host0004.com.com', 'host0005.com.com' ],
+            defaultValue: '',
+            description: 'The hosts that have been assigned to run DataNodes',
+            displayType: 'slaveHosts',
+            category: 'DataNode'
+          },
           {
             name: 'dfs.data.dir',
             displayName: 'DataNode directories',
@@ -132,26 +169,31 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
       },
       {
         serviceName: 'MapReduce',
-        configCategories: [ 'General', 'JobTracker', 'TaskTracker', 'Advanced' ],
+        configCategories: [
+          App.ServiceConfigCategory.create({ name: 'General'}),
+          App.ServiceConfigCategory.create({ name: 'JobTracker'}),
+          App.ServiceConfigCategory.create({ name: 'TaskTracker'}),
+          App.ServiceConfigCategory.create({ name: 'Advanced'})
+        ],
         configs: [
           {
             name: 'mapred.prop1',
             displayName: 'Prop1',
-            value: '',
+            value: '1',
             defaultValue: '0',
             category: 'General'
           },
           {
             name: 'jt.prop1',
             displayName: 'JT Prop1',
-            value: '',
+            value: '2',
             defaultValue: '128',
             category: 'JobTracker'
           },
           {
             name: 'tt.prop1',
             displayName: 'TT Prop1',
-            value: '',
+            value: '3',
             defaultValue: '256',
             category: 'TaskTracker'
           },
@@ -176,7 +218,9 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
       });
       _serviceConfig.configs.forEach(function(_serviceConfigProperty) {
         var serviceConfigProperty = App.ServiceConfigProperty.create(_serviceConfigProperty);
+        serviceConfigProperty.serviceConfig = serviceConfig;
         serviceConfig.configs.pushObject(serviceConfigProperty);
+        serviceConfigProperty.validate();
       });
 
       console.log('pushing ' + serviceConfig.serviceName);
@@ -184,7 +228,32 @@ App.InstallerStep7Controller = Em.ArrayController.extend({
     });
 
     this.set('selectedService', this.objectAt(0));
+  },
+
+  submit: function() {
+    if (!this.get('isSubmitDisabled')) {
+      App.get('router').transitionTo('step8');
+    }
+  },
+
+  showSlaveHosts: function(event) {
+    this.set('selectedSlaveHosts', event.context);
+    App.ModalPopup.show({
+      header: 'Slave Hosts',
+      bodyClass: Ember.View.extend({
+        templateName: require('templates/installer/slaveHostsMatrix')
+      })
+    });
+  },
+
+  addSlaveComponentGroup: function(event) {
+    App.ModalPopup.show({
+      header: 'Add a ' + event.context + ' Group',
+      bodyClass: Ember.View.extend({
+        templateName: require('templates/installer/slaveHostsMatrix')
+      })
+    });
   }
 
-})
-;
+});
+

+ 77 - 10
ambari-web/app/models/serviceConfig.js

@@ -20,7 +20,21 @@ var App = require('app');
 
 App.ServiceConfig = Ember.Object.extend({
   serviceName: '',
-  configCategories: []
+  configCategories: [],
+  configs: null,
+
+  errorCount: function() {
+    return this.get('configs').filterProperty('isValid', false).get('length');
+  }.property('configs.@each.isValid')
+});
+
+App.ServiceConfigCategory = Ember.Object.extend({
+  name: null,
+
+  isForSlaveComponent: function () {
+    return this.get('name') === 'DataNode' || this.get('name') === 'TaskTracker' ||
+      this.get('name') === 'RegionServer';
+  }.property('name')
 });
 
 App.ServiceConfigProperty = Ember.Object.extend({
@@ -30,20 +44,73 @@ App.ServiceConfigProperty = Ember.Object.extend({
   value: '',
   defaultValue: '',
   description: '',
-  displayType: 'string',
+  displayType: 'string',  // string, digits, number, directories, custom
   unit: '',
   category: 'General',
-  isRequired: true,
+  isRequired: true,  // by default a config property is required
+  isEditable: true, // by default a config property is editable
   errorMessage: '',
+  serviceConfig: null, // points to the parent App.ServiceConfig object
+
+  isValid: function() {
+    return this.get('errorMessage') === '';
+  }.property('errorMessage'),
 
   viewClass: function() {
-    if (this.get('displayType') === 'directories') {
-      return App.ServiceConfigTextArea;
-    } else if (this.get('displayType') === 'custom') {
-      return App.ServiceConfigBigTextArea;
-    } else {
-      return App.ServiceConfigTextField;
+    switch (this.get('displayType')) {
+      case 'directories':
+        return App.ServiceConfigTextArea;
+      case 'custom':
+        return App.ServiceConfigBigTextArea;
+      case 'masterHost':
+        return App.ServiceConfigMasterHostView;
+      case 'slaveHosts':
+        return App.ServiceConfigSlaveHostsView;
+      default:
+        return App.ServiceConfigTextField;
+    }
+  }.property('displayType'),
+
+  validate: function() {
+    var digitsRegex = /^\d+$/;
+    var numberRegex = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
+
+    var value = this.get('value');
+
+    var isError = false;
+
+    if (this.get('isRequired')) {
+      if (typeof value === 'string' && value.trim().length === 0) {
+        this.set('errorMessage', 'This is required');
+        isError = true;
+        console.log('required');
+      }
+    }
+
+    if (!isError) {
+      switch (this.get('displayType')) {
+        case 'digits':
+          if (!digitsRegex.test(value)) {
+            this.set('errorMessage', 'Must contain digits only');
+            isError = true;
+          }
+          break;
+        case 'number':
+          if (!numberRegex.test(value)) {
+            this.set('errorMessage', 'Must be a valid number');
+            isError = true;
+          }
+          break;
+        case 'directories':
+          break;
+        case 'custom':
+          break;
+      }
+    }
+    if (!isError) {
+      this.set('errorMessage', '');
+      console.log('setting errorMessage to blank');
     }
-  }.property('displayType')
+  }.observes('value')
 
 });

+ 11 - 1
ambari-web/app/styles/application.less

@@ -86,7 +86,7 @@ h1 {
             background-color: #f0f0f0;
         }
         .accordion-group {
-            margin-bottom:20px;
+            margin-bottom: 20px;
             .control-group {
                 margin: 10px 0;
             }
@@ -94,6 +94,16 @@ h1 {
                 margin-bottom: 0;
             }
         }
+        .badge {
+            margin-left: 4px;
+        }
+        .add-slave-component-group {
+            margin-bottom: 20px;
+        }
+        .master-host, .slave-hosts {
+            padding-top: 5px;
+            line-height: 20px;
+        }
     }
 }
 

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

@@ -37,6 +37,7 @@ require('templates/installer/step4');
 require('templates/installer/step5');
 require('templates/installer/step6');
 require('templates/installer/step7');
+require('templates/installer/slaveHostsMatrix');
 require('templates/installer/step8');
 require('templates/installer/step9');
 require('templates/installer/step10');

+ 4 - 2
ambari-web/app/templates/application.hbs

@@ -31,8 +31,10 @@
             </div>
         </div>
     </div>
-    <div id="content">
-    {{outlet}}
+    <div class="container">
+        <div id="content">
+            {{outlet}}
+        </div>
     </div>
 </div>
 

+ 9 - 0
ambari-web/app/templates/installer/slaveHosts.hbs

@@ -0,0 +1,9 @@
+{{#if view.hasNoHosts}}
+No host assigned
+{{/if}}
+{{#if view.hasOneHost}}
+{{value}}
+{{/if}}
+{{#if view.hasMultipleHosts}}
+<a href="#" {{action showSlaveHosts value target="controller"}}>{{value.firstObject}} and {{view.otherLength}} others</a>
+{{/if}}

+ 3 - 0
ambari-web/app/templates/installer/slaveHostsMatrix.hbs

@@ -0,0 +1,3 @@
+{{#each slaveHost in App.router.installerStep7Controller.selectedSlaveHosts}}
+<label class="checkbox">{{view Ember.Checkbox}}{{slaveHost}}</label>
+{{/each}}

+ 9 - 4
ambari-web/app/templates/installer/step7.hbs

@@ -16,11 +16,11 @@
 * limitations under the License.
 -->
 <div id="serviceConfig">
-<h2>{{App.messages.step7_header}}</h2>
+<h2>{{App.messages.step7_header}}{{isValid}}</h2>
 {{#view App.ServiceConfigTabs}}
 <ul class="nav nav-tabs">
 {{#each service in controller}}
-    <li><a href="#" data-toggle="tab" {{action selectService service on="click" target="view"}}>{{service.serviceName}}</a></li>
+    <li><a href="#" data-toggle="tab" {{action selectService service on="click" target="view"}}>{{service.serviceName}}{{#if service.errorCount}}<span class="badge badge-important">{{service.errorCount}}</span>{{/if}}</a></li>
 {{/each}}
 </ul>
 {{/view}}
@@ -29,7 +29,7 @@
     <div class="accordion-group">
         <div class="accordion-heading">
             <a class="accordion-toggle">
-                {{category}}
+                {{category.name}}
             </a>
         </div>
         {{#view App.ServiceConfigsByCategoryView categoryBinding="category" serviceConfigsBinding="selectedService.configs"}}
@@ -50,10 +50,15 @@
         </div>
         {{/view}}
     </div>
+    {{#if category.isForSlaveComponent}}
+    {{#view App.AddSlaveComponentGroupButton slaveComponentNameBinding="category.name"}}
+    <a class="btn add-slave-component-group" {{action addSlaveComponentGroup category.name target="controller"}}><i class="icon-plus-sign"></i> Add a {{category.name}} Group</a>
+    {{/view}}
+    {{/if}}
     {{/each}}
 </div>
 <div class="btn-area">
     <a class="btn" {{action back}}>Back</a>
-    <a class="btn btn-success" style="float:right" {{action submit target="controller"}}>Next</a>
+    <a {{bindAttr class=":btn :btn-success"}} {{bindAttr disabled="isSubmitDisabled"}} style="float:right" {{action submit target="controller"}}>Next</a>
 </div>
 </div>

+ 1 - 1
ambari-web/app/templates/main/dashboard.hbs

@@ -120,4 +120,4 @@
 			</div>
 		</div>
 	</div>
-</div>
+</div>

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

@@ -21,6 +21,7 @@
 // load all views here
 
 require('views/application');
+require('views/common/modalPopup');
 require('views/login');
 require('views/main');
 require('views/main/menu');
@@ -41,4 +42,4 @@ require('views/installer/step6');
 require('views/installer/step7');
 require('views/installer/step8');
 require('views/installer/step9');
-require('views/installer/step10');
+require('views/installer/step10');

+ 71 - 0
ambari-web/app/views/common/modalPopup.js

@@ -0,0 +1,71 @@
+/**
+ * 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');
+
+App.ModalPopup = Ember.View.extend({
+
+  template: Ember.Handlebars.compile([
+    '<div class="modal-backdrop"></div><div class="modal" id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-label" aria-hidden="true">',
+    '<div class="modal-header">',
+    '<a class="close" {{action onClose target="view"}}>x</a>',
+    '<h3 id="modal-label">{{view.header}}</h3>',
+    '</div>',
+    '<div class="modal-body">',
+    '{{#if bodyClass}}{{view bodyClass}}',
+    '{{else}}{{body}}{{/if}}',
+    '</div>',
+    '<div class="modal-footer">',
+    '{{#if view.secondary}}<a class="btn" {{action onSecondary target="view"}}>{{view.secondary}}</a>{{/if}}',
+    '{{#if view.primary}}<a class="btn btn-success" {{action onPrimary target="view"}}>{{view.primary}}</a>{{/if}}',
+    '</div>',
+    '</div>'
+  ].join('\n')),
+
+  header: '&nbsp;',
+  body: '&nbsp;',
+  // define bodyClass which extends Ember.View to use an arbitrary Handlebars template as the body
+  primary: 'OK',
+  secondary: 'Cancel',
+
+  onPrimary: function() {
+  },
+
+  onSecondary: function() {
+    this.hide();
+  },
+
+  onClose: function() {
+    this.hide();
+  },
+
+  hide: function() {
+    this.destroy();
+  }
+
+});
+
+App.ModalPopup.reopenClass({
+
+  show: function(options) {
+    var popup = this.create(options);
+    popup.appendTo('#wrapper');
+  }
+
+})
+

+ 82 - 12
ambari-web/app/views/installer/step7.js

@@ -21,11 +21,7 @@ var App = require('app');
 
 App.InstallerStep7View = Em.View.extend({
 
-  templateName: require('templates/installer/step7'),
-
-  submit: function(e) {
-    App.router.transitionTo('step8');
-  }
+  templateName: require('templates/installer/step7')
 
 });
 
@@ -37,8 +33,8 @@ App.ServiceConfigsByCategoryView = Ember.View.extend({
   serviceConfigs: null,  // General, Advanced, NameNode, SNameNode, DataNode, etc.
 
   categoryConfigs: function() {
-    return this.get('serviceConfigs').filterProperty('category', this.get('category'))
-  }.property('categoryConfigs.@each').cacheable()
+    return this.get('serviceConfigs').filterProperty('category', this.get('category.name'))
+  }.property('serviceConfigs.@each').cacheable()
 });
 
 App.ServiceConfigTabs = Ember.View.extend({
@@ -59,11 +55,15 @@ App.ServiceConfigTextField = Ember.TextField.extend({
   valueBinding: 'serviceConfig.value',
   classNames: ['span6'],
 
+  disabled: function() {
+    return !this.get('serviceConfig.isEditable');
+  }.property('serviceConfig.isEditable'),
+
   didInsertElement: function() {
     this.$().popover({
-      title: this.get('serviceConfig.name'),
+      title: this.get('serviceConfig.displayName') + '<br><small>' + this.get('serviceConfig.name') + '</small>',
       content: this.get('serviceConfig.description'),
-      placement: 'right',
+      placement: 'left',
       trigger: 'hover'
     });
   }
@@ -79,9 +79,9 @@ App.ServiceConfigTextArea = Ember.TextArea.extend({
 
   didInsertElement: function() {
     this.$().popover({
-      title: this.get('serviceConfig.name'),
+      title: this.get('serviceConfig.displayName') + '<br><small>' + this.get('serviceConfig.name') + '</small>',
       content: this.get('serviceConfig.description'),
-      placement: 'right',
+      placement: 'left',
       trigger: 'hover'
     });
   }
@@ -90,4 +90,74 @@ App.ServiceConfigTextArea = Ember.TextArea.extend({
 
 App.ServiceConfigBigTextArea = App.ServiceConfigTextArea.extend({
   rows: 10
-});
+});
+
+App.ServiceConfigMasterHostView = Ember.View.extend({
+
+  serviceConfig: null,
+  classNames: ['master-host'],
+  valueBinding: 'serviceConfig.value',
+
+  template: Ember.Handlebars.compile('{{value}}'),
+
+  didInsertElement: function() {
+    this.$().popover({
+      title: this.get('serviceConfig.displayName'),
+      content: this.get('serviceConfig.description'),
+      placement: 'left',
+      trigger: 'hover'
+    });
+  }
+ });
+
+App.ServiceConfigSlaveHostsView = Ember.View.extend({
+
+  classNames: ['slave-hosts'],
+  valueBinding: 'serviceConfig.value',
+
+  templateName: require('templates/installer/slaveHosts'),
+
+  hasNoHosts: function() {
+    return this.get('value').length === 0;
+  }.property('value'),
+
+  hasOneHost: function() {
+    return this.get('value').length === 1;
+  }.property('value'),
+
+  hasMultipleHosts: function() {
+    return this.get('value').length > 1;
+  }.property('value'),
+
+  otherLength: function() {
+    return this.get('value').length - 1;
+  }.property('value'),
+
+  didInsertElement: function() {
+    this.$().popover({
+      title: this.get('serviceConfig.displayName'),
+      content: this.get('serviceConfig.description'),
+      placement: 'left',
+      trigger: 'hover'
+    });
+  }
+
+});
+
+App.AddSlaveComponentGroupButton = Ember.View.extend({
+
+  tagName: 'span',
+
+  slaveComponentName: null,
+
+  didInsertElement: function () {
+    this.$().popover({
+      title: 'Add a ' + this.get('slaveComponentName') + ' Group',
+      content: 'If you need different settings on certain ' + this.get('slaveComponentName') + 's, you can add a ' + this.get('slaveComponentName') + ' group.<br>' +
+        'All ' + this.get('slaveComponentName') + 's within the same group will have the same set of settings.  You can create multiple groups.',
+      placement: 'left',
+      trigger: 'hover'
+    });
+  }
+
+});