11<script setup lang="ts">
2- import { Card } from ' @/components/ui/card'
2+ import { ref , computed , onMounted , onUnmounted , watch } from ' vue'
3+ import { useI18n } from ' vue-i18n'
4+ import { toast } from ' vue-sonner'
5+ import { useLogsStream } from ' @/composables/mcp-server/installation'
6+ import { McpLogsService } from ' @/services/mcpLogsService'
37import { LogsNoAccess } from ' @/components/mcp-server/installation'
8+ import { Table , TableBody , TableCell , TableHead , TableHeader , TableRow } from ' @/components/ui/table'
9+ import { Alert , AlertDescription } from ' @/components/ui/alert'
10+ import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from ' @/components/ui/select'
11+ import { HoverCard , HoverCardContent , HoverCardTrigger } from ' @/components/ui/hover-card'
12+ import { Card } from ' @/components/ui/card'
13+ import { AlertCircle , Radio } from ' lucide-vue-next'
414import type { McpInstallation } from ' @/types/mcp-installations'
15+ import type { McpLog } from ' @/types/mcp-logs'
516
617interface Props {
718 installation: McpInstallation
819 teamId: string
920 userTeamRole? : ' team_admin' | ' team_user' | null
1021}
1122
12- defineProps <Props >()
23+ const props = defineProps <Props >()
24+ const { t } = useI18n ()
25+
26+ const { logs, isConnected, isLoading, error, connect, disconnect } = useLogsStream ()
27+
28+ type FilterType = ' all' | ' info' | ' warn' | ' error' | ' debug'
29+ type ViewMode = ' live' | ' api'
30+ const filter = ref <FilterType >(' all' )
31+ const viewMode = ref <ViewMode >(' live' )
32+
33+ // Filtered logs based on filter selection
34+ const filteredLogs = computed (() => {
35+ if (filter .value === ' all' ) return logs .value
36+ return logs .value .filter (log => log .level === filter .value )
37+ })
38+
39+ // Format timestamp in Vercel style: "Dec 17 09:27:37.30"
40+ function formatTimestamp(dateString : string ): string {
41+ const date = new Date (dateString )
42+ const months = [' Jan' , ' Feb' , ' Mar' , ' Apr' , ' May' , ' Jun' , ' Jul' , ' Aug' , ' Sep' , ' Oct' , ' Nov' , ' Dec' ]
43+ const month = months [date .getMonth ()]
44+ const day = date .getDate ().toString ().padStart (2 , ' 0' )
45+ const hours = date .getHours ().toString ().padStart (2 , ' 0' )
46+ const minutes = date .getMinutes ().toString ().padStart (2 , ' 0' )
47+ const seconds = date .getSeconds ().toString ().padStart (2 , ' 0' )
48+ const ms = Math .floor (date .getMilliseconds () / 10 ).toString ().padStart (2 , ' 0' )
49+ return ` ${month } ${day } ${hours }:${minutes }:${seconds }.${ms } `
50+ }
51+
52+ // Format local timezone timestamp
53+ function formatLocalTimestamp(dateString : string ): string {
54+ const date = new Date (dateString )
55+ return date .toLocaleString (' de-DE' , {
56+ day: ' 2-digit' ,
57+ month: ' short' ,
58+ year: ' numeric' ,
59+ hour: ' 2-digit' ,
60+ minute: ' 2-digit' ,
61+ second: ' 2-digit' ,
62+ fractionalSecondDigits: 3
63+ } as Intl .DateTimeFormatOptions )
64+ }
65+
66+ // Format UTC timestamp
67+ function formatUtcTimestamp(dateString : string ): string {
68+ const date = new Date (dateString )
69+ return date .toLocaleString (' de-DE' , {
70+ day: ' 2-digit' ,
71+ month: ' short' ,
72+ year: ' numeric' ,
73+ hour: ' 2-digit' ,
74+ minute: ' 2-digit' ,
75+ second: ' 2-digit' ,
76+ fractionalSecondDigits: 3 ,
77+ timeZone: ' UTC'
78+ } as Intl .DateTimeFormatOptions )
79+ }
80+
81+ // Format relative time
82+ function formatRelativeTime(dateString : string ): string {
83+ const date = new Date (dateString )
84+ const now = new Date ()
85+ const diffMs = now .getTime () - date .getTime ()
86+ const diffSecs = Math .floor (diffMs / 1000 )
87+ const diffMins = Math .floor (diffSecs / 60 )
88+ const diffHours = Math .floor (diffMins / 60 )
89+ const diffDays = Math .floor (diffHours / 24 )
90+
91+ if (diffSecs < 60 ) return ` ${diffSecs } seconds ago `
92+ if (diffMins < 60 ) return ` ${diffMins } minute${diffMins > 1 ? ' s' : ' ' } ago `
93+ if (diffHours < 24 ) return ` ${diffHours } hour${diffHours > 1 ? ' s' : ' ' } ago `
94+ return ` ${diffDays } day${diffDays > 1 ? ' s' : ' ' } ago `
95+ }
96+
97+ // Get Unix timestamp in milliseconds
98+ function getUnixTimestamp(dateString : string ): number {
99+ return new Date (dateString ).getTime ()
100+ }
101+
102+ // Get user's timezone name
103+ function getUserTimezone(): string {
104+ return Intl .DateTimeFormat ().resolvedOptions ().timeZone
105+ }
106+
107+ // Get text color class based on log level
108+ function getLevelTextClass(level : McpLog [' level' ]): string {
109+ switch (level ) {
110+ case ' error' :
111+ return ' text-red-600 dark:text-red-500'
112+ case ' warn' :
113+ return ' text-amber-600 dark:text-amber-500'
114+ case ' debug' :
115+ return ' text-neutral-500 dark:text-neutral-400'
116+ case ' info' :
117+ default :
118+ return ' text-green-600 dark:text-green-500'
119+ }
120+ }
121+
122+ // Connect to SSE stream
123+ function connectStream() {
124+ const options: { level? : FilterType ; limit? : number } = {}
125+ // Note: We filter client-side for better UX, so we don't pass level filter to stream
126+ const url = McpLogsService .getStreamUrl (props .teamId , props .installation .id , { limit: 100 })
127+ connect (url )
128+ }
129+
130+ // Watch for view mode changes and show toast
131+ watch (viewMode , (newMode , oldMode ) => {
132+ if (! oldMode ) return // Skip initial mount
133+
134+ if (newMode === ' live' ) {
135+ connectStream ()
136+ toast .success (t (' mcpInstallations.details.logs.viewMode.switchedToLive' ), {
137+ description: t (' mcpInstallations.details.logs.viewMode.liveDescription' )
138+ })
139+ } else if (newMode === ' api' ) {
140+ disconnect ()
141+ toast (t (' mcpInstallations.details.logs.viewMode.switchedToApi' ), {
142+ description: t (' mcpInstallations.details.logs.viewMode.apiDescription' )
143+ })
144+ }
145+ })
146+
147+ onMounted (() => {
148+ // Only connect if user is admin
149+ if (props .userTeamRole === ' team_admin' ) {
150+ connectStream ()
151+ }
152+ })
153+
154+ onUnmounted (() => {
155+ disconnect ()
156+ })
13157 </script >
14158
15159<template >
@@ -18,64 +162,123 @@ defineProps<Props>()
18162 <LogsNoAccess v-if =" userTeamRole !== 'team_admin'" />
19163
20164 <!-- Logs Content for Admin Users -->
21- <Card v-else class =" rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950 p-6" >
22- <div class =" space-y-6" >
23- <!-- Title -->
24- <div >
25- <h3 class =" text-lg font-semibold" >Logs</h3 >
26- <p class =" text-sm text-muted-foreground mt-1" >
27- View and analyze MCP server logs
28- </p >
29- </div >
30-
31- <!-- Lorem Ipsum Content -->
32- <div class =" space-y-4" >
33- <p class =" text-neutral-800 dark:text-neutral-100" >
34- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
35- Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
36- </p >
165+ <div v-else >
166+ <!-- Error State (outside card) -->
167+ <Alert v-if =" error" variant =" destructive" class =" mb-6" >
168+ <AlertCircle class =" h-4 w-4" />
169+ <AlertDescription >
170+ {{ t('mcpInstallations.details.logs.error.description', { error }) }}
171+ </AlertDescription >
172+ </Alert >
37173
38- <p class =" text-neutral-800 dark:text-neutral-100" >
39- Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
40- Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
41- </p >
42-
43- <div class =" border-l-4 border-neutral-300 dark:border-neutral-700 pl-4 py-2 bg-neutral-50 dark:bg-neutral-900" >
44- <p class =" text-sm text-neutral-600 dark:text-neutral-400 italic" >
45- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
46- totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
47- </p >
174+ <!-- Main Card -->
175+ <Card v-else class =" rounded-lg border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950 p-0 gap-0" >
176+ <!-- Header with live indicator, view mode and filter -->
177+ <div class =" flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-800" >
178+ <div class =" flex items-center gap-2" >
179+ <div
180+ v-if =" viewMode === 'live'"
181+ class =" flex items-center gap-1.5 text-sm"
182+ :class =" isConnected ? 'text-green-600' : 'text-muted-foreground'"
183+ >
184+ <Radio class =" h-3 w-3" :class =" { 'animate-pulse': isConnected }" />
185+ <span v-if =" isConnected" >{{ t('mcpInstallations.details.logs.connection.live') }}</span >
186+ <span v-else-if =" isLoading" >{{ t('mcpInstallations.details.logs.connection.reconnecting') }}</span >
187+ <span v-else >{{ t('mcpInstallations.details.logs.connection.disconnected') }}</span >
188+ </div >
48189 </div >
49190
50- <p class =" text-neutral-800 dark:text-neutral-100" >
51- Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos
52- qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur,
53- adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.
54- </p >
55-
56- <div class =" grid grid-cols-1 md:grid-cols-2 gap-4 mt-6" >
57- <div class =" p-4 border border-neutral-200 dark:border-neutral-800 rounded-lg" >
58- <h4 class =" font-medium text-neutral-800 dark:text-neutral-100 mb-2" >Sample Log Entry</h4 >
59- <p class =" text-sm text-neutral-600 dark:text-neutral-400" >
60- At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti.
61- </p >
62- </div >
191+ <div class =" flex items-center gap-2" >
192+ <Select v-model =" viewMode" >
193+ <SelectTrigger class =" w-32" >
194+ <SelectValue />
195+ </SelectTrigger >
196+ <SelectContent >
197+ <SelectItem value =" live" >Live</SelectItem >
198+ <SelectItem value =" api" >API</SelectItem >
199+ </SelectContent >
200+ </Select >
63201
64- <div class =" p-4 border border-neutral-200 dark:border-neutral-800 rounded-lg" >
65- <h4 class =" font-medium text-neutral-800 dark:text-neutral-100 mb-2" >Another Log Entry</h4 >
66- <p class =" text-sm text-neutral-600 dark:text-neutral-400" >
67- Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio.
68- </p >
69- </div >
202+ <Select v-model =" filter" >
203+ <SelectTrigger class =" w-40" >
204+ <SelectValue />
205+ </SelectTrigger >
206+ <SelectContent >
207+ <SelectItem value =" all" >{{ t('mcpInstallations.details.logs.filter.all') }}</SelectItem >
208+ <SelectItem value =" info" >{{ t('mcpInstallations.details.logs.filter.info') }}</SelectItem >
209+ <SelectItem value =" warn" >{{ t('mcpInstallations.details.logs.filter.warn') }}</SelectItem >
210+ <SelectItem value =" error" >{{ t('mcpInstallations.details.logs.filter.error') }}</SelectItem >
211+ <SelectItem value =" debug" >{{ t('mcpInstallations.details.logs.filter.debug') }}</SelectItem >
212+ </SelectContent >
213+ </Select >
70214 </div >
215+ </div >
71216
72- <p class =" text-neutral-800 dark:text-neutral-100" >
73- Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates
74- repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut
75- reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
217+ <!-- Empty State -->
218+ <div v-if =" filteredLogs.length === 0" class =" text-center py-12 px-6" >
219+ <p class =" text-sm" >{{ t('mcpInstallations.details.logs.emptyState.title') }}</p >
220+ <p class =" mt-2 text-sm text-muted-foreground max-w-md mx-auto" >
221+ {{ t('mcpInstallations.details.logs.emptyState.description') }}
76222 </p >
77223 </div >
78- </div >
79- </Card >
224+
225+ <!-- Logs Table -->
226+ <div v-else >
227+ <Table >
228+ <TableHeader >
229+ <TableRow >
230+ <TableHead class =" w-24" >{{ t('mcpInstallations.details.logs.table.columns.level') }}</TableHead >
231+ <TableHead class =" w-40" >{{ t('mcpInstallations.details.logs.table.columns.time') }}</TableHead >
232+ <TableHead >{{ t('mcpInstallations.details.logs.table.columns.message') }}</TableHead >
233+ </TableRow >
234+ </TableHeader >
235+ <TableBody >
236+ <TableRow
237+ v-for =" log in filteredLogs"
238+ :key =" log.id"
239+ >
240+ <TableCell class =" w-24" >
241+ <span class =" text-xs font-medium" :class =" getLevelTextClass(log.level)" >
242+ {{ log.level.toUpperCase() }}
243+ </span >
244+ </TableCell >
245+ <TableCell class =" text-sm text-muted-foreground font-mono tabular-nums" >
246+ <HoverCard >
247+ <HoverCardTrigger class =" cursor-pointer" >
248+ {{ formatTimestamp(log.created_at) }}
249+ </HoverCardTrigger >
250+ <HoverCardContent align =" start" class =" w-auto" >
251+ <table class =" text-sm" >
252+ <tbody >
253+ <tr >
254+ <td class =" text-muted-foreground pr-4 py-0.5" >{{ getUserTimezone() }}</td >
255+ <td class =" font-mono tabular-nums text-right py-0.5" >{{ formatLocalTimestamp(log.created_at) }}</td >
256+ </tr >
257+ <tr >
258+ <td class =" text-muted-foreground pr-4 py-0.5" >UTC</td >
259+ <td class =" font-mono tabular-nums text-right py-0.5" >{{ formatUtcTimestamp(log.created_at) }}</td >
260+ </tr >
261+ <tr >
262+ <td class =" text-muted-foreground pr-4 py-0.5" >Relative</td >
263+ <td class =" text-right py-0.5" >{{ formatRelativeTime(log.created_at) }}</td >
264+ </tr >
265+ <tr >
266+ <td class =" text-muted-foreground pr-4 py-0.5" >Timestamp</td >
267+ <td class =" font-mono tabular-nums text-right py-0.5" >{{ getUnixTimestamp(log.created_at) }}</td >
268+ </tr >
269+ </tbody >
270+ </table >
271+ </HoverCardContent >
272+ </HoverCard >
273+ </TableCell >
274+ <TableCell class =" text-sm" >
275+ {{ log.message }}
276+ </TableCell >
277+ </TableRow >
278+ </TableBody >
279+ </Table >
280+ </div >
281+ </Card >
282+ </div >
80283 </div >
81284</template >
0 commit comments