You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@inlong.apache.org by he...@apache.org on 2023/04/29 14:18:47 UTC
[inlong] branch master updated: [INLONG-7789][Dashboard] Support create stream fields by statement (#7860)
This is an automated email from the ASF dual-hosted git repository.
healchow 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 bed95a3a7 [INLONG-7789][Dashboard] Support create stream fields by statement (#7860)
bed95a3a7 is described below
commit bed95a3a74ecb94ed957b5d017f65eecb03fa495
Author: feat <fe...@outlook.com>
AuthorDate: Sat Apr 29 22:18:40 2023 +0800
[INLONG-7789][Dashboard] Support create stream fields by statement (#7860)
---
.../plugins/streams/common/StreamDefaultInfo.ts | 1 +
.../src/ui/components/EditableTable/index.tsx | 101 +++++--
.../src/ui/components/FieldParseModule/index.tsx | 301 +++++++++++++++++++++
inlong-dashboard/src/ui/locales/cn.json | 11 +
inlong-dashboard/src/ui/locales/en.json | 11 +
5 files changed, 408 insertions(+), 17 deletions(-)
diff --git a/inlong-dashboard/src/plugins/streams/common/StreamDefaultInfo.ts b/inlong-dashboard/src/plugins/streams/common/StreamDefaultInfo.ts
index fc10636ba..49ac7c3b9 100644
--- a/inlong-dashboard/src/plugins/streams/common/StreamDefaultInfo.ts
+++ b/inlong-dashboard/src/plugins/streams/common/StreamDefaultInfo.ts
@@ -221,6 +221,7 @@ export class StreamDefaultInfo implements DataWithBackend, RenderRow, RenderList
props: values => ({
size: 'small',
canDelete: record => !(record.id && [110, 130].includes(values?.status)),
+ canBatchAdd: true,
columns: [
{
title: i18n.t('meta.Stream.FieldName'),
diff --git a/inlong-dashboard/src/ui/components/EditableTable/index.tsx b/inlong-dashboard/src/ui/components/EditableTable/index.tsx
index 52c35aea2..262a0d208 100644
--- a/inlong-dashboard/src/ui/components/EditableTable/index.tsx
+++ b/inlong-dashboard/src/ui/components/EditableTable/index.tsx
@@ -26,6 +26,7 @@ import HighSelect from '@/ui/components/HighSelect';
import { useUpdateEffect } from '@/ui/hooks';
import isEqual from 'lodash/isEqual';
import styles from './index.module.less';
+import FieldParseModule, { RowType } from '@/ui/components/FieldParseModule';
// Row data exposed to the outside
type RowValueType = Record<string, unknown>;
@@ -49,7 +50,7 @@ export interface ColumnsItemProps {
| ((val: unknown, rowVal: RowValueType, idx: number, isNew?: boolean) => FormCompProps);
rules?: FormItemProps['rules'];
// The value will be erased when invisible
- visible?: (val: unknown, rowVal: RowValueType) => boolean | boolean;
+ visible?: (val: unknown, rowVal: RowValueType) => boolean;
}
export interface EditableTableProps
@@ -67,6 +68,7 @@ export interface EditableTableProps
canDelete?: boolean | ((rowVal: RowValueType, idx: number, isNew?: boolean) => boolean);
// Can add a new line? Default: true.
canAdd?: boolean;
+ canBatchAdd?: boolean;
}
const getRowInitialValue = (columns: EditableTableProps['columns']) =>
@@ -103,6 +105,7 @@ const EditableTable = ({
required = true,
canDelete = true,
canAdd = true,
+ canBatchAdd = false,
...rest
}: EditableTableProps) => {
if (!id) {
@@ -152,13 +155,39 @@ const EditableTable = ({
};
const onDeleteRow = ({ _etid }: RecordType) => {
- const newData = [...data];
+ const newData: RecordType[] = [...data];
const index = newData.findIndex(item => item._etid === _etid);
newData.splice(index, 1);
setData(newData);
triggerChange(newData);
};
+ const onDeleteAllRow = () => {
+ const newData: RecordType[] = [];
+ setData(newData);
+ triggerChange(newData);
+ };
+
+ const onAppendByParseField = (fields: RowType[]) => {
+ const newRecord: RecordType[] = fields?.map((field: RowType) => ({
+ _etid: Math.random().toString(),
+ ...field,
+ }));
+ const newData = data.concat(newRecord);
+ setData(newData);
+ triggerChange(newData);
+ };
+
+ const onOverrideByParseField = (fields: RowType[]) => {
+ const newData = fields?.map(field => ({
+ _etid: Math.random().toString(),
+ ...field,
+ }));
+
+ setData(newData);
+ triggerChange(newData);
+ };
+
const onTextChange = (object: Record<string, unknown>, { _etid }: RecordType) => {
const newData = data.map(item => {
if (item._etid === _etid) {
@@ -274,23 +303,61 @@ const EditableTable = ({
),
} as any);
}
+ const [isParseFieldModalVisible, setIsParseFieldModalVisible] = useState(false);
return (
- <Table
- {...rest}
- dataSource={data}
- columns={tableColumns}
- rowKey="_etid"
- footer={
- editing && canAdd
- ? () => (
- <Button type="link" style={{ padding: 0 }} onClick={onAddRow}>
- {t('components.EditableTable.NewLine')}
- </Button>
- )
- : null
- }
- />
+ <>
+ <FieldParseModule
+ key={'field-parse-module'}
+ onOverride={onOverrideByParseField}
+ onAppend={onAppendByParseField}
+ visible={isParseFieldModalVisible}
+ onHide={() => {
+ setIsParseFieldModalVisible(false);
+ console.log('on hide');
+ }}
+ />
+ <Table
+ {...rest}
+ dataSource={data}
+ columns={tableColumns}
+ rowKey="_etid"
+ key={'table'}
+ footer={
+ editing && canAdd
+ ? () => (
+ <>
+ <Button
+ key={'new_line_button'}
+ type="link"
+ style={{ padding: 0 }}
+ onClick={onAddRow}
+ >
+ {t('components.EditableTable.NewLine')}
+ </Button>
+ <Button
+ key={'batch_add_line_button'}
+ type="link"
+ style={{ padding: 0 }}
+ onClick={() => setIsParseFieldModalVisible(true)}
+ disabled={!canBatchAdd}
+ >
+ {t('components.EditableTable.BatchParseField')}
+ </Button>
+ <Button
+ key={'delete_all_button'}
+ type="link"
+ style={{ padding: 0 }}
+ onClick={onDeleteAllRow}
+ >
+ {t('components.EditableTable.DeleteAll')}
+ </Button>
+ </>
+ )
+ : null
+ }
+ />
+ </>
);
};
diff --git a/inlong-dashboard/src/ui/components/FieldParseModule/index.tsx b/inlong-dashboard/src/ui/components/FieldParseModule/index.tsx
new file mode 100644
index 000000000..add2298bc
--- /dev/null
+++ b/inlong-dashboard/src/ui/components/FieldParseModule/index.tsx
@@ -0,0 +1,301 @@
+/*
+ * 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 React, { useState } from 'react';
+import { Button, Divider, Input, Modal, Radio, Space, Table } from 'antd';
+
+import {
+ CopyOutlined,
+ DatabaseOutlined,
+ DeleteOutlined,
+ FileAddOutlined,
+ FileOutlined,
+ ForkOutlined,
+ FormOutlined,
+ PlayCircleOutlined,
+} from '@ant-design/icons';
+import { useRequest } from '@/ui/hooks';
+import { useTranslation } from 'react-i18next';
+
+export interface RowType {
+ fieldName: string;
+ fieldType: string;
+ fieldComment: string;
+}
+
+interface FieldParseModuleProps {
+ visible: boolean;
+ onOverride: (fields: RowType[]) => void;
+ onAppend: (fields: RowType[]) => void;
+ onHide: () => void; // added onHide callback
+}
+
+const FieldParseModule: React.FC<FieldParseModuleProps> = ({
+ onOverride,
+ onAppend,
+ visible,
+ onHide,
+}) => {
+ const { t } = useTranslation();
+
+ const [selectedFormat, setSelectedFormat] = useState('json');
+
+ const [statement, setStatement] = useState('');
+ const [previewData, setPreviewData] = useState<RowType[]>([]);
+
+ const handleCancel = () => {
+ onHide(); // call onHide callback when closing module
+ };
+
+ const handleFormat = () => {
+ switch (selectedFormat) {
+ case 'json':
+ setStatement(JSON.stringify(JSON.parse(statement), null, 2));
+ break;
+ case 'sql':
+ setStatement(
+ statement.replace(/(FROM|JOIN|WHERE|GROUP BY|HAVING|ORDER BY|LIMIT)/g, '\n$1'),
+ );
+ break;
+ case 'csv':
+ break;
+ default:
+ break;
+ }
+ };
+
+ const handleAppend = () => {
+ // Append output value to the original fields list
+ onAppend([...previewData]);
+ onHide();
+ };
+
+ const handleOverride = () => {
+ onOverride(previewData);
+ onHide();
+ };
+ const { run: runParseFields } = useRequest(
+ {
+ url: '/stream/parseFields',
+ method: 'POST',
+ data: {
+ method: selectedFormat,
+ statement: statement,
+ },
+ },
+ {
+ manual: true,
+ onSuccess: res => {
+ console.log('parse fields success.');
+ setPreviewData(res);
+ },
+ },
+ );
+
+ const columns = [
+ {
+ title: 'Name',
+ dataIndex: 'fieldName',
+ key: 'fieldName',
+ },
+ {
+ title: 'Type',
+ dataIndex: 'fieldType',
+ key: 'fieldType',
+ },
+ {
+ title: 'Description',
+ dataIndex: 'fieldComment',
+ key: 'fieldComment',
+ },
+ ];
+
+ function onPasta() {
+ setPreviewData(null);
+ switch (selectedFormat) {
+ case 'json':
+ setStatement(`[
+ {
+ "name": "user_name",
+ "type": "string",
+ "desc": "the name of user"
+ },
+ {
+ "name": "user_age",
+ "type": "int",
+ "desc": "the age of user"
+ }
+]`);
+ break;
+ case 'sql':
+ setStatement(`CREATE TABLE Persons
+ (
+ user_name int comment 'the name of user',
+ user_age varchar(255) comment 'the age of user'
+ )`);
+ break;
+ case 'csv':
+ setStatement(`user_name,string,name of user
+user_age,int,age of user`);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return (
+ <>
+ <Modal
+ key={'field-parse-module'}
+ title={
+ <>
+ <FileAddOutlined />
+ {t('components.FieldParseModule.BatchAddField')}
+ </>
+ }
+ open={visible}
+ onCancel={handleCancel}
+ footer={[
+ <Space key="footer_space" size={'small'} style={{ width: '100%' }} direction={'vertical'}>
+ <Space key={'footer_content_space'}>
+ <Button
+ key={'doAppend'}
+ type="primary"
+ disabled={previewData === null || previewData.length === 0}
+ onClick={handleAppend}
+ >
+ {t('components.FieldParseModule.Append')}
+ </Button>
+ <Button
+ key={'doOverwrite'}
+ type="primary"
+ disabled={previewData === null || previewData.length === 0}
+ onClick={handleOverride}
+ >
+ {t('components.FieldParseModule.Override')}
+ </Button>
+ </Space>
+ </Space>,
+ ]}
+ >
+ <div>
+ <Radio.Group
+ key={'mode_radio_group'}
+ onChange={e => setSelectedFormat(e.target.value)}
+ value={selectedFormat}
+ style={{ marginBottom: 6 }}
+ >
+ <Radio.Button
+ key={'module_json'}
+ value="json"
+ onClick={() => {
+ setPreviewData(null);
+ }}
+ >
+ <ForkOutlined />
+ JSON
+ </Radio.Button>
+ <Radio.Button
+ key={'module_sql'}
+ value="sql"
+ onClick={() => {
+ setPreviewData(null);
+ }}
+ >
+ <DatabaseOutlined />
+ SQL
+ </Radio.Button>
+ <Radio.Button
+ key={'module_csv'}
+ value="csv"
+ onClick={() => {
+ setPreviewData(null);
+ }}
+ >
+ <FileOutlined />
+ CSV
+ </Radio.Button>
+ </Radio.Group>
+ </div>
+ <div>
+ {['json', 'sql', 'csv'].includes(selectedFormat) && (
+ <Input.TextArea
+ key={'statement_content'}
+ rows={16}
+ value={statement}
+ onChange={e => setStatement(e.target.value)}
+ />
+ )}
+ </div>
+ <div>
+ {selectedFormat !== 'excel' && (
+ <>
+ <Button
+ key={'format_button'}
+ icon={<FormOutlined />}
+ onClick={handleFormat}
+ disabled={statement?.length === 0}
+ size={'small'}
+ >
+ {t('components.FieldParseModule.Format')}
+ </Button>
+ <Button
+ key={'clear_button'}
+ onClick={() => {
+ setStatement('');
+ setPreviewData(null);
+ }}
+ icon={<DeleteOutlined />}
+ disabled={statement?.length === 0}
+ size={'small'}
+ >
+ {t('components.FieldParseModule.Empty')}
+ </Button>
+ <Button key={'pasta_button'} onClick={onPasta} icon={<CopyOutlined />} size={'small'}>
+ {t('components.FieldParseModule.PasteTemplate')}
+ </Button>
+ <Divider key={'divider_button'} type={'vertical'} />
+ <Button
+ key={'parse_button'}
+ type="primary"
+ onClick={runParseFields}
+ icon={<PlayCircleOutlined />}
+ disabled={statement?.length === 0}
+ >
+ {t('components.FieldParseModule.Parse')}
+ </Button>
+ </>
+ )}
+ </div>
+
+ <div>
+ <Table
+ key="previewTable"
+ rowKey="name"
+ columns={columns}
+ dataSource={previewData}
+ pagination={false}
+ />
+ </div>
+ </Modal>
+ </>
+ );
+};
+
+export default FieldParseModule;
diff --git a/inlong-dashboard/src/ui/locales/cn.json b/inlong-dashboard/src/ui/locales/cn.json
index 5d8ccaf3f..9fb0c9802 100644
--- a/inlong-dashboard/src/ui/locales/cn.json
+++ b/inlong-dashboard/src/ui/locales/cn.json
@@ -506,6 +506,17 @@
"meta.Nodes.Kudu.DefaultSocketReadTimeoutMs": "等待Socket默认超时(ms)",
"meta.Nodes.Kudu.StatisticsDisabled": "禁用统计信息收集",
"components.EditableTable.NewLine": "新增一行",
+ "components.EditableTable.BatchParseField": "批量解析字段",
+ "components.EditableTable.DeleteAll": "删除全部",
+ "components.FieldParseModule.BatchAddField": "批量添加",
+ "components.FieldParseModule.Append": "追加",
+ "components.FieldParseModule.Override": "覆盖",
+ "components.FieldParseModule.Format": "格式化",
+ "components.FieldParseModule.Empty": "清空",
+ "components.FieldParseModule.PasteTemplate": "粘贴模板",
+ "components.FieldParseModule.Parse": "解析",
+ "components.FieldParseModule.Upload": "上传",
+ "components.FieldParseModule.DownloadTemplate": "下载模板",
"components.FormGenerator.plugins.PleaseChoose": "请选择",
"components.FormGenerator.plugins.PleaseInput": "请输入",
"components.TextSwitch.Title": "高级选项",
diff --git a/inlong-dashboard/src/ui/locales/en.json b/inlong-dashboard/src/ui/locales/en.json
index 26fc49b09..64c4977cb 100644
--- a/inlong-dashboard/src/ui/locales/en.json
+++ b/inlong-dashboard/src/ui/locales/en.json
@@ -506,6 +506,17 @@
"meta.Nodes.Kudu.DefaultSocketReadTimeoutMs": "SocketReadTimeout(ms)",
"meta.Nodes.Kudu.StatisticsDisabled": "DisabledStatistics",
"components.EditableTable.NewLine": "New line",
+ "components.EditableTable.BatchParseField": "Batch add",
+ "components.EditableTable.DeleteAll": "Delete all",
+ "components.FieldParseModule.BatchAddField": "Batch adding",
+ "components.FieldParseModule.Append": "Append",
+ "components.FieldParseModule.Override": "Override",
+ "components.FieldParseModule.Format": "Format",
+ "components.FieldParseModule.Empty": "Empty",
+ "components.FieldParseModule.PasteTemplate": "Paste template",
+ "components.FieldParseModule.Parse": "Parse",
+ "components.FieldParseModule.Upload": "Upload",
+ "components.FieldParseModule.DownloadTemplate": "Download template",
"components.FormGenerator.plugins.PleaseChoose": "Please select",
"components.FormGenerator.plugins.PleaseInput": "Please input",
"components.TextSwitch.Title": "Advanced options",