This is an automated email from the ASF dual-hosted git repository.
aloyszhang 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 4cf7ccba60 [INLONG-10847][Dashboard] Agent type cluster node
management adds restart、reinstall, install log 、heartbeat detection and unLoad
operations (#10926)
4cf7ccba60 is described below
commit 4cf7ccba604251a34a89981b5176b5988675d0d9
Author: kamianlaida <[email protected]>
AuthorDate: Thu Aug 29 14:21:42 2024 +0800
[INLONG-10847][Dashboard] Agent type cluster node management adds
restart、reinstall, install log 、heartbeat detection and unLoad operations
(#10926)
---
inlong-dashboard/src/ui/locales/cn.json | 16 ++
inlong-dashboard/src/ui/locales/en.json | 17 +-
.../src/ui/pages/Clusters/HeartBeatModal.tsx | 113 ++++++++++++
.../src/ui/pages/Clusters/LogModal.tsx | 76 ++++++++
.../src/ui/pages/Clusters/NodeEditModal.tsx | 19 +-
.../src/ui/pages/Clusters/NodeManage.tsx | 191 ++++++++++++++++++++-
6 files changed, 421 insertions(+), 11 deletions(-)
diff --git a/inlong-dashboard/src/ui/locales/cn.json
b/inlong-dashboard/src/ui/locales/cn.json
index f6b1e5a493..b97e66643b 100644
--- a/inlong-dashboard/src/ui/locales/cn.json
+++ b/inlong-dashboard/src/ui/locales/cn.json
@@ -810,6 +810,21 @@
"pages.Clusters.Node.ProtocolType": "协议类型",
"pages.Clusters.Node.Agent": "Agent",
"pages.Clusters.Node.Agent.Version": "版本",
+ "pages.Cluster.Node.More": "更多",
+ "pages.Cluster.Node.Install": "重新安装",
+ "pages.Nodes.Restart": "重启",
+ "pages.Cluster.Node.InstallTitle": "确认重新安装吗?",
+ "pages.Cluster.Node.RestartTitle": "确认重启吗?",
+ "pages.Cluster.Node.Unload": "卸载",
+ "pages.Cluster.Node.UnloadTitle": "确认卸载吗?",
+ "pages.Cluster.Node.InstallLog": "安装日志",
+ "pages.Cluster.Node.InstallLog.None": "暂无日志",
+ "pages.Clusters.Node.Agent.HeartbeatDetection": "心跳检测",
+ "pages.Clusters.Node.Agent.HeartbeatInfo": "心跳信息",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.ModifyTime": "修改时间",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.ReportTime": "报告时间",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.Component": "类型",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.Instance": "Ip",
"pages.Clusters.Node.AgentInstaller": "安装包",
"pages.Clusters.Node.IsInstall": "安装方式",
"pages.Clusters.Node.ManualInstall": "手动安装",
@@ -824,6 +839,7 @@
"pages.Clusters.Node.Status.Normal": "正常",
"pages.Clusters.Node.Status.Timeout": "心跳超时",
"pages.Clusters.Node.LastModifier": "最后操作",
+ "pages.Clusters.Node.Creator": "创建人",
"pages.Clusters.Node.Create": "新建节点",
"pages.Clusters.Node.IpRule": "请输入正确的IP地址",
"pages.Clusters.Node.PortRule": "请输入正确的端口",
diff --git a/inlong-dashboard/src/ui/locales/en.json
b/inlong-dashboard/src/ui/locales/en.json
index 473f53d20d..c281478532 100644
--- a/inlong-dashboard/src/ui/locales/en.json
+++ b/inlong-dashboard/src/ui/locales/en.json
@@ -810,6 +810,21 @@
"pages.Clusters.Node.ProtocolType": "Protocol type",
"pages.Clusters.Node.Agent": "Agent",
"pages.Clusters.Node.Agent.Version": "Version",
+ "pages.Cluster.Node.More": "More",
+ "pages.Cluster.Node.Install": "Reinstall",
+ "pages.Nodes.Restart": "Restart",
+ "pages.Cluster.Node.InstallTitle": "Are you sure to reinstall?",
+ "pages.Cluster.Node.RestartTitle": "Are you sure to restart?",
+ "pages.Cluster.Node.Unload": "Uninstall",
+ "pages.Cluster.Node.UnloadTitle": "Are you sure to uninstall?",
+ "pages.Cluster.Node.InstallLog": "Install log",
+ "pages.Cluster.Node.InstallLog.None": "No logs yet",
+ "pages.Clusters.Node.Agent.HeartbeatDetection": "Heartbeat Detection",
+ "pages.Clusters.Node.Agent.HeartbeatInfo": "Heartbeat Info",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.ModifyTime": "Modify Time",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.ReportTime": "Report Time",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.Component": "Type",
+ "pages.Clusters.Node.Agent.HeartbeatInfo.Instance": "Ip",
"pages.Clusters.Node.AgentInstaller": "Installer",
"pages.Clusters.Node.IsInstall": "Installation",
"pages.Clusters.Node.ManualInstall": "Manual",
@@ -823,7 +838,7 @@
"pages.Clusters.Node.Status": "Status",
"pages.Clusters.Node.Status.Normal": "Normal",
"pages.Clusters.Node.Status.Timeout": "Timeout",
- "pages.Clusters.Node.LastModifier": "Last modifier",
+ "pages.Clusters.Node.Creator": "Creator",
"pages.Clusters.Node.Create": "Create",
"pages.Clusters.Node.IpRule": "Please enter the IP address correctly",
"pages.Clusters.Node.PortRule": "Please enter the port address correctly",
diff --git a/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx
b/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx
new file mode 100644
index 0000000000..50427f5182
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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, { useMemo } from 'react';
+import { Modal } from 'antd';
+import { ModalProps } from 'antd/es/modal';
+import { useRequest, useUpdateEffect } from '@/ui/hooks';
+import i18n from '@/i18n';
+import HighTable from '@/ui/components/HighTable';
+import { timestampFormat } from '@/core/utils';
+
+export interface Props extends ModalProps {
+ type?: string;
+ ip?: string;
+}
+
+const Comp: React.FC<Props> = ({ type, ip, ...modalProps }) => {
+ const { data: heartList, run: getHeartList } = useRequest(
+ {
+ url: '/heartbeat/component/list',
+ method: 'POST',
+ data: {
+ component: type,
+ inlongGroupId: '',
+ inlongStreamId: '',
+ instance: ip,
+ },
+ },
+ {
+ manual: true,
+ onSuccess: data => {
+ console.log(data);
+ },
+ },
+ );
+
+ const columns = useMemo(() => {
+ return [
+ {
+ title: i18n.t('pages.Clusters.Node.Agent.HeartbeatInfo.Component'),
+ dataIndex: 'component',
+ },
+ {
+ title: i18n.t('pages.Clusters.Node.Agent.HeartbeatInfo.Instance'),
+ dataIndex: 'instance',
+ },
+ {
+ title: i18n.t('pages.Clusters.Node.Agent.HeartbeatInfo.ModifyTime'),
+ dataIndex: 'modifyTime',
+ render: (text, record: any) => (
+ <>
+ <div>{record.modifyTime &&
timestampFormat(record.modifyTime)}</div>
+ </>
+ ),
+ },
+ {
+ title: i18n.t('pages.Clusters.Node.Agent.HeartbeatInfo.ReportTime'),
+ dataIndex: 'reportTime',
+ render: (text, record: any) => (
+ <>
+ <div>{record.modifyTime &&
timestampFormat(record.modifyTime)}</div>
+ </>
+ ),
+ },
+ ];
+ }, []);
+ const pagination = {
+ pageSize: 5,
+ current: 1,
+ total: heartList?.list?.length,
+ };
+ useUpdateEffect(() => {
+ if (modalProps.open) {
+ getHeartList();
+ }
+ }, [modalProps.open]);
+
+ return (
+ <Modal
+ {...modalProps}
+ title={i18n.t('pages.Clusters.Node.Agent.HeartbeatInfo')}
+ width={1200}
+ footer={null}
+ >
+ <HighTable
+ table={{
+ columns: columns,
+ rowKey: 'id',
+ dataSource: heartList?.list,
+ pagination,
+ }}
+ />
+ </Modal>
+ );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/ui/pages/Clusters/LogModal.tsx
b/inlong-dashboard/src/ui/pages/Clusters/LogModal.tsx
new file mode 100644
index 0000000000..740a89d202
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/Clusters/LogModal.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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 { Modal, Spin, Input } from 'antd';
+import { ModalProps } from 'antd/es/modal';
+import { useRequest, useUpdateEffect } from '@/ui/hooks';
+import i18n from '@/i18n';
+import StatusTag from '@/ui/components/StatusTag';
+
+export interface Props extends ModalProps {
+ id?: string;
+}
+
+const Comp: React.FC<Props> = ({ id, ...modalProps }) => {
+ const [log, setLog] = useState(null);
+
+ const {
+ data: logData,
+ loading: testLoad1,
+ run: getLog,
+ } = useRequest(id => ({ url: `/cluster/node/get/${id}` }), {
+ manual: true,
+ onSuccess: result => {
+ setLog(result.operateLog);
+ },
+ });
+
+ useUpdateEffect(() => {
+ if (modalProps.open) {
+ if (id) {
+ getLog(id);
+ }
+ }
+ }, [modalProps.open]);
+
+ return (
+ <Modal
+ {...modalProps}
+ title={i18n.t('pages.Cluster.Node.InstallLog')}
+ width={1200}
+ footer={null}
+ >
+ <div style={{ marginTop: '20px' }}>
+ <Spin spinning={testLoad1}>
+ <div>
+ <Input.TextArea
+ rows={log === '' || log === null ? 0 : 24}
+ value={
+ log === '' || log === null ?
i18n.t('pages.Cluster.Node.InstallLog.None') : log
+ }
+ />
+ </div>
+ </Spin>
+ </div>
+ </Modal>
+ );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
b/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
index 8801fc7be0..35447c1318 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
@@ -66,7 +66,6 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
submitData.version = savedData?.version;
}
if (type === 'AGENT') {
- submitData.protocolType = 'HTTP';
if (submitData.installer !== undefined) {
if (Array.isArray(submitData.moduleIdList)) {
submitData.moduleIdList =
submitData.moduleIdList.concat(submitData.installer);
@@ -139,6 +138,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
useUpdateEffect(() => {
if (modalProps.open) {
// open
+ setInstallType(false);
form.resetFields();
if (id) {
getData(id);
@@ -266,7 +266,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
name: 'identifyType',
initialValue: 'password',
hidden: type !== 'AGENT',
- visible: values => values?.isInstall,
+ visible: values => values?.isInstall &&
form.getFieldValue('isInstall'),
rules: [{ required: true }],
props: {
onChange: ({ target: { value } }) => {
@@ -292,7 +292,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
name: 'username',
rules: [{ required: true }],
hidden: type !== 'AGENT',
- visible: values => values?.isInstall,
+ visible: values => values?.isInstall &&
form.getFieldValue('isInstall'),
},
{
type: 'input',
@@ -300,7 +300,12 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
name: 'password',
rules: [{ required: true }],
hidden: type !== 'AGENT',
- visible: values => values?.isInstall && values?.identifyType ===
'password',
+ visible: values => {
+ return (
+ (values?.isInstall && values?.identifyType === 'password') ||
+ (form.getFieldValue('isInstall') &&
form.getFieldValue('identifyType') === 'password')
+ );
+ },
},
{
type: 'textarea',
@@ -321,7 +326,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
name: 'sshPort',
rules: [{ required: true }],
hidden: type !== 'AGENT',
- visible: values => values?.isInstall,
+ visible: values => values?.isInstall &&
form.getFieldValue('isInstall'),
},
{
type: 'select',
@@ -330,7 +335,6 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
isPro: type === 'AGENT',
hidden: type !== 'AGENT',
props: {
- mode: 'multiple',
options: {
requestAuto: true,
requestTrigger: ['onOpen'],
@@ -363,6 +367,9 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id,
type, clusterId, ...m
<Modal
{...modalProps}
title={i18n.t('pages.Clusters.Node.Name')}
+ afterClose={() => {
+ form.resetFields();
+ }}
footer={[
<Button key="cancel" onClick={e => modalProps.onCancel(e)}>
{i18n.t('basic.Cancel')}
diff --git a/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx
b/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx
index 1de113f003..cb85043cc5 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx
@@ -18,7 +18,7 @@
*/
import React, { useCallback, useMemo, useState } from 'react';
-import { Button, Modal, message } from 'antd';
+import { Button, Modal, message, Dropdown, Space } from 'antd';
import i18n from '@/i18n';
import { parse } from 'qs';
import HighTable from '@/ui/components/HighTable';
@@ -29,6 +29,10 @@ import NodeEditModal from './NodeEditModal';
import request from '@/core/utils/request';
import { timestampFormat } from '@/core/utils';
import { genStatusTag } from './status';
+import HeartBeatModal from '@/ui/pages/Clusters/HeartBeatModal';
+import LogModal from '@/ui/pages/Clusters/LogModal';
+import { DownOutlined } from '@ant-design/icons';
+import { MenuProps } from 'antd/es/menu';
const getFilterFormContent = defaultValues => [
{
@@ -55,6 +59,12 @@ const Comp: React.FC = () => {
const [nodeEditModal, setNodeEditModal] = useState<Record<string, unknown>>({
open: false,
});
+ const [logModal, setLogModal] = useState<Record<string, unknown>>({
+ open: false,
+ });
+ const [heartModal, setHeartModal] = useState<Record<string, unknown>>({
+ open: false,
+ });
const {
data,
@@ -76,7 +86,68 @@ const Comp: React.FC = () => {
const onEdit = ({ id }) => {
setNodeEditModal({ open: true, id });
};
+ const onUnload = useCallback(
+ ({ id }) => {
+ Modal.confirm({
+ title: i18n.t('pages.Cluster.Node.UnloadTitle'),
+ onOk: async () => {
+ await request({
+ url: `/cluster/node/unload/${id}`,
+ method: 'DELETE',
+ });
+ await getList();
+ message.success(i18n.t('basic.OperatingSuccess'));
+ },
+ });
+ },
+ [getList],
+ );
+ const onRestart = useCallback(
+ record => {
+ Modal.confirm({
+ title: i18n.t('pages.Cluster.Node.RestartTitle'),
+ onOk: async () => {
+ record.agentRestartTime = record?.agentRestartTime + 1;
+ delete record.isInstall;
+ await request({
+ url: `/cluster/node/update`,
+ method: 'POST',
+ data: record,
+ });
+ await getList();
+ message.success(i18n.t('basic.OperatingSuccess'));
+ },
+ });
+ },
+ [getList],
+ );
+ const onInstall = useCallback(
+ record => {
+ Modal.confirm({
+ title: i18n.t('pages.Cluster.Node.InstallTitle'),
+ onOk: async () => {
+ await request({
+ url: `/cluster/node/update`,
+ method: 'POST',
+ data: {
+ ...record,
+ isInstall: true,
+ },
+ });
+ await getList();
+ message.success(i18n.t('basic.OperatingSuccess'));
+ },
+ });
+ },
+ [getList],
+ );
+ const onLog = ({ id }) => {
+ setLogModal({ open: true, id });
+ };
+ const openHeartModal = ({ type, ip }) => {
+ setHeartModal({ open: true, type: type, ip: ip });
+ };
const onDelete = useCallback(
({ id }) => {
Modal.confirm({
@@ -115,7 +186,74 @@ const Comp: React.FC = () => {
current: +options.pageNum,
total: data?.total,
};
+ const [operationType, setOperationType] = useState('');
+ const { data: nodeData, run: getNodeData } = useRequest(
+ id => ({
+ url: `/cluster/node/get/${id}`,
+ }),
+ {
+ manual: true,
+ onSuccess: result => {
+ switch (operationType) {
+ case 'onRestart':
+ onRestart(result);
+ break;
+ case 'onUnload':
+ onUnload(result);
+ break;
+ case 'onInstall':
+ onInstall(result);
+ break;
+ default:
+ break;
+ }
+ },
+ },
+ );
+ const items: MenuProps['items'] = [
+ {
+ label: <Button
type="link">{i18n.t('pages.Cluster.Node.Install')}</Button>,
+ key: '0',
+ },
+ {
+ label: <Button type="link">{i18n.t('pages.Nodes.Restart')}</Button>,
+ key: '1',
+ },
+ {
+ label: <Button
type="link">{i18n.t('pages.Cluster.Node.Unload')}</Button>,
+ key: '2',
+ },
+ {
+ label: <Button
type="link">{i18n.t('pages.Cluster.Node.InstallLog')}</Button>,
+ key: '3',
+ },
+ {
+ label: <Button
type="link">{i18n.t('pages.Clusters.Node.Agent.HeartbeatDetection')}</Button>,
+ key: '4',
+ },
+ ];
+ const handleMenuClick = (key, record) => {
+ switch (key) {
+ case '0':
+ getNodeData(record.id).then(() => setOperationType('onInstall'));
+ break;
+ case '1':
+ getNodeData(record.id).then(() => setOperationType('onRestart'));
+ break;
+ case '2':
+ getNodeData(record.id).then(() => setOperationType('onUnload'));
+ break;
+ case '3':
+ onLog(record);
+ break;
+ case '4':
+ openHeartModal(record);
+ break;
+ default:
+ break;
+ }
+ };
const columns = useMemo(() => {
return [
{
@@ -140,10 +278,19 @@ const Comp: React.FC = () => {
dataIndex: 'status',
render: text => genStatusTag(text),
},
+ {
+ title: i18n.t('pages.Clusters.Node.Creator'),
+ dataIndex: 'creator',
+ render: (text, record: any) => (
+ <>
+ <div>{text}</div>
+ <div>{record.createTime &&
timestampFormat(record.createTime)}</div>
+ </>
+ ),
+ },
{
title: i18n.t('pages.Clusters.Node.LastModifier'),
dataIndex: 'modifier',
- width: 150,
render: (text, record: any) => (
<>
<div>{text}</div>
@@ -154,7 +301,8 @@ const Comp: React.FC = () => {
{
title: i18n.t('basic.Operating'),
dataIndex: 'action',
- width: 120,
+ key: 'operation',
+ width: 200,
render: (text, record) => (
<>
<Button type="link" onClick={() => onEdit(record)}>
@@ -163,6 +311,16 @@ const Comp: React.FC = () => {
<Button type="link" onClick={() => onDelete(record)}>
{i18n.t('basic.Delete')}
</Button>
+ {type === 'AGENT' && (
+ <Dropdown menu={{ items, onClick: ({ key }) =>
handleMenuClick(key, record) }}>
+ <a onClick={e => e.preventDefault()}>
+ <Space>
+ {i18n.t('pages.Cluster.Node.More')}
+ <DownOutlined />
+ </Space>
+ </a>
+ </Dropdown>
+ )}
</>
),
},
@@ -191,7 +349,14 @@ const Comp: React.FC = () => {
}
table={{
columns:
- type === 'AGENT' ? columns.filter(item => item.dataIndex !==
'enabledOnline') : columns,
+ type === 'AGENT'
+ ? columns.filter(
+ item =>
+ item.dataIndex !== 'enabledOnline' &&
+ item.dataIndex !== 'port' &&
+ item.dataIndex !== 'protocolType',
+ )
+ : columns,
rowKey: 'id',
dataSource: data?.list,
pagination,
@@ -211,6 +376,24 @@ const Comp: React.FC = () => {
}}
onCancel={() => setNodeEditModal({ open: false })}
/>
+ <LogModal
+ {...logModal}
+ open={logModal.open as boolean}
+ onOk={async () => {
+ await getList();
+ setLogModal({ open: false });
+ }}
+ onCancel={() => setLogModal({ open: false })}
+ />
+ <HeartBeatModal
+ {...heartModal}
+ open={heartModal.open as boolean}
+ onOk={async () => {
+ await getList();
+ setHeartModal({ open: false });
+ }}
+ onCancel={() => setHeartModal({ open: false })}
+ />
</PageContainer>
);
};