From 52545ccd23d2508376f66ea1fbd7fa9770be9bf0 Mon Sep 17 00:00:00 2001 From: Crazylychee <110229037+Crazylychee@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:47:14 +0800 Subject: [PATCH] [GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#310) --- frontend-new/src/assets/styles/theme.js | 108 ++++ .../components/DlqMessageDetailViewDialog.jsx | 73 +++ .../components/MessageDetailViewDialog.jsx | 208 ++++++++ .../MessageTraceDetailViewDialog.jsx | 470 ++++++++++++++++++ .../src/store/actions/themeActions.js | 23 + .../src/store/context/ThemeContext.js | 39 ++ frontend-new/src/store/index.js | 28 ++ .../src/store/reducers/themeReducer.js | 41 ++ 8 files changed, 990 insertions(+) create mode 100644 frontend-new/src/assets/styles/theme.js create mode 100644 frontend-new/src/components/DlqMessageDetailViewDialog.jsx create mode 100644 frontend-new/src/components/MessageDetailViewDialog.jsx create mode 100644 frontend-new/src/components/MessageTraceDetailViewDialog.jsx create mode 100644 frontend-new/src/store/actions/themeActions.js create mode 100644 frontend-new/src/store/context/ThemeContext.js create mode 100644 frontend-new/src/store/index.js create mode 100644 frontend-new/src/store/reducers/themeReducer.js 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} + + + + + {/* 移除“查看异常”按钮,因为现在直接在下方展示可展开内容 */} + + {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} ) + +
+
+ {t.TOPIC}}> + + + {t.PRODUCER_GROUP}}> + + + {t.MESSAGE_KEY}}> + + + {t.TAG}}> + + + + {t.BEGIN_TIMESTAMP}}> + + + {t.END_TIMESTAMP}}> + + + {t.COST_TIME}}> + + + {t.MSG_TYPE}}> + + + + {t.CLIENT_HOST}}> + + + {t.STORE_HOST}}> + + + {t.RETRY_TIMES}}> + + + {t.OFFSET_MSG_ID}}> + + +
+
+ + {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;