Skip to content

Commit 1e047f1

Browse files
committed
fix: new paired view
1 parent 8cd5f2f commit 1e047f1

File tree

5 files changed

+145
-25
lines changed

5 files changed

+145
-25
lines changed

frontend/src/components/LogTable/LogFilters.vue

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,6 @@
3333
<MagnifyingGlassIcon class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
3434
</div>
3535

36-
<!-- Toggle pairing -->
37-
<button
38-
@click="logStore.togglePairing"
39-
class="px-4 py-2 rounded-lg font-medium transition-colors"
40-
:class="[
41-
logStore.showPairing
42-
? 'bg-mcp-blue text-white'
43-
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
44-
]"
45-
title="Show request/response pairs"
46-
>
47-
<LinkIcon class="h-5 w-5" />
48-
</button>
49-
5036
<!-- Expand all -->
5137
<button
5238
@click="logStore.toggleExpandAll"
@@ -86,7 +72,7 @@
8672
<script setup>
8773
import { computed, ref, watch } from 'vue'
8874
import { useLogStore } from '@/stores/logs'
89-
import { MagnifyingGlassIcon, LinkIcon, TrashIcon, ArrowPathIcon, CodeBracketIcon } from '@heroicons/vue/24/outline'
75+
import { MagnifyingGlassIcon, TrashIcon, ArrowPathIcon, CodeBracketIcon } from '@heroicons/vue/24/outline'
9076
9177
const logStore = useLogStore()
9278

frontend/src/components/LogTable/LogRow.vue

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
<tr>
33
<td colspan="5" class="p-0">
44
<div
5-
class="cursor-pointer transition-colors"
5+
class="cursor-pointer transition-all relative"
66
:class="{
7-
'bg-blue-50 dark:bg-blue-900/20': isSelected,
8-
'ring-2 ring-blue-400 ring-opacity-50': isPaired && !isSelected,
7+
'bg-blue-100 dark:bg-blue-900/30 ring-2 ring-blue-500': isSelected,
98
'hover:bg-gray-50 dark:hover:bg-gray-700/50': !isSelected
109
}"
1110
@click="$emit('click')"
@@ -35,8 +34,17 @@
3534
</table>
3635
</div>
3736
<!-- Expanded JSON view -->
38-
<div v-if="isExpanded" class="px-4 py-3 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
39-
<pre class="text-xs font-mono text-gray-800 dark:text-gray-200 overflow-x-auto">{{ formattedJson }}</pre>
37+
<div v-if="isExpanded" class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
38+
<div class="px-4 py-3">
39+
<pre class="text-xs font-mono text-gray-800 dark:text-gray-200 overflow-x-auto">{{ formattedJson }}</pre>
40+
</div>
41+
42+
<!-- Paired messages -->
43+
<PairedMessages
44+
:current-log="log"
45+
:all-logs="allLogs"
46+
variant="compact"
47+
/>
4048
</div>
4149
</td>
4250
</tr>
@@ -46,17 +54,18 @@
4654
import { computed } from 'vue'
4755
import { getMessageType, getMessageSummary, formatTimestamp, getPortInfo, getDirectionIcon } from '@/utils/messageParser'
4856
import MessageTypeBadge from './MessageTypeBadge.vue'
57+
import PairedMessages from '@/components/common/PairedMessages.vue'
4958
5059
const props = defineProps({
5160
log: {
5261
type: Object,
5362
required: true
5463
},
55-
isSelected: {
56-
type: Boolean,
57-
default: false
64+
allLogs: {
65+
type: Array,
66+
default: () => []
5867
},
59-
isPaired: {
68+
isSelected: {
6069
type: Boolean,
6170
default: false
6271
},
@@ -81,4 +90,5 @@ const formattedJson = computed(() => {
8190
return props.log.message
8291
}
8392
})
93+
8494
</script>

frontend/src/components/LogTable/LogTable.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
v-for="log in displayLogs"
3333
:key="log.id || `${log.timestamp}-${log.src_port}`"
3434
:log="log"
35+
:all-logs="logStore.logs"
3536
:is-selected="logStore.selectedLogId === log.id"
36-
:is-paired="logStore.pairedLogs.has(log.id)"
3737
:is-expanded="logStore.expandAll"
3838
@click="handleLogClick(log)"
3939
/>

frontend/src/components/MessageDetail/MessageDetailModal.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@
7373
</div>
7474
</div>
7575

76+
<!-- Paired Messages -->
77+
<PairedMessages
78+
v-if="logStore.selectedLog"
79+
:current-log="logStore.selectedLog"
80+
:all-logs="logStore.logs"
81+
variant="full"
82+
class="mt-4"
83+
/>
84+
7685
<!-- Actions -->
7786
<div class="flex justify-end gap-2 mt-6">
7887
<button
@@ -104,6 +113,7 @@ import { ClipboardDocumentIcon } from '@heroicons/vue/24/outline'
104113
import { useLogStore } from '@/stores/logs'
105114
import { getMessageType, parseMessage } from '@/utils/messageParser'
106115
import MessageTypeBadge from '@/components/LogTable/MessageTypeBadge.vue'
116+
import PairedMessages from '@/components/common/PairedMessages.vue'
107117
108118
const logStore = useLogStore()
109119
const copied = ref(false)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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

Comments
 (0)