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

dockerzhang 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 e49d69e210 [INLONG-11187][Dashboard] Agent batch upgrade (#11437)
e49d69e210 is described below

commit e49d69e21042626c85cb4d92fec3b9280fc3b0db
Author: kamianlaida <[email protected]>
AuthorDate: Wed Oct 30 19:46:08 2024 +0800

    [INLONG-11187][Dashboard] Agent batch upgrade (#11437)
---
 inlong-dashboard/src/ui/locales/cn.json            |  16 +
 inlong-dashboard/src/ui/locales/en.json            |  16 +
 .../ui/pages/Clusters/AgentBatchUpdateModal.tsx    | 273 ++++++++++++
 .../src/ui/pages/Clusters/HeartBeatModal.tsx       |  41 +-
 .../src/ui/pages/Clusters/NodeEditModal.tsx        | 181 ++++----
 .../src/ui/pages/Clusters/NodeManage.tsx           | 459 ++++++++++++++++++---
 .../src/ui/pages/Clusters/OperationLogModal.tsx    | 101 +++++
 inlong-dashboard/src/ui/pages/Clusters/config.tsx  | 130 ++++++
 inlong-dashboard/src/ui/pages/Clusters/index.tsx   |   2 +-
 inlong-dashboard/src/ui/pages/Clusters/status.tsx  |  15 +
 10 files changed, 1068 insertions(+), 166 deletions(-)

diff --git a/inlong-dashboard/src/ui/locales/cn.json 
b/inlong-dashboard/src/ui/locales/cn.json
index 23f83e31aa..778c687270 100644
--- a/inlong-dashboard/src/ui/locales/cn.json
+++ b/inlong-dashboard/src/ui/locales/cn.json
@@ -6,6 +6,7 @@
   "basic.ConnectionSuccess": "连接成功",
   "basic.Save": "保存",
   "basic.Cancel": "取消",
+  "basic.Confirm": "确定",
   "basic.Create": "新建",
   "basic.Delete": "删除",
   "basic.DeleteConfirm": "确认删除吗?",
@@ -815,6 +816,7 @@
   "pages.Clusters.Name": "集群名称",
   "pages.Clusters.Tag": "集群标签",
   "pages.Clusters.InCharges": "责任人",
+  "pages.Clusters.Node.Description": "节点描述",
   "pages.Clusters.Description": "集群描述",
   "pages.Clusters.TestConnection": "测试连接",
   "pages.Clusters.Node.Name": "节点",
@@ -847,15 +849,29 @@
   "pages.Clusters.Node.SSHKey": "SSH 密钥",
   "pages.Clusters.Node.SSHPort": "SSH 端口",
   "pages.Clusters.Node.SSHKeyHelper": "请将公钥上传至 Agent 节点的 
~/.ssh/authorized_keys 文件中",
+  "pages.Clusters.Node.Version": "版本",
   "pages.Clusters.Node.Status": "状态",
   "pages.Clusters.Node.Status.Normal": "正常",
   "pages.Clusters.Node.Status.Timeout": "心跳超时",
+  "pages.Clusters.Node.Status.INSTALLING": "安装中",
+  "pages.Clusters.Node.Status.INSTALLFAILED": "安装失败",
+  "pages.Clusters.Node.Status.INSTALLSUCCESS": "安装成功",
   "pages.Clusters.Node.LastModifier": "最后操作",
   "pages.Clusters.Node.Creator": "创建人",
   "pages.Clusters.Node.Create": "新建节点",
   "pages.Clusters.Node.IpRule": "请输入正确的IP地址",
   "pages.Clusters.Node.PortRule": "请输入正确的端口",
   "pages.Clusters.Node.ProtocolTypeRule": "请输入正确的协议类型",
+  "pages.Clusters.Node.BatchUpdate": "批量操作",
+  "pages.Clusters.Node.BatchNum": "分批数",
+  "pages.Clusters.Node.Interval": "间隔",
+  "pages.Clusters.Node.Minute": "分钟",
+  "pages.Clusters.Node.OperationStatusQuery": "操作状态查询",
+  "pages.Clusters.Node.OperationType": "操作类型",
+  "pages.Clusters.Node.UpgradeAgentAndInstaller": "更新 Agent 和 Installer",
+  "pages.Clusters.Node.UpgradeAgent": "更新 Agent",
+  "pages.Clusters.Node.UpgradeInstaller": "更新 Installer",
+  "pages.Clusters.Node.RestartAgent": "重启 Agent",
   "pages.Clusters.Node.Online": "在线",
   "pages.Clusters.Pulsar.PulsarTenant": "默认租户",
   "pages.Clusters.Pulsar.TokenPlaceholder": "如果群集配置了令牌,则为必需",
diff --git a/inlong-dashboard/src/ui/locales/en.json 
b/inlong-dashboard/src/ui/locales/en.json
index 89f3af4be2..8bb5c15ab5 100644
--- a/inlong-dashboard/src/ui/locales/en.json
+++ b/inlong-dashboard/src/ui/locales/en.json
@@ -6,6 +6,7 @@
   "basic.ConnectionSuccess": "Connection success",
   "basic.Save": "Save",
   "basic.Cancel": "Cancel",
+  "basic.Confirm": "Confirm",
   "basic.Create": "Create",
   "basic.Delete": "Delete",
   "basic.DeleteConfirm": "Are you sure to delete?",
@@ -815,6 +816,7 @@
   "pages.Clusters.Name": "Cluster Name",
   "pages.Clusters.Tag": "Cluster Tag",
   "pages.Clusters.InCharges": "Owners",
+  "pages.Clusters.Node.Description": "Description",
   "pages.Clusters.Description": "Description",
   "pages.Clusters.TestConnection": "Test connection",
   "pages.Clusters.Node.Name": "Node",
@@ -847,15 +849,29 @@
   "pages.Clusters.Node.Password": "SSH Password",
   "pages.Clusters.Node.SSHPort": "SSH Port",
   "pages.Clusters.Node.SSHKeyHelper": "Please upload the public key to the 
~/.ssh/authorized_keys file of the Agent node",
+  "pages.Clusters.Node.Version":"Version",
   "pages.Clusters.Node.Status": "Status",
   "pages.Clusters.Node.Status.Normal": "Normal",
   "pages.Clusters.Node.Status.Timeout": "Timeout",
+  "pages.Clusters.Node.Status.INSTALLING": "Installing",
+  "pages.Clusters.Node.Status.INSTALLFAILED": "Install  Failed",
+  "pages.Clusters.Node.Status.INSTALLSUCCESS": "Install Success",
   "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",
   "pages.Clusters.Node.ProtocolTypeRule": "Please enter the protocol type 
correctly",
+  "pages.Clusters.Node.BatchUpdate": "Batch Operation",
+  "pages.Clusters.Node.BatchNum": "Batch Num",
+  "pages.Clusters.Node.Interval": "Interval",
+  "pages.Clusters.Node.Minute": "Minute",
+  "pages.Clusters.Node.OperationStatusQuery": "Operation Status Query",
+  "pages.Clusters.Node.OperationType": "Operation Type",
+  "pages.Clusters.Node.UpgradeAgentAndInstaller": "Upgrade Agent and 
Installer",
+  "pages.Clusters.Node.UpgradeAgent": "Upgrade Agent",
+  "pages.Clusters.Node.UpgradeInstaller": "Upgrade Installer",
+  "pages.Clusters.Node.RestartAgent": "Restart Agent",
   "pages.Clusters.Node.Online": "Online",
   "pages.Clusters.Pulsar.PulsarTenant": "Default tenant",
   "pages.Clusters.Pulsar.TokenPlaceholder": "Required if the cluster is 
configured with Token",
diff --git a/inlong-dashboard/src/ui/pages/Clusters/AgentBatchUpdateModal.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/AgentBatchUpdateModal.tsx
new file mode 100644
index 0000000000..e56a8cb7e3
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/Clusters/AgentBatchUpdateModal.tsx
@@ -0,0 +1,273 @@
+/*
+ * 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, useState } from 'react';
+import { Button, message, Modal } from 'antd';
+import { ModalProps } from 'antd/es/modal';
+import i18n from '@/i18n';
+import FormGenerator, { useForm } from '@/ui/components/FormGenerator';
+import dayjs, { Dayjs } from 'dayjs';
+
+export interface Props extends ModalProps {
+  agentList?: [];
+  agentTotal?: number;
+  parentId?: number;
+  openStatusModal?: () => void;
+  getArgs?: (args) => void;
+}
+
+const Comp: React.FC<Props> = ({ agentList, agentTotal, parentId, 
...modalProps }) => {
+  const [form] = useForm();
+  const content = () => [
+    {
+      type: 'inputnumber',
+      label: i18n.t('pages.Clusters.Node.BatchNum'),
+      name: 'batchNum',
+      initialValue: 2,
+      rules: [
+        {
+          required: true,
+        },
+      ],
+    },
+    {
+      type: 'inputnumber',
+      label: i18n.t('pages.Clusters.Node.Interval'),
+      name: 'interval',
+      initialValue: 5,
+      rules: [
+        {
+          required: true,
+        },
+      ],
+      suffix: i18n.t('pages.Clusters.Node.Minute'),
+    },
+    {
+      type: 'radio',
+      label: i18n.t('pages.Clusters.Node.OperationType'),
+      name: 'operationType',
+      initialValue: 0,
+      rules: [{ required: true }],
+      props: {
+        style: {
+          display: 'flex',
+          flexDirection: 'column',
+          justifyContent: 'start',
+          alignItems: 'start',
+        },
+        options: [
+          {
+            label: i18n.t('pages.Clusters.Node.UpgradeAgentAndInstaller'),
+            value: 0,
+          },
+          {
+            label: i18n.t('pages.Clusters.Node.UpgradeAgent'),
+            value: 1,
+          },
+          {
+            label: i18n.t('pages.Clusters.Node.UpgradeInstaller'),
+            value: 2,
+          },
+          {
+            label: i18n.t('pages.Clusters.Node.RestartAgent'),
+            value: 3,
+          },
+        ],
+      },
+    },
+    {
+      type: 'select',
+      label: i18n.t('pages.Clusters.Node.Agent.Version'),
+      name: 'moduleIdList',
+      visible: values => values.operationType !== 2 && values.operationType 
!== 3,
+      props: {
+        options: {
+          requestAuto: true,
+          requestTrigger: ['onOpen'],
+          requestService: keyword => ({
+            url: '/module/list',
+            method: 'POST',
+            data: {
+              keyword,
+              pageNum: 1,
+              pageSize: 9999,
+            },
+          }),
+          requestParams: {
+            formatResult: result =>
+              result?.list
+                ?.filter(item => item.type === 'AGENT')
+                .map(item => ({
+                  ...item,
+                  label: `${item.name} ${item.version}`,
+                  value: item.id,
+                })),
+          },
+        },
+      },
+      rules: [
+        {
+          required: true,
+        },
+      ],
+    },
+    {
+      type: 'select',
+      label: i18n.t('pages.Clusters.Node.AgentInstaller'),
+      name: 'installer',
+      visible: values => values.operationType === 0 || values.operationType 
=== 2,
+      props: {
+        options: {
+          requestAuto: true,
+          requestTrigger: ['onOpen'],
+          requestService: keyword => ({
+            url: '/module/list',
+            method: 'POST',
+            data: {
+              keyword,
+              pageNum: 1,
+              pageSize: 9999,
+            },
+          }),
+          requestParams: {
+            formatResult: result =>
+              result?.list
+                ?.filter(item => item.type === 'INSTALLER')
+                .map(item => ({
+                  ...item,
+                  label: `${item.name} ${item.version}`,
+                  value: item.id,
+                })),
+          },
+        },
+      },
+      rules: [
+        {
+          required: true,
+        },
+      ],
+    },
+  ];
+
+  const valuesToSubmitList = (agentList, values, submitList) => {
+    switch (values.operationType) {
+      case 0:
+        agentList.forEach(item => {
+          delete item.protocolType;
+          item.moduleIdList = [values.moduleIdList, values.installer];
+          submitList.push({
+            ...item,
+            moduleIdList: [values.moduleIdList, values.installer],
+            isInstall: true,
+          });
+        });
+        break;
+      case 1:
+        agentList.forEach(item => {
+          delete item.protocolType;
+          item.moduleIdList = [values.moduleIdList, item.moduleIdList[1]];
+          submitList.push({
+            ...item,
+          });
+          delete item.isInstall;
+        });
+        break;
+
+      case 2:
+        agentList.forEach(item => {
+          delete item.protocolType;
+          item.isInstall = true;
+          item.moduleIdList = [item.moduleIdList[0], values.installer];
+          submitList.push({
+            ...item,
+          });
+        });
+        break;
+
+      case 3:
+        agentList.forEach(item => {
+          delete item.protocolType;
+          submitList.push({
+            ...item,
+          });
+          delete item.isInstall;
+        });
+        break;
+    }
+  };
+  const batchUpdate = async (agentList, onOk: (e: 
React.MouseEvent<HTMLButtonElement>) => void) => {
+    const values = await form.validateFields();
+    const submitList = [];
+
+    valuesToSubmitList(agentList, values, submitList);
+    console.log('submitList', submitList);
+    const baseBatchSize =
+      Math.floor(submitList.length / values.batchNum) === 0
+        ? 1
+        : Math.floor(submitList.length / values.batchNum);
+    const remainder = submitList.length % values.batchNum;
+    const map = new Map();
+    const batchNum = agentList.length < values.batchNum ? agentList.length : 
values.batchNum;
+    console.log('batchNum', batchNum, 'baseBatchSize', baseBatchSize, 
'remainder', remainder);
+
+    for (let i = 1; i <= batchNum; i++) {
+      if (i === batchNum) {
+        map.set(i, submitList.slice((i - 1) * baseBatchSize, 
submitList.length));
+      } else {
+        map.set(i, submitList.slice((i - 1) * baseBatchSize, i * 
baseBatchSize));
+      }
+    }
+    const args = {
+      map: map,
+      interval: values.interval,
+      operationType: values.operationType,
+      ids: agentList.map(item => item.id),
+      submitDataList: submitList,
+    };
+    modalProps.getArgs(args);
+    modalProps?.onOk(values);
+    modalProps?.openStatusModal();
+  };
+
+  return (
+    <Modal
+      {...modalProps}
+      title={i18n.t('pages.Clusters.Node.BatchUpdate')}
+      width={600}
+      footer={[
+        <Button key="cancel" onClick={e => modalProps.onCancel(e)}>
+          {i18n.t('basic.Cancel')}
+        </Button>,
+        <Button
+          key="update"
+          type="primary"
+          onClick={async e => {
+            await batchUpdate(agentList, modalProps.onOk);
+          }}
+        >
+          {i18n.t('basic.Confirm')}
+        </Button>,
+      ]}
+    >
+      <FormGenerator content={content()} form={form} useMaxWidth labelWrap />
+    </Modal>
+  );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx
index 50427f5182..3895f1f986 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx
@@ -17,33 +17,40 @@
  * under the License.
  */
 
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo, useState } 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';
+import { defaultSize } from '@/configs/pagination';
 
 export interface Props extends ModalProps {
   type?: string;
   ip?: string;
 }
 
-const Comp: React.FC<Props> = ({ type, ip, ...modalProps }) => {
+const Comp: React.FC<Props> = ({ ...modalProps }) => {
+  const [options, setOptions] = useState({
+    inlongGroupId: '',
+    inlongStreamId: '',
+    pageNum: 1,
+    pageSize: defaultSize,
+  });
+
   const { data: heartList, run: getHeartList } = useRequest(
     {
       url: '/heartbeat/component/list',
       method: 'POST',
       data: {
-        component: type,
-        inlongGroupId: '',
-        inlongStreamId: '',
-        instance: ip,
+        ...options,
+        component: 'AGENT',
+        instance: modalProps.ip,
       },
     },
     {
-      manual: true,
+      refreshDeps: [options],
       onSuccess: data => {
         console.log(data);
       },
@@ -81,11 +88,20 @@ const Comp: React.FC<Props> = ({ type, ip, ...modalProps }) 
=> {
     ];
   }, []);
   const pagination = {
-    pageSize: 5,
-    current: 1,
-    total: heartList?.list?.length,
+    pageSize: +options.pageSize,
+    current: +options.pageNum,
+    total: heartList?.total,
+  };
+
+  const onChange = ({ current: pageNum, pageSize }) => {
+    setOptions(prev => ({
+      ...prev,
+      pageNum,
+      pageSize,
+    }));
   };
-  useUpdateEffect(() => {
+
+  useEffect(() => {
     if (modalProps.open) {
       getHeartList();
     }
@@ -102,8 +118,9 @@ const Comp: React.FC<Props> = ({ type, ip, ...modalProps }) 
=> {
         table={{
           columns: columns,
           rowKey: 'id',
-          dataSource: heartList?.list,
+          dataSource: heartList?.list || [],
           pagination,
+          onChange,
         }}
       />
     </Modal>
diff --git a/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
index 80b7355120..5a38817553 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
@@ -47,6 +47,10 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
           // Only keep the first element and give the rest to the 'installer'
           result.installer = result?.moduleIdList.slice(1);
           result.moduleIdList = result?.moduleIdList.slice(0, 1);
+          if (result.username) {
+            setInstallType(true);
+            result.isInstall = true;
+          }
         }
         form.setFieldsValue(result);
       },
@@ -64,48 +68,47 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
     if (isUpdate) {
       submitData.id = id;
       submitData.version = savedData?.version;
-    }
-    if (type === 'AGENT') {
-      if (submitData.installer !== undefined) {
-        if (Array.isArray(submitData.moduleIdList)) {
-          submitData.moduleIdList = 
submitData.moduleIdList.concat(submitData.installer);
-        } else {
-          submitData.moduleIdList = 
[submitData.moduleIdList].concat(submitData.installer);
+      if (type === 'AGENT') {
+        if (!submitData.isInstall) {
+          submitData.username = '';
+          submitData.password = '';
+          submitData.sshPort = '';
+          submitData.sshKey = '';
+        }
+      }
+      if (type === 'AGENT') {
+        if (submitData.installer !== undefined) {
+          if (Array.isArray(submitData.moduleIdList)) {
+            submitData.moduleIdList = 
submitData.moduleIdList.concat(submitData.installer);
+          } else {
+            submitData.moduleIdList = 
[submitData.moduleIdList].concat(submitData.installer);
+          }
         }
       }
+      await request({
+        url: `/cluster/node/${isUpdate ? 'update' : 'save'}`,
+        method: 'POST',
+        data: submitData,
+      });
+      await modalProps?.onOk(submitData);
+      message.success(i18n.t('basic.OperatingSuccess'));
     }
-    await request({
-      url: `/cluster/node/${isUpdate ? 'update' : 'save'}`,
-      method: 'POST',
-      data: submitData,
-    });
-    await modalProps?.onOk(submitData);
-    message.success(i18n.t('basic.OperatingSuccess'));
   };
 
-  const { data: agentInstaller, run: getAgentInstall } = useRequest(
-    () => ({
+  const [agentInstaller, setAgentInstaller] = useState([]);
+  const getAgentInstaller = async () => {
+    const result = await request({
       url: '/module/list',
       method: 'POST',
       data: {
         pageNum: 1,
         pageSize: 9999,
       },
-    }),
-    {
-      manual: true,
-      onSuccess: result => {
-        const temp = result?.list
-          ?.filter(item => item.type === 'INSTALLER')
-          .map(item => ({
-            ...item,
-            label: `${item.name} ${item.version}`,
-            value: item.id,
-          }));
-        form.setFieldValue('installer', temp[0].id);
-      },
-    },
-  );
+    });
+    console.log('result', result);
+    setAgentInstaller(result.list?.sort((a, b) => b.modifyTime - 
a.modifyTime));
+    return result;
+  };
 
   const { data: sshKeys, run: getSSHKeys } = useRequest(
     () => ({
@@ -123,9 +126,10 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
   const testSSHConnection = async () => {
     const values = await form.validateFields();
     const submitData = {
-      ...values,
-      type,
-      parentId: savedData?.parentId || clusterId,
+      ip: values.ip,
+      sshPort: values.sshPort,
+      username: values.username,
+      password: values.password,
     };
     await request({
       url: '/cluster/node/testSSHConnection',
@@ -140,18 +144,25 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ 
id, type, clusterId, ...m
       // open
       setInstallType(false);
       form.resetFields();
+      getAgentInstaller();
       if (id) {
         getData(id);
-      } else {
-        if (type === 'AGENT') {
-          getAgentInstall();
-        }
       }
     }
   }, [modalProps.open]);
 
+  useEffect(() => {
+    form.setFieldValue('identifyType', 'password');
+    if (modalProps.open && !id) {
+      form.setFieldValue(
+        'installer',
+        agentInstaller?.filter(item => item.type === 'INSTALLER')?.[0]?.id,
+      );
+    }
+  }, [agentInstaller]);
+
   const content = useMemo(() => {
-    return [
+    return Id => [
       {
         type: 'input',
         label: 'IP',
@@ -159,9 +170,13 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         rules: [
           {
             pattern: rulesPattern.ip,
+            required: type === 'AGENT',
             message: i18n.t('pages.Clusters.Node.IpRule'),
           },
         ],
+        props: {
+          disabled: !!Id,
+        },
       },
       {
         type: 'inputnumber',
@@ -205,34 +220,20 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ 
id, type, clusterId, ...m
         name: 'moduleIdList',
         hidden: type !== 'AGENT',
         props: {
-          options: {
-            requestAuto: true,
-            requestTrigger: ['onOpen'],
-            requestService: keyword => ({
-              url: '/module/list',
-              method: 'POST',
-              data: {
-                keyword,
-                pageNum: 1,
-                pageSize: 9999,
-              },
-            }),
-            requestParams: {
-              formatResult: result =>
-                result?.list
-                  ?.filter(item => item.type === 'AGENT')
-                  .map(item => ({
-                    ...item,
-                    label: `${item.name} ${item.version}`,
-                    value: item.id,
-                  })),
-            },
-          },
+          filterOption: (input, option) =>
+            (option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
+          options: agentInstaller
+            ?.filter(item => item.type === 'AGENT')
+            .map(item => ({
+              ...item,
+              label: `${item.name} ${item.version}`,
+              value: item.id,
+            })),
         },
       },
       {
         type: 'textarea',
-        label: i18n.t('pages.Clusters.Description'),
+        label: i18n.t('pages.Clusters.Node.Description'),
         name: 'description',
         props: {
           maxLength: 256,
@@ -267,7 +268,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         name: 'identifyType',
         initialValue: 'password',
         hidden: type !== 'AGENT',
-        visible: values => values?.isInstall && 
form.getFieldValue('isInstall'),
+        visible: values => isInstall,
         rules: [{ required: true }],
         props: {
           onChange: ({ target: { value } }) => {
@@ -293,7 +294,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         name: 'username',
         rules: [{ required: true }],
         hidden: type !== 'AGENT',
-        visible: values => values?.isInstall && 
form.getFieldValue('isInstall'),
+        visible: isInstall,
       },
       {
         type: 'input',
@@ -301,12 +302,10 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ 
id, type, clusterId, ...m
         name: 'password',
         rules: [{ required: true }],
         hidden: type !== 'AGENT',
-        visible: values => {
-          return (
-            (values?.isInstall && values?.identifyType === 'password') ||
-            (form.getFieldValue('isInstall') && 
form.getFieldValue('identifyType') === 'password')
-          );
-        },
+        visible: values =>
+          isInstall &&
+          (values?.identifyType === 'password' ||
+            form.getFieldValue('identifyType') === 'password'),
       },
       {
         type: 'textarea',
@@ -315,7 +314,9 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         name: 'sshKey',
         rules: [{ required: true }],
         hidden: type !== 'AGENT',
-        visible: values => values?.isInstall && values?.identifyType === 
'sshKey',
+        visible: values =>
+          isInstall &&
+          (values?.identifyType === 'sshKey' || 
form.getFieldValue('identifyType') === 'sshKey'),
         props: {
           readOnly: true,
           autoSize: true,
@@ -327,7 +328,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         name: 'sshPort',
         rules: [{ required: true }],
         hidden: type !== 'AGENT',
-        visible: values => values?.isInstall && 
form.getFieldValue('isInstall'),
+        visible: values => isInstall,
       },
       {
         type: 'select',
@@ -336,33 +337,17 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ 
id, type, clusterId, ...m
         isPro: type === 'AGENT',
         hidden: type !== 'AGENT',
         props: {
-          options: {
-            requestAuto: true,
-            requestTrigger: ['onOpen'],
-            requestService: keyword => ({
-              url: '/module/list',
-              method: 'POST',
-              data: {
-                keyword,
-                pageNum: 1,
-                pageSize: 9999,
-              },
-            }),
-            requestParams: {
-              formatResult: result =>
-                result?.list
-                  ?.filter(item => item.type === 'INSTALLER')
-                  .map(item => ({
-                    ...item,
-                    label: `${item.name} ${item.version}`,
-                    value: item.id,
-                  })),
-            },
-          },
+          options: agentInstaller
+            ?.filter(item => item.type === 'INSTALLER')
+            .map(item => ({
+              ...item,
+              label: `${item.name} ${item.version}`,
+              value: item.id,
+            })),
         },
       },
     ];
-  }, []);
+  }, [isInstall, agentInstaller, modalProps.open]);
 
   return (
     <Modal
@@ -385,7 +370,7 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         ),
       ]}
     >
-      <FormGenerator content={content} form={form} useMaxWidth />
+      <FormGenerator content={content(id)} form={form} useMaxWidth />
     </Modal>
   );
 };
diff --git a/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx
index cb85043cc5..7e2a558792 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 
'react';
 import { Button, Modal, message, Dropdown, Space } from 'antd';
 import i18n from '@/i18n';
 import { parse } from 'qs';
@@ -28,17 +28,28 @@ import { useRequest, useLocation } from '@/ui/hooks';
 import NodeEditModal from './NodeEditModal';
 import request from '@/core/utils/request';
 import { timestampFormat } from '@/core/utils';
-import { genStatusTag } from './status';
+import { genStatusTag, statusList } 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';
+import { useForm } from 'antd/es/form/Form';
+import { getModuleList, versionMap } from '@/ui/pages/Clusters/config';
+import AgentBatchUpdateModal from '@/ui/pages/Clusters/AgentBatchUpdateModal';
+import OperationLogModal from '@/ui/pages/Clusters/OperationLogModal';
 
 const getFilterFormContent = defaultValues => [
   {
     type: 'inputsearch',
     name: 'keyword',
   },
+  {
+    type: 'select',
+    name: 'status',
+    props: {
+      options: statusList,
+    },
+  },
 ];
 
 const Comp: React.FC = () => {
@@ -47,6 +58,7 @@ const Comp: React.FC = () => {
     () => (parse(location.search.slice(1)) as Record<string, string>) || {},
     [location.search],
   );
+  const [form] = useForm();
 
   const [options, setOptions] = useState({
     keyword: '',
@@ -54,10 +66,12 @@ const Comp: React.FC = () => {
     pageNum: 1,
     type,
     parentId: +clusterId,
+    status: '',
   });
 
   const [nodeEditModal, setNodeEditModal] = useState<Record<string, unknown>>({
     open: false,
+    agentInstallerList: [],
   });
   const [logModal, setLogModal] = useState<Record<string, unknown>>({
     open: false,
@@ -65,6 +79,12 @@ const Comp: React.FC = () => {
   const [heartModal, setHeartModal] = useState<Record<string, unknown>>({
     open: false,
   });
+  const [operationLogModal, setOperationLogModal] = useState<Record<string, 
unknown>>({
+    open: false,
+  });
+  const [agentBatchUpdateModal, setAgentBatchUpdateModal] = 
useState<Record<string, unknown>>({
+    open: false,
+  });
 
   const {
     data,
@@ -109,13 +129,21 @@ const Comp: React.FC = () => {
         onOk: async () => {
           record.agentRestartTime = record?.agentRestartTime + 1;
           delete record.isInstall;
-          await request({
-            url: `/cluster/node/update`,
-            method: 'POST',
-            data: record,
-          });
+          try {
+            const response = await request({
+              url: `/cluster/node/update`,
+              method: 'POST',
+              data: record,
+            });
+            if (response.success) {
+              message.success(i18n.t('basic.OperatingSuccess'));
+            } else {
+              Modal.destroyAll();
+            }
+          } catch (e) {
+            Modal.destroyAll();
+          }
           await getList();
-          message.success(i18n.t('basic.OperatingSuccess'));
         },
       });
     },
@@ -126,28 +154,39 @@ const Comp: React.FC = () => {
       Modal.confirm({
         title: i18n.t('pages.Cluster.Node.InstallTitle'),
         onOk: async () => {
-          await request({
-            url: `/cluster/node/update`,
-            method: 'POST',
-            data: {
-              ...record,
-              isInstall: true,
-            },
-          });
+          try {
+            const response = await request({
+              url: `/cluster/node/update`,
+              method: 'POST',
+              data: {
+                ...record,
+                isInstall: true,
+              },
+            });
+            if (response.success) {
+              message.success(i18n.t('basic.OperatingSuccess'));
+            } else {
+              Modal.destroyAll();
+            }
+          } catch (e) {
+            Modal.destroyAll();
+          }
           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 openOperationLogModal = ({ ip }) => {
+    setOperationLogModal({ open: true, ip: ip });
+  };
+
   const onDelete = useCallback(
     ({ id }) => {
       Modal.confirm({
@@ -208,6 +247,7 @@ const Comp: React.FC = () => {
           default:
             break;
         }
+        return result;
       },
     },
   );
@@ -232,6 +272,10 @@ const Comp: React.FC = () => {
       label: <Button 
type="link">{i18n.t('pages.Clusters.Node.Agent.HeartbeatDetection')}</Button>,
       key: '4',
     },
+    {
+      label: <Button 
type="link">{i18n.t('pages.GroupDetail.OperationLog')}</Button>,
+      key: '5',
+    },
   ];
   const handleMenuClick = (key, record) => {
     switch (key) {
@@ -250,10 +294,122 @@ const Comp: React.FC = () => {
       case '4':
         openHeartModal(record);
         break;
+      case '5':
+        openOperationLogModal(record);
+        break;
       default:
         break;
     }
   };
+  const agentInstallerList = useRef([]);
+  const [agentVersionObj, setAgentVersionObj] = useState({});
+  useEffect(() => {
+    (() => {
+      getModuleList().then(res => {
+        agentInstallerList.current = res?.list;
+        setAgentVersionObj(versionMap(res?.list));
+        console.log(agentInstallerList.current, agentVersionObj);
+      });
+    })();
+  }, [type]);
+  const onOpenAgentModal = () => {
+    setAgentStatusModal({ open: true });
+  };
+  const [nodeList, setNodeList] = useState([]);
+  const statusPagination = {
+    total: nodeList?.length,
+  };
+
+  const [isSmall, setIsSmall] = useState(window.innerWidth < 1600);
+
+  useEffect(() => {
+    const handleResize = () => {
+      console.log('window.innerWidth', window.innerWidth);
+      setIsSmall(window.innerWidth < 1600);
+    };
+
+    window.addEventListener('resize', handleResize);
+    return () => {
+      window.removeEventListener('resize', handleResize);
+    };
+  }, []);
+
+  const getOperationMenu = useMemo(() => {
+    return isSmall
+      ? [
+          {
+            title: i18n.t('basic.Operating'),
+            dataIndex: 'action',
+            key: 'operation',
+            width: isSmall ? 200 : 400,
+            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>
+                {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>
+                )}
+              </>
+            ),
+          },
+        ]
+      : [
+          {
+            title: i18n.t('basic.Operating'),
+            dataIndex: 'action',
+            key: 'operation',
+            width: isSmall ? 200 : 400,
+            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>
+                <Button
+                  type="link"
+                  onClick={() => getNodeData(record.id).then(() => 
setOperationType('onInstall'))}
+                >
+                  {i18n.t('pages.Cluster.Node.Install')}
+                </Button>
+                <Button
+                  type="link"
+                  onClick={() => getNodeData(record.id).then(() => 
setOperationType('onRestart'))}
+                >
+                  {i18n.t('pages.Nodes.Restart')}
+                </Button>
+                <Button
+                  type="link"
+                  onClick={() => getNodeData(record.id).then(() => 
setOperationType('onUnload'))}
+                >
+                  {i18n.t('pages.Cluster.Node.Unload')}
+                </Button>
+                <Button type="link" onClick={() => onLog(record)}>
+                  {i18n.t('pages.Cluster.Node.InstallLog')}
+                </Button>
+                <Button type="link" onClick={() => openHeartModal(record)}>
+                  {i18n.t('pages.Clusters.Node.Agent.HeartbeatDetection')}
+                </Button>
+                <Button type="link" onClick={() => 
openOperationLogModal(record)}>
+                  {i18n.t('pages.GroupDetail.OperationLog')}
+                </Button>
+              </>
+            ),
+          },
+        ];
+  }, [isSmall]);
   const columns = useMemo(() => {
     return [
       {
@@ -278,6 +434,14 @@ const Comp: React.FC = () => {
         dataIndex: 'status',
         render: text => genStatusTag(text),
       },
+      {
+        title: i18n.t('pages.Clusters.Node.Agent.Version'),
+        dataIndex: 'moduleIdList',
+        render: (text, record) => {
+          const index = text.slice(0, 1)[0];
+          return agentVersionObj[index];
+        },
+      },
       {
         title: i18n.t('pages.Clusters.Node.Creator'),
         dataIndex: 'creator',
@@ -298,35 +462,138 @@ const Comp: React.FC = () => {
           </>
         ),
       },
-      {
-        title: i18n.t('basic.Operating'),
-        dataIndex: 'action',
-        key: 'operation',
-        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>
-            {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>
-            )}
-          </>
-        ),
-      },
-    ];
-  }, [onDelete]);
+    ].concat(getOperationMenu);
+  }, [onDelete, onInstall, onUnload, type, agentVersionObj, isSmall]);
+
+  const [disabled, setDisabled] = useState(true);
+  const finalStatus = useRef(false);
+  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+  const [statusAgentList, setStatusAgentList] = useState([]);
+  const [batchUpdateArgs, setBatchUpdateArgs] = useState({
+    map: [],
+    interval: 0,
+    ids: [],
+    operationType: 0,
+    submitDataList: [],
+  });
+  const getBatchUpdateArgs = args => {
+    setBatchUpdateArgs(args);
+  };
+  const rowSelection = {
+    selectedRowKeys,
+    onChange: (selectedRowKeys, selectedRows) => {
+      setSelectedRowKeys(selectedRowKeys);
+      setDisabled(selectedRowKeys.length <= 0);
+      setNodeList(selectedRows);
+    },
+  };
+  const onUpdate = async submitData =>
+    await request({
+      url: '/cluster/node/update',
+      method: 'POST',
+      data: submitData,
+    });
+  const getAgentNode = async id =>
+    await request({
+      url: `/cluster/node/get/${id}`,
+      method: 'GET',
+    });
+  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
+  const batchUpdate = async () => {
+    const { map, interval, submitDataList } = batchUpdateArgs;
+    setStatusAgentList(submitDataList);
+    const entries = Array.from(map.entries());
+    for (let i = 0; i < entries.length; i++) {
+      const [key, value] = entries[i];
+      for (let j = 0; j < value.length; j++) {
+        const agentNode = await getAgentNode(value[j]?.id);
+        if (i === entries.length - 1 && j === value.length - 1) {
+          onUpdate({
+            ...value[j],
+            version: agentNode.version,
+            agentRestartTime:
+              batchUpdateArgs.operationType === 3
+                ? value[j].agentRestartTime + 1
+                : value[j].agentRestartTime,
+          });
+          await delay(1000);
+          await closeStatusModal();
+          setStatusAgentList([]);
+        } else {
+          onUpdate({
+            ...value[j],
+            version: agentNode.version,
+            agentRestartTime:
+              batchUpdateArgs.operationType === 3
+                ? value[j].agentRestartTime + 1
+                : value[j].agentRestartTime,
+          });
+        }
+      }
+      if (i < entries.length - 1) {
+        finalStatus.current = false;
+        await delay(interval * 60 * 1000);
+      }
+    }
+    new Promise(resolve => {
+      finalStatus.current = true;
+    });
+  };
+  const getStatusAgentList = option => {
+    const { ids } = batchUpdateArgs;
+    request({
+      url: '/cluster/node/list',
+      method: 'POST',
+      data: { ...option },
+    }).then(response => {
+      const list = response?.list.filter(item => ids.includes(item.id));
+      setStatusAgentList(list);
+    });
+  };
+
+  useEffect(() => {
+    batchUpdate();
+  }, [batchUpdateArgs]);
+  const [agentStatusModal, setAgentStatusModal] = useState({
+    open: false,
+  });
+
+  useEffect(() => {
+    const internalId = setInterval(() => {
+      if (agentStatusModal.open) {
+        getStatusAgentList({
+          pageSize: 99999,
+          pageNum: 1,
+          type: 'AGENT',
+          parentId: +clusterId,
+        });
+      }
+    }, 10000);
+    return () => {
+      clearInterval(internalId);
+    };
+  }, [agentStatusModal.open]);
 
+  const closeStatusModal = async () => {
+    const completeList = statusAgentList.filter(item => item.status !== 2);
+    if (completeList.length === statusAgentList.length && finalStatus.current) 
{
+      setStatusAgentList([]);
+    }
+    setNodeList([]);
+    setSelectedRowKeys([]);
+    setDisabled(true);
+    await getList();
+    setAgentStatusModal({ open: false });
+  };
+  const tableProps = { rowSelection: {} };
+  if (type === 'AGENT') {
+    tableProps.rowSelection = {
+      type: 'checkbox',
+      ...rowSelection,
+    };
+  } else {
+    delete tableProps.rowSelection;
+  }
   return (
     <PageContainer
       breadcrumb={[
@@ -340,23 +607,63 @@ const Comp: React.FC = () => {
       <HighTable
         filterForm={{
           content: getFilterFormContent(options),
+          form: form,
           onFilter,
         }}
         suffix={
-          <Button type="primary" onClick={() => setNodeEditModal({ open: true 
})}>
-            {i18n.t('pages.Clusters.Node.Create')}
-          </Button>
+          <>
+            {type === 'AGENT' && (
+              <Button
+                type="primary"
+                disabled={disabled}
+                onClick={() =>
+                  setAgentBatchUpdateModal({
+                    open: true,
+                    agentList: nodeList,
+                    openStatusModal: onOpenAgentModal,
+                    getArgs: getBatchUpdateArgs,
+                  })
+                }
+              >
+                {i18n.t('pages.Clusters.Node.BatchUpdate')}
+              </Button>
+            )}
+            {statusAgentList.length > 0 && (
+              <Button
+                type="primary"
+                onClick={() =>
+                  setAgentStatusModal({
+                    open: true,
+                  })
+                }
+              >
+                {i18n.t('pages.Clusters.Node.OperationStatusQuery')}
+              </Button>
+            )}
+            <Button
+              type="primary"
+              onClick={() =>
+                setNodeEditModal({ open: true, agentInstallerList: 
agentInstallerList.current })
+              }
+            >
+              {i18n.t('pages.Clusters.Node.Create')}
+            </Button>
+          </>
         }
         table={{
+          ...tableProps,
           columns:
             type === 'AGENT'
               ? columns.filter(
                   item =>
                     item.dataIndex !== 'enabledOnline' &&
                     item.dataIndex !== 'port' &&
-                    item.dataIndex !== 'protocolType',
+                    item.dataIndex !== 'protocolType' &&
+                    item.dataIndex !== 'modifier',
                 )
-              : columns,
+              : columns.filter(
+                  item => item.dataIndex !== 'moduleIdList' && item.dataIndex 
!== 'installer',
+                ),
           rowKey: 'id',
           dataSource: data?.list,
           pagination,
@@ -388,12 +695,54 @@ const Comp: React.FC = () => {
       <HeartBeatModal
         {...heartModal}
         open={heartModal.open as boolean}
-        onOk={async () => {
-          await getList();
-          setHeartModal({ open: false });
-        }}
         onCancel={() => setHeartModal({ open: false })}
       />
+      <AgentBatchUpdateModal
+        {...agentBatchUpdateModal}
+        open={agentBatchUpdateModal.open as boolean}
+        onOk={() => {
+          setAgentBatchUpdateModal({ open: false });
+        }}
+        onCancel={() => setAgentBatchUpdateModal({ open: false })}
+      />
+
+      <OperationLogModal
+        {...operationLogModal}
+        onOk={() => {
+          setOperationLogModal({ open: false });
+        }}
+        onCancel={() => setOperationLogModal({ open: false })}
+      ></OperationLogModal>
+      <Modal
+        open={agentStatusModal.open}
+        title={i18n.t('basic.Status')}
+        width={1400}
+        footer={[
+          <Button
+            key="cancel"
+            onClick={async () => {
+              await closeStatusModal();
+            }}
+          >
+            {i18n.t('pages.GroupDetail.Stream.Closed')}
+          </Button>,
+        ]}
+      >
+        <HighTable
+          table={{
+            columns: columns.filter(
+              item =>
+                item.dataIndex !== 'action' &&
+                item.dataIndex !== 'enabledOnline' &&
+                item.dataIndex !== 'port' &&
+                item.dataIndex !== 'protocolType',
+            ),
+            rowKey: 'id',
+            dataSource: statusAgentList,
+            pagination: statusPagination,
+          }}
+        />
+      </Modal>
     </PageContainer>
   );
 };
diff --git a/inlong-dashboard/src/ui/pages/Clusters/OperationLogModal.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/OperationLogModal.tsx
new file mode 100644
index 0000000000..274fe01c4a
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/Clusters/OperationLogModal.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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, { useEffect, useMemo, useState } from 'react';
+import { Button, message, Modal } from 'antd';
+import i18n from '@/i18n';
+import { ModalProps } from 'antd/es/modal';
+import { defaultSize } from '@/configs/pagination';
+import { useRequest } from '@/ui/hooks';
+import HighTable from '@/ui/components/HighTable';
+import { getFormContent, getTableColumns } from '@/ui/pages/Clusters/config';
+export interface Props extends ModalProps {
+  ip?: string;
+  operationType?: string;
+}
+
+const Comp: React.FC<Props> = ({ ...modalProps }) => {
+  const [options, setOptions] = useState({
+    pageSize: defaultSize,
+    pageNum: 1,
+  });
+
+  const { data: sourceData, run } = useRequest(
+    {
+      url: '/operationLog/list',
+      method: 'POST',
+      data: {
+        ...options,
+        ip: modalProps.ip,
+        operationTarget: 'CLUSTER_NODE',
+      },
+    },
+    {
+      manual: true,
+    },
+  );
+
+  const pagination = {
+    pageSize: options.pageSize,
+    current: options.pageNum,
+    total: sourceData?.total,
+  };
+  const onChange = ({ current: pageNum, pageSize }) => {
+    setOptions(prev => ({
+      ...prev,
+      pageNum,
+      pageSize,
+    }));
+  };
+
+  const onFilter = allValues => {
+    console.log(allValues);
+    setOptions(prev => ({
+      ...prev,
+      ...allValues,
+      pageNum: 1,
+    }));
+  };
+
+  useEffect(() => {
+    if (modalProps.open) {
+      run();
+    }
+  }, [modalProps.open, options]);
+
+  return (
+    <Modal {...modalProps} title={i18n.t('操作日志')} width={1200}>
+      <HighTable
+        filterForm={{
+          content: getFormContent(),
+          onFilter,
+        }}
+        table={{
+          columns: getTableColumns,
+          rowKey: 'id',
+          dataSource: sourceData?.list || [],
+          pagination,
+          onChange,
+        }}
+      />
+    </Modal>
+  );
+};
+
+export default Comp;
diff --git a/inlong-dashboard/src/ui/pages/Clusters/config.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/config.tsx
new file mode 100644
index 0000000000..82c1689cdc
--- /dev/null
+++ b/inlong-dashboard/src/ui/pages/Clusters/config.tsx
@@ -0,0 +1,130 @@
+/*
+ * 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 request from '@/core/utils/request';
+import i18n from '@/i18n';
+import { Tooltip } from 'antd';
+import { dateFormat } from '@/core/utils';
+import React from 'react';
+
+export const getModuleList = async () => {
+  return await request({
+    url: '/module/list',
+    method: 'POST',
+    data: {
+      pageNum: 1,
+      pageSize: 9999,
+    },
+  });
+};
+
+export const versionMap = data => {
+  return data
+    ?.filter(item => item.type === 'AGENT')
+    .map(item => ({
+      ...item,
+      label: `${item.name} ${item.version}`,
+      value: item.id,
+    }))
+    .reduce(
+      (acc, cur) => ({
+        ...acc,
+        [cur.value]: cur.label,
+      }),
+      {},
+    );
+};
+
+export const installerMap = () => {
+  return getModuleList().then(res =>
+    res?.list
+      ?.filter(item => item.type === 'INSTALLER')
+      .map(item => ({
+        ...item,
+        label: `${item.name} ${item.version}`,
+        value: item.id,
+      }))
+      .reduce(
+        (acc, cur) => ({
+          ...acc,
+          [cur.value]: cur.label,
+        }),
+        {},
+      ),
+  );
+};
+
+const typeList = [
+  {
+    label: 'Create',
+    value: 'CREATE',
+  },
+  {
+    label: 'Update',
+    value: 'UPDATE',
+  },
+  {
+    label: 'Delete',
+    value: 'DELETE',
+  },
+  {
+    label: 'Get',
+    value: 'GET',
+  },
+];
+
+export const getFormContent = () => [
+  {
+    type: 'select',
+    label: i18n.t('pages.GroupDetail.OperationLog.OperationType'),
+    name: 'operationType',
+    props: {
+      allowClear: true,
+      dropdownMatchSelectWidth: false,
+      options: typeList,
+    },
+  },
+];
+
+export const getTableColumns = [
+  {
+    title: i18n.t('pages.GroupDetail.OperationLog.Table.Operator'),
+    dataIndex: 'operator',
+  },
+  {
+    title: i18n.t('pages.GroupDetail.OperationLog.Table.OperationType'),
+    dataIndex: 'operationType',
+    render: text => typeList.find(c => c.value === text)?.label || text,
+  },
+  {
+    title: i18n.t('pages.GroupDetail.OperationLog.Table.Log'),
+    dataIndex: 'body',
+    ellipsis: true,
+    render: body => (
+      <Tooltip placement="topLeft" title={body}>
+        {body}
+      </Tooltip>
+    ),
+  },
+  {
+    title: i18n.t('pages.GroupDetail.OperationLog.Table.OperationTime'),
+    dataIndex: 'requestTime',
+    render: text => dateFormat(new Date(text)),
+  },
+];
diff --git a/inlong-dashboard/src/ui/pages/Clusters/index.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/index.tsx
index d5ffea25ec..a42f5052db 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/index.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/index.tsx
@@ -169,7 +169,7 @@ const Comp: React.FC = () => {
                   {i18n.t('pages.Clusters.Node.Name')}
                 </Link>
               )}
-              {record.type !== 'DATAPROXY' && record.type !== 'AGENT' && (
+              {record.type !== 'DATAPROXY' && (
                 <Button type="link" onClick={() => onEdit(record)}>
                   {i18n.t('basic.Edit')}
                 </Button>
diff --git a/inlong-dashboard/src/ui/pages/Clusters/status.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/status.tsx
index afeaf4e2bb..434916a271 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/status.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/status.tsx
@@ -39,6 +39,21 @@ export const statusList: StatusProp[] = [
     value: 2,
     type: 'error',
   },
+  {
+    label: i18n.t('pages.Clusters.Node.Status.INSTALLING'),
+    value: 3,
+    type: 'primary',
+  },
+  {
+    label: i18n.t('pages.Clusters.Node.Status.INSTALLFAILED'),
+    value: 4,
+    type: 'error',
+  },
+  {
+    label: i18n.t('pages.Clusters.Node.Status.INSTALLSUCCESS'),
+    value: 5,
+    type: 'success',
+  },
 ];
 
 export const statusMap = statusList.reduce(

Reply via email to