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 <165994047+wohainilao...@users.noreply.github.com>
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>
   );
 };

Reply via email to