diff --git a/frontend/src/components/views/RequestView.tsx b/frontend/src/components/views/RequestView.tsx index 9b79314..f7038ef 100644 --- a/frontend/src/components/views/RequestView.tsx +++ b/frontend/src/components/views/RequestView.tsx @@ -2,8 +2,11 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { Tooltip, TooltipTrigger, TooltipPopup, TooltipProvider } from '@/components/ui/tooltip'; import { fetchRequests } from '../../api/requests'; +import { fetchSources } from '../../api/sources'; import { ApiError } from '../../api/errors'; +import { DB_LOGOS } from '../../lib/db-logos'; import type { Request } from '../../types/request'; +import type { DatabaseType } from '../../types/datasource'; function formatTime(timestamp: string): string { const date = new Date(timestamp); @@ -29,6 +32,44 @@ function formatDate(timestamp: string): string { }); } +function parseUserAgent(ua: string): string { + // Check for common browsers in order of specificity + // Edge (Chromium-based) + const edgeMatch = ua.match(/Edg(?:e|A|iOS)?\/(\d+)/); + if (edgeMatch) return `Edge ${edgeMatch[1]}`; + + // Opera + const operaMatch = ua.match(/(?:OPR|Opera)\/(\d+)/); + if (operaMatch) return `Opera ${operaMatch[1]}`; + + // Chrome (must check after Edge/Opera since they include Chrome in UA) + const chromeMatch = ua.match(/Chrome\/(\d+)/); + if (chromeMatch && !ua.includes('Edg') && !ua.includes('OPR')) { + return `Chrome ${chromeMatch[1]}`; + } + + // Safari (must check after Chrome since Chrome includes Safari in UA) + const safariMatch = ua.match(/Version\/(\d+(?:\.\d+)?)\s+Safari/); + if (safariMatch) return `Safari ${safariMatch[1]}`; + + // Firefox + const firefoxMatch = ua.match(/Firefox\/(\d+)/); + if (firefoxMatch) return `Firefox ${firefoxMatch[1]}`; + + // Claude Desktop / Electron apps + if (ua.includes('Claude')) return 'Claude Desktop'; + if (ua.includes('Electron')) return 'Electron App'; + + // Cursor + if (ua.includes('Cursor')) return 'Cursor'; + + // Generic fallback - try to extract something useful + const genericMatch = ua.match(/^(\w+)\/[\d.]+/); + if (genericMatch) return genericMatch[1]; + + return ua.length > 20 ? ua.substring(0, 20) + '...' : ua; +} + function SqlTooltip({ sql, children }: { sql: string; children: React.ReactElement }) { return ( @@ -87,19 +128,25 @@ function StatusBadge({ success, error }: { success: boolean; error?: string }) { export default function RequestView() { const [requests, setRequests] = useState([]); + const [sourceTypes, setSourceTypes] = useState>({}); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [selectedSource, setSelectedSource] = useState(null); useEffect(() => { - fetchRequests() - .then((data) => { - setRequests(data.requests); + Promise.all([fetchRequests(), fetchSources()]) + .then(([requestsData, sourcesData]) => { + setRequests(requestsData.requests); + const typeMap: Record = {}; + for (const source of sourcesData) { + typeMap[source.id] = source.type; + } + setSourceTypes(typeMap); setIsLoading(false); }) .catch((err) => { - console.error('Failed to fetch requests:', err); - const message = err instanceof ApiError ? err.message : 'Failed to load requests'; + console.error('Failed to fetch data:', err); + const message = err instanceof ApiError ? err.message : 'Failed to load data'; setError(message); setIsLoading(false); }); @@ -158,16 +205,24 @@ export default function RequestView() { {sourceIds.map((sourceId) => { const count = requests.filter((r) => r.sourceId === sourceId).length; + const dbType = sourceTypes[sourceId]; return ( ); @@ -180,9 +235,6 @@ export default function RequestView() { Time - - Client - Tool @@ -192,6 +244,9 @@ export default function RequestView() { Result + + Client + @@ -200,9 +255,6 @@ export default function RequestView() { {formatDate(request.timestamp)} {formatTime(request.timestamp)} - - {request.client} - {request.durationMs}ms + + {parseUserAgent(request.client)} + ))}