Skip to content

Commit f88b795

Browse files
author
Lasim
committed
feat(frontend): implement RequestDetailSheet for request details display
1 parent b429b28 commit f88b795

File tree

3 files changed

+189
-129
lines changed

3 files changed

+189
-129
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { useI18n } from 'vue-i18n'
4+
import {
5+
Sheet,
6+
SheetContent,
7+
SheetDescription,
8+
SheetHeader,
9+
SheetTitle,
10+
} from '@/components/ui/sheet'
11+
import { Button } from '@/components/ui/button'
12+
import { Copy, Check } from 'lucide-vue-next'
13+
import type { McpRequestLog } from '@/types/mcp-request-logs'
14+
15+
interface Props {
16+
request: McpRequestLog | null
17+
open: boolean
18+
}
19+
20+
interface Emits {
21+
(e: 'update:open', value: boolean): void
22+
}
23+
24+
const props = defineProps<Props>()
25+
const emit = defineEmits<Emits>()
26+
const { t } = useI18n()
27+
28+
const copiedField = ref<string | null>(null)
29+
30+
function handleOpenChange(value: boolean) {
31+
emit('update:open', value)
32+
}
33+
34+
async function copyToClipboard(text: string, field: string) {
35+
try {
36+
await navigator.clipboard.writeText(text)
37+
copiedField.value = field
38+
setTimeout(() => {
39+
copiedField.value = null
40+
}, 2000)
41+
} catch (err) {
42+
console.error('Failed to copy:', err)
43+
}
44+
}
45+
</script>
46+
47+
<template>
48+
<Sheet :open="open" @update:open="handleOpenChange">
49+
<SheetContent class="sm:max-w-2xl overflow-y-auto">
50+
<SheetHeader>
51+
<SheetTitle>{{ t('mcpInstallations.details.requests.detail.title') }}</SheetTitle>
52+
<SheetDescription v-if="request">
53+
{{ t('mcpInstallations.details.requests.detail.toolName') }}: {{ request.tool_name }}
54+
</SheetDescription>
55+
</SheetHeader>
56+
57+
<div v-if="request" class="px-4 pb-4 space-y-6">
58+
<div class="space-y-2">
59+
<div class="text-sm">
60+
<span class="font-medium">{{ t('mcpInstallations.details.requests.detail.user') }}:</span>
61+
<span v-if="request.user" class="text-muted-foreground ml-1">
62+
{{ request.user.user_name }} ({{ request.user.email }})
63+
</span>
64+
<span v-else class="text-muted-foreground ml-1">—</span>
65+
</div>
66+
67+
<div class="text-sm">
68+
<span class="font-medium">{{ t('mcpInstallations.details.requests.detail.responseTime') }}:</span>
69+
<span class="text-muted-foreground ml-1">{{ request.response_time_ms }}ms</span>
70+
</div>
71+
72+
<div class="text-sm">
73+
<span class="font-medium">{{ t('mcpInstallations.details.requests.detail.status') }}:</span>
74+
<span v-if="request.success" class="text-green-600 ml-1">{{ t('mcpInstallations.details.requests.table.values.success') }}</span>
75+
<span v-else class="text-red-600 ml-1">{{ t('mcpInstallations.details.requests.table.values.failed') }}</span>
76+
</div>
77+
78+
<div class="text-sm">
79+
<span class="font-medium">{{ t('mcpInstallations.details.requests.detail.timestamp') }}:</span>
80+
<span class="text-muted-foreground ml-1 font-mono">{{ request.created_at }}</span>
81+
</div>
82+
</div>
83+
84+
<div v-if="request.error_message">
85+
<div class="text-sm font-medium mb-2">{{ t('mcpInstallations.details.requests.detail.error') }}</div>
86+
<div class="text-sm text-red-600 bg-red-50 dark:bg-red-950/20 p-3 rounded-md">
87+
{{ request.error_message }}
88+
</div>
89+
</div>
90+
91+
<!-- Timeline Flow -->
92+
<div class="space-y-4">
93+
<!-- Request Started -->
94+
<div class="flex items-center gap-2">
95+
<svg fill="none" height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg">
96+
<circle cx="5" cy="5" r="4.25" stroke="currentColor" stroke-width="1.5" class="text-muted-foreground"></circle>
97+
</svg>
98+
<span class="text-sm font-medium">Request started</span>
99+
</div>
100+
101+
<!-- Arrow Down -->
102+
<div class="flex items-center gap-2">
103+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-muted-foreground" style="margin-left: -3px;">
104+
<path d="M8 2V14M8 14L4 10M8 14L12 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
105+
</svg>
106+
</div>
107+
108+
<!-- Parameters Box -->
109+
<div class="rounded-md border border-border bg-muted/30">
110+
<div class="rounded-md bg-background border-border px-3 py-2">
111+
<div class="flex items-center justify-between mb-2">
112+
<span class="text-sm font-medium">{{ t('mcpInstallations.details.requests.detail.parameters') }}</span>
113+
<Button
114+
size="sm"
115+
variant="ghost"
116+
@click="copyToClipboard(JSON.stringify(request.tool_params, null, 2), 'params')"
117+
>
118+
<Check v-if="copiedField === 'params'" class="h-4 w-4" />
119+
<Copy v-else class="h-4 w-4" />
120+
</Button>
121+
</div>
122+
<pre class="bg-muted p-3 rounded-md text-xs overflow-x-auto">{{ JSON.stringify(request.tool_params, null, 2) }}</pre>
123+
</div>
124+
</div>
125+
126+
<!-- Arrow Down -->
127+
<div class="flex items-center gap-2">
128+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-muted-foreground" style="margin-left: -3px;">
129+
<path d="M8 2V14M8 14L4 10M8 14L12 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
130+
</svg>
131+
</div>
132+
133+
<!-- Response Box -->
134+
<div class="rounded-md border border-border bg-muted/30">
135+
<div class="rounded-md bg-background border-border px-3 py-2">
136+
<div class="flex items-center justify-between mb-2">
137+
<span class="text-sm font-medium">{{ t('mcpInstallations.details.requests.detail.response') }}</span>
138+
<Button
139+
size="sm"
140+
variant="ghost"
141+
@click="copyToClipboard(JSON.stringify(request.tool_response, null, 2), 'response')"
142+
>
143+
<Check v-if="copiedField === 'response'" class="h-4 w-4" />
144+
<Copy v-else class="h-4 w-4" />
145+
</Button>
146+
</div>
147+
<pre class="bg-muted p-3 rounded-md text-xs overflow-x-auto">{{ JSON.stringify(request.tool_response, null, 2) }}</pre>
148+
</div>
149+
</div>
150+
151+
<!-- Arrow Down -->
152+
<div class="flex items-center gap-2">
153+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="text-muted-foreground" style="margin-left: -3px;">
154+
<path d="M8 2V14M8 14L4 10M8 14L12 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
155+
</svg>
156+
</div>
157+
158+
<!-- Request Finished -->
159+
<div class="flex items-center gap-2">
160+
<svg fill="none" height="10" viewBox="0 0 10 10" width="10" xmlns="http://www.w3.org/2000/svg">
161+
<circle cx="5" cy="5" r="4.25" stroke="currentColor" stroke-width="1.5" :class="request.success ? 'text-green-600' : 'text-red-600'"></circle>
162+
<circle cx="5" cy="5" r="2.5" :class="request.success ? 'fill-green-600' : 'fill-red-600'"></circle>
163+
</svg>
164+
<span class="text-sm font-medium">Request finished in {{ request.response_time_ms }}ms</span>
165+
</div>
166+
</div>
167+
</div>
168+
</SheetContent>
169+
</Sheet>
170+
</template>

services/frontend/src/components/mcp-server/installation/RequestsTab.vue

Lines changed: 18 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,13 @@ import { useI18n } from 'vue-i18n'
44
import { toast } from 'vue-sonner'
55
import { useRequestsStream } from '@/composables/mcp-server/installation'
66
import { McpRequestLogsService } from '@/services/mcpRequestLogsService'
7+
import { RequestDetailSheet } from '@/components/mcp-server/installation'
78
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
8-
import { Button } from '@/components/ui/button'
99
import { Alert, AlertDescription } from '@/components/ui/alert'
1010
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
11-
import {
12-
Dialog,
13-
DialogContent,
14-
DialogDescription,
15-
DialogHeader,
16-
DialogTitle,
17-
} from '@/components/ui/dialog'
1811
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
1912
import { Card } from '@/components/ui/card'
20-
import { AlertCircle, AlertTriangle, Eye, Radio, Copy, Check } from 'lucide-vue-next'
13+
import { AlertTriangle, Eye, Radio } from 'lucide-vue-next'
2114
import type { McpInstallation } from '@/types/mcp-installations'
2215
import type { McpRequestLog } from '@/types/mcp-request-logs'
2316
@@ -36,8 +29,7 @@ type ViewMode = 'live' | 'api'
3629
const filter = ref<FilterType>('all')
3730
const viewMode = ref<ViewMode>('live')
3831
const selectedRequest = ref<McpRequestLog | null>(null)
39-
const showDetailDialog = ref(false)
40-
const copiedField = ref<string | null>(null)
32+
const showDetailSheet = ref(false)
4133
4234
// Filtered requests based on filter selection
4335
const filteredRequests = computed(() => {
@@ -115,33 +107,10 @@ function getUserTimezone(): string {
115107
return Intl.DateTimeFormat().resolvedOptions().timeZone
116108
}
117109
118-
// Format JSON for display
119-
function formatJson(value: unknown): string {
120-
if (value === null || value === undefined) return 'null'
121-
try {
122-
return JSON.stringify(value, null, 2)
123-
} catch {
124-
return String(value)
125-
}
126-
}
127-
128-
// Copy to clipboard
129-
async function copyToClipboard(value: unknown, field: string) {
130-
try {
131-
await navigator.clipboard.writeText(formatJson(value))
132-
copiedField.value = field
133-
setTimeout(() => {
134-
copiedField.value = null
135-
}, 2000)
136-
} catch {
137-
console.error('Failed to copy to clipboard')
138-
}
139-
}
140-
141-
// Open detail dialog
110+
// Open detail sheet
142111
function openDetail(request: McpRequestLog) {
143112
selectedRequest.value = request
144-
showDetailDialog.value = true
113+
showDetailSheet.value = true
145114
}
146115
147116
// Connect to SSE stream
@@ -261,7 +230,12 @@ onUnmounted(() => {
261230
</TableRow>
262231
</TableHeader>
263232
<TableBody>
264-
<TableRow v-for="request in filteredRequests" :key="request.id">
233+
<TableRow
234+
v-for="request in filteredRequests"
235+
:key="request.id"
236+
class="cursor-pointer"
237+
@click="openDetail(request)"
238+
>
265239
<TableCell class="w-10 pr-0">
266240
<AlertTriangle
267241
v-if="!request.success"
@@ -315,104 +289,19 @@ onUnmounted(() => {
315289
{{ request.response_time_ms }}ms
316290
</TableCell>
317291
<TableCell class="w-12">
318-
<Button variant="ghost" size="sm" @click="openDetail(request)">
319-
<Eye class="h-4 w-4" />
320-
</Button>
292+
<Eye class="h-4 w-4 text-muted-foreground" />
321293
</TableCell>
322294
</TableRow>
323295
</TableBody>
324296
</Table>
325297
</div>
326298
</Card>
327299

328-
<!-- Detail Dialog -->
329-
<Dialog v-model:open="showDetailDialog">
330-
<DialogContent class="max-w-3xl max-h-[80vh] overflow-y-auto">
331-
<DialogHeader>
332-
<DialogTitle>{{ t('mcpInstallations.details.requests.detail.title') }}</DialogTitle>
333-
<DialogDescription>
334-
{{ selectedRequest?.tool_name }}
335-
</DialogDescription>
336-
</DialogHeader>
337-
338-
<div v-if="selectedRequest" class="space-y-4 mt-4">
339-
<!-- Status and Timing -->
340-
<div class="grid grid-cols-2 gap-4">
341-
<div>
342-
<div class="text-sm font-medium text-muted-foreground mb-1">
343-
{{ t('mcpInstallations.details.requests.detail.status') }}
344-
</div>
345-
<div class="flex items-center gap-2 text-sm">
346-
<AlertTriangle v-if="!selectedRequest.success" class="h-4 w-4 text-amber-500" />
347-
<span>{{ selectedRequest.success ? t('mcpInstallations.details.requests.table.values.success') : t('mcpInstallations.details.requests.table.values.failed') }}</span>
348-
</div>
349-
</div>
350-
<div>
351-
<div class="text-sm font-medium text-muted-foreground mb-1">
352-
{{ t('mcpInstallations.details.requests.detail.responseTime') }}
353-
</div>
354-
<div class="text-sm tabular-nums">{{ selectedRequest.response_time_ms }}ms</div>
355-
</div>
356-
</div>
357-
358-
<!-- User and Timestamp -->
359-
<div class="grid grid-cols-2 gap-4">
360-
<div>
361-
<div class="text-sm font-medium text-muted-foreground mb-1">
362-
{{ t('mcpInstallations.details.requests.detail.user') }}
363-
</div>
364-
<div class="text-sm">
365-
<div v-if="selectedRequest.user">{{ selectedRequest.user.user_name }}</div>
366-
<div v-else class="text-muted-foreground italic">Unknown</div>
367-
</div>
368-
</div>
369-
<div>
370-
<div class="text-sm font-medium text-muted-foreground mb-1">
371-
{{ t('mcpInstallations.details.requests.detail.timestamp') }}
372-
</div>
373-
<div class="text-sm font-mono tabular-nums">{{ formatLocalTimestamp(selectedRequest.created_at) }}</div>
374-
</div>
375-
</div>
376-
377-
<!-- Error Message (if failed) -->
378-
<div v-if="!selectedRequest.success && selectedRequest.error_message">
379-
<div class="text-sm font-medium text-muted-foreground mb-1">
380-
{{ t('mcpInstallations.details.requests.detail.error') }}
381-
</div>
382-
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3 text-sm text-red-800 dark:text-red-300">
383-
{{ selectedRequest.error_message }}
384-
</div>
385-
</div>
386-
387-
<!-- Parameters -->
388-
<div>
389-
<div class="flex items-center justify-between mb-1">
390-
<div class="text-sm font-medium text-muted-foreground">
391-
{{ t('mcpInstallations.details.requests.detail.parameters') }}
392-
</div>
393-
<Button variant="ghost" size="sm" @click="copyToClipboard(selectedRequest.tool_params, 'params')">
394-
<Check v-if="copiedField === 'params'" class="h-3 w-3" />
395-
<Copy v-else class="h-3 w-3" />
396-
</Button>
397-
</div>
398-
<pre class="bg-muted rounded-md p-3 text-sm overflow-x-auto max-h-48">{{ formatJson(selectedRequest.tool_params) }}</pre>
399-
</div>
400-
401-
<!-- Response -->
402-
<div>
403-
<div class="flex items-center justify-between mb-1">
404-
<div class="text-sm font-medium text-muted-foreground">
405-
{{ t('mcpInstallations.details.requests.detail.response') }}
406-
</div>
407-
<Button variant="ghost" size="sm" @click="copyToClipboard(selectedRequest.tool_response, 'response')">
408-
<Check v-if="copiedField === 'response'" class="h-3 w-3" />
409-
<Copy v-else class="h-3 w-3" />
410-
</Button>
411-
</div>
412-
<pre class="bg-muted rounded-md p-3 text-sm overflow-x-auto max-h-64">{{ formatJson(selectedRequest.tool_response) }}</pre>
413-
</div>
414-
</div>
415-
</DialogContent>
416-
</Dialog>
300+
<!-- Detail Sheet -->
301+
<RequestDetailSheet
302+
:request="selectedRequest"
303+
:open="showDetailSheet"
304+
@update:open="showDetailSheet = $event"
305+
/>
417306
</div>
418307
</template>

services/frontend/src/components/mcp-server/installation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { default as UserConfiguration } from './UserConfiguration.vue'
55
export { default as DangerZone } from './DangerZone.vue'
66
export { default as InstallationTabs } from './InstallationTabs.vue'
77
export { default as RequestsTab } from './RequestsTab.vue'
8+
export { default as RequestDetailSheet } from './RequestDetailSheet.vue'

0 commit comments

Comments
 (0)