[GSOC][RIP-78][ISSUES#308] Add part of refactored front-end files (#310)

This commit is contained in:
Crazylychee
2025-06-16 13:47:14 +08:00
committed by GitHub
parent b75ace4804
commit 52545ccd23
8 changed files with 990 additions and 0 deletions

View File

@@ -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,
};

View File

@@ -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 (
<div style={{ padding: '20px' }}>
<Form layout="horizontal" labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Form.Item label="Message ID:">
<Text strong>{messageView.msgId}</Text>
</Form.Item>
<Form.Item label="Topic:">
<Text strong>{messageView.topic}</Text>
</Form.Item>
<Form.Item label="Properties:">
<Input.TextArea
value={typeof messageView.properties === 'object' ? JSON.stringify(messageView.properties, null, 2) : messageView.properties}
style={{ minHeight: 100, resize: 'none' }}
readOnly
/>
</Form.Item>
<Form.Item label="ReconsumeTimes:">
<Text strong>{messageView.reconsumeTimes}</Text>
</Form.Item>
<Form.Item label="Tag:">
<Text strong>{messageView.properties?.TAGS}</Text>
</Form.Item>
<Form.Item label="Key:">
<Text strong>{messageView.properties?.KEYS}</Text>
</Form.Item>
<Form.Item label="Storetime:">
<Text strong>{moment(messageView.storeTimestamp).format('YYYY-MM-DD HH:mm:ss')}</Text>
</Form.Item>
<Form.Item label="StoreHost:">
<Text strong>{messageView.storeHost}</Text>
</Form.Item>
<Form.Item label="Message body:">
<Input.TextArea
value={messageView.messageBody}
style={{ minHeight: 100, resize: 'none' }}
readOnly
/>
</Form.Item>
</Form>
</div>
);
};
export default DlqMessageDetailViewDialog;

View File

@@ -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 (
<Modal
title={t.MESSAGE_DETAIL}
open={visible} // Ant Design 5.x 版本中visible 属性已更名为 open
onCancel={onCancel}
footer={[
<Button key="close" onClick={onCancel}>
{t.CLOSE}
</Button>,
]}
width={900}
destroyOnHidden={true} // 使用新的 destroyOnHidden 替代 destroyOnClose
>
<Spin spinning={loading} tip={t.LOADING}>
{error && (
<Paragraph type="danger" style={{ textAlign: 'center' }}>
{error}
</Paragraph>
)}
{messageDetail ? ( // 确保 messageDetail 存在时才渲染内容
<>
{/* 消息信息部分 */}
<Descriptions title={<Text strong>{t.MESSAGE_INFO}</Text>} bordered column={2} size="small" style={{ marginBottom: 20 }}>
<Descriptions.Item label="Topic" span={2}><Text copyable>{messageDetail.messageView.topic}</Text></Descriptions.Item>
<Descriptions.Item label="Message ID" span={2}><Text copyable>{messageDetail.messageView.msgId}</Text></Descriptions.Item>
<Descriptions.Item label="StoreHost">{messageDetail.messageView.storeHost}</Descriptions.Item>
<Descriptions.Item label="BornHost">{messageDetail.messageView.bornHost}</Descriptions.Item>
<Descriptions.Item label="StoreTime">
{moment(messageDetail.messageView.storeTimestamp).format("YYYY-MM-DD HH:mm:ss")}
</Descriptions.Item>
<Descriptions.Item label="BornTime">
{moment(messageDetail.messageView.bornTimestamp).format("YYYY-MM-DD HH:mm:ss")}
</Descriptions.Item>
<Descriptions.Item label="Queue ID">{messageDetail.messageView.queueId}</Descriptions.Item>
<Descriptions.Item label="Queue Offset">{messageDetail.messageView.queueOffset}</Descriptions.Item>
<Descriptions.Item label="StoreSize">{messageDetail.messageView.storeSize} bytes</Descriptions.Item>
<Descriptions.Item label="ReconsumeTimes">{messageDetail.messageView.reconsumeTimes}</Descriptions.Item>
<Descriptions.Item label="BodyCRC">{messageDetail.messageView.bodyCRC}</Descriptions.Item>
<Descriptions.Item label="SysFlag">{messageDetail.messageView.sysFlag}</Descriptions.Item>
<Descriptions.Item label="Flag">{messageDetail.messageView.flag}</Descriptions.Item>
<Descriptions.Item label="PreparedTransactionOffset">{messageDetail.messageView.preparedTransactionOffset}</Descriptions.Item>
</Descriptions>
{/* 消息属性部分 */}
{Object.keys(messageDetail.messageView.properties).length > 0 && (
<Descriptions title={<Text strong>{t.MESSAGE_PROPERTIES}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}>
{Object.entries(messageDetail.messageView.properties).map(([key, value]) => (
<Descriptions.Item label={key} key={key}><Text copyable>{value}</Text></Descriptions.Item>
))}
</Descriptions>
)}
{/* 消息体部分 */}
<Descriptions title={<Text strong>{t.MESSAGE_BODY}</Text>} bordered column={1} size="small" style={{ marginBottom: 20 }}>
<Descriptions.Item>
<Paragraph
copyable
ellipsis={{
rows: 5,
expandable: true,
symbol: t.SHOW_ALL_CONTENT,
}}
>
{messageDetail.messageView.messageBody}
</Paragraph>
</Descriptions.Item>
</Descriptions>
{/* 消息轨迹列表部分 */}
{messageDetail.messageTrackList && messageDetail.messageTrackList.length > 0 ? (
<>
<Text strong>{t.MESSAGE_TRACKING}</Text>
<div style={{ marginTop: 10 }}>
{messageDetail.messageTrackList.map((track, index) => (
<Descriptions bordered column={1} size="small" key={index} style={{ marginBottom: 15 }}>
<Descriptions.Item label={t.CONSUMER_GROUP}>
{track.consumerGroup}
</Descriptions.Item>
<Descriptions.Item label={t.TRACK_TYPE}>
<Tag color={
track.trackType === 'CONSUMED_SOME_TIME_OK' ? 'success' :
track.trackType === 'NOT_ONLINE' ? 'default' :
track.trackType === 'PULL_SUCCESS' ? 'processing' :
track.trackType === 'NO_MATCHED_CONSUMER' ? 'warning' :
'error'
}>
{track.trackType}
</Tag>
</Descriptions.Item>
<Descriptions.Item label={t.OPERATION}>
<Button
icon={<SyncOutlined />}
onClick={() => onResendMessage(messageDetail.messageView, track.consumerGroup)}
size="small"
style={{ marginRight: 8 }}
>
{t.RESEND_MESSAGE}
</Button>
{/* 移除“查看异常”按钮,因为现在直接在下方展示可展开内容 */}
</Descriptions.Item>
{track.exceptionDesc && (
<Descriptions.Item label={t.EXCEPTION_SUMMARY}>
{/* 异常信息截断显示,点击“查看更多”可展开 */}
<Paragraph
ellipsis={{
rows: 2, // 默认显示2行
expandable: true,
symbol: <Text style={{ color: '#1890ff', cursor: 'pointer' }}>{t.READ_MORE}</Text>, // 蓝色展开文本
}}
>
{track.exceptionDesc}
</Paragraph>
</Descriptions.Item>
)}
</Descriptions>
))}
</div>
</>
) : (
<Paragraph>{t.NO_TRACKING_INFO}</Paragraph>
)}
</>
) : (
// 当 messageDetail 为 null 时,可以显示一个占位符或者不显示内容
!loading && !error && <Paragraph style={{ textAlign: 'center' }}>{t.NO_MESSAGE_DETAIL_AVAILABLE}</Paragraph>
)}
</Spin>
</Modal>
);
};
export default MessageDetailViewDialog;

View File

@@ -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}<br />`
}
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)}<br/>`
}
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}<br />
${buildTraceInfo('Begin Timestamp', buildTimeStamp(traceNode.beginTimestamp))}
${buildTraceInfo('End Timestamp', buildTimeStamp(traceNode.endTimestamp))}
Client Host: ${traceNode.clientHost}<br />
Store Host: ${traceNode.storeHost}<br />
Retry Times: ${traceNode.retryTimes < 0 ? 'N/A' : traceNode.retryTimes}<br />
${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) => <Tag color={text === 'COMMIT_MESSAGE' ? 'green' : (text === 'ROLLBACK_MESSAGE' ? 'red' : 'default')}>{text}</Tag> },
{ title: t.FROM_TRANSACTION_CHECK, dataIndex: 'fromTransactionCheck', key: 'fromTransactionCheck', align: 'center', render: (text) => (text ? <Tag color="blue">{t.YES}</Tag> : <Tag color="purple">{t.NO}</Tag>) },
{ 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) => <Tag color={text === 'SUCCESS' ? 'green' : (text === 'FAILED' ? 'red' : 'default')}>{text}</Tag> },
{ 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 (
<div style={{ padding: '20px', backgroundColor: '#f0f2f5' }}>
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
<Collapse defaultActiveKey={['messageTraceGraph']} expandIconPosition="right">
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.MESSAGE_TRACE_GRAPH}</Typography.Title>} key="messageTraceGraph">
<div ref={messageTraceGraphRef} style={{ height: 500, width: '100%', backgroundColor: '#fff', padding: '10px' }}>
{/* ECharts message trace graph will be rendered here */}
{(!producerNode && subscriptionNodeList.length === 0) && (
<Text type="secondary" style={{ display: 'block', textAlign: 'center', marginTop: '150px' }}>{t.TRACE_GRAPH_PLACEHOLDER}</Text>
)}
</div>
</Panel>
</Collapse>
</div>
<div style={{ marginBottom: '20px', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
<Collapse defaultActiveKey={['sendMessageTrace']} expandIconPosition="right">
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.SEND_MESSAGE_TRACE}</Typography.Title>} key="sendMessageTrace">
{!producerNode ? (
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_PRODUCER_TRACE_DATA}</Paragraph>
) : (
<div style={{ padding: '16px', backgroundColor: '#fff' }}>
<Typography.Title level={4} style={{ marginBottom: '20px' }}>
{t.SEND_MESSAGE_INFO} : ( {t.MESSAGE_ID} <Text strong copyable>{producerNode.msgId}</Text> )
</Typography.Title>
<Form layout="vertical" colon={false}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '16px' }}>
<Form.Item label={<Text strong>{t.TOPIC}</Text>}>
<Input value={producerNode.topic} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.PRODUCER_GROUP}</Text>}>
<Input value={producerNode.groupName} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.MESSAGE_KEY}</Text>}>
<Input value={producerNode.keys} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.TAG}</Text>}>
<Input value={producerNode.tags} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.BEGIN_TIMESTAMP}</Text>}>
<Input value={moment(producerNode.traceNode.beginTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.END_TIMESTAMP}</Text>}>
<Input value={moment(producerNode.traceNode.endTimestamp).format('YYYY-MM-DD HH:mm:ss.SSS')} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.COST_TIME}</Text>}>
<Input value={`${producerNode.traceNode.costTime === 0 ? '<1' : producerNode.traceNode.costTime}ms`} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.MSG_TYPE}</Text>}>
<Input value={producerNode.traceNode.msgType} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.CLIENT_HOST}</Text>}>
<Input value={producerNode.traceNode.clientHost} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.STORE_HOST}</Text>}>
<Input value={producerNode.traceNode.storeHost} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.RETRY_TIMES}</Text>}>
<Input value={producerNode.traceNode.retryTimes} readOnly />
</Form.Item>
<Form.Item label={<Text strong>{t.OFFSET_MSG_ID}</Text>}>
<Input value={producerNode.offSetMsgId} readOnly />
</Form.Item>
</div>
</Form>
{producerNode.transactionNodeList && producerNode.transactionNodeList.length > 0 && (
<div style={{ marginTop: '30px' }}>
<Typography.Title level={4} style={{ marginBottom: '15px' }}>{t.CHECK_TRANSACTION_INFO}:</Typography.Title>
<Table
columns={transactionColumns}
dataSource={producerNode.transactionNodeList}
rowKey={(record, index) => `transaction_${index}`}
bordered
pagination={false}
size="middle"
scroll={{ x: 'max-content' }}
/>
</div>
)}
</div>
)}
</Panel>
</Collapse>
</div>
<div style={{ borderRadius: '8px', overflow: 'hidden', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)' }}>
<Collapse defaultActiveKey={['consumeMessageTrace']} expandIconPosition="right">
<Panel header={<Typography.Title level={3} style={{ margin: 0, color: '#333' }}>{t.CONSUME_MESSAGE_TRACE}</Typography.Title>} key="consumeMessageTrace">
{subscriptionNodeList.length === 0 ? (
<Paragraph style={{ padding: '16px', textAlign: 'center', color: '#666' }}>{t.NO_CONSUMER_TRACE_DATA}</Paragraph>
) : (
<div style={{ padding: '16px', backgroundColor: '#fff' }}>
{subscriptionNodeList.map(subscriptionNode => (
<Collapse
key={subscriptionNode.subscriptionGroup}
style={{ marginBottom: '10px', border: '1px solid #e0e0e0', borderRadius: '4px' }}
defaultActiveKey={[subscriptionNode.subscriptionGroup]}
ghost
>
<Panel
header={<Typography.Title level={4} style={{ margin: 0 }}>{t.SUBSCRIPTION_GROUP}: <Text strong>{subscriptionNode.subscriptionGroup}</Text></Typography.Title>}
key={subscriptionNode.subscriptionGroup}
>
<Table
columns={consumeColumns}
dataSource={subscriptionNode.consumeNodeList}
rowKey={(record, index) => `${subscriptionNode.subscriptionGroup}_${index}`}
bordered
pagination={false}
size="middle"
scroll={{ x: 'max-content' }}
/>
</Panel>
</Collapse>
))}
</div>
)}
</Panel>
</Collapse>
</div>
</div>
);
};
export default MessageTraceDetailViewDialog;

View File

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

View File

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

View File

@@ -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;

View File

@@ -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;