@geekmidas/studio 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
- package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
- package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
- package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
- package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
- package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
- package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
- package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
- package/dist/Studio-CYzz3wD2.d.cts +152 -0
- package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
- package/dist/Studio-D5yGscb8.d.mts +152 -0
- package/dist/Studio-D5yGscb8.d.mts.map +1 -0
- package/dist/data/index.cjs +1 -1
- package/dist/data/index.d.cts +1 -1
- package/dist/data/index.d.mts +1 -1
- package/dist/data/index.mjs +1 -1
- package/dist/index.cjs +33 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -131
- package/dist/index.d.mts +4 -131
- package/dist/index.mjs +33 -3
- package/dist/index.mjs.map +1 -1
- package/dist/server/hono.cjs +168 -21
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.d.cts +13 -2
- package/dist/server/hono.d.cts.map +1 -0
- package/dist/server/hono.d.mts +13 -2
- package/dist/server/hono.d.mts.map +1 -0
- package/dist/server/hono.mjs +168 -21
- package/dist/server/hono.mjs.map +1 -1
- package/dist/types-BZv87Ikv.mjs.map +1 -1
- package/dist/types-CMttUZYk.cjs.map +1 -1
- package/package.json +5 -5
- package/src/Studio.ts +341 -292
- package/src/__tests__/Studio.spec.ts +447 -0
- package/src/data/DataBrowser.ts +147 -143
- package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
- package/src/data/__tests__/filtering.integration.spec.ts +726 -726
- package/src/data/__tests__/introspection.integration.spec.ts +340 -340
- package/src/data/__tests__/pagination.spec.ts +123 -0
- package/src/data/filtering.ts +154 -154
- package/src/data/introspection.ts +141 -141
- package/src/data/pagination.ts +15 -15
- package/src/index.ts +22 -24
- package/src/server/__tests__/hono.integration.spec.ts +605 -347
- package/src/server/hono.ts +392 -190
- package/src/types.ts +138 -138
- package/src/ui-assets.ts +10 -13
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +9 -9
- package/ui/CHANGELOG.md +12 -0
- package/ui/package.json +28 -22
- package/ui/src/App.tsx +95 -235
- package/ui/src/api.ts +184 -42
- package/ui/src/components/FilterPanel.tsx +198 -198
- package/ui/src/components/NavRail.tsx +183 -0
- package/ui/src/components/RowDetail.tsx +106 -106
- package/ui/src/components/StudioHeader.tsx +109 -0
- package/ui/src/components/TableList.tsx +49 -49
- package/ui/src/components/TableView.tsx +530 -485
- package/ui/src/main.tsx +3 -3
- package/ui/src/pages/DashboardPage.tsx +500 -0
- package/ui/src/pages/DatabasePage.tsx +226 -0
- package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
- package/ui/src/pages/ExceptionsPage.tsx +268 -0
- package/ui/src/pages/LogsPage.tsx +228 -0
- package/ui/src/pages/MonitoringPage.tsx +46 -0
- package/ui/src/pages/PerformancePage.tsx +307 -0
- package/ui/src/pages/RequestsPage.tsx +379 -0
- package/ui/src/providers/StudioProvider.tsx +194 -0
- package/ui/src/styles.css +53 -142
- package/ui/src/types.ts +154 -30
- package/ui/tsconfig.tsbuildinfo +1 -1
- package/ui/vite.config.ts +6 -6
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +0 -1
package/ui/src/main.tsx
CHANGED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AreaTimeSeriesChart,
|
|
3
|
+
Badge,
|
|
4
|
+
MetricCard,
|
|
5
|
+
SparkBar,
|
|
6
|
+
} from '@geekmidas/ui';
|
|
7
|
+
import {
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
ArrowRight,
|
|
10
|
+
CheckCircle2,
|
|
11
|
+
Clock,
|
|
12
|
+
Database,
|
|
13
|
+
FileText,
|
|
14
|
+
Network,
|
|
15
|
+
TrendingUp,
|
|
16
|
+
Zap,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
19
|
+
import { Link } from 'react-router-dom';
|
|
20
|
+
import { getEndpointMetrics, getMetrics } from '../api';
|
|
21
|
+
import { useStudio } from '../providers/StudioProvider';
|
|
22
|
+
import type { EndpointMetrics, RequestMetrics } from '../types';
|
|
23
|
+
|
|
24
|
+
export function DashboardPage() {
|
|
25
|
+
const { stats, requests, exceptions, loading, realtimeMetrics, connected } =
|
|
26
|
+
useStudio();
|
|
27
|
+
|
|
28
|
+
// Fetch aggregated metrics and endpoint data
|
|
29
|
+
const [metricsData, setMetricsData] = useState<RequestMetrics | null>(null);
|
|
30
|
+
const [endpoints, setEndpoints] = useState<EndpointMetrics[]>([]);
|
|
31
|
+
|
|
32
|
+
const fetchMetrics = useCallback(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
36
|
+
const timeRange = {
|
|
37
|
+
start: oneHourAgo.toISOString(),
|
|
38
|
+
end: now.toISOString(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const [metrics, endpointData] = await Promise.all([
|
|
42
|
+
getMetrics(timeRange),
|
|
43
|
+
getEndpointMetrics({ limit: 5, ...timeRange }),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
setMetricsData(metrics);
|
|
47
|
+
setEndpoints(endpointData);
|
|
48
|
+
} catch (_error) {}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
fetchMetrics();
|
|
53
|
+
const interval = setInterval(fetchMetrics, 30000);
|
|
54
|
+
return () => clearInterval(interval);
|
|
55
|
+
}, [fetchMetrics]);
|
|
56
|
+
|
|
57
|
+
// Calculate local metrics from recent data for sparkline
|
|
58
|
+
const localMetrics = useMemo(() => {
|
|
59
|
+
if (!requests.length) {
|
|
60
|
+
return {
|
|
61
|
+
avgDuration: 0,
|
|
62
|
+
errorRate: 0,
|
|
63
|
+
requestsPerMinute: [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const totalDuration = requests.reduce((sum, r) => sum + r.duration, 0);
|
|
68
|
+
const avgDuration = totalDuration / requests.length;
|
|
69
|
+
|
|
70
|
+
const errorCount = requests.filter((r) => r.status >= 400).length;
|
|
71
|
+
const errorRate = (errorCount / requests.length) * 100;
|
|
72
|
+
|
|
73
|
+
// Group requests by minute for spark chart
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const oneMinuteAgo = now - 60 * 1000;
|
|
76
|
+
const recentRequests = requests.filter(
|
|
77
|
+
(r) => new Date(r.timestamp).getTime() > oneMinuteAgo,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Create 6 buckets of 10 seconds each
|
|
81
|
+
const buckets = Array(6).fill(0);
|
|
82
|
+
for (const r of recentRequests) {
|
|
83
|
+
const age = now - new Date(r.timestamp).getTime();
|
|
84
|
+
const bucket = Math.floor(age / 10000);
|
|
85
|
+
if (bucket >= 0 && bucket < 6) {
|
|
86
|
+
buckets[5 - bucket]++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
avgDuration,
|
|
92
|
+
errorRate,
|
|
93
|
+
requestsPerMinute: buckets,
|
|
94
|
+
};
|
|
95
|
+
}, [requests]);
|
|
96
|
+
|
|
97
|
+
// Sort endpoints by p95 latency for "slowest" view
|
|
98
|
+
const slowestEndpoints = useMemo(() => {
|
|
99
|
+
return [...endpoints].sort((a, b) => b.p95Duration - a.p95Duration);
|
|
100
|
+
}, [endpoints]);
|
|
101
|
+
|
|
102
|
+
// Calculate service health
|
|
103
|
+
const serviceHealth = useMemo(() => {
|
|
104
|
+
const errorRate = realtimeMetrics?.errorRate ?? localMetrics.errorRate;
|
|
105
|
+
const hasRecentErrors = exceptions.some((e) => {
|
|
106
|
+
const age = Date.now() - new Date(e.timestamp).getTime();
|
|
107
|
+
return age < 5 * 60 * 1000; // Last 5 minutes
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (errorRate > 10 || hasRecentErrors) {
|
|
111
|
+
return { status: 'unhealthy', label: 'Issues Detected', color: 'red' };
|
|
112
|
+
}
|
|
113
|
+
if (errorRate > 5) {
|
|
114
|
+
return { status: 'degraded', label: 'Degraded', color: 'yellow' };
|
|
115
|
+
}
|
|
116
|
+
return { status: 'healthy', label: 'Healthy', color: 'green' };
|
|
117
|
+
}, [realtimeMetrics, localMetrics.errorRate, exceptions]);
|
|
118
|
+
|
|
119
|
+
if (loading) {
|
|
120
|
+
return (
|
|
121
|
+
<div className="flex-1 flex items-center justify-center">
|
|
122
|
+
<div className="text-muted-foreground">Loading...</div>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="p-6 space-y-6">
|
|
129
|
+
{/* Service Health Banner */}
|
|
130
|
+
<div
|
|
131
|
+
className={`flex items-center gap-3 p-4 rounded-lg border ${
|
|
132
|
+
serviceHealth.color === 'green'
|
|
133
|
+
? 'bg-green-500/10 border-green-500/30'
|
|
134
|
+
: serviceHealth.color === 'yellow'
|
|
135
|
+
? 'bg-yellow-500/10 border-yellow-500/30'
|
|
136
|
+
: 'bg-red-500/10 border-red-500/30'
|
|
137
|
+
}`}
|
|
138
|
+
>
|
|
139
|
+
<CheckCircle2
|
|
140
|
+
className={`h-5 w-5 ${
|
|
141
|
+
serviceHealth.color === 'green'
|
|
142
|
+
? 'text-green-500'
|
|
143
|
+
: serviceHealth.color === 'yellow'
|
|
144
|
+
? 'text-yellow-500'
|
|
145
|
+
: 'text-red-500'
|
|
146
|
+
}`}
|
|
147
|
+
/>
|
|
148
|
+
<div className="flex-1">
|
|
149
|
+
<div className="flex items-center gap-2">
|
|
150
|
+
<span className="font-medium">Service Status:</span>
|
|
151
|
+
<span
|
|
152
|
+
className={
|
|
153
|
+
serviceHealth.color === 'green'
|
|
154
|
+
? 'text-green-500'
|
|
155
|
+
: serviceHealth.color === 'yellow'
|
|
156
|
+
? 'text-yellow-500'
|
|
157
|
+
: 'text-red-500'
|
|
158
|
+
}
|
|
159
|
+
>
|
|
160
|
+
{serviceHealth.label}
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
<p className="text-sm text-muted-foreground">
|
|
164
|
+
{connected ? 'Real-time monitoring active' : 'Connecting...'}
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
{realtimeMetrics && (
|
|
168
|
+
<div className="text-right text-sm">
|
|
169
|
+
<div className="text-muted-foreground">
|
|
170
|
+
{realtimeMetrics.requestsPerSecond.toFixed(1)} req/s
|
|
171
|
+
</div>
|
|
172
|
+
<div className="text-muted-foreground">
|
|
173
|
+
p95: {formatDuration(realtimeMetrics.p95)}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Stats Grid */}
|
|
180
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
181
|
+
<MetricCard
|
|
182
|
+
title="Total Requests"
|
|
183
|
+
value={metricsData?.totalRequests ?? stats?.requests ?? 0}
|
|
184
|
+
icon={<Network className="h-4 w-4" />}
|
|
185
|
+
sparklineData={localMetrics.requestsPerMinute}
|
|
186
|
+
description="last hour"
|
|
187
|
+
trend="neutral"
|
|
188
|
+
trendValue={`${(metricsData?.requestsPerSecond ?? 0).toFixed(1)}/s`}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
<MetricCard
|
|
192
|
+
title="Avg Duration"
|
|
193
|
+
value={formatDuration(
|
|
194
|
+
metricsData?.avgDuration ?? localMetrics.avgDuration,
|
|
195
|
+
)}
|
|
196
|
+
icon={<Clock className="h-4 w-4" />}
|
|
197
|
+
trend="neutral"
|
|
198
|
+
trendValue={`p95: ${formatDuration(metricsData?.p95Duration ?? 0)}`}
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
<MetricCard
|
|
202
|
+
title="Success Rate"
|
|
203
|
+
value={`${(metricsData?.successRate ?? 100 - localMetrics.errorRate).toFixed(1)}%`}
|
|
204
|
+
icon={<TrendingUp className="h-4 w-4" />}
|
|
205
|
+
trend={
|
|
206
|
+
(metricsData?.successRate ?? 100) >= 99
|
|
207
|
+
? 'up'
|
|
208
|
+
: (metricsData?.successRate ?? 100) >= 95
|
|
209
|
+
? 'neutral'
|
|
210
|
+
: 'down'
|
|
211
|
+
}
|
|
212
|
+
/>
|
|
213
|
+
|
|
214
|
+
<MetricCard
|
|
215
|
+
title="Exceptions"
|
|
216
|
+
value={stats?.exceptions ?? 0}
|
|
217
|
+
icon={<AlertTriangle className="h-4 w-4" />}
|
|
218
|
+
trend={(stats?.exceptions ?? 0) > 0 ? 'down' : 'up'}
|
|
219
|
+
trendValue={(stats?.exceptions ?? 0) > 0 ? 'Active' : 'None'}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Request Volume Chart */}
|
|
224
|
+
{metricsData?.timeSeries && metricsData.timeSeries.length > 0 && (
|
|
225
|
+
<div className="p-4 bg-surface rounded-lg border border-border">
|
|
226
|
+
<div className="flex items-center justify-between mb-4">
|
|
227
|
+
<h3 className="text-sm font-medium">Request Volume (last hour)</h3>
|
|
228
|
+
<Link
|
|
229
|
+
to="/analytics"
|
|
230
|
+
className="text-xs text-accent hover:underline"
|
|
231
|
+
>
|
|
232
|
+
View Analytics →
|
|
233
|
+
</Link>
|
|
234
|
+
</div>
|
|
235
|
+
<AreaTimeSeriesChart
|
|
236
|
+
data={metricsData.timeSeries.map((p) => ({
|
|
237
|
+
timestamp: p.timestamp,
|
|
238
|
+
value: p.count,
|
|
239
|
+
secondaryValue: p.errorCount,
|
|
240
|
+
}))}
|
|
241
|
+
primaryLabel="Requests"
|
|
242
|
+
secondaryLabel="Errors"
|
|
243
|
+
primaryColor="blue"
|
|
244
|
+
secondaryColor="red"
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{/* Fallback to sparkline if no time series data */}
|
|
250
|
+
{(!metricsData?.timeSeries || metricsData.timeSeries.length === 0) &&
|
|
251
|
+
localMetrics.requestsPerMinute.length > 0 && (
|
|
252
|
+
<div className="p-4 bg-surface rounded-lg border border-border">
|
|
253
|
+
<h3 className="text-sm font-medium mb-3">
|
|
254
|
+
Request Activity (last 60s)
|
|
255
|
+
</h3>
|
|
256
|
+
<SparkBar
|
|
257
|
+
data={localMetrics.requestsPerMinute}
|
|
258
|
+
height={60}
|
|
259
|
+
color="var(--color-accent)"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{/* Quick Links */}
|
|
265
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
266
|
+
<QuickLinkCard
|
|
267
|
+
to="/requests"
|
|
268
|
+
icon={<Network className="h-5 w-5" />}
|
|
269
|
+
title="Requests"
|
|
270
|
+
description="View HTTP requests and responses"
|
|
271
|
+
count={stats?.requests}
|
|
272
|
+
/>
|
|
273
|
+
<QuickLinkCard
|
|
274
|
+
to="/logs"
|
|
275
|
+
icon={<FileText className="h-5 w-5" />}
|
|
276
|
+
title="Logs"
|
|
277
|
+
description="Browse application logs"
|
|
278
|
+
count={stats?.logs}
|
|
279
|
+
/>
|
|
280
|
+
<QuickLinkCard
|
|
281
|
+
to="/database"
|
|
282
|
+
icon={<Database className="h-5 w-5" />}
|
|
283
|
+
title="Database"
|
|
284
|
+
description="Browse and query your database"
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Slowest Endpoints */}
|
|
289
|
+
{slowestEndpoints.length > 0 && (
|
|
290
|
+
<div className="bg-surface rounded-lg border border-border">
|
|
291
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
292
|
+
<div className="flex items-center gap-2">
|
|
293
|
+
<Zap className="h-4 w-4 text-amber-500" />
|
|
294
|
+
<h3 className="font-medium">Slowest Endpoints</h3>
|
|
295
|
+
</div>
|
|
296
|
+
<Link
|
|
297
|
+
to="/analytics"
|
|
298
|
+
className="text-sm text-accent hover:underline flex items-center gap-1"
|
|
299
|
+
>
|
|
300
|
+
View all <ArrowRight className="h-3 w-3" />
|
|
301
|
+
</Link>
|
|
302
|
+
</div>
|
|
303
|
+
<div className="divide-y divide-border">
|
|
304
|
+
{slowestEndpoints.slice(0, 5).map((endpoint, i) => (
|
|
305
|
+
<Link
|
|
306
|
+
key={`${endpoint.method}-${endpoint.path}`}
|
|
307
|
+
to={`/analytics/endpoint?method=${encodeURIComponent(endpoint.method)}&path=${encodeURIComponent(endpoint.path)}`}
|
|
308
|
+
className="flex items-center gap-3 px-4 py-2.5 hover:bg-surface-hover transition-colors"
|
|
309
|
+
>
|
|
310
|
+
<span className="text-xs text-muted-foreground w-4">
|
|
311
|
+
#{i + 1}
|
|
312
|
+
</span>
|
|
313
|
+
<Badge variant={getMethodVariant(endpoint.method)}>
|
|
314
|
+
{endpoint.method}
|
|
315
|
+
</Badge>
|
|
316
|
+
<span className="flex-1 truncate text-sm font-mono">
|
|
317
|
+
{endpoint.path}
|
|
318
|
+
</span>
|
|
319
|
+
<div className="text-right">
|
|
320
|
+
<div className="text-sm font-medium text-amber-500">
|
|
321
|
+
{formatDuration(endpoint.p95Duration)}
|
|
322
|
+
</div>
|
|
323
|
+
<div className="text-xs text-muted-foreground">p95</div>
|
|
324
|
+
</div>
|
|
325
|
+
</Link>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{/* Recent Activity */}
|
|
332
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
333
|
+
{/* Recent Requests */}
|
|
334
|
+
<div className="bg-surface rounded-lg border border-border">
|
|
335
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
336
|
+
<h3 className="font-medium">Recent Requests</h3>
|
|
337
|
+
<Link
|
|
338
|
+
to="/requests"
|
|
339
|
+
className="text-sm text-accent hover:underline flex items-center gap-1"
|
|
340
|
+
>
|
|
341
|
+
View all <ArrowRight className="h-3 w-3" />
|
|
342
|
+
</Link>
|
|
343
|
+
</div>
|
|
344
|
+
<div className="divide-y divide-border">
|
|
345
|
+
{requests.slice(0, 5).map((request) => (
|
|
346
|
+
<Link
|
|
347
|
+
key={request.id}
|
|
348
|
+
to={`/requests/${request.id}`}
|
|
349
|
+
className="flex items-center gap-3 px-4 py-2.5 hover:bg-surface-hover transition-colors"
|
|
350
|
+
>
|
|
351
|
+
<MethodBadge method={request.method} />
|
|
352
|
+
<span className="flex-1 truncate text-sm">{request.path}</span>
|
|
353
|
+
<StatusBadge status={request.status} />
|
|
354
|
+
<span className="text-xs text-muted-foreground">
|
|
355
|
+
{formatDuration(request.duration)}
|
|
356
|
+
</span>
|
|
357
|
+
</Link>
|
|
358
|
+
))}
|
|
359
|
+
{requests.length === 0 && (
|
|
360
|
+
<div className="px-4 py-8 text-center text-muted-foreground text-sm">
|
|
361
|
+
No requests yet
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
{/* Recent Exceptions */}
|
|
368
|
+
<div className="bg-surface rounded-lg border border-border">
|
|
369
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
370
|
+
<h3 className="font-medium">Recent Exceptions</h3>
|
|
371
|
+
<Link
|
|
372
|
+
to="/exceptions"
|
|
373
|
+
className="text-sm text-accent hover:underline flex items-center gap-1"
|
|
374
|
+
>
|
|
375
|
+
View all <ArrowRight className="h-3 w-3" />
|
|
376
|
+
</Link>
|
|
377
|
+
</div>
|
|
378
|
+
<div className="divide-y divide-border">
|
|
379
|
+
{exceptions.slice(0, 5).map((exception) => (
|
|
380
|
+
<Link
|
|
381
|
+
key={exception.id}
|
|
382
|
+
to={`/exceptions/${exception.id}`}
|
|
383
|
+
className="block px-4 py-2.5 hover:bg-surface-hover transition-colors"
|
|
384
|
+
>
|
|
385
|
+
<div className="flex items-center justify-between mb-1">
|
|
386
|
+
<span className="text-sm font-medium text-red-400">
|
|
387
|
+
{exception.name}
|
|
388
|
+
</span>
|
|
389
|
+
<span className="text-xs text-muted-foreground">
|
|
390
|
+
{formatTime(exception.timestamp)}
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
<p className="text-sm text-muted-foreground truncate">
|
|
394
|
+
{exception.message}
|
|
395
|
+
</p>
|
|
396
|
+
</Link>
|
|
397
|
+
))}
|
|
398
|
+
{exceptions.length === 0 && (
|
|
399
|
+
<div className="px-4 py-8 text-center text-muted-foreground text-sm">
|
|
400
|
+
No exceptions - great!
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function QuickLinkCard({
|
|
411
|
+
to,
|
|
412
|
+
icon,
|
|
413
|
+
title,
|
|
414
|
+
description,
|
|
415
|
+
count,
|
|
416
|
+
}: {
|
|
417
|
+
to: string;
|
|
418
|
+
icon: React.ReactNode;
|
|
419
|
+
title: string;
|
|
420
|
+
description: string;
|
|
421
|
+
count?: number;
|
|
422
|
+
}) {
|
|
423
|
+
return (
|
|
424
|
+
<Link
|
|
425
|
+
to={to}
|
|
426
|
+
className="group p-4 bg-surface rounded-lg border border-border hover:border-accent/50 transition-colors"
|
|
427
|
+
>
|
|
428
|
+
<div className="flex items-start gap-3">
|
|
429
|
+
<div className="p-2 rounded-md bg-surface-hover text-muted-foreground group-hover:text-accent transition-colors">
|
|
430
|
+
{icon}
|
|
431
|
+
</div>
|
|
432
|
+
<div className="flex-1 min-w-0">
|
|
433
|
+
<div className="flex items-center gap-2">
|
|
434
|
+
<h3 className="font-medium">{title}</h3>
|
|
435
|
+
{count !== undefined && (
|
|
436
|
+
<span className="text-xs text-muted-foreground">({count})</span>
|
|
437
|
+
)}
|
|
438
|
+
</div>
|
|
439
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
440
|
+
</div>
|
|
441
|
+
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-accent transition-colors" />
|
|
442
|
+
</div>
|
|
443
|
+
</Link>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function MethodBadge({ method }: { method: string }) {
|
|
448
|
+
const colors: Record<string, string> = {
|
|
449
|
+
GET: 'bg-blue-500/20 text-blue-400',
|
|
450
|
+
POST: 'bg-green-500/20 text-green-400',
|
|
451
|
+
PUT: 'bg-amber-500/20 text-amber-400',
|
|
452
|
+
PATCH: 'bg-purple-500/20 text-purple-400',
|
|
453
|
+
DELETE: 'bg-red-500/20 text-red-400',
|
|
454
|
+
};
|
|
455
|
+
return (
|
|
456
|
+
<span
|
|
457
|
+
className={`text-xs font-medium px-1.5 py-0.5 rounded ${colors[method] || 'bg-slate-500/20 text-slate-400'}`}
|
|
458
|
+
>
|
|
459
|
+
{method}
|
|
460
|
+
</span>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function StatusBadge({ status }: { status: number }) {
|
|
465
|
+
const color =
|
|
466
|
+
status >= 500
|
|
467
|
+
? 'text-red-400'
|
|
468
|
+
: status >= 400
|
|
469
|
+
? 'text-amber-400'
|
|
470
|
+
: status >= 300
|
|
471
|
+
? 'text-blue-400'
|
|
472
|
+
: 'text-green-400';
|
|
473
|
+
return <span className={`text-xs font-medium ${color}`}>{status}</span>;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function formatDuration(ms: number) {
|
|
477
|
+
if (ms < 1) return '<1ms';
|
|
478
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
479
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function formatTime(timestamp: string) {
|
|
483
|
+
return new Date(timestamp).toLocaleTimeString();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function getMethodVariant(
|
|
487
|
+
method: string,
|
|
488
|
+
): 'get' | 'post' | 'put' | 'patch' | 'delete' | 'secondary' {
|
|
489
|
+
const variants: Record<
|
|
490
|
+
string,
|
|
491
|
+
'get' | 'post' | 'put' | 'patch' | 'delete' | 'secondary'
|
|
492
|
+
> = {
|
|
493
|
+
GET: 'get',
|
|
494
|
+
POST: 'post',
|
|
495
|
+
PUT: 'put',
|
|
496
|
+
PATCH: 'patch',
|
|
497
|
+
DELETE: 'delete',
|
|
498
|
+
};
|
|
499
|
+
return variants[method.toUpperCase()] || 'secondary';
|
|
500
|
+
}
|