@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
  3. package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
  4. package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
  5. package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
  6. package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
  7. package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
  8. package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
  9. package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
  10. package/dist/Studio-CYzz3wD2.d.cts +152 -0
  11. package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
  12. package/dist/Studio-D5yGscb8.d.mts +152 -0
  13. package/dist/Studio-D5yGscb8.d.mts.map +1 -0
  14. package/dist/data/index.cjs +1 -1
  15. package/dist/data/index.d.cts +1 -1
  16. package/dist/data/index.d.mts +1 -1
  17. package/dist/data/index.mjs +1 -1
  18. package/dist/index.cjs +33 -3
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +4 -131
  21. package/dist/index.d.mts +4 -131
  22. package/dist/index.mjs +33 -3
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/server/hono.cjs +168 -21
  25. package/dist/server/hono.cjs.map +1 -1
  26. package/dist/server/hono.d.cts +13 -2
  27. package/dist/server/hono.d.cts.map +1 -0
  28. package/dist/server/hono.d.mts +13 -2
  29. package/dist/server/hono.d.mts.map +1 -0
  30. package/dist/server/hono.mjs +168 -21
  31. package/dist/server/hono.mjs.map +1 -1
  32. package/dist/types-BZv87Ikv.mjs.map +1 -1
  33. package/dist/types-CMttUZYk.cjs.map +1 -1
  34. package/package.json +5 -5
  35. package/src/Studio.ts +341 -292
  36. package/src/__tests__/Studio.spec.ts +447 -0
  37. package/src/data/DataBrowser.ts +147 -143
  38. package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
  39. package/src/data/__tests__/filtering.integration.spec.ts +726 -726
  40. package/src/data/__tests__/introspection.integration.spec.ts +340 -340
  41. package/src/data/__tests__/pagination.spec.ts +123 -0
  42. package/src/data/filtering.ts +154 -154
  43. package/src/data/introspection.ts +141 -141
  44. package/src/data/pagination.ts +15 -15
  45. package/src/index.ts +22 -24
  46. package/src/server/__tests__/hono.integration.spec.ts +605 -347
  47. package/src/server/hono.ts +392 -190
  48. package/src/types.ts +138 -138
  49. package/src/ui-assets.ts +10 -13
  50. package/tsconfig.json +9 -0
  51. package/tsdown.config.ts +9 -9
  52. package/ui/CHANGELOG.md +12 -0
  53. package/ui/package.json +28 -22
  54. package/ui/src/App.tsx +95 -235
  55. package/ui/src/api.ts +184 -42
  56. package/ui/src/components/FilterPanel.tsx +198 -198
  57. package/ui/src/components/NavRail.tsx +183 -0
  58. package/ui/src/components/RowDetail.tsx +106 -106
  59. package/ui/src/components/StudioHeader.tsx +109 -0
  60. package/ui/src/components/TableList.tsx +49 -49
  61. package/ui/src/components/TableView.tsx +530 -485
  62. package/ui/src/main.tsx +3 -3
  63. package/ui/src/pages/DashboardPage.tsx +500 -0
  64. package/ui/src/pages/DatabasePage.tsx +226 -0
  65. package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
  66. package/ui/src/pages/ExceptionsPage.tsx +268 -0
  67. package/ui/src/pages/LogsPage.tsx +228 -0
  68. package/ui/src/pages/MonitoringPage.tsx +46 -0
  69. package/ui/src/pages/PerformancePage.tsx +307 -0
  70. package/ui/src/pages/RequestsPage.tsx +379 -0
  71. package/ui/src/providers/StudioProvider.tsx +194 -0
  72. package/ui/src/styles.css +53 -142
  73. package/ui/src/types.ts +154 -30
  74. package/ui/tsconfig.tsbuildinfo +1 -1
  75. package/ui/vite.config.ts +6 -6
  76. package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
  77. package/dist/DataBrowser-c-Gs6PZB.cjs.map +0 -1
package/ui/src/main.tsx CHANGED
@@ -4,7 +4,7 @@ import { App } from './App';
4
4
  import './styles.css';
5
5
 
6
6
  createRoot(document.getElementById('root')!).render(
7
- <StrictMode>
8
- <App />
9
- </StrictMode>,
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
10
  );
@@ -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
+ }