【成神之路】Ambari实战-051-UI-如何通过配置修改ambari样式-前端View扩展

1. 页面渲染概述

  • 作用: 总体介绍页面渲染的两种方式,分别是基于 widgetTypeMapdisplayType 的渲染。
  • 渲染方式:
    1. widgetTypeMap:基于 theme.jsonwidget 配置渲染。
    2. displayType:前端通过配置 displayType 来控制不同的控件渲染。

2. 基于 widgetTypeMap 的渲染

2.1 作用

  • 定义: 通过 theme.json 中的 widget 配置,前端通过 widgetTypeMap 映射来决定页面控件的类型。
  • 使用场景: 通常用于控制页面控件的展示,如输入框、密码框、滑动条等。

2.2 渲染效果说明

widgetTypeMap 是 Ambari 中将不同配置类型映射到具体控件的核心机制。根据 widget.type,前端会通过映射表加载相应的控件视图。以下是常见控件类型及其渲染效果说明。

2.2.1 常见控件类型列表:
控件类型对应的控件视图渲染效果
checkboxCheckboxConfigWidgetView渲染一个复选框,适用于布尔值类型的配置。
comboComboConfigWidgetView渲染一个下拉菜单,允许用户从多个预定义选项中选择一个。
directoryTextFieldConfigWidgetView渲染一个文本输入框,通常用于文件路径的输入。
directoriesDirectoryConfigWidgetView渲染一个多目录选择框,允许用户输入多个目录路径。
listListConfigWidgetView渲染一个多选列表,允许用户选择多个值。
passwordPasswordConfigWidgetView渲染一个密码输入框,输入时文字会被隐藏。
radio-buttonsRadioButtonConfigWidgetView渲染单选按钮,适用于有多个互斥选项的配置场景。
sliderSliderConfigWidgetView渲染一个滑块,用于调整数值范围。
text-fieldTextFieldConfigWidgetView渲染一个普通文本输入框,适合简单的文本输入。
time-interval-spinnerTimeIntervalSpinnerView渲染时间间隔选择器,适用于设置时间间隔或超时时间。
toggleToggleConfigWidgetView渲染开关按钮,通常用于启用或禁用某项功能。
text-areaStringConfigWidgetView渲染一个多行文本框,适合输入长文本内容。
labelLabelView渲染一个只读标签,用于显示不可编辑的信息。
test-db-connectionTestDbConnectionWidgetView渲染一个测试数据库连接的按钮,点击后执行连接测试并显示结果。

2.2.2 代码示例

以下是 widgetTypeMap 的代码片段,其中展示了不同 widget.type 与视图组件的对应关系:

代码出处:app/mixins/common/configs/enhanced_configs.js

在这里插入图片描述

/**
   * ConfigType-Widget map
   * key - widget type
   * value - widget view
   * @type {object}
   */
  widgetTypeMap: {
    checkbox: 'CheckboxConfigWidgetView',
    combo: 'ComboConfigWidgetView',
    directory: 'TextFieldConfigWidgetView',
    directories: 'DirectoryConfigWidgetView',
    list: 'ListConfigWidgetView',
    password: 'PasswordConfigWidgetView',
    'radio-buttons': 'RadioButtonConfigWidgetView',
    slider: 'SliderConfigWidgetView',
    'text-field': 'TextFieldConfigWidgetView',
    'time-interval-spinner': 'TimeIntervalSpinnerView',
    toggle: 'ToggleConfigWidgetView',
    'text-area': 'StringConfigWidgetView',
    'label': 'LabelView',
    'test-db-connection': 'TestDbConnectionWidgetView'
  }

2.3 渲染代码逻辑

  • 🤔突破口:“type”: “test-db-connection” 是在传统定义中没有找到的

    查阅了很多资料,目前 Test Connention 的按钮渲染,在原始配置中无法看到如何配置。涉及配置的点击事件,也没找寻到哪里可以进行配置。所以我把目光聚焦在按钮上。

			{
        "config": "admin-properties/db_host",
        "widget": {
          "type": "text-field"
        }
      },
      {
        "config": "admin-properties/db_password",
        "widget": {
          "type": "password"
        }
      },
			{
        "config": "ranger-env/test_db_connection",
        "widget": {
          "type": "test-db-connection",
          "display-name": "Test Connection",
          "required-properties": {
            "jdbc.driver.class": "ranger-admin-site/ranger.jpa.jdbc.driver",
            "jdbc.driver.url": "ranger-admin-site/ranger.jpa.jdbc.url",
            "db.connection.source.host": "ranger-site/ranger_admin_hosts",
            "db.type": "admin-properties/DB_FLAVOR",
            "db.connection.destination.host": "admin-properties/db_host",
            "db.connection.user": "admin-properties/db_user",
            "db.connection.password": "admin-properties/db_password"
          }
        }
      }
  • 核心代码片段: widgetTypeMap 是渲染的核心逻辑,前端根据 widget.type 使用相应的视图控件。
widgetTypeMap: {
checkbox: 'CheckboxConfigWidgetView',
combo: 'ComboConfigWidgetView',
password: 'PasswordConfigWidgetView',
'test-db-connection': 'TestDbConnectionWidgetView',
...
}
  • 代码示例(较为核心的代码均保留):

在这里插入图片描述

//@Todo merge with CheckDBConnectionView
App.TestDbConnectionWidgetView = App.ConfigWidgetView.extend({
templateName: require('templates/common/configs/widgets/test_db_connection_widget'),
classNames: ['widget'],
dbInfo: require('data/db_properties_info'),



didInsertElement: function () {
 var requiredProperties = this.get('config.stackConfigProperty.widget.required-properties');
 var serviceName = this.get('config.serviceName');
 var serviceConfigs = this.get('controller.stepConfigs').findProperty('serviceName',serviceName).get('configs');
 var requiredServiceConfigs = Object.keys(requiredProperties).map(function(key){
   var split = requiredProperties[key].split('/');
   var fileName =  split[0] + '.xml';
   var configName = split[1];
   var requiredConfig = serviceConfigs.filterProperty('filename',fileName).findProperty('name', configName);
   if (!requiredConfig) {
     var componentName = App.config.getComponentName(configName);
     var stackComponent = App.StackServiceComponent.find(componentName);
     if (stackComponent && stackComponent.get('componentName')) {
       var value = this.get('controller').getComponentHostValue(componentName,
         this.get('controller.wizardController.content.masterComponentHosts'),
         this.get('controller.wizardController.content.slaveComponentHosts'));
       var hProperty = App.config.createHostNameProperty(serviceName, componentName, value, stackComponent);
       return App.ServiceConfigProperty.create(hProperty);
     }
   } else {
     return requiredConfig;
   }
 }, this);

 this.set('requiredProperties', requiredServiceConfigs);
 this.setDbProperties(requiredProperties);
 this.getAmbariProperties();
},



/**
*  This function is used to set Database name and master host name
* @param requiredProperties: `config.stackConfigProperty.widget.required-properties` as stated in the theme
*/
setDbProperties: function(requiredProperties) {
 var dbProperties = {
   'db.connection.source.host' : 'masterHostName',
   'db.type' : 'db_type',
   'db.connection.user': 'user_name',
   'db.connection.password': 'user_passwd',
   'jdbc.driver.url': 'db_connection_url',
   'db.type.label': 'db_type_label'
 };

 for (var key in dbProperties) {
   var masterHostNameProperty = requiredProperties[key];
   if (masterHostNameProperty) {
     var split = masterHostNameProperty.split('/');
     var fileName = split[0] + '.xml';
     var configName = split[1];
     var dbConfig = this.get('requiredProperties').filterProperty('filename', fileName).findProperty('name', configName);
     this.set(dbProperties[key], dbConfig);
   }
 }
},

/**
* `Action` method for starting connect to current database.
*
* @method connectToDatabase
**/
connectToDatabase: function () {
 if (this.get('isBtnDisabled')) return;
 this.set('isRequestResolved', false);
 App.db.set('tmp', this.get('db_connection_url.serviceName') + '_connection', {});
 this.setConnectingStatus(true);
 if (App.get('testMode')) {
   this.startPolling();
 } else {
   this.runCheckConnection();
 }
},

/**
* runs check connections methods depending on service
* @return {void}
* @method runCheckConnection
*/
runCheckConnection: function () {
 this.createCustomAction();
},


/**
* Run custom action for database connection.
*
* @method createCustomAction
**/
createCustomAction: function () {
 var connectionProperties = this.getProperties('db_connection_url','user_name', 'user_passwd');
 var db_name = this.dbInfo.dpPropertiesMap[dbUtils.getDBType(this.get('db_type').value)].db_type;
 var isServiceInstalled = App.Service.find(this.get('config.serviceName')).get('isLoaded');
 for (var key in connectionProperties) {
   if (connectionProperties.hasOwnProperty(key)) {
     connectionProperties[key] = connectionProperties[key].value;
   }
 }
 var params = $.extend(true, {}, {db_name: db_name}, connectionProperties, this.get('ambariProperties'));
 var filteredHosts =  Array.isArray(this.get('masterHostName.value')) ? this.get('masterHostName.value') : [this.get('masterHostName.value')];
 App.ajax.send({
   name: (isServiceInstalled) ? 'cluster.custom_action.create' : 'custom_action.create',
   sender: this,
   data: {
     requestInfo: {
       parameters: params
     },
     filteredHosts: filteredHosts
   },
   success: 'onCreateActionSuccess',
   error: 'onCreateActionError'
 });
},
/**
* Run updater if task is created successfully.
*
* @method onConnectActionS
**/
onCreateActionSuccess: function (data) {
 this.set('currentRequestId', data.Requests.id);
 App.ajax.send({
   name: 'custom_action.request',
   sender: this,
   data: {
     requestId: this.get('currentRequestId')
   },
   success: 'setCurrentTaskId'
 });
},

setCurrentTaskId: function (data) {
 this.set('currentTaskId', data.items[0].Tasks.id);
 this.startPolling();
},

startPolling: function () {
 if (this.get('isConnecting'))
   this.getTaskInfo();
},

getTaskInfo: function () {
 var request = App.ajax.send({
   name: 'custom_action.request',
   sender: this,
   data: {
     requestId: this.get('currentRequestId'),
     taskId: this.get('currentTaskId')
   },
   success: 'getTaskInfoSuccess'
 });
 this.set('request', request);
},

preparedDBProperties: function() {
 var propObj = {};
 var serviceName = this.get('config.serviceName');
 var serviceConfigs = this.get('controller.stepConfigs').findProperty('serviceName',serviceName).get('configs');
 for (var key in this.get('propertiesPattern')) {
   var propName = this.getConnectionProperty(this.get('propertiesPattern')[key], true);
   propObj[propName] = serviceConfigs.findProperty('name', propName).get('value');
 }
 return propObj;
}.property(),

requiredProps: function() {
 var ranger = App.StackService.find().findProperty('serviceName', 'RANGER');
 var propertiesMap = {
   OOZIE: ['oozie.db.schema.name', 'oozie.service.JPAService.jdbc.username', 'oozie.service.JPAService.jdbc.password', 'oozie.service.JPAService.jdbc.driver', 'oozie.service.JPAService.jdbc.url'],
   HIVE: ['ambari.hive.db.schema.name', 'javax.jdo.option.ConnectionUserName', 'javax.jdo.option.ConnectionPassword', 'javax.jdo.option.ConnectionDriverName', 'javax.jdo.option.ConnectionURL'],
   KERBEROS: ['kdc_hosts'],
   RANGER: ranger && ranger.compareCurrentVersion('0.5') > -1 ?
     ['db_user', 'db_password', 'db_name', 'ranger.jpa.jdbc.url', 'ranger.jpa.jdbc.driver'] :
     ['db_user', 'db_password', 'db_name', 'ranger_jdbc_connection_url', 'ranger_jdbc_driver'],
   RANGER_KMS: ['db_user', 'db_password', 'ranger.ks.jpa.jdbc.url', 'ranger.ks.jpa.jdbc.driver']
 };
 return propertiesMap[this.get('parentView.content.serviceName')];
}.property(),

getConnectionProperty: function (regexp, isGetName) {
 var serviceName = this.get('config.serviceName');
 var serviceConfigs = this.get('controller.stepConfigs').findProperty('serviceName',serviceName).get('configs');
 var propertyName = this.get('requiredProps').filter(function (item) {
   return regexp.test(item);
 })[0];
 return (isGetName) ? propertyName : serviceConfigs.findProperty('name', propertyName).get('value');
},

propertiesPattern: function() {
 var patterns = {
   db_connection_url: /jdbc\.url|connection_url|connectionurl|kdc_hosts/ig
 };
 if (this.get('parentView.service.serviceName') != "KERBEROS") {
   patterns.user_name = /(username|dblogin|db_user)$/ig;
   patterns.user_passwd = /(dbpassword|password|db_password)$/ig;
 }
 return patterns;
}.property('parentView.service.serviceName'),

getTaskInfoSuccess: function (data) {
 var task = data.Tasks;
 this.set('responseFromServer', {
   stderr: task.stderr,
   stdout: task.stdout
 });
 if (task.status === 'COMPLETED') {
   var structuredOut = task.structured_out.db_connection_check;
   if (structuredOut.exit_code != 0) {
     this.set('responseFromServer', {
       stderr: task.stderr,
       stdout: task.stdout,
       structuredOut: structuredOut.message
     });
     this.setResponseStatus('failed');
   } else {
     App.db.set('tmp', this.get('db_connection_url.serviceName') + '_connection', this.get('preparedDBProperties'));
     this.setResponseStatus('success');
   }
 }
 if (task.status === 'FAILED') {
   this.setResponseStatus('failed');
 }
 if (/PENDING|QUEUED|IN_PROGRESS/.test(task.status)) {
   Em.run.later(this, function () {
     this.startPolling();
   }, this.get('pollInterval'));
 }
},

onCreateActionError: function (jqXhr, status, errorMessage) {
 this.setResponseStatus('failed');
 this.set('responseFromServer', errorMessage);
},

setResponseStatus: function (isSuccess) {
 var db_type = this.dbInfo.dpPropertiesMap[dbUtils.getDBType(this.get('db_type').value)].db_type.toUpperCase();
 var isSuccess = isSuccess == 'success';
 this.setConnectingStatus(false);
 this.set('responseCaption', isSuccess ? Em.I18n.t('services.service.config.database.connection.success') : Em.I18n.t('services.service.config.database.connection.failed'));
 this.set('isConnectionSuccess', isSuccess);
 this.set('isRequestResolved', true);
 if (this.get('logsPopup')) {
   var statusString = isSuccess ? 'common.success' : 'common.error';
   this.set('logsPopup.header', Em.I18n.t('services.service.config.connection.logsPopup.header').format(db_type, Em.I18n.t(statusString)));
 }
},
/**
* Switch captions and statuses for active/non-active request.
*
* @method setConnectionStatus
* @param {Boolean} [active]
*/
setConnectingStatus: function (active) {
 if (active) {
   this.set('responseCaption', Em.I18n.t('services.service.config.database.connection.inProgress'));
 }
 this.set('controller.testConnectionInProgress', !!active);
 this.set('btnCaption', !!active ? Em.I18n.t('services.service.config.database.btn.connecting') : Em.I18n.t('services.service.config.database.btn.idle'));
 this.set('isConnecting', !!active);
},
/**
* Set view to init status.
*
* @method restore
**/
restore: function () {
 if (this.get('request')) {
   this.get('request').abort();
   this.set('request', null);
 }
 this.set('responseCaption', null);
 this.set('responseFromServer', null);
 this.setConnectingStatus(false);
 this.set('isRequestResolved', false);
},
/**
* `Action` method for showing response from server in popup.
*
* @method showLogsPopup
**/
showLogsPopup: function () {
 if (this.get('isConnectionSuccess')) return;
 var _this = this;
 var db_type = this.dbInfo.dpPropertiesMap[dbUtils.getDBType(this.get('db_type').value)].db_type.toUpperCase();
 var statusString = this.get('isRequestResolved') ? 'common.error' : 'common.testing';
 var popup = App.showAlertPopup(Em.I18n.t('services.service.config.connection.logsPopup.header').format(db_type, Em.I18n.t(statusString)), null, function () {
   _this.set('logsPopup', null);
 });
 popup.reopen({
   onClose: function () {
     this._super();
     _this.set('logsPopup', null);
   }
 });
 if (typeof this.get('responseFromServer') == 'object') {
   popup.set('bodyClass', Em.View.extend({
     checkDBConnectionView: _this,
     templateName: require('templates/common/error_log_body'),
     openedTask: function () {
       return this.get('checkDBConnectionView.responseFromServer');
     }.property('checkDBConnectionView.responseFromServer.stderr', 'checkDBConnectionView.responseFromServer.stdout', 'checkDBConnectionView.responseFromServer.structuredOut')
   }));
 } else {
   popup.set('body', this.get('responseFromServer'));
 }
 this.set('logsPopup', popup);
 return popup;
}
});

⬇️⬇️⬇️查看全部内容⬇️⬇️⬇️


更多详细内容请关注我们的微信公众号:发送"文章"关键字获取

或加入QQ1群,了解版本动向,解答大数据问题。


⬆️⬆️⬆️查看全部内容⬆️⬆️⬆️

3.5 使用的模板路径

  • 模板路径: app/templates/wizard/controls_service_config_radio_buttons.hbs

  • 模板代码:

在这里插入图片描述

在这里插入图片描述

看到了熟悉的options

{{!
* 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.
}}

{{#each option in view.options}}
{{#unless option.hidden}}
 <div class="radio">
   {{view App.ServiceConfigRadioButton nameBinding="view.name" valueBinding="option.displayName" radioIdBinding="option.radioId"}}
   <label {{bindAttr for="option.radioId" class="option.className"}}>
     {{option.displayName}} &nbsp;
   </label>
 </div>
{{/unless}}
{{/each}}

总结:

  1. 使用了 HBS 模板技术进行前端控件渲染 🎨
    在 Ambari 的 UI 配置页面中,通过 Handlebars (HBS) 模板技术动态生成 HTML 结构。结合数据源,这种技术能够灵活地渲染各种配置控件,使页面展现更具扩展性和定制化能力。
  2. 主题配置的渲染路径 📂
    主题配置下,控件渲染使用 app/templates/common/configs/widgets 目录中的模板。例如,radio button 控件的渲染模板路径为:
    app/templates/common/configs/widgets/radio_button_config.hbs
  3. 高级设置页面的渲染路径 🛠️
    在高级设置页面,控件通过 app/templates/wizard/ 目录下的模板文件进行渲染。例如,radio button 控件在此页面的渲染模板为:
    app/templates/wizard/controls_service_config_radio_buttons.hbs
  4. 自定义组件的集成挑战 🚧
    如果你想集成自定义组件(尤其是按钮级别的控件,如 test-db-connection),不修改源码的情况下,集成难度较大。很多交互逻辑和属性配置需要在前端额外处理,因此需要手写一些 JavaScript 逻辑和 Handlebars 模板,增加了集成的复杂度。⚙️
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值