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;