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> ); };