diff --git a/inlong-dashboard/src/ui/locales/cn.json b/inlong-dashboard/src/ui/locales/cn.json index 23f83e31aa6..778c687270e 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 89f3af4be20..8bb5c15ab5f 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 00000000000..e56a8cb7e39 --- /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 = ({ 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) => 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 ( + modalProps.onCancel(e)}> + {i18n.t('basic.Cancel')} + , + , + ]} + > + + + ); +}; + +export default Comp; diff --git a/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx b/inlong-dashboard/src/ui/pages/Clusters/HeartBeatModal.tsx index 50427f51829..3895f1f9862 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 = ({ type, ip, ...modalProps }) => { +const Comp: React.FC = ({ ...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 = ({ 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 = ({ type, ip, ...modalProps }) => { table={{ columns: columns, rowKey: 'id', - dataSource: heartList?.list, + dataSource: heartList?.list || [], pagination, + onChange, }} /> diff --git a/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx b/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx index 80b7355120d..5a38817553d 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 ( = ({ id, type, clusterId, ...m ), ]} > - + ); }; diff --git a/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx b/inlong-dashboard/src/ui/pages/Clusters/NodeManage.tsx index cb85043cc56..7e2a5587921 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) || {}, [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>({ open: false, + agentInstallerList: [], }); const [logModal, setLogModal] = useState>({ open: false, @@ -65,6 +79,12 @@ const Comp: React.FC = () => { const [heartModal, setHeartModal] = useState>({ open: false, }); + const [operationLogModal, setOperationLogModal] = useState>({ + open: false, + }); + const [agentBatchUpdateModal, setAgentBatchUpdateModal] = useState>({ + 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: , key: '4', }, + { + label: , + 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) => ( + <> + + + {type === 'AGENT' && ( + handleMenuClick(key, record) }}> + e.preventDefault()}> + + {i18n.t('pages.Cluster.Node.More')} + + + + + )} + + ), + }, + ] + : [ + { + title: i18n.t('basic.Operating'), + dataIndex: 'action', + key: 'operation', + width: isSmall ? 200 : 400, + render: (text, record) => ( + <> + + + + + + + + + + ), + }, + ]; + }, [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) => ( - <> - - - {type === 'AGENT' && ( - handleMenuClick(key, record) }}> - e.preventDefault()}> - - {i18n.t('pages.Cluster.Node.More')} - - - - - )} - - ), - }, - ]; - }, [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 ( { setNodeEditModal({ open: true })}> - {i18n.t('pages.Clusters.Node.Create')} - + <> + {type === 'AGENT' && ( + + )} + {statusAgentList.length > 0 && ( + + )} + + } 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 = () => { { - await getList(); - setHeartModal({ open: false }); - }} onCancel={() => setHeartModal({ open: false })} /> + { + setAgentBatchUpdateModal({ open: false }); + }} + onCancel={() => setAgentBatchUpdateModal({ open: false })} + /> + + { + setOperationLogModal({ open: false }); + }} + onCancel={() => setOperationLogModal({ open: false })} + > + { + await closeStatusModal(); + }} + > + {i18n.t('pages.GroupDetail.Stream.Closed')} + , + ]} + > + + item.dataIndex !== 'action' && + item.dataIndex !== 'enabledOnline' && + item.dataIndex !== 'port' && + item.dataIndex !== 'protocolType', + ), + rowKey: 'id', + dataSource: statusAgentList, + pagination: statusPagination, + }} + /> + ); }; 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 00000000000..274fe01c4af --- /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 = ({ ...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 ( + + + + ); +}; + +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 00000000000..82c1689cdcb --- /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 => ( + + {body} + + ), + }, + { + 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 d5ffea25ec3..a42f5052db6 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')} )} - {record.type !== 'DATAPROXY' && record.type !== 'AGENT' && ( + {record.type !== 'DATAPROXY' && ( diff --git a/inlong-dashboard/src/ui/pages/Clusters/status.tsx b/inlong-dashboard/src/ui/pages/Clusters/status.tsx index afeaf4e2bbf..434916a2717 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(