@@ -2,8 +2,11 @@ import { useEffect, useState } from 'react';
22import { Link } from 'react-router-dom' ;
33import { Tooltip , TooltipTrigger , TooltipPopup , TooltipProvider } from '@/components/ui/tooltip' ;
44import { fetchRequests } from '../../api/requests' ;
5+ import { fetchSources } from '../../api/sources' ;
56import { ApiError } from '../../api/errors' ;
7+ import { DB_LOGOS } from '../../lib/db-logos' ;
68import type { Request } from '../../types/request' ;
9+ import type { DatabaseType } from '../../types/datasource' ;
710
811function formatTime ( timestamp : string ) : string {
912 const date = new Date ( timestamp ) ;
@@ -29,6 +32,44 @@ function formatDate(timestamp: string): string {
2932 } ) ;
3033}
3134
35+ function parseUserAgent ( ua : string ) : string {
36+ // Check for common browsers in order of specificity
37+ // Edge (Chromium-based)
38+ const edgeMatch = ua . match ( / E d g (?: e | A | i O S ) ? \/ ( \d + ) / ) ;
39+ if ( edgeMatch ) return `Edge ${ edgeMatch [ 1 ] } ` ;
40+
41+ // Opera
42+ const operaMatch = ua . match ( / (?: O P R | O p e r a ) \/ ( \d + ) / ) ;
43+ if ( operaMatch ) return `Opera ${ operaMatch [ 1 ] } ` ;
44+
45+ // Chrome (must check after Edge/Opera since they include Chrome in UA)
46+ const chromeMatch = ua . match ( / C h r o m e \/ ( \d + ) / ) ;
47+ if ( chromeMatch && ! ua . includes ( 'Edg' ) && ! ua . includes ( 'OPR' ) ) {
48+ return `Chrome ${ chromeMatch [ 1 ] } ` ;
49+ }
50+
51+ // Safari (must check after Chrome since Chrome includes Safari in UA)
52+ const safariMatch = ua . match ( / V e r s i o n \/ ( \d + (?: \. \d + ) ? ) \s + S a f a r i / ) ;
53+ if ( safariMatch ) return `Safari ${ safariMatch [ 1 ] } ` ;
54+
55+ // Firefox
56+ const firefoxMatch = ua . match ( / F i r e f o x \/ ( \d + ) / ) ;
57+ if ( firefoxMatch ) return `Firefox ${ firefoxMatch [ 1 ] } ` ;
58+
59+ // Claude Desktop / Electron apps
60+ if ( ua . includes ( 'Claude' ) ) return 'Claude Desktop' ;
61+ if ( ua . includes ( 'Electron' ) ) return 'Electron App' ;
62+
63+ // Cursor
64+ if ( ua . includes ( 'Cursor' ) ) return 'Cursor' ;
65+
66+ // Generic fallback - try to extract something useful
67+ const genericMatch = ua . match ( / ^ ( \w + ) \/ [ \d . ] + / ) ;
68+ if ( genericMatch ) return genericMatch [ 1 ] ;
69+
70+ return ua . length > 20 ? ua . substring ( 0 , 20 ) + '...' : ua ;
71+ }
72+
3273function SqlTooltip ( { sql, children } : { sql : string ; children : React . ReactElement } ) {
3374 return (
3475 < Tooltip >
@@ -87,19 +128,25 @@ function StatusBadge({ success, error }: { success: boolean; error?: string }) {
87128
88129export default function RequestView ( ) {
89130 const [ requests , setRequests ] = useState < Request [ ] > ( [ ] ) ;
131+ const [ sourceTypes , setSourceTypes ] = useState < Record < string , DatabaseType > > ( { } ) ;
90132 const [ isLoading , setIsLoading ] = useState ( true ) ;
91133 const [ error , setError ] = useState < string | null > ( null ) ;
92134 const [ selectedSource , setSelectedSource ] = useState < string | null > ( null ) ;
93135
94136 useEffect ( ( ) => {
95- fetchRequests ( )
96- . then ( ( data ) => {
97- setRequests ( data . requests ) ;
137+ Promise . all ( [ fetchRequests ( ) , fetchSources ( ) ] )
138+ . then ( ( [ requestsData , sourcesData ] ) => {
139+ setRequests ( requestsData . requests ) ;
140+ const typeMap : Record < string , DatabaseType > = { } ;
141+ for ( const source of sourcesData ) {
142+ typeMap [ source . id ] = source . type ;
143+ }
144+ setSourceTypes ( typeMap ) ;
98145 setIsLoading ( false ) ;
99146 } )
100147 . catch ( ( err ) => {
101- console . error ( 'Failed to fetch requests :' , err ) ;
102- const message = err instanceof ApiError ? err . message : 'Failed to load requests ' ;
148+ console . error ( 'Failed to fetch data :' , err ) ;
149+ const message = err instanceof ApiError ? err . message : 'Failed to load data ' ;
103150 setError ( message ) ;
104151 setIsLoading ( false ) ;
105152 } ) ;
@@ -158,16 +205,24 @@ export default function RequestView() {
158205 </ button >
159206 { sourceIds . map ( ( sourceId ) => {
160207 const count = requests . filter ( ( r ) => r . sourceId === sourceId ) . length ;
208+ const dbType = sourceTypes [ sourceId ] ;
161209 return (
162210 < button
163211 key = { sourceId }
164212 onClick = { ( ) => setSelectedSource ( sourceId ) }
165- className = { `px-3 py-1 text-sm font-medium rounded-full transition-colors ${
213+ className = { `px-3 py-1 text-sm font-medium rounded-full transition-colors flex items-center gap-1.5 ${
166214 selectedSource === sourceId
167215 ? 'bg-primary text-primary-foreground'
168216 : 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground'
169217 } `}
170218 >
219+ { dbType && (
220+ < img
221+ src = { DB_LOGOS [ dbType ] }
222+ alt = { `${ dbType } logo` }
223+ className = "w-4 h-4"
224+ />
225+ ) }
171226 { sourceId } ({ count } )
172227 </ button >
173228 ) ;
@@ -180,9 +235,6 @@ export default function RequestView() {
180235 < th className = "px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap" >
181236 Time
182237 </ th >
183- < th className = "px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap" >
184- Client
185- </ th >
186238 < th className = "px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap" >
187239 Tool
188240 </ th >
@@ -192,6 +244,9 @@ export default function RequestView() {
192244 < th className = "px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap" >
193245 Result
194246 </ th >
247+ < th className = "px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap" >
248+ Client
249+ </ th >
195250 </ tr >
196251 </ thead >
197252 < tbody className = "divide-y divide-border" >
@@ -200,9 +255,6 @@ export default function RequestView() {
200255 < td className = "px-4 py-2 text-sm text-muted-foreground whitespace-nowrap" >
201256 { formatDate ( request . timestamp ) } { formatTime ( request . timestamp ) }
202257 </ td >
203- < td className = "px-4 py-2 text-sm text-muted-foreground whitespace-nowrap" >
204- { request . client }
205- </ td >
206258 < td className = "px-4 py-2 text-sm whitespace-nowrap" >
207259 < Link
208260 to = { `/source/${ request . sourceId } ` }
@@ -224,6 +276,9 @@ export default function RequestView() {
224276 < span className = "text-muted-foreground" > { request . durationMs } ms</ span >
225277 </ div >
226278 </ td >
279+ < td className = "px-4 py-2 text-sm text-muted-foreground whitespace-nowrap" >
280+ { parseUserAgent ( request . client ) }
281+ </ td >
227282 </ tr >
228283 ) ) }
229284 </ tbody >
0 commit comments