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 <[email protected]> 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;
