This is an automated email from the ASF dual-hosted git repository.

leezng 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 da1a03b1e [INLONG-6786][Dashboard] Supoort Apache Hudi sink management 
(#6791)
da1a03b1e is described below

commit da1a03b1e628da3a64fe2ab69ba45777d978b77d
Author: averyzhang <dearzhangzuof...@gmail.com>
AuthorDate: Fri Dec 9 10:05:47 2022 +0800

    [INLONG-6786][Dashboard] Supoort Apache Hudi sink management (#6791)
---
 inlong-dashboard/src/locales/cn.json               |  17 +
 inlong-dashboard/src/locales/en.json               |  16 +
 inlong-dashboard/src/metas/sinks/defaults/Hudi.ts  | 398 +++++++++++++++++++++
 inlong-dashboard/src/metas/sinks/defaults/index.ts |   5 +
 4 files changed, 436 insertions(+)

diff --git a/inlong-dashboard/src/locales/cn.json 
b/inlong-dashboard/src/locales/cn.json
index e6e264360..16ba0ba58 100644
--- a/inlong-dashboard/src/locales/cn.json
+++ b/inlong-dashboard/src/locales/cn.json
@@ -178,6 +178,23 @@
   "meta.Sinks.Iceberg.FieldDescription": "字段描述",
   "meta.Sinks.Iceberg.PartitionStrategy": "分区策略",
   "meta.Sinks.Iceberg.DataNodeName": "数据节点",
+  "meta.Sinks.Hudi.DbName": "DB名称",
+  "meta.Sinks.Hudi.TableName": "表名称",
+  "meta.Sinks.Hudi.Warehouse": "仓库路径",
+  "meta.Sinks.Hudi.FileFormat": "⽂件格式",
+  "meta.Sinks.Hudi.Description": "表描述",
+  "meta.Sinks.Hudi.ExtList": "属性",
+  "meta.Sinks.Hudi.DataConsistency": "数据一致性",
+  "meta.Sinks.Hudi.FieldName": "字段名",
+  "meta.Sinks.Hudi.FieldNameRule": "以英文字母或下划线开头,只能包含英文字母、数字、下划线",
+  "meta.Sinks.Hudi.FieldType": "字段类型",
+  "meta.Sinks.Hudi.FieldDescription": "字段描述",
+  "meta.Sinks.Hudi.PrimaryKey": "主键",
+  "meta.Sinks.Hudi.PartitionFieldList": "分区字段",
+  "meta.Sinks.Hudi.PrimaryKeyHelper": "主键字段,以逗号(,)分割",
+  "meta.Sinks.Hudi.PartitionFieldListHelp": "字段类型若为timestamp,则必须设置此字段值的格式,支持 
MICROSECONDS,MILLISECONDS,SECONDS,SQL,ISO_8601,以及自定义,比如:yyyy-MM-dd HH:mm:ss 等",
+  "meta.Sinks.Hudi.FieldFormat": "字段格式",
+  "meta.Sinks.Hudi.ExtListHelper": "hudi表的DDL属性需带前缀'ddl.'",
   "meta.Sinks.Greenplum.TableName": "表名称",
   "meta.Sinks.Greenplum.PrimaryKey": "主键",
   "meta.Sinks.Greenplum.FieldName": "字段名",
diff --git a/inlong-dashboard/src/locales/en.json 
b/inlong-dashboard/src/locales/en.json
index e20e88c85..bbe1abac3 100644
--- a/inlong-dashboard/src/locales/en.json
+++ b/inlong-dashboard/src/locales/en.json
@@ -178,6 +178,22 @@
   "meta.Sinks.Iceberg.FieldDescription": "FieldDescription",
   "meta.Sinks.Iceberg.PartitionStrategy": "PartitionStrategy",
   "meta.Sinks.Iceberg.DataNodeName": "DataNode",
+  "meta.Sinks.Hudi.DbName": "DbName",
+  "meta.Sinks.Hudi.TableName": "TableName",
+  "meta.Sinks.Hudi.Warehouse": "Warehouse",
+  "meta.Sinks.Hudi.FileFormat": "FileFormat",
+  "meta.Sinks.Hudi.Description": "Description",
+  "meta.Sinks.Hudi.ExtList": "ExtList",
+  "meta.Sinks.Hudi.DataConsistency": "DataConsistency",
+  "meta.Sinks.Hudi.FieldName": "FieldName",
+  "meta.Sinks.Hudi.FieldNameRule": "At the beginning of English letters or 
underscore, only English letters, numbers, and underscores",
+  "meta.Sinks.Hudi.FieldType": "FieldType",
+  "meta.Sinks.Hudi.FieldDescription": "FieldDescription",
+  "meta.Sinks.Hudi.PrimaryKey": "PrimaryKey",
+  "meta.Sinks.Hudi.PrimaryKeyHelper": "The Primary key fields, separated by 
commas (,)",
+  "meta.Sinks.Hudi.PartitionFieldList": "PartitionFieldList",
+  "meta.Sinks.Hudi.PartitionFieldListHelp": "If the field type is timestamp, 
you must set the format of the field value, support MICROSECONDS, MILLISECONDS, 
SECONDS, SQL, ISO_8601, and custom, such as: yyyy-MM-dd HH:mm:ss, etc.",
+  "meta.Sinks.Hudi.ExtListHelper": "The DDL attribute of the hudi table needs 
to be prefixed with 'ddl.'",
   "meta.Sinks.Greenplum.TableName": "TableName",
   "meta.Sinks.Greenplum.PrimaryKey": "PrimaryKey",
   "meta.Sinks.Greenplum.FieldName": "FieldName",
diff --git a/inlong-dashboard/src/metas/sinks/defaults/Hudi.ts 
b/inlong-dashboard/src/metas/sinks/defaults/Hudi.ts
new file mode 100644
index 000000000..ad4648ac2
--- /dev/null
+++ b/inlong-dashboard/src/metas/sinks/defaults/Hudi.ts
@@ -0,0 +1,398 @@
+/*
+ * 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 '@/metas/DataWithBackend';
+import { RenderRow } from '@/metas/RenderRow';
+import { RenderList } from '@/metas/RenderList';
+import i18n from '@/i18n';
+import EditableTable from '@/components/EditableTable';
+import { sourceFields } from '../common/sourceFields';
+import { SinkInfo } from '../common/SinkInfo';
+
+const { I18n } = DataWithBackend;
+const { FieldDecorator } = RenderRow;
+const { ColumnDecorator } = RenderList;
+
+const hudiFieldTypes = [
+  'int',
+  'long',
+  'string',
+  'float',
+  'double',
+  'date',
+  'timestamp',
+  'time',
+  'boolean',
+  'decimal',
+  'timestamptz',
+  'binary',
+  'fixed',
+  'uuid',
+].map(item => ({
+  label: item,
+  value: item,
+}));
+
+const matchPartitionStrategies = fieldType => {
+  const data = [
+    {
+      label: 'None',
+      value: 'None',
+      disabled: false,
+    },
+    {
+      label: 'Identity',
+      value: 'Identity',
+      disabled: false,
+    },
+    {
+      label: 'Year',
+      value: 'Year',
+      disabled: !['timestamp', 'date'].includes(fieldType),
+    },
+    {
+      label: 'Month',
+      value: 'Month',
+      disabled: !['timestamp', 'date'].includes(fieldType),
+    },
+    {
+      label: 'Day',
+      value: 'Day',
+      disabled: !['timestamp', 'date'].includes(fieldType),
+    },
+    {
+      label: 'Hour',
+      value: 'Hour',
+      disabled: fieldType !== 'timestamp',
+    },
+    {
+      label: 'Bucket',
+      value: 'Bucket',
+      disabled: ![
+        'string',
+        'boolean',
+        'short',
+        'int',
+        'long',
+        'float',
+        'double',
+        'decimal',
+      ].includes(fieldType),
+    },
+    {
+      label: 'Truncate',
+      value: 'Truncate',
+      disabled: !['string', 'int', 'long', 'binary', 
'decimal'].includes(fieldType),
+    },
+  ];
+
+  return data.filter(item => !item.disabled);
+};
+
+export default class HudiSink extends SinkInfo implements DataWithBackend, 
RenderRow, RenderList {
+  @FieldDecorator({
+    type: 'input',
+    rules: [{ required: true }],
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('meta.Sinks.Hudi.DbName')
+  dbName: string;
+
+  @FieldDecorator({
+    type: 'input',
+    rules: [{ required: true }],
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('meta.Sinks.Hudi.TableName')
+  tableName: string;
+
+  @FieldDecorator({
+    type: 'radio',
+    rules: [{ required: true }],
+    initialValue: 1,
+    tooltip: i18n.t('meta.Sinks.EnableCreateResourceHelp'),
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+      options: [
+        {
+          label: i18n.t('basic.Yes'),
+          value: 1,
+        },
+        {
+          label: i18n.t('basic.No'),
+          value: 0,
+        },
+      ],
+    }),
+  })
+  @I18n('meta.Sinks.EnableCreateResource')
+  enableCreateResource: number;
+
+  @FieldDecorator({
+    type: 'input',
+    rules: [{ required: true }],
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+      placeholder: 'thrift://127.0.0.1:9083',
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('Catalog URI')
+  catalogUri: string;
+
+  @FieldDecorator({
+    type: 'input',
+    rules: [{ required: true }],
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+      placeholder: 'hdfs://127.0.0.1:9000/user/hudi/warehouse',
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('meta.Sinks.Hudi.Warehouse')
+  warehouse: string;
+
+  @FieldDecorator({
+    type: 'select',
+    rules: [{ required: true }],
+    initialValue: 'Parquet',
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+      options: [
+        {
+          label: 'Parquet',
+          value: 'Parquet',
+        },
+        // {
+        //   label: 'Orc',
+        //   value: 'Orc',
+        // },
+        // {
+        //   label: 'Avro',
+        //   value: 'Avro',
+        // },
+      ],
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('meta.Sinks.Hudi.FileFormat')
+  fileFormat: string;
+
+  @FieldDecorator({
+    type: EditableTable,
+    rules: [{ required: false }],
+    initialValue: [],
+    tooltip: i18n.t('meta.Sinks.Hudi.ExtListHelper'),
+    props: values => ({
+      size: 'small',
+      columns: [
+        {
+          title: 'Key',
+          dataIndex: 'keyName',
+          props: {
+            disabled: [110, 130].includes(values?.status),
+          },
+        },
+        {
+          title: 'Value',
+          dataIndex: 'keyValue',
+          props: {
+            disabled: [110, 130].includes(values?.status),
+          },
+        },
+      ],
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('meta.Sinks.Hudi.ExtList')
+  extList: string;
+
+  @FieldDecorator({
+    type: 'select',
+    rules: [{ required: true }],
+    initialValue: 'EXACTLY_ONCE',
+    isPro: true,
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+      options: [
+        {
+          label: 'EXACTLY_ONCE',
+          value: 'EXACTLY_ONCE',
+        },
+        {
+          label: 'AT_LEAST_ONCE',
+          value: 'AT_LEAST_ONCE',
+        },
+      ],
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('meta.Sinks.Hudi.DataConsistency')
+  dataConsistency: string;
+
+  @FieldDecorator({
+    type: EditableTable,
+    props: values => ({
+      size: 'small',
+      editing: ![110, 130].includes(values?.status),
+      columns: getFieldListColumns(values),
+    }),
+  })
+  sinkFieldList: Record<string, unknown>[];
+
+  @FieldDecorator({
+    type: EditableTable,
+    tooltip: i18n.t('meta.Sinks.Hudi.PartitionFieldListHelp'),
+    col: 24,
+    props: {
+      size: 'small',
+      required: false,
+      columns: [
+        {
+          title: i18n.t('meta.Sinks.Hudi.FieldName'),
+          dataIndex: 'fieldName',
+          rules: [{ required: true }],
+        },
+        {
+          title: i18n.t('meta.Sinks.Hudi.FieldType'),
+          dataIndex: 'fieldType',
+          type: 'select',
+          initialValue: 'string',
+          props: {
+            options: ['string', 'timestamp'].map(item => ({
+              label: item,
+              value: item,
+            })),
+          },
+        },
+        {
+          title: i18n.t('meta.Sinks.Hudi.FieldFormat'),
+          dataIndex: 'fieldFormat',
+          type: 'autocomplete',
+          props: {
+            options: ['MICROSECONDS', 'MILLISECONDS', 'SECONDS', 'SQL', 
'ISO_8601'].map(item => ({
+              label: item,
+              value: item,
+            })),
+          },
+          rules: [{ required: true }],
+          visible: (text, record) => record.fieldType === 'timestamp',
+        },
+      ],
+    },
+  })
+  @I18n('meta.Sinks.Hudi.PartitionFieldList')
+  partitionFieldList: Record<string, unknown>[];
+
+  @FieldDecorator({
+    type: 'input',
+    tooltip: i18n.t('meta.Sinks.Hudi.PrimaryKeyHelper'),
+    rules: [{ required: true }],
+    props: values => ({
+      disabled: [110, 130].includes(values?.status),
+    }),
+  })
+  @ColumnDecorator()
+  @I18n('meta.Sinks.Hudi.PrimaryKey')
+  primaryKey: string;
+}
+
+const getFieldListColumns = sinkValues => {
+  return [
+    ...sourceFields,
+    {
+      title: `Hudi ${i18n.t('meta.Sinks.Hudi.FieldName')}`,
+      width: 110,
+      dataIndex: 'fieldName',
+      rules: [
+        { required: true },
+        {
+          pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/,
+          message: i18n.t('meta.Sinks.Hudi.FieldNameRule'),
+        },
+      ],
+      props: (text, record, idx, isNew) => ({
+        disabled: [110, 130].includes(sinkValues?.status as number) && !isNew,
+      }),
+    },
+    {
+      title: `Hudi ${i18n.t('meta.Sinks.Hudi.FieldType')}`,
+      dataIndex: 'fieldType',
+      width: 130,
+      initialValue: hudiFieldTypes[0].value,
+      type: 'select',
+      rules: [{ required: true }],
+      props: (text, record, idx, isNew) => ({
+        options: hudiFieldTypes,
+        onChange: value => {
+          const partitionStrategies = matchPartitionStrategies(value);
+          if (partitionStrategies.every(item => item.value !== 
record.partitionStrategy)) {
+            return {
+              partitionStrategy: partitionStrategies[0].value,
+            };
+          }
+        },
+        disabled: [110, 130].includes(sinkValues?.status as number) && !isNew,
+      }),
+    },
+    {
+      title: 'Length',
+      dataIndex: 'fieldLength',
+      type: 'inputnumber',
+      props: {
+        min: 0,
+      },
+      initialValue: 1,
+      rules: [{ type: 'number', required: true }],
+      visible: (text, record) => record.fieldType === 'fixed',
+    },
+    {
+      title: 'Precision',
+      dataIndex: 'fieldPrecision',
+      type: 'inputnumber',
+      props: {
+        min: 0,
+      },
+      initialValue: 1,
+      rules: [{ type: 'number', required: true }],
+      visible: (text, record) => record.fieldType === 'decimal',
+    },
+    {
+      title: 'Scale',
+      dataIndex: 'fieldScale',
+      type: 'inputnumber',
+      props: {
+        min: 0,
+      },
+      initialValue: 1,
+      rules: [{ type: 'number', required: true }],
+      visible: (text, record) => record.fieldType === 'decimal',
+    },
+    {
+      title: i18n.t('meta.Sinks.Hudi.FieldDescription'),
+      dataIndex: 'fieldComment',
+    },
+  ];
+};
diff --git a/inlong-dashboard/src/metas/sinks/defaults/index.ts 
b/inlong-dashboard/src/metas/sinks/defaults/index.ts
index 700013573..3ea2ef16d 100644
--- a/inlong-dashboard/src/metas/sinks/defaults/index.ts
+++ b/inlong-dashboard/src/metas/sinks/defaults/index.ts
@@ -61,6 +61,11 @@ export const allDefaultSinks: 
MetaExportWithBackendList<SinkMetaType> = [
     value: 'ICEBERG',
     LoadEntity: () => import('./Iceberg'),
   },
+  {
+    label: 'Hudi',
+    value: 'HUDI',
+    LoadEntity: () => import('./Hudi'),
+  },
   {
     label: 'Kafka',
     value: 'KAFKA',

Reply via email to