This is an automated email from the ASF dual-hosted git repository. dockerzhang pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/inlong.git
The following commit(s) were added to refs/heads/master by this push: new ffda109fac [INLONG-11766][Dashboard] Support SQL source and SQL node (#11767) ffda109fac is described below commit ffda109faca7f9fb194da013926a37485f488c19 Author: kamianlaida <165994047+wohainilao...@users.noreply.github.com> AuthorDate: Fri Feb 21 14:05:39 2025 +0800 [INLONG-11766][Dashboard] Support SQL source and SQL node (#11767) --- inlong-dashboard/src/core/utils/pattern.ts | 1 + .../src/plugins/groups/common/GroupDefaultInfo.ts | 2 +- inlong-dashboard/src/plugins/images/COS.png | Bin 0 -> 79078 bytes inlong-dashboard/src/plugins/images/SQL.png | Bin 0 -> 152893 bytes inlong-dashboard/src/plugins/nodes/defaults/SQL.ts | 61 +++++ .../src/plugins/nodes/defaults/index.ts | 5 + .../plugins/sources/common/SourceDefaultInfo.ts | 2 + .../src/plugins/sources/defaults/SQL.ts | 264 +++++++++++++++++++++ .../src/plugins/sources/defaults/index.ts | 5 + .../src/ui/components/CheckCard/index.tsx | 29 ++- inlong-dashboard/src/ui/locales/cn.json | 9 +- inlong-dashboard/src/ui/locales/en.json | 9 +- 12 files changed, 383 insertions(+), 4 deletions(-) diff --git a/inlong-dashboard/src/core/utils/pattern.ts b/inlong-dashboard/src/core/utils/pattern.ts index 7f4dc7df21..814ff738df 100644 --- a/inlong-dashboard/src/core/utils/pattern.ts +++ b/inlong-dashboard/src/core/utils/pattern.ts @@ -28,4 +28,5 @@ export default { url: /^https?:\/\/(([a-zA-Z0-9_-])+(\.)?)*(:\d+)?(\/((\.)?(\?)?=?&?[a-zA-Z0-9_-](\?)?)*)*$/i, port: /^([1-9](\d{0,3}))$|^([1-5]\d{4})$|^(6[0-4]\d{3})$|^(65[0-4]\d{2})$|^(655[0-2]\d)$|^(6553[0-5])$/, version: /^((\d+)\.(\d+)\.(\d+))$/, + sql: /^jdbc:(mysql|postgresql|oracle|sqlserver):\/\/[^\/]+(\/.*)?$/, }; diff --git a/inlong-dashboard/src/plugins/groups/common/GroupDefaultInfo.ts b/inlong-dashboard/src/plugins/groups/common/GroupDefaultInfo.ts index 47397b9999..9a37f94d48 100644 --- a/inlong-dashboard/src/plugins/groups/common/GroupDefaultInfo.ts +++ b/inlong-dashboard/src/plugins/groups/common/GroupDefaultInfo.ts @@ -57,7 +57,7 @@ export class GroupDefaultInfo implements DataWithBackend, RenderRow, RenderList @FieldDecorator({ type: 'input', props: { - maxLength: 32, + maxLength: 200, }, }) @I18n('meta.Group.InlongGroupName') diff --git a/inlong-dashboard/src/plugins/images/COS.png b/inlong-dashboard/src/plugins/images/COS.png new file mode 100644 index 0000000000..f6d1ce5a66 Binary files /dev/null and b/inlong-dashboard/src/plugins/images/COS.png differ diff --git a/inlong-dashboard/src/plugins/images/SQL.png b/inlong-dashboard/src/plugins/images/SQL.png new file mode 100644 index 0000000000..2018f6f931 Binary files /dev/null and b/inlong-dashboard/src/plugins/images/SQL.png differ diff --git a/inlong-dashboard/src/plugins/nodes/defaults/SQL.ts b/inlong-dashboard/src/plugins/nodes/defaults/SQL.ts new file mode 100644 index 0000000000..686e55de35 --- /dev/null +++ b/inlong-dashboard/src/plugins/nodes/defaults/SQL.ts @@ -0,0 +1,61 @@ +/* + * 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. + */ + +import { DataWithBackend } from '@/plugins/DataWithBackend'; +import { RenderRow } from '@/plugins/RenderRow'; +import { RenderList } from '@/plugins/RenderList'; +import { NodeInfo } from '../common/NodeInfo'; +import rulesPattern from '@/core/utils/pattern'; +import i18n from '@/i18n'; + +const { I18n } = DataWithBackend; +const { FieldDecorator } = RenderRow; + +export default class SQLNode extends NodeInfo implements DataWithBackend, RenderRow, RenderList { + @FieldDecorator({ + type: 'input', + rules: [ + { + required: true, + pattern: rulesPattern.sql, + message: i18n.t('meta.SQL.Check'), + }, + ], + props: { + placeholder: + 'jdbc:mysql://xxx.xxx.xxx.xxx:3306/data_base_name?useSSL=false&serverTimezone=UTC', + }, + }) + @I18n('meta.Nodes.MySQL.Url') + url: string; + + @FieldDecorator({ + type: 'input', + rules: [{ required: true }], + }) + @I18n('meta.Nodes.MySQL.Username') + username: string; + + @FieldDecorator({ + type: 'password', + rules: [{ required: true }], + }) + @I18n('meta.Nodes.MySQL.Password') + token: string; +} diff --git a/inlong-dashboard/src/plugins/nodes/defaults/index.ts b/inlong-dashboard/src/plugins/nodes/defaults/index.ts index b25fd69cf9..c2623019c5 100644 --- a/inlong-dashboard/src/plugins/nodes/defaults/index.ts +++ b/inlong-dashboard/src/plugins/nodes/defaults/index.ts @@ -106,4 +106,9 @@ export const allDefaultNodes: MetaExportWithBackendList<NodeMetaType> = [ value: 'COS', LoadEntity: () => import('./COS'), }, + { + label: 'SQL', + value: 'SQL', + LoadEntity: () => import('./SQL'), + }, ]; diff --git a/inlong-dashboard/src/plugins/sources/common/SourceDefaultInfo.ts b/inlong-dashboard/src/plugins/sources/common/SourceDefaultInfo.ts index b89a77ada9..c396305c15 100644 --- a/inlong-dashboard/src/plugins/sources/common/SourceDefaultInfo.ts +++ b/inlong-dashboard/src/plugins/sources/common/SourceDefaultInfo.ts @@ -201,6 +201,7 @@ export class SourceDefaultInfo implements DataWithBackend, RenderRow, RenderList label: item.label, value: item.value, image: loadImage(item.label), + isSmallImage: item.label === 'SQL' || item.label === 'COS' || item.label === 'File', })), }); } @@ -222,6 +223,7 @@ export class SourceDefaultInfo implements DataWithBackend, RenderRow, RenderList label: item.label, value: item.value, image: loadImage(item.label), + isSmallImage: item.label === 'SQL' || item.label === 'COS' || item.label === 'File', })), }); } diff --git a/inlong-dashboard/src/plugins/sources/defaults/SQL.ts b/inlong-dashboard/src/plugins/sources/defaults/SQL.ts new file mode 100644 index 0000000000..b632492f09 --- /dev/null +++ b/inlong-dashboard/src/plugins/sources/defaults/SQL.ts @@ -0,0 +1,264 @@ +/* + * 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. + */ + +import { DataWithBackend } from '@/plugins/DataWithBackend'; +import { RenderRow } from '@/plugins/RenderRow'; +import { RenderList } from '@/plugins/RenderList'; +import i18n from '@/i18n'; +import rulesPattern from '@/core/utils/pattern'; +import { SourceInfo } from '../common/SourceInfo'; +import MultiSelectWithALL, { ALL_OPTION_VALUE } from '@/ui/components/MultiSelectWithAll'; + +const { I18n } = DataWithBackend; +const { FieldDecorator, IngestionField } = RenderRow; +const { ColumnDecorator } = RenderList; + +export default class SQLSource + extends SourceInfo + implements DataWithBackend, RenderRow, RenderList +{ + @FieldDecorator({ + type: 'select', + rules: [{ required: true }], + props: values => ({ + disabled: Boolean(values.id), + showSearch: true, + allowClear: true, + filterOption: false, + options: { + requestTrigger: ['onOpen', 'onSearch'], + requestService: keyword => ({ + url: '/cluster/list', + method: 'POST', + data: { + keyword, + type: 'AGENT', + clusterTag: values.clusterTag, + pageNum: 1, + pageSize: 10, + }, + }), + requestParams: { + formatResult: result => + result?.list?.map(item => ({ + ...item, + label: item.displayName, + value: item.name, + })), + }, + }, + onChange: (value, option) => { + return { + clusterId: option.id, + }; + }, + }), + }) + @ColumnDecorator() + @IngestionField() + @I18n('meta.Sources.File.ClusterName') + inlongClusterName: string; + + @FieldDecorator({ + type: 'text', + hidden: true, + props: values => ({ + disabled: Boolean(values.id), + }), + }) + @I18n('clusterId') + @IngestionField() + clusterId: number; + + @FieldDecorator({ + type: 'select', + rules: [ + { + pattern: rulesPattern.ip, + message: i18n.t('meta.Sources.File.IpRule'), + required: true, + }, + ], + props: values => ({ + disabled: Boolean(values.id), + showSearch: true, + allowClear: true, + filterOption: false, + options: { + requestTrigger: ['onOpen', 'onSearch'], + requestService: keyword => ({ + url: '/cluster/node/list', + method: 'POST', + data: { + keyword, + parentId: values.clusterId, + pageNum: 1, + pageSize: 10, + }, + }), + requestParams: { + formatResult: result => + result?.list?.map(item => ({ + ...item, + label: item.ip, + value: item.ip, + })), + }, + }, + }), + }) + @ColumnDecorator() + @IngestionField() + @I18n('meta.Sources.File.DataSourceIP') + agentIp: string; + + @FieldDecorator({ + type: 'select', + rules: [{ required: true }], + props: values => ({ + showSearch: true, + allowClear: true, + filterOption: false, + disabled: [200, 201, 202, 204, 205, 300, 301, 302, 304, 305].includes(values?.status), + options: { + requestAuto: true, + requestTrigger: ['onOpen', 'onSearch'], + requestService: keyword => ({ + url: '/node/list', + method: 'POST', + data: { + keyword, + pageNum: 1, + pageSize: 20, + type: 'SQL', + }, + }), + requestParams: { + formatResult: result => + result?.list?.map(item => ({ + ...item, + label: item.displayName, + value: item.name, + })), + }, + }, + }), + }) + @IngestionField() + @ColumnDecorator() + @I18n('meta.Sources.COS.DataNode') + dataNodeName: string; + + @FieldDecorator({ + type: 'input', + rules: [{ required: true }], + props: values => ({ + disabled: Boolean(values.id), + }), + }) + @IngestionField() + @ColumnDecorator() + @I18n('meta.Sinks.SQL.Sql') + sql: string; + + @FieldDecorator({ + type: 'radio', + initialValue: 'H', + props: values => ({ + disabled: Boolean(values.id), + options: [ + { + label: i18n.t('meta.Sources.File.Cycle.Day'), + value: 'D', + }, + { + label: i18n.t('meta.Sources.File.Cycle.Hour'), + value: 'H', + }, + ], + }), + }) + @IngestionField() + @I18n('meta.Sources.File.Cycle') + cycleUnit: string; + + @FieldDecorator({ + type: 'input', + tooltip: i18n.t('meta.Sources.File.TimeOffsetHelp'), + initialValue: '-1h', + rules: [ + { + pattern: /-?\d+[mhd]$/, + required: true, + message: i18n.t('meta.Sources.File.TimeOffsetRules'), + }, + ], + props: values => ({ + disabled: Boolean(values.id), + }), + }) + @IngestionField() + @I18n('meta.Sources.File.TimeOffset') + timeOffset: string; + + @FieldDecorator({ + type: 'inputnumber', + rules: [{ required: true }], + initialValue: 20, + props: values => ({ + min: 1, + max: 100, + precision: 0, + disabled: Boolean(values.id), + }), + }) + @IngestionField() + @I18n('meta.Sources.File.MaxFileCount') + maxInstanceCount: string; + + @FieldDecorator({ + type: 'inputnumber', + initialValue: 1000, + rules: [{ required: true }], + props: values => ({ + disabled: Boolean(values.id), + }), + }) + @IngestionField() + @I18n('meta.Sinks.SQL.FetchSize') + fetchSize: string; + + @FieldDecorator({ + type: 'select', + initialValue: 'GMT+8:00', + props: values => ({ + disabled: Boolean(values.id), + options: [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, + -12, + ].map(item => ({ + label: Math.sign(item) === 1 || Math.sign(item) === 0 ? `GMT+${item}:00` : `GMT${item}:00`, + value: Math.sign(item) === 1 || Math.sign(item) === 0 ? `GMT+${item}:00` : `GMT${item}:00`, + })), + }), + }) + @IngestionField() + @I18n('meta.Sources.File.TimeZone') + dataTimeZone: string; +} diff --git a/inlong-dashboard/src/plugins/sources/defaults/index.ts b/inlong-dashboard/src/plugins/sources/defaults/index.ts index 48a4093e52..2c55bd8ee3 100644 --- a/inlong-dashboard/src/plugins/sources/defaults/index.ts +++ b/inlong-dashboard/src/plugins/sources/defaults/index.ts @@ -100,4 +100,9 @@ export const allDefaultSources: MetaExportWithBackendList<SourceMetaType> = [ value: 'COS', LoadEntity: () => import('./COS'), }, + { + label: 'SQL', + value: 'SQL', + LoadEntity: () => import('./SQL'), + }, ]; diff --git a/inlong-dashboard/src/ui/components/CheckCard/index.tsx b/inlong-dashboard/src/ui/components/CheckCard/index.tsx index 467bc51222..a8a2ecde7d 100644 --- a/inlong-dashboard/src/ui/components/CheckCard/index.tsx +++ b/inlong-dashboard/src/ui/components/CheckCard/index.tsx @@ -26,6 +26,7 @@ export interface CheckCardOption { label: string; value: string | number; image?: string | Promise<{ default: string }>; + isSmallImage?: boolean; } export interface CheckCardProps { @@ -45,6 +46,7 @@ const CheckCard: React.FC<CheckCardProps> = ({ options, value, onChange, disable const [isExpand, setExpandStatus] = useState(!Boolean(currentValue)); + const [smallImageMap, setSmallImageMap] = useState({}); const { token } = useToken(); useEffect(() => { @@ -77,6 +79,16 @@ const CheckCard: React.FC<CheckCardProps> = ({ options, value, onChange, disable })(); }, [options]); + useEffect(() => { + setSmallImageMap( + options + .filter(item => item.isSmallImage) + .reduce((acc, item) => { + acc[item.label] = item.isSmallImage; + return acc; + }, {}), + ); + }, [options]); const handleCardClick = newValue => { setExpandStatus(false); if (newValue !== currentValue) { @@ -91,7 +103,22 @@ const CheckCard: React.FC<CheckCardProps> = ({ options, value, onChange, disable <Tooltip placement="top" title={label}> <div className={styles.cardInfo}> {logoMap[label] ? ( - <img height="100%" alt={label} src={logoMap[label]}></img> + !smallImageMap[label] ? ( + <img height="100%" alt={label} src={logoMap[label]}></img> + ) : ( + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + width: '100%', + }} + > + <img style={{ marginLeft: 10 }} height="100%" alt={label} src={logoMap[label]}></img> + <span style={{ marginLeft: 10 }}>{label}</span> + </div> + ) ) : ( <> <DatabaseOutlined style={{ fontSize: 20 }} /> diff --git a/inlong-dashboard/src/ui/locales/cn.json b/inlong-dashboard/src/ui/locales/cn.json index c49db0d276..598e1be34e 100644 --- a/inlong-dashboard/src/ui/locales/cn.json +++ b/inlong-dashboard/src/ui/locales/cn.json @@ -1062,5 +1062,12 @@ "meta.Sinks.DirtyData.DirtyType.FieldMappingError":"字段映射错误", "meta.Sinks.DirtyData.DirtyType.LoadError":"加载错误", "meta.Sinks.DirtyData.Search.KeyWordHelp":"请输入关键字", - "meta.Sinks.DirtyData.Search.KeyWord":"关键字" + "meta.Sinks.DirtyData.Search.KeyWord":"关键字", + "meta.Sources.COS.DataNode": "数据节点", + "meta.Sinks.SQL.Sql": "Sql", + "meta.Sinks.SQL.JdbcUrl": "JDBC 地址", + "meta.Sinks.SQL.Username": "用户名", + "meta.Sinks.SQL.Password": "密码", + "meta.Sinks.SQL.FetchSize": "捕获大小", + "meta.SQL.Check": "校验失败,请输入正确的SQL连接地址" } diff --git a/inlong-dashboard/src/ui/locales/en.json b/inlong-dashboard/src/ui/locales/en.json index b87b2c15c8..25878c36ee 100644 --- a/inlong-dashboard/src/ui/locales/en.json +++ b/inlong-dashboard/src/ui/locales/en.json @@ -1056,5 +1056,12 @@ "meta.Sinks.DirtyData.DirtyType.FieldMappingError":"Field Mapping Error", "meta.Sinks.DirtyData.DirtyType.LoadError":"Load Error", "meta.Sinks.DirtyData.Search.KeyWordHelp":"Please enter a keyword", - "meta.Sinks.DirtyData.Search.KeyWord":"Key word" + "meta.Sinks.DirtyData.Search.KeyWord":"Key word", + "meta.Sources.COS.DataNode": "Data node", + "meta.Sinks.SQL.Sql": "Sql", + "meta.Sinks.SQL.JdbcUrl": "JDBC url", + "meta.Sinks.SQL.Username": "username", + "meta.Sinks.SQL.Password": "password", + "meta.Sinks.SQL.FetchSize": "Fetch size", + "meta.SQL.Check": "Check failed, please enter the correct SQL connection address" }