@geekmidas/studio 2.0.1 → 10.0.0-alpha.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.
Files changed (50) hide show
  1. package/package.json +8 -5
  2. package/CHANGELOG.md +0 -60
  3. package/src/Studio.ts +0 -367
  4. package/src/__tests__/Studio.spec.ts +0 -447
  5. package/src/data/DataBrowser.ts +0 -170
  6. package/src/data/__tests__/DataBrowser.integration.spec.ts +0 -418
  7. package/src/data/__tests__/filtering.integration.spec.ts +0 -741
  8. package/src/data/__tests__/introspection.integration.spec.ts +0 -352
  9. package/src/data/__tests__/pagination.spec.ts +0 -123
  10. package/src/data/filtering.ts +0 -191
  11. package/src/data/index.ts +0 -1
  12. package/src/data/introspection.ts +0 -220
  13. package/src/data/pagination.ts +0 -33
  14. package/src/index.ts +0 -29
  15. package/src/server/__tests__/hono.integration.spec.ts +0 -619
  16. package/src/server/hono.ts +0 -427
  17. package/src/types.ts +0 -278
  18. package/src/ui-assets.ts +0 -37
  19. package/tsconfig.json +0 -9
  20. package/tsdown.config.ts +0 -13
  21. package/ui/CHANGELOG.md +0 -26
  22. package/ui/index.html +0 -12
  23. package/ui/node_modules/.bin/tsc +0 -21
  24. package/ui/node_modules/.bin/tsserver +0 -21
  25. package/ui/node_modules/.bin/vite +0 -21
  26. package/ui/package.json +0 -30
  27. package/ui/src/App.tsx +0 -100
  28. package/ui/src/api.ts +0 -213
  29. package/ui/src/components/FilterPanel.tsx +0 -213
  30. package/ui/src/components/NavRail.tsx +0 -183
  31. package/ui/src/components/RowDetail.tsx +0 -119
  32. package/ui/src/components/StudioHeader.tsx +0 -109
  33. package/ui/src/components/TableList.tsx +0 -58
  34. package/ui/src/components/TableView.tsx +0 -564
  35. package/ui/src/main.tsx +0 -10
  36. package/ui/src/pages/DashboardPage.tsx +0 -500
  37. package/ui/src/pages/DatabasePage.tsx +0 -226
  38. package/ui/src/pages/EndpointDetailsPage.tsx +0 -288
  39. package/ui/src/pages/ExceptionsPage.tsx +0 -268
  40. package/ui/src/pages/LogsPage.tsx +0 -228
  41. package/ui/src/pages/MonitoringPage.tsx +0 -46
  42. package/ui/src/pages/PerformancePage.tsx +0 -307
  43. package/ui/src/pages/RequestsPage.tsx +0 -379
  44. package/ui/src/providers/StudioProvider.tsx +0 -194
  45. package/ui/src/styles.css +0 -105
  46. package/ui/src/types.ts +0 -174
  47. package/ui/src/vite-env.d.ts +0 -1
  48. package/ui/tsconfig.json +0 -21
  49. package/ui/tsconfig.tsbuildinfo +0 -1
  50. package/ui/vite.config.ts +0 -12
@@ -1,500 +0,0 @@
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
- }