1+ <template >
2+ <div v-if =" pairedMessages.length > 0" :class =" containerClass" >
3+ <div :class =" headerClass" >
4+ <span class =" text-sm font-medium text-gray-700 dark:text-gray-300" >
5+ Paired {{ pairedMessages.length === 1 ? 'Message' : 'Messages' }} (same JSON-RPC ID)
6+ </span >
7+ </div >
8+ <div :class =" contentClass" >
9+ <div v-for =" (pairedLog, index) in pairedMessages" :key =" index" :class =" messageClass" >
10+ <div :class =" messageHeaderClass" >
11+ <div class =" flex items-center gap-3" >
12+ <MessageTypeBadge :type =" getMessageType(pairedLog.message)" />
13+ <span class =" text-gray-600 dark:text-gray-400" >{{ formatTimestamp(pairedLog.timestamp) }}</span >
14+ </div >
15+ <span class =" text-xs text-gray-500 dark:text-gray-500 font-mono" >
16+ {{ pairedLog.src_ip }}:{{ pairedLog.src_port }} → {{ pairedLog.dst_ip }}:{{ pairedLog.dst_port }}
17+ </span >
18+ </div >
19+ <div :class =" messageContentClass" >
20+ <pre class =" text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre overflow-x-auto" >{{ formatPairedJson(pairedLog.message) }}</pre >
21+ </div >
22+ </div >
23+ </div >
24+ </div >
25+ </template >
26+
27+ <script setup>
28+ import { computed } from ' vue'
29+ import { getMessageType , formatTimestamp } from ' @/utils/messageParser'
30+ import MessageTypeBadge from ' @/components/LogTable/MessageTypeBadge.vue'
31+
32+ const props = defineProps ({
33+ currentLog: {
34+ type: Object ,
35+ required: true
36+ },
37+ allLogs: {
38+ type: Array ,
39+ required: true
40+ },
41+ variant: {
42+ type: String ,
43+ default: ' compact' // 'compact' for LogRow, 'full' for Modal
44+ }
45+ })
46+
47+ const pairedMessages = computed (() => {
48+ if (! props .allLogs .length ) return []
49+
50+ try {
51+ const parsed = JSON .parse (props .currentLog .message )
52+ if (! parsed .id ) return []
53+
54+ // Find all messages with the same JSON-RPC ID, excluding the current one
55+ return props .allLogs .filter (log => {
56+ if (log === props .currentLog ) return false
57+ try {
58+ const otherParsed = JSON .parse (log .message )
59+ return otherParsed .id === parsed .id
60+ } catch {
61+ return false
62+ }
63+ })
64+ } catch {
65+ return []
66+ }
67+ })
68+
69+ function formatPairedJson (message ) {
70+ try {
71+ const parsed = JSON .parse (message)
72+ return JSON .stringify (parsed, null , 2 )
73+ } catch {
74+ return message
75+ }
76+ }
77+
78+ // Computed classes based on variant
79+ const containerClass = computed (() => {
80+ return props .variant === ' full'
81+ ? ' border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden'
82+ : ' border-t border-gray-200 dark:border-gray-700 px-4 py-3'
83+ })
84+
85+ const headerClass = computed (() => {
86+ return props .variant === ' full'
87+ ? ' bg-gray-50 dark:bg-gray-700 px-4 py-2'
88+ : ' text-xs font-medium text-gray-600 dark:text-gray-400 mb-2'
89+ })
90+
91+ const contentClass = computed (() => {
92+ return props .variant === ' full'
93+ ? ' p-4 space-y-4 max-h-96 overflow-y-auto'
94+ : ' '
95+ })
96+
97+ const messageClass = computed (() => {
98+ return props .variant === ' full'
99+ ? ' border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden'
100+ : ' mb-3 last:mb-0'
101+ })
102+
103+ const messageHeaderClass = computed (() => {
104+ return props .variant === ' full'
105+ ? ' bg-gray-100 dark:bg-gray-800 px-3 py-2 flex items-center justify-between text-sm'
106+ : ' flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400 mb-1'
107+ })
108+
109+ const messageContentClass = computed (() => {
110+ return props .variant === ' full'
111+ ? ' p-3 bg-gray-50 dark:bg-gray-900'
112+ : ' bg-gray-100 dark:bg-gray-800 rounded p-2'
113+ })
114+ </script >
0 commit comments