diff --git a/README.md b/README.md index cca899a..50cd82d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## 开始 安装依赖 ``` -yarn +yarn install ``` 开始开发 @@ -15,6 +15,104 @@ yarn yarn dev ``` +## 决策平台开发: + +### A.项目运行 + +#### 1. 工程`decision-webui-dev`添加代理(可跳过) +```js + webpack/webpack.config + "/plugin/dcm": { + pathRewrite: { "^/plugin/dcm": "" }, + target: "http://localhost:10002", + }, +``` +#### 2. 工程`decision-webui-dev`引入 + fr环境:`templates/bundle.report.html` bi环境:`templates/bundle.bi.html` + +```html + // css 文件: + + + + + + // js 文件 + +``` + 若未设1,将`/plugin/dcm`替换成`http://localhost:10002`亦可 +#### 3. 启动工程[decision-webui-dev]以及数据连接[desicion-webui-dcm]工程 + +#### 4. 此时工程`decision-webui-dev`的`http://localhost:9002/#management/connnection`数据连接模块已替换成该工程 + +### B.插件形式添加数据连接-数据库 + +#### 1. 以多版本的tdsql为例 单一版本数据库不需drivers,versions,hasSchemas + +```js +BI.config("dec.connection.provider.datebase", function (provider) { + BI.isFunction(provider.registerJdbcDatabase) && provider.registerJdbcDatabase({ + text: 'TDSQL', // 数据库名称 + databaseType: 'tdsql', // 数据库key + driver: 'org.postgresql.Driver', // 默认驱动 + drivers: { + "pgsql": ["org.postgresql.Driver"], + "mysql": ["com.mysql.jdbc.Driver"] + }, // 驱动可选项,version: array[driver],[0]为该版本的默认驱动 + versions: ["pgsql", "mysql"], // array[version] + urls: { + "org.postgresql.Driver": "jdbc:postgresql://hostname:port/database?finedbType=tdsql-pgsql", + "com.mysql.jdbc.Driver": "jdbc:mysql://hostname:port/database?finedbType=tdsql-mysql" + }, // urlkey : url 一个驱动对应一个url + url: 'jdbc:postgresql://hostname:port/database?finedbType=tdsql-pgsql', + commonly: false, + internal: true, + type: 'jdbc', 数据库类型 + hasSchema: true, // 默认是否支持模式 + hasSchemas: { + "pgsql": true, + "mysql": false, + },是否支持模式 version: boolean + kerberos: false, // 是否添加kerberos认证方式 + }, function (url) { + var result = url.match(/^jdbc:(mysql|postgresql):\/\/([0-9a-zA-Z_\\.-]+)(:([0-9|port]+))?\/([0-9a-zA-Z_\\.]+)(.*)finedbType=([^&]+)(|(&.*))/i); // 匹配正则 + if (result) { + return { + host: result[2], //主机 + port: result[4] === "port" ? "" : result[4], // 端口 + databaseName: result[5], // 数据库名称 + version: result[7].split('-')[1] ?? "pgsql", // 版本 单版本不要返回这个 + }; + } +//适配原先tbase的url + result = url.match(/^jdbc:postgresql:\/\/([0-9a-zA-Z_\\.-]+)(:([0-9|port]+))?\/([0-9a-zA-Z_\\.]+)(.*)/i); + if (result) { + return { + host: result[1], + port: result[3] === "port" ? "" : result[3], + databaseName: result[4], + version: "pgsql", + }; + } + + }); + }); +``` + +### C 工程开发 + +#### 1. 图片资源添加 + 工程`decision-webui-dev` + decision-webui/dist/images/1x/icon/database + decision-webui/dist/images/2x/icon/database + +#### 2. 国际化添加 + 工程`decision-webui-dev` + decision-i18n/decision-main-i18n/src/main/resources/com/fr/decision/web/i18n + +#### 3. 版本控制 + 版本和平台保持一致 + ## 接口文档: ### 增加数据连接类型 使用`BI.config`,ConstantName名称为`dec.constant.database.conf.connect.types`,值为连接的名称 diff --git a/src/modules/app.provider.ts b/src/modules/app.provider.ts index 791d2be..6ce5bce 100644 --- a/src/modules/app.provider.ts +++ b/src/modules/app.provider.ts @@ -26,7 +26,7 @@ BI.provider('dec.connection.provider.datebase', function () { urlInfo: greenplumUrl[9], }; } - const result = url.match(/^jdbc:(mysql|sqlserver|db2|dm|impala|kylin|phoenix|derby|gbase|gbasedbt-sqli|informix-sqli|h2|postgresql|hive2|vertica|kingbase|presto|redshift|postgresql|clickhouse|trino):(thin:([0-9a-zA-Z/]*)?@|thin:([0-9a-zA-Z/]*)?@\/\/|\/\/|)([0-9a-zA-Z_\\.-]+)(:([0-9|port]+))?(\/|;DatabaseName=)?([^]+)?(.*)/i); + const result = url.match(/^jdbc:(mysql|sqlserver|db2|dm|impala|kylin|phoenix|derby|gbase|gbasedbt-sqli|informix-sqli|h2|postgresql|hive2|vertica|kingbase|presto|redshift|postgresql|clickhouse|trino|sybase:Tds):(thin:([0-9a-zA-Z/]*)?@|thin:([0-9a-zA-Z/]*)?@\/\/|\/\/|)([0-9a-zA-Z_\\.-]+)(:([0-9|port]+))?(\/|;DatabaseName=)?([^]+)?(.*)/i); if (result) { return { host: result[5], diff --git a/src/modules/components/test_status/test_status.ts b/src/modules/components/test_status/test_status.ts index 8929f7d..a055e6e 100644 --- a/src/modules/components/test_status/test_status.ts +++ b/src/modules/components/test_status/test_status.ts @@ -29,7 +29,9 @@ export class TestStatus extends BI.Widget { failDriverMessage: Label; driverLink: FloatLeftLayout; detail: VerticalLayout; - failMaskers:any; + failMaskers: any; + + extraContainer: VerticalLayout; watch = { status: (status: string) => { @@ -38,8 +40,9 @@ export class TestStatus extends BI.Widget { } render() { + const LAYOUT_WIDTH = 400; const { loadingCls, loadingText, successCls, successText, failCls, failText, retryText } = this.options; - var self=this; + return { type: BI.CenterAdaptLayout.xtype, cls: 'bi-z-index-mask', @@ -71,10 +74,10 @@ export class TestStatus extends BI.Widget { tipCls: failCls, tipText: failText, retryText, - ref:(_ref:any)=>{ - self.failMaskers=_ref; - if(this.failMessage.getText()===''){ - this.failMaskers.populateFail(BI.i18nText("Dec-Conn-ect-Failed"),false); + ref: (_ref: TipFail) => { + this.failMaskers = _ref; + if (BI.isEmptyString(this.failMessage.getText())) { + this.failMaskers.populateFail(BI.i18nText('Dec-Conn-ect-Failed'), false); } }, listeners: [ @@ -123,10 +126,17 @@ export class TestStatus extends BI.Widget { scrolly: true, height: 75, items: [ + { + type: BI.VerticalLayout.xtype, + width: LAYOUT_WIDTH, + ref: (_ref: VerticalLayout) => { + this.extraContainer = _ref; + } + }, { type: BI.Label.xtype, whiteSpace: 'normal', - width: 400, + width: LAYOUT_WIDTH, textAlign: 'left', text: '', ref: (_ref: Label) => { @@ -175,7 +185,7 @@ export class TestStatus extends BI.Widget { this.store.setStatus(TEST_STATUS.SUCCESS); } - setFail(message: string='', driver = '', link = '') { + setFail(message: string = '', driver = '', link = '') { this.store.setStatus(TEST_STATUS.FAIL); this.failMessage.setText(message); this.failDriverMessage.setVisible(!!driver); @@ -189,4 +199,14 @@ export class TestStatus extends BI.Widget { setLoading() { this.store.setStatus(TEST_STATUS.LOADING); } + + /** + * 设置报错弹窗自定义展示内容 + */ + setExtraContainer(container: Obj) { + BI.createWidget({ + ...container, + element: this.extraContainer, + }); + } } diff --git a/src/modules/constants/constant.ts b/src/modules/constants/constant.ts index dbf1b28..fa34f1f 100644 --- a/src/modules/constants/constant.ts +++ b/src/modules/constants/constant.ts @@ -831,7 +831,7 @@ export const DEFAULT_JDBC_POOL = { testOnBorrow: true, testOnReturn: false, testWhileIdle: false, - timeBetweenEvictionRunsMillis: -1, + timeBetweenEvictionRunsMillis: 60000, numTestsPerEvictionRun: 3, minEvictableIdleTimeMillis: 1800, }; diff --git a/src/modules/crud/api.ts b/src/modules/crud/api.ts index daa9e4d..01ed8dd 100644 --- a/src/modules/crud/api.ts +++ b/src/modules/crud/api.ts @@ -5,6 +5,7 @@ import { ConnectionPoolType, SocketResult, ResultType, + checkDriverStatusParams, } from './crud.typings'; export interface Api { @@ -46,6 +47,17 @@ export interface Api { */ testConnection(data: Connection): Promise; + /** + * 获取驱动加载路径 + */ + getDriverLoadPath(data: Connection): Promise>; + + /** + * 检测驱动冲突状态 + * @param data 驱动路径 + */ + checkDriverStatus(data: checkDriverStatusParams): Promise>; + /** * 获取连接池数据 * @param name diff --git a/src/modules/crud/crud.typings.d.ts b/src/modules/crud/crud.typings.d.ts index 0193583..8a5c8b9 100644 --- a/src/modules/crud/crud.typings.d.ts +++ b/src/modules/crud/crud.typings.d.ts @@ -306,8 +306,13 @@ export interface SocketResult { errorMsg?: string; } -export interface ResultType { - data?: any; +export interface ResultType { + data?: T; errorCode?: string; errorMsg?: string; } + +export type checkDriverStatusParams = { + path: string; + driver: ConnectionJDBC['driver']; +} \ No newline at end of file diff --git a/src/modules/crud/decision.api.ts b/src/modules/crud/decision.api.ts index a77f406..dbcf141 100644 --- a/src/modules/crud/decision.api.ts +++ b/src/modules/crud/decision.api.ts @@ -1,5 +1,5 @@ import { Api } from './api'; -import { Connection, TestRequest, ConnectionPoolType, SocketResult, ConnectionLicInfo } from './crud.typings'; +import { Connection, TestRequest, ConnectionPoolType, SocketResult, ConnectionLicInfo, ResultType, checkDriverStatusParams } from './crud.typings'; import { requestGet, requestDelete, requestPost, requestPut } from './crud.service'; import { editStatusEvent, errorCode } from '@constants/env'; @@ -48,6 +48,27 @@ export class DecisionApi implements Api { return requestPost('test', form); } + /** + * 获取驱动加载路径 + * @returns + */ + getDriverLoadPath(data: Connection): Promise> { + const form = { + ...data, + connectionData: JSON.stringify(data.connectionData), + }; + + return requestPost('driver/path', form); + } + + /** + * 检测驱动冲突状态 + * @param data 驱动路径 + */ + checkDriverStatus(data: checkDriverStatusParams): Promise> { + return requestGet(Dec.Utils.getEncodeURL('test/driver/conflict', '', data)); + } + getConnectionPool(name: string): Promise<{ data?: ConnectionPoolType }> { return requestGet(`pool/info?connectionName=${encodeURIComponent(name)}`, {}); } diff --git a/src/modules/crud/design.api.ts b/src/modules/crud/design.api.ts index 643ad8e..d629b16 100644 --- a/src/modules/crud/design.api.ts +++ b/src/modules/crud/design.api.ts @@ -1,5 +1,5 @@ import { Api } from './api'; -import { Connection, TestRequest, ConnectionPoolType, SocketResult, ConnectionLicInfo } from './crud.typings'; +import { Connection, TestRequest, ConnectionPoolType, SocketResult, ConnectionLicInfo, ResultType, ConnectionJDBC, checkDriverStatusParams } from './crud.typings'; import { requestGet } from './crud.service'; // TODO: 此页面的接口等待设计器提供相应的方法 @@ -39,6 +39,27 @@ export class DesignApi implements Api { }); } + /** + * 获取驱动加载路径 + * @param name + * @returns + */ + getDriverLoadPath(data: Connection): Promise> { + return new Promise(resolve => { + resolve({ data: '' }); + }); + } + + /** + * 检测驱动冲突状态 + * @param data 驱动路径 + */ + checkDriverStatus(data: checkDriverStatusParams): Promise> { + return new Promise(resolve => { + resolve({ data: false }); + }); + } + getConnectionPool(name: string): Promise<{ data: ConnectionPoolType }> { return new Promise(resolve => { resolve({ diff --git a/src/modules/pages/connection/list/list_item/list_item.ts b/src/modules/pages/connection/list/list_item/list_item.ts index 58537d7..60736d3 100644 --- a/src/modules/pages/connection/list/list_item/list_item.ts +++ b/src/modules/pages/connection/list/list_item/list_item.ts @@ -6,6 +6,9 @@ import { hasRegistered } from '../list.service'; import { connectionCanEdit, getTextByDatabaseType, getChartLength } from '../../../../app.service'; import { testConnection } from '../../../maintain/forms/form.server'; import { DownListCombo, Label, SignEditor } from '@fui/core'; +import { ApiFactory } from '../../../../crud/apiFactory'; + +const api = new ApiFactory().create(); @shortcut() @store(ListItemModel) @@ -206,8 +209,17 @@ export class ListItem extends BI.BasicButton { } private testConnectionAction() { - const { name } = this.options; - testConnection(this.model.connections.find(item => item.connectionName === name)); + // 接口返回的内容是对称加密的,前端要先解密再用新加密传回去 + const connection = this.model.connections + .find(item => item.connectionName === this.options.name); + + if (BI.isNull(connection)) return; + + const validationQuery = connection?.connectionData?.connectionPoolAttr?.validationQuery || ''; + + BI.set(connection, 'connectionData.connectionPoolAttr.validationQuery', api.getCipher(api.getPlain(validationQuery))); + + testConnection(connection); } private itemActionCalculate() { diff --git a/src/modules/pages/maintain/forms/components/form.jdbc.ts b/src/modules/pages/maintain/forms/components/form.jdbc.ts index 869f63d..7db6718 100644 --- a/src/modules/pages/maintain/forms/components/form.jdbc.ts +++ b/src/modules/pages/maintain/forms/components/form.jdbc.ts @@ -1015,6 +1015,14 @@ export class FormJdbc extends BI.Widget { watermark: BI.i18nText('Dec-Dcm_Connection_Form_Host'), allowBlank: false, value: sshIp || 'hostname', + listeners: [ + { + eventName: BI.Editor.EVENT_CHANGE, + action: () => { + this.form.sshSecret.setValue(""); + } + } + ] }, ], }, @@ -1038,6 +1046,14 @@ export class FormJdbc extends BI.Widget { valueRangeConfig, ], value: String(sshPort || 22), + listeners: [ + { + eventName: BI.Editor.EVENT_CHANGE, + action: () => { + this.form.sshSecret.setValue(""); + } + } + ] }, ], }, diff --git a/src/modules/pages/maintain/forms/form.server.ts b/src/modules/pages/maintain/forms/form.server.ts index 67647fc..2098ac1 100644 --- a/src/modules/pages/maintain/forms/form.server.ts +++ b/src/modules/pages/maintain/forms/form.server.ts @@ -5,6 +5,7 @@ import { TestStatus } from '../../../components/test_status/test_status'; import { getJdbcDatabaseType } from '../../../app.service'; import { ApiFactory } from '../../../crud/apiFactory'; const api = new ApiFactory().create(); + export function testConnection(value: Connection): Promise { return new Promise(resolve => { let testStatus = null; @@ -15,16 +16,18 @@ export function testConnection(value: Connection): Promise { return false; } + const id = BI.UUID(); const testConnection = () => { const formValue = value; + api.testConnection(formValue).then(re => { if (re && re.errorCode) { - if(re.errorCode === DecCst.ErrorCode.NO_IP_AUTHORIZED){ + if (re.errorCode === DecCst.ErrorCode.NO_IP_AUTHORIZED) { testStatus.setFail(); return; } - // 判断是否是缺少驱动,如果缺少驱动则显示下载驱动的连接 + // 判断是否是缺少驱动,如果缺少驱动则显示下载驱动的连接 if (api.isDriverError(re.errorCode)) { if (formValue.connectionType === connectionType.JDBC) { const driver = (formValue.connectionData as ConnectionJDBC).driver; @@ -44,7 +47,11 @@ export function testConnection(value: Connection): Promise { } else if (re.errorCode === errorCode.DUPLICATE_NAMES) { testStatus.setFail(BI.i18nText(re.errorMsg)); } else { + // 不缺少驱动,但连接失败,打印出当前驱动加载路径,并显示检测驱动按钮 testStatus.setFail(re.errorMsg); + api.getDriverLoadPath(formValue).then(res => { + testStatus.setExtraContainer(createDriverTestContainer(res.data)); + }) } } else if (re.data) { testStatus.setSuccess(); @@ -59,7 +66,54 @@ export function testConnection(value: Connection): Promise { BI.Maskers.remove(id); } }); + + /** + * 驱动及冲突检测内容,补充到报错弹窗里 + */ + function createDriverTestContainer(path: string) { + return { + type: BI.VerticalLayout.xtype, + vgap: 5, + items: [ + { + type: BI.Label.xtype, + text: BI.i18nText('Dec-Connection_Driver_Current_Load_Path', path), + textAlign: 'left', + whiteSpace: 'normal', + }, + { + type: BI.TextButton.xtype, + cls: 'bi-high-light', + text: BI.i18nText('Dec-Connection_Driver_Check'), + textAlign: 'left', + handler: () => { + api.checkDriverStatus({ + driver: (formValue.connectionData as ConnectionJDBC).driver, + path, + }).then(res => { + const isDriverConflict = res.data; + + testStatus.setExtraContainer({ + type: BI.VerticalLayout.xtype, + items: [ + { + type: BI.Label.xtype, + textAlign: 'left', + text: isDriverConflict + ? BI.i18nText('Dec-Connection_Driver_Has_Confilt_Tip') + : BI.i18nText('Dec-Connection_Driver_No_Confilt_Tip'), + cls: isDriverConflict ? 'bi-error' : '', + } + ] + }) + }); + } + } + ] + } + } }; + BI.Maskers.create(id, null, { render: { type: TestStatus.xtype, diff --git a/types/globals.d.ts b/types/globals.d.ts index f868d4b..0eb147e 100644 --- a/types/globals.d.ts +++ b/types/globals.d.ts @@ -16,6 +16,7 @@ declare const Dec: { personal: { username: string; }; + Utils: Obj; reqByEncrypt: (method: AxiosType.X_Method, url: string, data?: any, config?: AxiosType.X_AxiosRequestConfig) => {}, socketEmit: (type: string, name: string, callback: (re: any) => void) => void; // req