|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | + * contributor license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright ownership. |
| 5 | + * The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | + * (the "License"); you may not use this file except in compliance with |
| 7 | + * the License. You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | + |
| 18 | +import React from 'react'; |
| 19 | +import { Modal, Button, Typography, Descriptions, Tag, Spin, notification } from 'antd'; |
| 20 | +import moment from 'moment'; |
| 21 | +import { ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons'; |
| 22 | +import { useLanguage } from '../i18n/LanguageContext'; |
| 23 | +import { remoteApi } from '../api/remoteApi/remoteApi'; // 确保这个路径正确 |
| 24 | + |
| 25 | +const { Text, Paragraph } = Typography; |
| 26 | + |
| 27 | +const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResendMessage }) => { |
| 28 | + const { t } = useLanguage(); |
| 29 | + const [loading, setLoading] = React.useState(true); |
| 30 | + const [messageDetail, setMessageDetail] = React.useState(null); |
| 31 | + const [error, setError] = React.useState(null); |
| 32 | + |
| 33 | + React.useEffect(() => { |
| 34 | + const fetchMessageDetail = async () => { |
| 35 | + // 只有当 visible 为 true 且 messageId 和 topic 存在时才进行数据请求 |
| 36 | + if (!visible || !messageId || !topic) { |
| 37 | + // 如果 Modal 不可见或者必要参数缺失,则不加载数据 |
| 38 | + setMessageDetail(null); // 清空旧数据 |
| 39 | + setError(null); // 清空错误信息 |
| 40 | + setLoading(false); // 停止加载状态 |
| 41 | + return; |
| 42 | + } |
| 43 | + |
| 44 | + setLoading(true); |
| 45 | + setError(null); // 在每次新的请求前清除之前的错误 |
| 46 | + try { |
| 47 | + const resp = await remoteApi.viewMessage(messageId, topic); |
| 48 | + if (resp.status === 0) { |
| 49 | + setMessageDetail(resp.data); |
| 50 | + } else { |
| 51 | + const errorMessage = resp.errMsg || t.FETCH_MESSAGE_DETAIL_FAILED; |
| 52 | + setError(errorMessage); |
| 53 | + notification.error({ |
| 54 | + message: t.ERROR, |
| 55 | + description: errorMessage, |
| 56 | + }); |
| 57 | + } |
| 58 | + } catch (err) { |
| 59 | + const errorMessage = t.FETCH_MESSAGE_DETAIL_FAILED; |
| 60 | + setError(errorMessage); |
| 61 | + notification.error({ |
| 62 | + message: t.ERROR, |
| 63 | + description: errorMessage, |
| 64 | + }); |
| 65 | + console.error("Error fetching message detail:", err); |
| 66 | + } finally { |
| 67 | + setLoading(false); |
| 68 | + } |
| 69 | + }; |
| 70 | + |
| 71 | + fetchMessageDetail(); |
| 72 | + }, [visible, messageId, topic, t]); // 依赖项中添加 visible,确保在 Modal 显示时触发 |
| 73 | + |
| 74 | + |
| 75 | + // handleShowExceptionDesc 方法不再需要,因为我们直接使用 Paragraph 的 ellipsis |
| 76 | + |
| 77 | + return ( |
| 78 | + <Modal |
| 79 | + title={t.MESSAGE_DETAIL} |
| 80 | + open={visible} // Ant Design 5.x 版本中,visible 属性已更名为 open |
| 81 | + onCancel={onCancel} |
| 82 | + footer={[ |
| 83 | + <Button key="close" onClick={onCancel}> |
| 84 | + {t.CLOSE} |
| 85 | + </Button>, |
| 86 | + ]} |
| 87 | + width={900} |
| 88 | + destroyOnHidden={true} // 使用新的 destroyOnHidden 替代 destroyOnClose |
| 89 | + > |
| 90 | + <Spin spinning={loading} tip={t.LOADING}> |
| 91 | + {error && ( |
| 92 | + <Paragraph type="danger" style={{ textAlign: 'center' }}> |
| 93 | + {error} |
| 94 | + </Paragraph> |
| 95 | + )} |
| 96 | + {messageDetail ? ( // 确保 messageDetail 存在时才渲染内容 |
| 97 | + <> |
| 98 | + {/* 消息信息部分 */} |
| 99 | + <Descriptions title={<Text strong>{t.MESSAGE_INFO}</Text>} bordered column={2} size="small" style={{ marginBottom: 20 }}> |
| 100 | + <Descriptions.Item label="Topic" span={2}><Text copyable>{messageDetail.messageView.topic}</Text></Descriptions.Item> |
| 101 | + <Descriptions.Item label="Message ID" span={2}><Text copyable>{messageDetail.messageView.msgId}</Text></Descriptions.Item> |
| 102 | + <Descriptions.Item label="StoreHost">{messageDetail.messageView.storeHost}</Descriptions.Item> |
| 103 | + <Descriptions.Item label="BornHost">{messageDetail.messageView.bornHost}</Descriptions.Item> |
| 104 | + <Descriptions.Item label="StoreTime"> |
| 105 | + {moment(messageDetail.messageView.storeTimestamp).format("YYYY-MM-DD HH:mm:ss")} |
| 106 | + </Descriptions.Item> |
| 107 | + <Descriptions.Item label="BornTime"> |
| 108 | + {moment(messageDetail.messageView.bornTimestamp).format("YYYY-MM-DD HH:mm:ss")} |
| 109 | + </Descriptions.Item> |
| 110 | + <Descriptions.Item label="Queue ID">{messageDetail.messageView.queueId}</Descriptions.Item> |
| 111 | + <Descriptions.Item label="Queue Offset">{messageDetail.messageView.queueOffset}</Descriptions.Item> |
| 112 | + <Descriptions.Item label="StoreSize">{messageDetail.messageView.storeSize} bytes</Descriptions.Item> |
| 113 | + <Descriptions.Item label="ReconsumeTimes">{messageDetail.messageView.reconsumeTimes}</Descriptions.Item> |
| 114 | + <Descriptions.Item label="BodyCRC">{messageDetail.messageView.bodyCRC}</Descriptions.Item> |
| 115 | + <Descriptions.Item label="SysFlag">{messageDetail.messageView.sysFlag}</Descriptions.Item> |
| 116 | + <Descriptions.Item label="Flag">{messageDetail.messageView.flag}</Descriptions.Item> |
| 117 | + <Descriptions.Item label="PreparedTransactionOffset">{messageDetail.messageView.preparedTransactionOffset}</Descriptions.Item> |
| 118 | + </Descriptions> |
| 119 | + |
| 120 | + {/* 消息属性部分 */} |
| 121 | + {Object.keys(messageDetail.messageView.properties).length > 0 && ( |
| 122 | + <Descriptions title={<Text strong>{t.MESSAGE_PROPERTIES}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}> |
| 123 | + {Object.entries(messageDetail.messageView.properties).map(([key, value]) => ( |
| 124 | + <Descriptions.Item label={key} key={key}><Text copyable>{value}</Text></Descriptions.Item> |
| 125 | + ))} |
| 126 | + </Descriptions> |
| 127 | + )} |
| 128 | + |
| 129 | + {/* 消息体部分 */} |
| 130 | + <Descriptions title={<Text strong>{t.MESSAGE_BODY}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}> |
| 131 | + <Descriptions.Item> |
| 132 | + <Paragraph |
| 133 | + copyable |
| 134 | + ellipsis={{ |
| 135 | + rows: 5, |
| 136 | + expandable: true, |
| 137 | + symbol: t.SHOW_ALL_CONTENT, |
| 138 | + }} |
| 139 | + > |
| 140 | + {messageDetail.messageView.messageBody} |
| 141 | + </Paragraph> |
| 142 | + </Descriptions.Item> |
| 143 | + </Descriptions> |
| 144 | + |
| 145 | + {/* 消息轨迹列表部分 */} |
| 146 | + {messageDetail.messageTrackList && messageDetail.messageTrackList.length > 0 ? ( |
| 147 | + <> |
| 148 | + <Text strong>{t.MESSAGE_TRACKING}</Text> |
| 149 | + <div style={{ marginTop: 10 }}> |
| 150 | + {messageDetail.messageTrackList.map((track, index) => ( |
| 151 | + <Descriptions bordered column={1} size="small" key={index} style={{ marginBottom: 15 }}> |
| 152 | + <Descriptions.Item label={t.CONSUMER_GROUP}> |
| 153 | + {track.consumerGroup} |
| 154 | + </Descriptions.Item> |
| 155 | + <Descriptions.Item label={t.TRACK_TYPE}> |
| 156 | + <Tag color={ |
| 157 | + track.trackType === 'CONSUMED_SOME_TIME_OK' ? 'success' : |
| 158 | + track.trackType === 'NOT_ONLINE' ? 'default' : |
| 159 | + track.trackType === 'PULL_SUCCESS' ? 'processing' : |
| 160 | + track.trackType === 'NO_MATCHED_CONSUMER' ? 'warning' : |
| 161 | + 'error' |
| 162 | + }> |
| 163 | + {track.trackType} |
| 164 | + </Tag> |
| 165 | + </Descriptions.Item> |
| 166 | + <Descriptions.Item label={t.OPERATION}> |
| 167 | + <Button |
| 168 | + icon={<SyncOutlined />} |
| 169 | + onClick={() => onResendMessage(messageDetail.messageView, track.consumerGroup)} |
| 170 | + size="small" |
| 171 | + style={{ marginRight: 8 }} |
| 172 | + > |
| 173 | + {t.RESEND_MESSAGE} |
| 174 | + </Button> |
| 175 | + {/* 移除“查看异常”按钮,因为现在直接在下方展示可展开内容 */} |
| 176 | + </Descriptions.Item> |
| 177 | + {track.exceptionDesc && ( |
| 178 | + <Descriptions.Item label={t.EXCEPTION_SUMMARY}> |
| 179 | + {/* 异常信息截断显示,点击“查看更多”可展开 */} |
| 180 | + <Paragraph |
| 181 | + ellipsis={{ |
| 182 | + rows: 2, // 默认显示2行 |
| 183 | + expandable: true, |
| 184 | + symbol: <Text style={{ color: '#1890ff', cursor: 'pointer' }}>{t.READ_MORE}</Text>, // 蓝色展开文本 |
| 185 | + }} |
| 186 | + > |
| 187 | + {track.exceptionDesc} |
| 188 | + </Paragraph> |
| 189 | + </Descriptions.Item> |
| 190 | + )} |
| 191 | + </Descriptions> |
| 192 | + ))} |
| 193 | + </div> |
| 194 | + </> |
| 195 | + ) : ( |
| 196 | + <Paragraph>{t.NO_TRACKING_INFO}</Paragraph> |
| 197 | + )} |
| 198 | + </> |
| 199 | + ) : ( |
| 200 | + // 当 messageDetail 为 null 时,可以显示一个占位符或者不显示内容 |
| 201 | + !loading && !error && <Paragraph style={{ textAlign: 'center' }}>{t.NO_MESSAGE_DETAIL_AVAILABLE}</Paragraph> |
| 202 | + )} |
| 203 | + </Spin> |
| 204 | + </Modal> |
| 205 | + ); |
| 206 | +}; |
| 207 | + |
| 208 | +export default MessageDetailViewDialog; |
0 commit comments