Преглед изворни кода

AMBARI-724. Add tabs, dynamic form generation, validation errors, and info popovers for Customize Services page in Installer. (Contributed by yusaku)

git-svn-id: https://svn.apache.org/repos/asf/incubator/ambari/branches/AMBARI-666@1383791 13f79535-47bb-0310-9956-ffa450edef68
Yusaku Sako пре 12 година
родитељ
комит
0333bbe196

+ 3 - 0
AMBARI-666-CHANGES.txt

@@ -12,6 +12,9 @@ AMBARI-666 branch (unreleased changes)
 
   NEW FEATURES
 
+  AMBARI-724. Add tabs, dynamic form generation, validation errors, and info
+  popovers for Customize Services page in Installer (yusaku) 
+
   AMBARI-714. Job FSM Impl and tests. (hitesh)
 
   AMBARI-721. Remove Hardwareprofile class since its not needed anymore.

+ 167 - 99
ambari-web/app/controllers/installer/step7.js

@@ -16,107 +16,175 @@
  * limitations under the License.
  */
 
+var App = require('app');
+
 App.InstallerStep7Controller = Em.ArrayController.extend({
 
   name: 'installerStep7Controller',
 
-  content: [
-    Em.Object.create({
-      serviceName: 'HDFS',
-      configCategories: [ 'General', 'NameNode', 'SNameNode', 'DataNode', 'Advanced' ],
-      configs: [{
-        name: 'dfs.prop1',
-        displayName: 'Prop1',
-        value: '',
-        defaultValue: '100',
-        description: 'This is Prop1',
-        unit: 'MB',
-        category: 'General'
-      }, {
-        name: 'dfs.prop2',
-        displayName: 'Prop2',
-        value: '',
-        defaultValue: '0',
-        category: 'General'
-      }, {
-        name: 'dfs.adv.prop1',
-        displayName: 'Adv Prop1',
-        value: '',
-        defaultValue: '100',
-        category: 'Advanced'
-      }, {
-        name: 'dfs.adv.prop2',
-        displayName: 'Adv Prop2',
-        value: '',
-        defaultValue: '0',
-        category: 'Advanced'
-      }, {
-        name: 'dfs.namenode.dir',
-        displayName: 'NameNode directories',
-        value: '',
-        defaultValue: '',
-        category: 'NameNode'
-      }, {
-        name: 'dfs.namenode.prop1',
-        displayName: 'NameNode Prop1',
-        value: '',
-        defaultValue: 'default (nn)',
-        category: 'NameNode'
-      }, {
-        name: 'fs.checkpoint.dir',
-        displayName: 'SNameNode directories',
-        value: '',
-        defaultValue: '',
-        category: 'SNameNode'
-      }, {
-        name: 'fs.checkpoint.prop1',
-        displayName: 'SNameNode Prop1',
-        value: '',
-        defaultValue: 'default (snn)',
-        category: 'SNameNode'
-      }, {
-        name: 'dfs.data.dir',
-        displayName: 'DataNode directories',
-        value: '',
-        defaultValue: '',
-        category: 'DataNode'
-      }, {
-        name: 'dfs.data.prop1',
-        displayName: 'DataNode Prop1',
-        value: '',
-        defaultValue: 'default (dn)',
-        category: 'DataNode'
-      }]
-    }),
-    Em.Object.create({
-      serviceName: 'MapReduce',
-      configCategories: [ 'General', 'JobTracker', 'TaskTracker', 'Advanced' ],
-      configs: [{
-        name: 'mapred.prop1',
-        displayName: 'Prop1',
-        value: '',
-        defaultValue: '0',
-        category: 'General'
-      }, {
-        name: 'jt.prop1',
-        displayName: 'JT Prop1',
-        value: '',
-        defaultValue: '128',
-        category: 'JobTracker'
-      }, {
-        name: 'tt.prop1',
-        displayName: 'TT Prop1',
-        value: '',
-        defaultValue: '256',
-        category: 'TaskTracker'
-      }, {
-        name: 'mapred.adv.prop1',
-        displayName: 'Adv Prop1',
-        value: '',
-        defaultValue: '1024',
-        category: 'Advanced'
-      }]
-    })
-  ]
+  content: [],
+
+  selectedService: null,
+
+  submit: function () {
+    // validate all fields
+    this.get('content');
+  },
+
+  init: function () {
+    var mockData = [
+      {
+        serviceName: 'HDFS',
+        configCategories: [ 'General', 'NameNode', 'SNameNode', 'DataNode', 'Advanced' ],
+        configs: [
+          {
+            name: 'dfs.prop1',
+            displayName: 'Prop1',
+            value: '',
+            defaultValue: '100',
+            description: 'This is Prop1',
+            displayType: 'string',
+            unit: 'MB',
+            category: 'General',
+            errorMessage: 'Prop1 validation error'
+          },
+          {
+            name: 'dfs.prop2',
+            displayName: 'Prop2',
+            value: '',
+            defaultValue: '0',
+            description: 'This is Prop2',
+            displayType: 'int',
+            category: 'General'
+          },
+          {
+            name: 'dfs.adv.prop1',
+            displayName: 'Adv Prop1',
+            value: '',
+            defaultValue: '100',
+            description: 'This is Adv Prop1',
+            displayType: 'int',
+            category: 'Advanced'
+          },
+          {
+            name: 'dfs.adv.prop2',
+            displayName: 'Adv Prop2',
+            value: '',
+            displayType: 'string',
+            defaultValue: 'This is Adv Prop2',
+            category: 'Advanced'
+          },
+          {
+            name: 'hdfs-site.xml',
+            displayName: 'hdfs-site.xml',
+            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.',
+            displayType: 'custom',
+            category: 'Advanced'
+          },
+          {
+            name: 'dfs.namenode.dir',
+            displayName: 'NameNode directories',
+            value: '/grid/0/hadoop/namenode\r\n/grid/1/hadoop/namenode',
+            defaultValue: '',
+            displayType: 'directories',
+            category: 'NameNode'
+          },
+          {
+            name: 'dfs.namenode.prop1',
+            displayName: 'NameNode Prop1',
+            value: '',
+            defaultValue: 'default (nn)',
+            category: 'NameNode'
+          },
+          {
+            name: 'fs.checkpoint.dir',
+            displayName: 'SNameNode directories',
+            value: '',
+            defaultValue: '',
+            displayType: 'directories',
+            category: 'SNameNode'
+          },
+          {
+            name: 'fs.checkpoint.prop1',
+            displayName: 'SNameNode Prop1',
+            value: '',
+            defaultValue: 'default (snn)',
+            category: 'SNameNode'
+          },
+          {
+            name: 'dfs.data.dir',
+            displayName: 'DataNode directories',
+            value: '',
+            defaultValue: '',
+            displayType: 'directories',
+            category: 'DataNode'
+          },
+          {
+            name: 'dfs.data.prop1',
+            displayName: 'DataNode Prop1',
+            value: '',
+            defaultValue: 'default (dn)',
+            category: 'DataNode'
+          }
+        ]
+      },
+      {
+        serviceName: 'MapReduce',
+        configCategories: [ 'General', 'JobTracker', 'TaskTracker', 'Advanced' ],
+        configs: [
+          {
+            name: 'mapred.prop1',
+            displayName: 'Prop1',
+            value: '',
+            defaultValue: '0',
+            category: 'General'
+          },
+          {
+            name: 'jt.prop1',
+            displayName: 'JT Prop1',
+            value: '',
+            defaultValue: '128',
+            category: 'JobTracker'
+          },
+          {
+            name: 'tt.prop1',
+            displayName: 'TT Prop1',
+            value: '',
+            defaultValue: '256',
+            category: 'TaskTracker'
+          },
+          {
+            name: 'mapred.adv.prop1',
+            displayName: 'Adv Prop1',
+            value: '',
+            defaultValue: '1024',
+            category: 'Advanced'
+          }
+        ]
+      }
+    ];
+
+    var self = this;
+
+    mockData.forEach(function(_serviceConfig) {
+      var serviceConfig = App.ServiceConfig.create({
+        serviceName: _serviceConfig.serviceName,
+        configCategories: _serviceConfig.configCategories,
+        configs: []
+      });
+      _serviceConfig.configs.forEach(function(_serviceConfigProperty) {
+        var serviceConfigProperty = App.ServiceConfigProperty.create(_serviceConfigProperty);
+        serviceConfig.configs.pushObject(serviceConfigProperty);
+      });
+
+      console.log('pushing ' + serviceConfig.serviceName);
+      self.content.pushObject(serviceConfig);
+    });
+
+    this.set('selectedService', this.objectAt(0));
+  }
 
-});
+})
+;

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

@@ -19,4 +19,5 @@
 
 // load all models here
 
-require('models/cluster');
+require('models/cluster');
+require('models/serviceConfig')

+ 49 - 0
ambari-web/app/models/serviceConfig.js

@@ -0,0 +1,49 @@
+/**
+ * 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.ServiceConfig = Ember.Object.extend({
+  serviceName: '',
+  configCategories: []
+});
+
+App.ServiceConfigProperty = Ember.Object.extend({
+
+  name: '',
+  displayName: '',
+  value: '',
+  defaultValue: '',
+  description: '',
+  displayType: 'string',
+  unit: '',
+  category: 'General',
+  isRequired: true,
+  errorMessage: '',
+
+  viewClass: function() {
+    if (this.get('displayType') === 'directories') {
+      return App.ServiceConfigTextArea;
+    } else if (this.get('displayType') === 'custom') {
+      return App.ServiceConfigBigTextArea;
+    } else {
+      return App.ServiceConfigTextField;
+    }
+  }.property('displayType')
+
+});

+ 18 - 5
ambari-web/app/styles/application.less

@@ -81,10 +81,23 @@ h1 {
     .btn-area {
         margin-top: 20px;
     }
-    .accordion-heading {
-        background-color: #f0f0f0;
-    }
-    .accordion-group {
-        margin-bottom:20px;
+    #serviceConfig {
+        .accordion-heading {
+            background-color: #f0f0f0;
+        }
+        .accordion-group {
+            margin-bottom:20px;
+            .control-group {
+                margin: 10px 0;
+            }
+            form {
+                margin-bottom: 0;
+            }
+        }
     }
 }
+
+a:focus {
+    outline: none;
+}
+

+ 20 - 13
ambari-web/app/templates/installer/step7.hbs

@@ -15,38 +15,45 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 -->
-
+<div id="serviceConfig">
 <h2>{{App.messages.step7_header}}</h2>
+{{#view App.ServiceConfigTabs}}
 <ul class="nav nav-tabs">
-{{#each controller}}
-    <li><a href="#" data-toggle="tab">{{serviceName}}</a></li>
+{{#each service in controller}}
+    <li><a href="#" data-toggle="tab" {{action selectService service on="click" target="view"}}>{{service.serviceName}}</a></li>
 {{/each}}
 </ul>
-{{#each service in controller}}
+{{/view}}
 <div class="accordion">
-    {{#each category in service.configCategories}}
+    {{#each category in selectedService.configCategories}}
     <div class="accordion-group">
         <div class="accordion-heading">
             <a class="accordion-toggle">
                 {{category}}
             </a>
         </div>
-        {{#view App.ServiceConfigsByCategoryView categoryBinding="category" serviceConfigsBinding="service.configs"}}
+        {{#view App.ServiceConfigsByCategoryView categoryBinding="category" serviceConfigsBinding="selectedService.configs"}}
         <div class="accordion-body collapse in">
             <div class="accordion-inner">
-            {{#each view.categoryConfigs}}
-                <label>{{displayName}} - {{name}}</label>
-                <div class="input-append">{{#view Ember.TextField}}{{/view}}{{#if unit}}<span class="add-on">{{unit}}</span>{{/if}}</div>
-                {{description}}
-            {{/each}}
+                <form class="form-horizontal">
+                    {{#each view.categoryConfigs}}
+                        <div {{bindAttr class="errorMessage:error: :control-group"}}>
+                            <label class="control-label">{{displayName}}</label>
+                            <div class="controls">
+                                {{view viewClass serviceConfigBinding="this"}}
+                                <span class="help-inline">{{errorMessage}}</span>
+                            </div>
+                        </div>
+                    {{/each}}
+                </form>
             </div>
         </div>
         {{/view}}
     </div>
     {{/each}}
 </div>
-{{/each}}
 <div class="btn-area">
     <a class="btn" {{action back}}>Back</a>
-    <a class="btn btn-success" style="float:right" {{action next}}>Next</a>
+    <a class="btn btn-success" style="float:right" {{action submit target="controller"}}>Next</a>
+</div>
 </div>

+ 52 - 3
ambari-web/app/views/installer/step7.js

@@ -24,7 +24,6 @@ App.InstallerStep7View = Em.View.extend({
   templateName: require('templates/installer/step7'),
 
   submit: function(e) {
-    alert(this.get('controller.clusterName'));
     App.router.transitionTo('step8');
   }
 
@@ -36,9 +35,59 @@ App.ServiceConfigsByCategoryView = Ember.View.extend({
 
   category: null,
   serviceConfigs: null,  // General, Advanced, NameNode, SNameNode, DataNode, etc.
-  //require('templates/installer/serviceConfigsByCategory'),
 
   categoryConfigs: function() {
-    return this.get("serviceConfigs").filterProperty('category', this.get('category'))
+    return this.get('serviceConfigs').filterProperty('category', this.get('category'))
   }.property('categoryConfigs.@each').cacheable()
 });
+
+App.ServiceConfigTabs = Ember.View.extend({
+
+  selectService: function(event) {
+    this.set('controller.selectedService', event.context);
+  },
+
+  didInsertElement: function() {
+    this.$('a:first').tab('show');
+  }
+
+});
+
+App.ServiceConfigTextField = Ember.TextField.extend({
+
+  serviceConfig: null,
+  valueBinding: 'serviceConfig.value',
+  classNames: ['span6'],
+
+  didInsertElement: function() {
+    this.$().popover({
+      title: this.get('serviceConfig.name'),
+      content: this.get('serviceConfig.description'),
+      placement: 'right',
+      trigger: 'hover'
+    });
+  }
+
+});
+
+App.ServiceConfigTextArea = Ember.TextArea.extend({
+
+  serviceConfig: null,
+  valueBinding: 'serviceConfig.value',
+  rows: 4,
+  classNames: ['span6'],
+
+  didInsertElement: function() {
+    this.$().popover({
+      title: this.get('serviceConfig.name'),
+      content: this.get('serviceConfig.description'),
+      placement: 'right',
+      trigger: 'hover'
+    });
+  }
+
+});
+
+App.ServiceConfigBigTextArea = App.ServiceConfigTextArea.extend({
+  rows: 10
+});