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",