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(