diff --git a/frontend-new/src/assets/styles/theme.js b/frontend-new/src/assets/styles/theme.js
new file mode 100644
index 0000000..d900978
--- /dev/null
+++ b/frontend-new/src/assets/styles/theme.js
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+export const defaultTheme = {
+ token: {
+ colorPrimary: '#0cb5fb', // 主题色
+ borderRadius: 1.5, // 组件圆角
+ },
+ components: {
+ Button: {
+ colorPrimary: '#1c324a', // 普通按钮主题色
+ },
+ Layout: {
+ headerBg: '#1c324a', // 设置 Header 的背景色为 #1c324a
+ headerColor: '#ffffff', // 设置 Header 内文本颜色为白色
+ backgroundColor: '#ffffff', // 设置 Layout 的背景色为白色
+ colorBgLayout: '#f9fcfe',
+ },
+ Menu: {
+ darkItemBg: '#1c324a',
+ horizontalItemSelectedBg: '#0cb5fb',
+ itemSelectedColor: '#ffffff',
+ itemColor: '#ffffff',
+ colorText: 'rgba(255, 255, 255, 0.88)', // Adjust for dark theme menu
+ activeBarBorderWidth: 0,
+ },
+ Drawer: {
+ colorBgElevated: '#1c324a', // Drawer 背景色
+ },
+ },
+};
+
+export const pinkTheme = {
+ token: {
+ colorPrimary: '#FF69B4', // 热粉色
+ borderRadius: 1.5,
+ },
+ components: {
+ Button: {
+ colorPrimary: '#FFC0CB', // 深粉色
+ },
+ Layout: {
+ headerBg: '#FFC0CB', // 粉色
+ headerColor: '#000000', // 黑色文本
+ backgroundColor: '#F8F8FF', // 幽灵白
+ colorBgLayout: '#faf4f4',
+ },
+ Menu: {
+ darkItemBg: '#FFC0CB', // 粉色
+ horizontalItemSelectedBg: '#FF69B4',
+ itemSelectedColor: '#ffffff',
+ itemColor: '#000000', // 黑色文本
+ colorText: 'rgba(0, 0, 0, 0.88)',
+ activeBarBorderWidth: 0,
+ },
+ Drawer: {
+ colorBgElevated: '#FFC0CB', // Drawer 背景色
+ },
+ },
+};
+
+export const greenTheme = {
+ token: {
+ colorPrimary: '#52c41a', // 绿色
+ borderRadius: 1.5,
+ },
+ components: {
+ Button: {
+ colorPrimary: '#7cb305', // 橄榄绿
+ },
+ Layout: {
+ headerBg: '#3f673f', // 深绿色
+ headerColor: '#ffffff', // 白色文本
+ backgroundColor: '#f6ffed',
+ colorBgLayout: '#ebf8eb',
+ },
+ Menu: {
+ darkItemBg: '#3f673f', // 深绿色
+ horizontalItemSelectedBg: '#52c41a',
+ itemSelectedColor: '#ffffff',
+ itemColor: '#ffffff',
+ colorText: 'rgba(255, 255, 255, 0.88)',
+ activeBarBorderWidth: 0,
+ },
+ Drawer: {
+ colorBgElevated: '#3f673f', // Drawer 背景色
+ },
+ },
+};
+
+export const themes = {
+ default: defaultTheme,
+ pink: pinkTheme,
+ green: greenTheme,
+};
diff --git a/frontend-new/src/components/DlqMessageDetailViewDialog.jsx b/frontend-new/src/components/DlqMessageDetailViewDialog.jsx
new file mode 100644
index 0000000..cb109cd
--- /dev/null
+++ b/frontend-new/src/components/DlqMessageDetailViewDialog.jsx
@@ -0,0 +1,73 @@
+/*
+ * 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 from 'react';
+import { Form, Input, Typography, Modal } from 'antd';
+import moment from 'moment';
+import { useLanguage } from '../i18n/LanguageContext'; // 根据实际路径调整
+
+const { Text } = Typography;
+
+const DlqMessageDetailViewDialog = ({ ngDialogData }) => {
+ const { t } = useLanguage();
+
+ const messageView = ngDialogData?.messageView || {};
+
+ return (
+
+
+ {messageView.msgId}
+
+
+ {messageView.topic}
+
+
+
+
+
+ {messageView.reconsumeTimes}
+
+
+ {messageView.properties?.TAGS}
+
+
+ {messageView.properties?.KEYS}
+
+
+ {moment(messageView.storeTimestamp).format('YYYY-MM-DD HH:mm:ss')}
+
+
+ {messageView.storeHost}
+
+
+
+
+
+
+ );
+};
+
+export default DlqMessageDetailViewDialog;
diff --git a/frontend-new/src/components/MessageDetailViewDialog.jsx b/frontend-new/src/components/MessageDetailViewDialog.jsx
new file mode 100644
index 0000000..09c72d4
--- /dev/null
+++ b/frontend-new/src/components/MessageDetailViewDialog.jsx
@@ -0,0 +1,208 @@
+/*
+ * 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 from 'react';
+import { Modal, Button, Typography, Descriptions, Tag, Spin, notification } from 'antd';
+import moment from 'moment';
+import { ExclamationCircleOutlined, SyncOutlined } from '@ant-design/icons';
+import { useLanguage } from '../i18n/LanguageContext';
+import { remoteApi } from '../api/remoteApi/remoteApi'; // 确保这个路径正确
+
+const { Text, Paragraph } = Typography;
+
+const MessageDetailViewDialog = ({ visible, onCancel, messageId, topic, onResendMessage }) => {
+ const { t } = useLanguage();
+ const [loading, setLoading] = React.useState(true);
+ const [messageDetail, setMessageDetail] = React.useState(null);
+ const [error, setError] = React.useState(null);
+
+ React.useEffect(() => {
+ const fetchMessageDetail = async () => {
+ // 只有当 visible 为 true 且 messageId 和 topic 存在时才进行数据请求
+ if (!visible || !messageId || !topic) {
+ // 如果 Modal 不可见或者必要参数缺失,则不加载数据
+ setMessageDetail(null); // 清空旧数据
+ setError(null); // 清空错误信息
+ setLoading(false); // 停止加载状态
+ return;
+ }
+
+ setLoading(true);
+ setError(null); // 在每次新的请求前清除之前的错误
+ try {
+ const resp = await remoteApi.viewMessage(messageId, topic);
+ if (resp.status === 0) {
+ setMessageDetail(resp.data);
+ } else {
+ const errorMessage = resp.errMsg || t.FETCH_MESSAGE_DETAIL_FAILED;
+ setError(errorMessage);
+ notification.error({
+ message: t.ERROR,
+ description: errorMessage,
+ });
+ }
+ } catch (err) {
+ const errorMessage = t.FETCH_MESSAGE_DETAIL_FAILED;
+ setError(errorMessage);
+ notification.error({
+ message: t.ERROR,
+ description: errorMessage,
+ });
+ console.error("Error fetching message detail:", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchMessageDetail();
+ }, [visible, messageId, topic, t]); // 依赖项中添加 visible,确保在 Modal 显示时触发
+
+
+ // handleShowExceptionDesc 方法不再需要,因为我们直接使用 Paragraph 的 ellipsis
+
+ return (
+
+ {t.CLOSE}
+ ,
+ ]}
+ width={900}
+ destroyOnHidden={true} // 使用新的 destroyOnHidden 替代 destroyOnClose
+ >
+
+ {error && (
+
+ {error}
+
+ )}
+ {messageDetail ? ( // 确保 messageDetail 存在时才渲染内容
+ <>
+ {/* 消息信息部分 */}
+ {t.MESSAGE_INFO}} bordered column={2} size="small" style={{ marginBottom: 20 }}>
+ {messageDetail.messageView.topic}
+ {messageDetail.messageView.msgId}
+ {messageDetail.messageView.storeHost}
+ {messageDetail.messageView.bornHost}
+
+ {moment(messageDetail.messageView.storeTimestamp).format("YYYY-MM-DD HH:mm:ss")}
+
+
+ {moment(messageDetail.messageView.bornTimestamp).format("YYYY-MM-DD HH:mm:ss")}
+
+ {messageDetail.messageView.queueId}
+ {messageDetail.messageView.queueOffset}
+ {messageDetail.messageView.storeSize} bytes
+ {messageDetail.messageView.reconsumeTimes}
+ {messageDetail.messageView.bodyCRC}
+ {messageDetail.messageView.sysFlag}
+ {messageDetail.messageView.flag}
+ {messageDetail.messageView.preparedTransactionOffset}
+
+
+ {/* 消息属性部分 */}
+ {Object.keys(messageDetail.messageView.properties).length > 0 && (
+ {t.MESSAGE_PROPERTIES}} bordered column={1} size="small" style={{ marginBottom: 20 }}>
+ {Object.entries(messageDetail.messageView.properties).map(([key, value]) => (
+ {value}
+ ))}
+
+ )}
+
+ {/* 消息体部分 */}
+ {t.MESSAGE_BODY}} bordered column={1} size="small" style={{ marginBottom: 20 }}>
+
+
+ {messageDetail.messageView.messageBody}
+
+
+
+
+ {/* 消息轨迹列表部分 */}
+ {messageDetail.messageTrackList && messageDetail.messageTrackList.length > 0 ? (
+ <>
+ {t.MESSAGE_TRACKING}
+
+ {messageDetail.messageTrackList.map((track, index) => (
+
+
+ {track.consumerGroup}
+
+
+
+ {track.trackType}
+
+
+
+ }
+ onClick={() => onResendMessage(messageDetail.messageView, track.consumerGroup)}
+ size="small"
+ style={{ marginRight: 8 }}
+ >
+ {t.RESEND_MESSAGE}
+
+ {/* 移除“查看异常”按钮,因为现在直接在下方展示可展开内容 */}
+
+ {track.exceptionDesc && (
+
+ {/* 异常信息截断显示,点击“查看更多”可展开 */}
+ {t.READ_MORE}, // 蓝色展开文本
+ }}
+ >
+ {track.exceptionDesc}
+
+
+ )}
+
+ ))}
+
+ >
+ ) : (
+ {t.NO_TRACKING_INFO}
+ )}
+ >
+ ) : (
+ // 当 messageDetail 为 null 时,可以显示一个占位符或者不显示内容
+ !loading && !error && {t.NO_MESSAGE_DETAIL_AVAILABLE}
+ )}
+
+
+ );
+};
+
+export default MessageDetailViewDialog;
diff --git a/frontend-new/src/components/MessageTraceDetailViewDialog.jsx b/frontend-new/src/components/MessageTraceDetailViewDialog.jsx
new file mode 100644
index 0000000..f3b09da
--- /dev/null
+++ b/frontend-new/src/components/MessageTraceDetailViewDialog.jsx
@@ -0,0 +1,470 @@
+/*
+ * 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, useRef } from 'react';
+import { Form, Input, Typography, Collapse, Table, Tag } from 'antd';
+import moment from 'moment';
+import { useLanguage } from '../i18n/LanguageContext';
+import Paragraph from "antd/es/skeleton/Paragraph";
+import * as echarts from 'echarts'; // Import ECharts
+
+const { Text } = Typography;
+const { Panel } = Collapse;
+
+// Constants for styling and formatting, derived from the example
+const SUCCESS_COLOR = '#75d874';
+const ERROR_COLOR = 'red';
+const UNKNOWN_COLOR = 'yellow';
+const TRANSACTION_COMMIT_COLOR = SUCCESS_COLOR;
+const TRANSACTION_ROLLBACK_COLOR = ERROR_COLOR;
+const TRANSACTION_UNKNOWN_COLOR = 'grey';
+const TIME_FORMAT_PATTERN = "YYYY-MM-DD HH:mm:ss.SSS";
+const DEFAULT_DISPLAY_DURATION = 10 * 1000;
+const TRANSACTION_CHECK_COST_TIME = 50; // transactionTraceNode do not have costTime, assume it cost 50ms
+
+const MessageTraceDetailViewDialog = ({ ngDialogData }) => {
+ const { t } = useLanguage();
+ const messageTraceGraphRef = useRef(null);
+
+ const producerNode = ngDialogData?.producerNode;
+ const subscriptionNodeList = ngDialogData?.subscriptionNodeList || [];
+ const messageTraceViews = ngDialogData?.messageTraceViews || []; // This data structure seems redundant for the Gantt chart but can be used for extra tooltip details.
+
+ useEffect(() => {
+ if (messageTraceGraphRef.current && ngDialogData) {
+ const chart = echarts.init(messageTraceGraphRef.current);
+
+ let data = [];
+ let dataZoomEnd = 100;
+ let startTime = Number.MAX_VALUE;
+ let endTime = 0;
+ let messageGroups = []; // This will be our Y-axis categories
+
+ if (producerNode) {
+ startTime = +producerNode.traceNode.beginTimestamp;
+ endTime = +producerNode.traceNode.endTimestamp;
+ }
+
+ // Helper functions from the provided example
+ function buildNodeColor(traceNode) {
+ if (traceNode.transactionState != null) {
+ switch (traceNode.transactionState) {
+ case 'COMMIT_MESSAGE':
+ return TRANSACTION_COMMIT_COLOR;
+ case 'ROLLBACK_MESSAGE':
+ return TRANSACTION_ROLLBACK_COLOR;
+ case 'UNKNOW':
+ return TRANSACTION_UNKNOWN_COLOR;
+ default:
+ return ERROR_COLOR;
+ }
+ }
+ switch (traceNode.status) {
+ case 'FAILED': // Changed 'failed' to 'FAILED' to match backend typically
+ return ERROR_COLOR;
+ case 'UNKNOWN': // Changed 'unknown' to 'UNKNOWN'
+ return UNKNOWN_COLOR;
+ default:
+ return SUCCESS_COLOR;
+ }
+ }
+
+ function formatXAxisTime(value) {
+ let duration = Math.max(0, value - startTime);
+ if (duration < 1000)
+ return timeFormat(duration, 'ms');
+ duration /= 1000;
+ if (duration < 60)
+ return timeFormat(duration, 's');
+ duration /= 60;
+ if (duration < 60)
+ return timeFormat(duration, 'min');
+ duration /= 60;
+ return timeFormat(duration, 'h');
+ }
+
+ function timeFormat(duration, unit) {
+ return duration.toFixed(2) + unit;
+ }
+
+ function buildTraceInfo(itemName, itemValue) {
+ if (itemValue) {
+ return `${itemName}: ${itemValue} `
+ }
+ return "";
+ }
+
+ function formatCostTimeStr(costTime) {
+ if (costTime < 0) {
+ return "";
+ }
+ let costTimeStr = costTime;
+ if (costTime === 0) {
+ costTimeStr = '<1'
+ }
+ return `${costTimeStr}ms`;
+ }
+
+ function buildCostTimeInfo(costTime) {
+ if (costTime < 0) {
+ return "";
+ }
+ return `Cost Time: ${formatCostTimeStr(costTime)} `
+ }
+ function buildTimeStamp(timestamp) {
+ if (timestamp < 0) {
+ return 'N/A';
+ }
+ return moment(timestamp).format(TIME_FORMAT_PATTERN);
+ }
+
+ function formatNodeToolTip(params) {
+ let traceNode = params.data.traceData.traceNode;
+ return `
+ ${buildCostTimeInfo(traceNode.costTime)}
+ Status: ${traceNode.status}
+ ${buildTraceInfo('Begin Timestamp', buildTimeStamp(traceNode.beginTimestamp))}
+ ${buildTraceInfo('End Timestamp', buildTimeStamp(traceNode.endTimestamp))}
+ Client Host: ${traceNode.clientHost}
+ Store Host: ${traceNode.storeHost}
+ Retry Times: ${traceNode.retryTimes < 0 ? 'N/A' : traceNode.retryTimes}
+ ${buildTraceInfo('Message Type', traceNode.msgType)}
+ ${buildTraceInfo('Transaction ID', traceNode.transactionId)}
+ ${buildTraceInfo('Transaction State', traceNode.transactionState)}
+ ${buildTraceInfo('From Transaction Check', traceNode.fromTransactionCheck)}
+ `;
+ }
+
+ function calcGraphTimestamp(timestamp, relativeTimeStamp, duration, addDuration) {
+ if (timestamp > 0) {
+ return timestamp;
+ }
+ if (duration < 0) {
+ return relativeTimeStamp;
+ }
+ return addDuration ? relativeTimeStamp + duration : relativeTimeStamp - duration;
+ }
+
+ function addTraceData(traceNode, index, groupName) {
+ if (traceNode.beginTimestamp < 0 && traceNode.endTimestamp < 0) {
+ return;
+ }
+ let beginTimestamp = calcGraphTimestamp(traceNode.beginTimestamp, traceNode.endTimestamp, traceNode.costTime, false);
+ let endTimestamp = calcGraphTimestamp(traceNode.endTimestamp, traceNode.beginTimestamp, traceNode.costTime, true);
+ if (endTimestamp === beginTimestamp) {
+ endTimestamp = beginTimestamp + 1; // Ensure a minimum duration for visualization
+ }
+ data.push({
+ name: groupName, // To display group name in tooltip or for internal reference
+ value: [
+ index,
+ beginTimestamp,
+ endTimestamp,
+ traceNode.costTime
+ ],
+ itemStyle: {
+ normal: {
+ color: buildNodeColor(traceNode),
+ opacity: 1
+ }
+ },
+ traceData: {
+ traceNode: traceNode
+ }
+ });
+ startTime = Math.min(startTime, beginTimestamp);
+ endTime = Math.max(endTime, endTimestamp);
+ }
+
+ // Populate data for the Gantt chart
+ subscriptionNodeList.forEach(item => {
+ messageGroups.push(item.subscriptionGroup);
+ });
+
+ subscriptionNodeList.forEach((subscriptionNode, index) => {
+ subscriptionNode.consumeNodeList.forEach(traceNode => addTraceData(traceNode, index, subscriptionNode.subscriptionGroup));
+ });
+
+ if (producerNode) {
+ messageGroups.push(producerNode.groupName);
+ let producerNodeIndex = messageGroups.length - 1;
+ addTraceData(producerNode.traceNode, producerNodeIndex, producerNode.groupName);
+ producerNode.transactionNodeList.forEach(transactionNode => {
+ transactionNode.beginTimestamp = Math.max(producerNode.traceNode.endTimestamp,
+ transactionNode.endTimestamp - TRANSACTION_CHECK_COST_TIME);
+ addTraceData(transactionNode, producerNodeIndex, producerNode.groupName);
+ endTime = Math.max(endTime, transactionNode.endTimestamp);
+ });
+ }
+
+ let totalDuration = endTime - startTime;
+ if (totalDuration > DEFAULT_DISPLAY_DURATION) {
+ dataZoomEnd = (DEFAULT_DISPLAY_DURATION / totalDuration) * 100;
+ }
+
+ function renderItem(params, api) {
+ let messageGroupIndex = api.value(0); // Y-axis index
+ let start = api.coord([api.value(1), messageGroupIndex]); // X-axis start time, Y-axis group index
+ let end = api.coord([api.value(2), messageGroupIndex]); // X-axis end time, Y-axis group index
+ let height = api.size([0, 1])[1] * 0.6; // Height of the bar
+
+ let rectShape = echarts.graphic.clipRectByRect({
+ x: start[0],
+ y: start[1] - height / 2,
+ width: Math.max(end[0] - start[0], 1), // Ensure minimum width
+ height: height
+ }, {
+ x: params.coordSys.x,
+ y: params.coordSys.y,
+ width: params.coordSys.width,
+ height: params.coordSys.height
+ });
+
+ return rectShape && {
+ type: 'rect',
+ transition: ['shape'],
+ shape: rectShape,
+ style: api.style({
+ text: formatCostTimeStr(api.value(3)), // Display cost time on the bar
+ textFill: '#000',
+ textAlign: 'right'
+ })
+ };
+ }
+
+ const option = {
+ tooltip: {
+ formatter: function (params) {
+ return formatNodeToolTip(params);
+ }
+ },
+ title: {
+ text: producerNode ? `Message Trace: ${producerNode.topic}` : "Message Trace",
+ left: 'center'
+ },
+ dataZoom: [
+ {
+ type: 'slider',
+ filterMode: 'weakFilter',
+ showDataShadow: false,
+ top: 'bottom', // Position at the bottom
+ start: 0,
+ end: dataZoomEnd,
+ labelFormatter: function (value) {
+ return formatXAxisTime(value + startTime); // Adjust label to show relative time from start
+ }
+ },
+ {
+ type: 'inside',
+ filterMode: 'weakFilter'
+ }
+ ],
+ grid: {
+ height: 300,
+ left: '10%', // Adjust left margin for Y-axis labels
+ right: '10%'
+ },
+ xAxis: {
+ min: startTime,
+ scale: true,
+ axisLabel: {
+ formatter: function (value) {
+ return formatXAxisTime(value);
+ }
+ }
+ },
+ yAxis: {
+ data: messageGroups, // Use group names as Y-axis categories
+ axisLabel: {
+ formatter: function (value, index) {
+ // Display the group name on the Y-axis
+ return value;
+ }
+ }
+ },
+ series: [
+ {
+ type: 'custom',
+ renderItem: renderItem,
+ encode: {
+ x: [1, 2], // Use beginTimestamp and endTimestamp for X-axis
+ y: 0 // Use the index for Y-axis category
+ },
+ data: data
+ }
+ ]
+ };
+
+ chart.setOption(option);
+
+ const resizeChart = () => chart.resize();
+ window.addEventListener('resize', resizeChart);
+
+ return () => {
+ window.removeEventListener('resize', resizeChart);
+ chart.dispose();
+ };
+ }
+ }, [ngDialogData, t]); // Add t as a dependency for the useEffect hook
+
+ // ... (rest of your existing component code)
+ const transactionColumns = [
+ { title: t.TIMESTAMP, dataIndex: 'beginTimestamp', key: 'beginTimestamp', align: 'center', render: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') },
+ { title: t.TRANSACTION_STATE, dataIndex: 'transactionState', key: 'transactionState', align: 'center', render: (text) => {text} },
+ { title: t.FROM_TRANSACTION_CHECK, dataIndex: 'fromTransactionCheck', key: 'fromTransactionCheck', align: 'center', render: (text) => (text ? {t.YES} : {t.NO} ) },
+ { title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center' },
+ { title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center' },
+ ];
+
+ const consumeColumns = [
+ { title: t.BEGIN_TIMESTAMP, dataIndex: 'beginTimestamp', key: 'beginTimestamp', align: 'center', render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') },
+ { title: t.END_TIMESTAMP, dataIndex: 'endTimestamp', key: 'endTimestamp', align: 'center', render: (text) => text < 0 ? 'N/A' : moment(text).format('YYYY-MM-DD HH:mm:ss.SSS') },
+ { title: t.COST_TIME, dataIndex: 'costTime', key: 'costTime', align: 'center', render: (text) => text < 0 ? 'N/A' : `${text === 0 ? '<1' : text}ms` },
+ { title: t.STATUS, dataIndex: 'status', key: 'status', align: 'center', render: (text) => {text} },
+ { title: t.RETRY_TIMES, dataIndex: 'retryTimes', key: 'retryTimes', align: 'center', render: (text) => text < 0 ? 'N/A' : text },
+ { title: t.CLIENT_HOST, dataIndex: 'clientHost', key: 'clientHost', align: 'center' },
+ { title: t.STORE_HOST, dataIndex: 'storeHost', key: 'storeHost', align: 'center' },
+ ];
+
+ return (
+
+
+
+ {t.MESSAGE_TRACE_GRAPH}} key="messageTraceGraph">
+
+ {/* ECharts message trace graph will be rendered here */}
+ {(!producerNode && subscriptionNodeList.length === 0) && (
+ {t.TRACE_GRAPH_PLACEHOLDER}
+ )}
+
+
+
+
+
+
+
+ {t.SEND_MESSAGE_TRACE}} key="sendMessageTrace">
+ {!producerNode ? (
+ {t.NO_PRODUCER_TRACE_DATA}
+ ) : (
+
+
+ {t.SEND_MESSAGE_INFO} : ( {t.MESSAGE_ID} {producerNode.msgId} )
+
+
+
+ {producerNode.transactionNodeList && producerNode.transactionNodeList.length > 0 && (
+
+
{t.CHECK_TRANSACTION_INFO}:
+
`transaction_${index}`}
+ bordered
+ pagination={false}
+ size="middle"
+ scroll={{ x: 'max-content' }}
+ />
+
+ )}
+
+ )}
+
+
+
+
+
+
+ {t.CONSUME_MESSAGE_TRACE}} key="consumeMessageTrace">
+ {subscriptionNodeList.length === 0 ? (
+ {t.NO_CONSUMER_TRACE_DATA}
+ ) : (
+
+ {subscriptionNodeList.map(subscriptionNode => (
+
+ {t.SUBSCRIPTION_GROUP}: {subscriptionNode.subscriptionGroup} }
+ key={subscriptionNode.subscriptionGroup}
+ >
+ `${subscriptionNode.subscriptionGroup}_${index}`}
+ bordered
+ pagination={false}
+ size="middle"
+ scroll={{ x: 'max-content' }}
+ />
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+};
+
+export default MessageTraceDetailViewDialog;
diff --git a/frontend-new/src/store/actions/themeActions.js b/frontend-new/src/store/actions/themeActions.js
new file mode 100644
index 0000000..887e0fa
--- /dev/null
+++ b/frontend-new/src/store/actions/themeActions.js
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+export const SET_THEME = 'SET_THEME';
+
+export const setTheme = (themeName) => ({
+ type: SET_THEME,
+ payload: themeName,
+});
diff --git a/frontend-new/src/store/context/ThemeContext.js b/frontend-new/src/store/context/ThemeContext.js
new file mode 100644
index 0000000..7f2ce65
--- /dev/null
+++ b/frontend-new/src/store/context/ThemeContext.js
@@ -0,0 +1,39 @@
+/*
+ * 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 { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { themes, defaultTheme } from '../../assets/styles/theme';
+import { setTheme } from '../actions/themeActions';
+
+export const useTheme = () => {
+ // 从 Redux store 中取出 currentThemeName
+ const currentThemeName = useSelector(state => state.theme.currentThemeName);
+ const dispatch = useDispatch();
+
+
+ const currentTheme = themes[currentThemeName] || defaultTheme;
+
+ useEffect(() => {
+ localStorage.setItem('appTheme', currentThemeName);
+ }, [currentThemeName]);
+
+ return {
+ currentTheme,
+ currentThemeName,
+ setCurrentThemeName: (themeName) => dispatch(setTheme(themeName)),
+ };
+};
diff --git a/frontend-new/src/store/index.js b/frontend-new/src/store/index.js
new file mode 100644
index 0000000..f303f5d
--- /dev/null
+++ b/frontend-new/src/store/index.js
@@ -0,0 +1,28 @@
+/*
+ * 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 { createStore,combineReducers } from 'redux';
+import themeReducer from './reducers/themeReducer';
+
+// 组合所有的 reducers
+const rootReducer = combineReducers({
+ theme: themeReducer, // theme 状态将通过 state.theme 访问
+});
+
+// 创建 Redux store
+const store = createStore(rootReducer);
+
+export default store;
diff --git a/frontend-new/src/store/reducers/themeReducer.js b/frontend-new/src/store/reducers/themeReducer.js
new file mode 100644
index 0000000..9e131ab
--- /dev/null
+++ b/frontend-new/src/store/reducers/themeReducer.js
@@ -0,0 +1,41 @@
+/*
+ * 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 { SET_THEME } from '../actions/themeActions';
+
+const getInitialTheme = () => {
+ return localStorage.getItem('appTheme') || 'default';
+};
+
+const initialState = {
+ currentThemeName: getInitialTheme(),
+};
+
+const themeReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case SET_THEME:
+ // 注意:reducer 应该返回新的状态对象,而不是直接修改旧状态
+ return {
+ ...state,
+ currentThemeName: action.payload,
+ };
+ default:
+ return state;
+ }
+};
+
+export default themeReducer;