Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 67 additions & 12 deletions frontend/src/components/views/RequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 (
<Tooltip>
Expand Down Expand Up @@ -87,19 +128,25 @@ function StatusBadge({ success, error }: { success: boolean; error?: string }) {

export default function RequestView() {
const [requests, setRequests] = useState<Request[]>([]);
const [sourceTypes, setSourceTypes] = useState<Record<string, DatabaseType>>({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedSource, setSelectedSource] = useState<string | null>(null);

useEffect(() => {
fetchRequests()
.then((data) => {
setRequests(data.requests);
Promise.all([fetchRequests(), fetchSources()])
.then(([requestsData, sourcesData]) => {
setRequests(requestsData.requests);
const typeMap: Record<string, DatabaseType> = {};
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);
});
Comment on lines +137 to 152
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Promise.all means that if either fetchRequests or fetchSources fails, the entire data load fails and the user sees an error screen even if requests data could have been loaded successfully. Consider handling these API calls separately or implementing partial failure handling so users can still view request data even if source type information fails to load.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -158,16 +205,24 @@ export default function RequestView() {
</button>
{sourceIds.map((sourceId) => {
const count = requests.filter((r) => r.sourceId === sourceId).length;
const dbType = sourceTypes[sourceId];
return (
<button
key={sourceId}
onClick={() => setSelectedSource(sourceId)}
className={`px-3 py-1 text-sm font-medium rounded-full transition-colors ${
className={`px-3 py-1 text-sm font-medium rounded-full transition-colors flex items-center gap-1.5 ${
selectedSource === sourceId
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
{dbType && (
<img
src={DB_LOGOS[dbType]}
alt={`${dbType} logo`}
className="w-4 h-4"
/>
)}
{sourceId} ({count})
</button>
);
Expand All @@ -180,9 +235,6 @@ export default function RequestView() {
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap">
Time
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap">
Client
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap">
Tool
</th>
Expand All @@ -192,6 +244,9 @@ export default function RequestView() {
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap">
Result
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap">
Client
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
Expand All @@ -200,9 +255,6 @@ export default function RequestView() {
<td className="px-4 py-2 text-sm text-muted-foreground whitespace-nowrap">
{formatDate(request.timestamp)} {formatTime(request.timestamp)}
</td>
<td className="px-4 py-2 text-sm text-muted-foreground whitespace-nowrap">
{request.client}
</td>
<td className="px-4 py-2 text-sm whitespace-nowrap">
<Link
to={`/source/${request.sourceId}`}
Expand All @@ -224,6 +276,9 @@ export default function RequestView() {
<span className="text-muted-foreground">{request.durationMs}ms</span>
</div>
</td>
<td className="px-4 py-2 text-sm text-muted-foreground whitespace-nowrap">
{parseUserAgent(request.client)}
Copy link

Copilot AI Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parseUserAgent function is called for every row during each render. With up to 100 requests per source and potential re-renders when filtering or other state changes occur, this could result in unnecessary repeated parsing. Consider using useMemo to cache the parsed user agent values based on the requests array to avoid redundant regex operations.

Copilot uses AI. Check for mistakes.
</td>
</tr>
))}
</tbody>
Expand Down