Skip to content

Commit 4cd97ce

Browse files
authored
feat: improve request table (#218)
* feat: add database logo to the request filter tab * chore: shorten browser user agent text * chore: reorder
1 parent f95ed7a commit 4cd97ce

File tree

1 file changed

+67
-12
lines changed

1 file changed

+67
-12
lines changed

frontend/src/components/views/RequestView.tsx

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { useEffect, useState } from 'react';
22
import { Link } from 'react-router-dom';
33
import { Tooltip, TooltipTrigger, TooltipPopup, TooltipProvider } from '@/components/ui/tooltip';
44
import { fetchRequests } from '../../api/requests';
5+
import { fetchSources } from '../../api/sources';
56
import { ApiError } from '../../api/errors';
7+
import { DB_LOGOS } from '../../lib/db-logos';
68
import type { Request } from '../../types/request';
9+
import type { DatabaseType } from '../../types/datasource';
710

811
function 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(/Edg(?:e|A|iOS)?\/(\d+)/);
39+
if (edgeMatch) return `Edge ${edgeMatch[1]}`;
40+
41+
// Opera
42+
const operaMatch = ua.match(/(?:OPR|Opera)\/(\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(/Chrome\/(\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(/Version\/(\d+(?:\.\d+)?)\s+Safari/);
53+
if (safariMatch) return `Safari ${safariMatch[1]}`;
54+
55+
// Firefox
56+
const firefoxMatch = ua.match(/Firefox\/(\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+
3273
function 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

88129
export 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

Comments
 (0)