/* * 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 {Collapse, Form, Input, Table, Tag, Typography} 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;