You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@inlong.apache.org by do...@apache.org on 2022/08/30 02:07:51 UTC

[inlong] 04/04: [INLONG-5731][Dashboard] Add data node management (#5732)

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

dockerzhang pushed a commit to branch release-1.3.0
in repository https://gitbox.apache.org/repos/asf/inlong.git

commit 221853ec9fd08a58473c9b8cf79ab3f5d0d17062
Author: Daniel <le...@apache.org>
AuthorDate: Mon Aug 29 19:30:17 2022 +0800

    [INLONG-5731][Dashboard] Add data node management (#5732)
---
 inlong-dashboard/src/configs/menus/index.ts      |   4 +
 inlong-dashboard/src/configs/routes/index.tsx    |  10 +-
 inlong-dashboard/src/i18n.ts                     |   2 +
 inlong-dashboard/src/locales/cn.json             |   8 +
 inlong-dashboard/src/locales/en.json             |   8 +
 inlong-dashboard/src/metas/common.ts             |   5 +-
 inlong-dashboard/src/metas/nodes/hive.tsx        |  47 ++++++
 inlong-dashboard/src/metas/nodes/index.tsx       |  87 +++++++++++
 inlong-dashboard/src/pages/Nodes/DetailModal.tsx | 105 +++++++++++++
 inlong-dashboard/src/pages/Nodes/index.tsx       | 182 +++++++++++++++++++++++
 10 files changed, 451 insertions(+), 7 deletions(-)

diff --git a/inlong-dashboard/src/configs/menus/index.ts b/inlong-dashboard/src/configs/menus/index.ts
index 3bed57279..12d134f38 100644
--- a/inlong-dashboard/src/configs/menus/index.ts
+++ b/inlong-dashboard/src/configs/menus/index.ts
@@ -48,6 +48,10 @@ const menus: MenuItemType[] = [
       },
     ],
   },
+  {
+    path: '/node',
+    name: i18n.t('configs.menus.Node'),
+  },
   {
     path: '/process',
     name: i18n.t('configs.menus.ApprovalManagement'),
diff --git a/inlong-dashboard/src/configs/routes/index.tsx b/inlong-dashboard/src/configs/routes/index.tsx
index 3391d54e0..c63c5983a 100644
--- a/inlong-dashboard/src/configs/routes/index.tsx
+++ b/inlong-dashboard/src/configs/routes/index.tsx
@@ -88,11 +88,6 @@ const routes: RouteProps[] = [
       },
     ],
   },
-  // {
-  //   path: '/dataSources',
-  //   component: () => import('@/pages/DataSources'),
-  //   exact: true,
-  // },
   {
     path: '/user',
     component: () => import('@/pages/UserManagement'),
@@ -120,6 +115,11 @@ const routes: RouteProps[] = [
     component: () => import('@/pages/ClusterTags'),
     exact: true,
   },
+  {
+    path: '/node',
+    component: () => import('@/pages/Nodes'),
+    exact: true,
+  },
   {
     component: () => import('@/pages/Error/404'),
   },
diff --git a/inlong-dashboard/src/i18n.ts b/inlong-dashboard/src/i18n.ts
index de60f1212..3c7e6a0fe 100644
--- a/inlong-dashboard/src/i18n.ts
+++ b/inlong-dashboard/src/i18n.ts
@@ -34,6 +34,7 @@ const resources = {
       'configs.menus.SystemManagement': 'System',
       'configs.menus.UserManagement': 'User',
       'configs.menus.ResponsibleManagement': 'ApprovalManagement',
+      'configs.menus.Node': 'Nodes',
     },
   },
   cn: {
@@ -47,6 +48,7 @@ const resources = {
       'configs.menus.SystemManagement': '系统管理',
       'configs.menus.UserManagement': '用户管理',
       'configs.menus.ResponsibleManagement': '审批责任人管理',
+      'configs.menus.Node': '节点管理',
     },
   },
 };
diff --git a/inlong-dashboard/src/locales/cn.json b/inlong-dashboard/src/locales/cn.json
index 328831c33..948e13cd5 100644
--- a/inlong-dashboard/src/locales/cn.json
+++ b/inlong-dashboard/src/locales/cn.json
@@ -263,6 +263,14 @@
   "meta.Consumption.MasterAddress": "Master地址",
   "meta.Consumption.Yes": "是",
   "meta.Consumption.OwnersExtra": "消费责任人,可查看、修改消费信息",
+  "meta.Nodes.Name": "节点名称",
+  "meta.Nodes.Type": "类型",
+  "meta.Nodes.Owners": "责任人",
+  "meta.Nodes.Description": "描述",
+  "meta.Nodes.Hive.DataPath": "数据路径",
+  "meta.Nodes.Hive.DataPathHelp": "DB的存储路径,不包括表名,如:hdfs://127.0.0.1:9000/warehouse/inlong.db",
+  "meta.Nodes.Hive.ConfDir": "配置路径",
+  "meta.Nodes.Hive.ConfDirHelp": "将 Hive 集群的 hive-site.xml 文件上传到 HDFS 的某个目录下,如:/user/hive/conf",
   "components.EditableTable.NewLine": "新增一行",
   "components.FormGenerator.plugins.PleaseChoose": "请选择",
   "components.FormGenerator.plugins.PleaseInput": "请输入",
diff --git a/inlong-dashboard/src/locales/en.json b/inlong-dashboard/src/locales/en.json
index 8838587c3..266b5a3ee 100644
--- a/inlong-dashboard/src/locales/en.json
+++ b/inlong-dashboard/src/locales/en.json
@@ -263,6 +263,14 @@
   "meta.Consumption.MasterAddress": "Master address",
   "meta.Consumption.Yes": "Yes",
   "meta.Consumption.OwnersExtra": "Consumption in charges, they can view, modify consumption information",
+  "meta.Nodes.Name": "Name",
+  "meta.Nodes.Type": "Type",
+  "meta.Nodes.Owners": "Owners",
+  "meta.Nodes.Description": "Description",
+  "meta.Nodes.Hive.DataPath": "DataPath",
+  "meta.Nodes.Hive.DataPathHelp": "Storage path of the DB, excluding the table name, such as: hdfs://127.0.0.1:9000/warehouse/inlong.db",
+  "meta.Nodes.Hive.ConfDir": "ConfDir",
+  "meta.Nodes.Hive.ConfDirHelp": "Upload the hive-site.xml file of the Hive cluster to a directory in HDFS, such as: /user/hive/conf",
   "components.EditableTable.NewLine": "New line",
   "components.FormGenerator.plugins.PleaseChoose": "Please select",
   "components.FormGenerator.plugins.PleaseInput": "Please input",
diff --git a/inlong-dashboard/src/metas/common.ts b/inlong-dashboard/src/metas/common.ts
index 59a7c293f..0c32bbd37 100644
--- a/inlong-dashboard/src/metas/common.ts
+++ b/inlong-dashboard/src/metas/common.ts
@@ -28,12 +28,13 @@ export interface FieldItemType extends FormItemProps {
 
 export const genFields = (
   fieldsDefault: FieldItemType[],
-  fieldsExtends: FieldItemType[],
-): FormItemProps[] => {
+  fieldsExtends?: FieldItemType[],
+): FieldItemType[] => {
   const output: FieldItemType[] = [];
   const fields = fieldsDefault.concat(fieldsExtends);
   while (fields.length) {
     const fieldItem = fields.shift();
+    if (!fieldItem) continue;
     if (fieldItem.position) {
       const [positionType, positionName] = fieldItem.position;
       const index = output.findIndex(item => item.name === positionName);
diff --git a/inlong-dashboard/src/metas/nodes/hive.tsx b/inlong-dashboard/src/metas/nodes/hive.tsx
new file mode 100644
index 000000000..74436f2dc
--- /dev/null
+++ b/inlong-dashboard/src/metas/nodes/hive.tsx
@@ -0,0 +1,47 @@
+/*
+ * 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 i18n from '@/i18n';
+import type { FieldItemType } from '@/metas/common';
+
+export const hive: FieldItemType[] = [
+  {
+    type: 'input',
+    label: 'JDBC URL',
+    name: 'jdbcUrl',
+    rules: [{ required: true }],
+    initialValue: 'jdbc:hive2://127.0.0.1:10000',
+  },
+  {
+    type: 'input',
+    label: i18n.t('meta.Sinks.Hive.DataPath'),
+    name: 'dataPath',
+    rules: [{ required: true }],
+    tooltip: i18n.t('meta.Sinks.DataPathHelp'),
+    initialValue: 'hdfs://127.0.0.1:9000/user/hive/warehouse/default',
+  },
+  {
+    type: 'input',
+    label: i18n.t('meta.Sinks.Hive.ConfDir'),
+    name: 'hiveConfDir',
+    rules: [{ required: true }],
+    tooltip: i18n.t('meta.Sinks.Hive.ConfDirHelp'),
+    initialValue: '/usr/hive/conf',
+  },
+];
diff --git a/inlong-dashboard/src/metas/nodes/index.tsx b/inlong-dashboard/src/metas/nodes/index.tsx
new file mode 100644
index 000000000..c2d47b5fa
--- /dev/null
+++ b/inlong-dashboard/src/metas/nodes/index.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 from 'react';
+import i18n from '@/i18n';
+import UserSelect from '@/components/UserSelect';
+import type { FieldItemType } from '@/metas/common';
+import { genFields, genForm, genTable } from '@/metas/common';
+import { hive } from './hive';
+
+const allNodes = [
+  {
+    label: 'Hive',
+    value: 'HIVE',
+    fields: hive,
+  },
+];
+
+const defaultCommonFields: FieldItemType[] = [
+  {
+    type: 'input',
+    label: i18n.t('meta.Nodes.Name'),
+    name: 'name',
+    rules: [{ required: true }],
+    props: {
+      maxLength: 128,
+    },
+    _renderTable: true,
+  },
+  {
+    type: 'select',
+    label: i18n.t('meta.Nodes.Type'),
+    name: 'type',
+    initialValue: allNodes[0].value,
+    rules: [{ required: true }],
+    props: {
+      options: allNodes.map(item => ({
+        label: item.label,
+        value: item.value,
+      })),
+    },
+    _renderTable: true,
+  },
+  {
+    type: <UserSelect mode="multiple" currentUserClosable={false} />,
+    label: i18n.t('meta.Nodes.Owners'),
+    name: 'inCharges',
+    rules: [{ required: true }],
+    _renderTable: true,
+  },
+  {
+    type: 'textarea',
+    label: i18n.t('meta.Nodes.Description'),
+    name: 'description',
+    props: {
+      maxLength: 256,
+    },
+  },
+];
+
+export const nodes = allNodes.map(item => {
+  const itemFields = defaultCommonFields.concat(item.fields);
+  const fields = genFields(itemFields);
+
+  return {
+    ...item,
+    fields,
+    form: genForm(fields),
+    table: genTable(fields),
+  };
+});
diff --git a/inlong-dashboard/src/pages/Nodes/DetailModal.tsx b/inlong-dashboard/src/pages/Nodes/DetailModal.tsx
new file mode 100644
index 000000000..b6c672ada
--- /dev/null
+++ b/inlong-dashboard/src/pages/Nodes/DetailModal.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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, useMemo } from 'react';
+import { Modal, message } from 'antd';
+import { ModalProps } from 'antd/es/modal';
+import FormGenerator, { useForm } from '@/components/FormGenerator';
+import { useRequest, useUpdateEffect } from '@/hooks';
+import request from '@/utils/request';
+import { nodes } from '@/metas/nodes';
+import i18n from '@/i18n';
+
+export interface Props extends ModalProps {
+  // Require when edit
+  id?: string;
+}
+
+const Comp: React.FC<Props> = ({ id, ...modalProps }) => {
+  const [form] = useForm();
+
+  const [type, setType] = useState(nodes[0].value);
+
+  const { data: savedData, run: getData } = useRequest(
+    id => ({
+      url: `/node/get/${id}`,
+    }),
+    {
+      manual: true,
+      formatResult: result => ({
+        ...result,
+        inCharges: result.inCharges?.split(','),
+        clusterTags: result.clusterTags?.split(','),
+      }),
+      onSuccess: result => {
+        form.setFieldsValue(result);
+        setType(result.type);
+      },
+    },
+  );
+
+  const onOk = async () => {
+    const values = await form.validateFields();
+    const isUpdate = id;
+    const submitData = {
+      ...values,
+      inCharges: values.inCharges?.join(','),
+      clusterTags: values.clusterTags?.join(','),
+    };
+    if (isUpdate) {
+      submitData.id = id;
+      submitData.version = savedData?.version;
+    }
+    await request({
+      url: `/node/${isUpdate ? 'update' : 'save'}`,
+      method: 'POST',
+      data: submitData,
+    });
+    await modalProps?.onOk(submitData);
+    message.success(i18n.t('basic.OperatingSuccess'));
+  };
+
+  useUpdateEffect(() => {
+    if (modalProps.visible) {
+      // open
+      form.resetFields();
+      if (id) {
+        getData(id);
+      }
+    }
+  }, [modalProps.visible]);
+
+  const content = useMemo(() => {
+    const current = nodes.find(item => item.value === type);
+    return current?.form;
+  }, [type]);
+
+  return (
+    <Modal {...modalProps} title={id ? i18n.t('basic.Detail') : i18n.t('basic.Create')} onOk={onOk}>
+      <FormGenerator
+        content={content}
+        form={form}
+        onValuesChange={(c, values) => setType(values.type)}
+        useMaxWidth
+      />
+    </Modal>
+  );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/pages/Nodes/index.tsx b/inlong-dashboard/src/pages/Nodes/index.tsx
new file mode 100644
index 000000000..37c25957f
--- /dev/null
+++ b/inlong-dashboard/src/pages/Nodes/index.tsx
@@ -0,0 +1,182 @@
+/*
+ * 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, { useCallback, useMemo, useState } from 'react';
+import { Button, Modal, message } from 'antd';
+import i18n from '@/i18n';
+import HighTable from '@/components/HighTable';
+import { PageContainer } from '@/components/PageContainer';
+import { defaultSize } from '@/configs/pagination';
+import { useRequest } from '@/hooks';
+import { nodes } from '@/metas/nodes';
+import DetailModal from './DetailModal';
+import request from '@/utils/request';
+
+const getFilterFormContent = defaultValues => [
+  {
+    type: 'inputsearch',
+    name: 'keyword',
+  },
+  {
+    type: 'radiobutton',
+    name: 'type',
+    label: i18n.t('meta.Nodes.Type'),
+    initialValue: defaultValues.type,
+    props: {
+      buttonStyle: 'solid',
+      options: nodes.map(item => ({
+        label: item.label,
+        value: item.value,
+      })),
+    },
+  },
+];
+
+const Comp: React.FC = () => {
+  const [options, setOptions] = useState({
+    keyword: '',
+    pageSize: defaultSize,
+    pageNum: 1,
+    type: nodes[0].value,
+  });
+
+  const [detailModal, setDetailModal] = useState<Record<string, unknown>>({
+    visible: false,
+  });
+
+  const { data, loading, run: getList } = useRequest(
+    {
+      url: '/node/list',
+      method: 'POST',
+      data: {
+        ...options,
+      },
+    },
+    {
+      refreshDeps: [options],
+    },
+  );
+
+  const onEdit = ({ id }) => {
+    setDetailModal({ visible: true, id });
+  };
+
+  const onDelete = useCallback(
+    ({ id }) => {
+      Modal.confirm({
+        title: i18n.t('basic.DeleteConfirm'),
+        onOk: async () => {
+          await request({
+            url: `/node/delete/${id}`,
+            method: 'DELETE',
+          });
+          await getList();
+          message.success(i18n.t('basic.DeleteSuccess'));
+        },
+      });
+    },
+    [getList],
+  );
+
+  const onChange = ({ current: pageNum, pageSize }) => {
+    setOptions(prev => ({
+      ...prev,
+      pageNum,
+      pageSize,
+    }));
+  };
+
+  const onFilter = allValues => {
+    setOptions(prev => ({
+      ...prev,
+      ...allValues,
+      pageNum: 1,
+    }));
+  };
+
+  const pagination = {
+    pageSize: +options.pageSize,
+    current: +options.pageNum,
+    total: data?.total,
+  };
+
+  const columns = useMemo(() => {
+    const current = nodes.find(item => item.value === options.type);
+    if (!current?.table) return [];
+
+    return current.table
+      .map(item => ({
+        ...item,
+        ellipsisMulti: 2,
+      }))
+      .concat([
+        {
+          title: i18n.t('basic.Operating'),
+          dataIndex: 'action',
+          width: 200,
+          render: (text, record) => (
+            <>
+              <Button type="link" onClick={() => onEdit(record)}>
+                {i18n.t('basic.Edit')}
+              </Button>
+              <Button type="link" onClick={() => onDelete(record)}>
+                {i18n.t('basic.Delete')}
+              </Button>
+            </>
+          ),
+        } as any,
+      ]);
+  }, [options.type, onDelete]);
+
+  return (
+    <PageContainer useDefaultBreadcrumb={false}>
+      <HighTable
+        filterForm={{
+          content: getFilterFormContent(options),
+          onFilter,
+        }}
+        suffix={
+          <Button type="primary" onClick={() => setDetailModal({ visible: true })}>
+            {i18n.t('basic.Create')}
+          </Button>
+        }
+        table={{
+          columns,
+          rowKey: 'id',
+          dataSource: data?.list,
+          pagination,
+          loading,
+          onChange,
+        }}
+      />
+
+      <DetailModal
+        {...detailModal}
+        visible={detailModal.visible as boolean}
+        onOk={async () => {
+          await getList();
+          setDetailModal({ visible: false });
+        }}
+        onCancel={() => setDetailModal({ visible: false })}
+      />
+    </PageContainer>
+  );
+};
+
+export default Comp;