1. 页面渲染概述
- 作用: 总体介绍页面渲染的两种方式,分别是基于
widgetTypeMap
和displayType
的渲染。 - 渲染方式:
widgetTypeMap
:基于theme.json
的widget
配置渲染。displayType
:前端通过配置displayType
来控制不同的控件渲染。
2. 基于 widgetTypeMap
的渲染
2.1 作用
- 定义: 通过
theme.json
中的widget
配置,前端通过widgetTypeMap
映射来决定页面控件的类型。 - 使用场景: 通常用于控制页面控件的展示,如输入框、密码框、滑动条等。
2.2 渲染效果说明
widgetTypeMap
是 Ambari 中将不同配置类型映射到具体控件的核心机制。根据 widget.type
,前端会通过映射表加载相应的控件视图。以下是常见控件类型及其渲染效果说明。
2.2.1 常见控件类型列表:
控件类型 | 对应的控件视图 | 渲染效果 |
---|---|---|
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.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}}
</label>
</div>
{{/unless}}
{{/each}}
总结:
- 使用了 HBS 模板技术进行前端控件渲染 🎨
在 Ambari 的 UI 配置页面中,通过 Handlebars (HBS) 模板技术动态生成 HTML 结构。结合数据源,这种技术能够灵活地渲染各种配置控件,使页面展现更具扩展性和定制化能力。 - 主题配置的渲染路径 📂
主题配置下,控件渲染使用app/templates/common/configs/widgets
目录中的模板。例如,radio button 控件的渲染模板路径为:
app/templates/common/configs/widgets/radio_button_config.hbs
。 - 高级设置页面的渲染路径 🛠️
在高级设置页面,控件通过app/templates/wizard/
目录下的模板文件进行渲染。例如,radio button 控件在此页面的渲染模板为:
app/templates/wizard/controls_service_config_radio_buttons.hbs
。 - 自定义组件的集成挑战 🚧
如果你想集成自定义组件(尤其是按钮级别的控件,如 test-db-connection),不修改源码的情况下,集成难度较大。很多交互逻辑和属性配置需要在前端额外处理,因此需要手写一些 JavaScript 逻辑和 Handlebars 模板,增加了集成的复杂度。⚙️