@gagandeep023/api-gateway 0.1.0 → 0.2.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.
@@ -250,6 +250,257 @@
250
250
  font-size: 0.8rem;
251
251
  }
252
252
 
253
+ /* API Keys Section */
254
+ .gw-keys-section {
255
+ margin-top: 32px;
256
+ }
257
+
258
+ .gw-keys-header {
259
+ margin-bottom: 20px;
260
+ }
261
+
262
+ .gw-keys-header h2 {
263
+ font-size: 1.2rem;
264
+ color: var(--gw-text-primary);
265
+ margin: 0 0 6px;
266
+ }
267
+
268
+ .gw-keys-header p {
269
+ color: var(--gw-text-muted);
270
+ font-size: 0.85rem;
271
+ margin: 0;
272
+ }
273
+
274
+ .gw-keys-create {
275
+ background: var(--gw-bg-card);
276
+ border: 1px solid var(--gw-border);
277
+ border-radius: var(--gw-radius);
278
+ padding: 20px;
279
+ margin-bottom: 16px;
280
+ }
281
+
282
+ .gw-keys-form {
283
+ display: flex;
284
+ gap: 10px;
285
+ align-items: center;
286
+ }
287
+
288
+ .gw-keys-input {
289
+ flex: 1;
290
+ padding: 10px 14px;
291
+ background: var(--gw-bg-primary);
292
+ border: 1px solid var(--gw-border);
293
+ border-radius: var(--gw-radius-sm);
294
+ color: var(--gw-text-primary);
295
+ font-size: 0.85rem;
296
+ font-family: inherit;
297
+ outline: none;
298
+ transition: border-color var(--gw-transition);
299
+ }
300
+
301
+ .gw-keys-input:focus {
302
+ border-color: var(--gw-accent);
303
+ }
304
+
305
+ .gw-keys-input::placeholder {
306
+ color: var(--gw-text-muted);
307
+ }
308
+
309
+ .gw-keys-select {
310
+ padding: 10px 14px;
311
+ background: var(--gw-bg-primary);
312
+ border: 1px solid var(--gw-border);
313
+ border-radius: var(--gw-radius-sm);
314
+ color: var(--gw-text-primary);
315
+ font-size: 0.85rem;
316
+ font-family: var(--gw-font-mono);
317
+ outline: none;
318
+ cursor: pointer;
319
+ }
320
+
321
+ .gw-keys-btn {
322
+ padding: 10px 20px;
323
+ background: var(--gw-accent);
324
+ color: #0a0a0a;
325
+ border: none;
326
+ border-radius: var(--gw-radius-sm);
327
+ font-size: 0.85rem;
328
+ font-weight: 600;
329
+ cursor: pointer;
330
+ white-space: nowrap;
331
+ transition: opacity var(--gw-transition);
332
+ }
333
+
334
+ .gw-keys-btn:hover {
335
+ opacity: 0.9;
336
+ }
337
+
338
+ .gw-keys-btn:disabled {
339
+ opacity: 0.5;
340
+ cursor: not-allowed;
341
+ }
342
+
343
+ .gw-keys-error {
344
+ margin-top: 10px;
345
+ color: var(--gw-danger);
346
+ font-size: 0.8rem;
347
+ }
348
+
349
+ .gw-keys-list {
350
+ display: flex;
351
+ flex-direction: column;
352
+ gap: 12px;
353
+ }
354
+
355
+ .gw-key-card {
356
+ background: var(--gw-bg-card);
357
+ border: 1px solid var(--gw-border);
358
+ border-radius: var(--gw-radius);
359
+ padding: 16px 20px;
360
+ transition: border-color var(--gw-transition);
361
+ }
362
+
363
+ .gw-key-card:hover {
364
+ border-color: var(--gw-border-light);
365
+ }
366
+
367
+ .gw-key-card.gw-key-revoked {
368
+ opacity: 0.5;
369
+ }
370
+
371
+ .gw-key-top {
372
+ display: flex;
373
+ justify-content: space-between;
374
+ align-items: center;
375
+ margin-bottom: 10px;
376
+ }
377
+
378
+ .gw-key-name {
379
+ color: var(--gw-text-primary);
380
+ font-weight: 600;
381
+ font-size: 0.9rem;
382
+ }
383
+
384
+ .gw-key-badges {
385
+ display: flex;
386
+ gap: 8px;
387
+ }
388
+
389
+ .gw-key-tier {
390
+ padding: 2px 8px;
391
+ border-radius: 4px;
392
+ font-size: 0.7rem;
393
+ font-weight: 600;
394
+ font-family: var(--gw-font-mono);
395
+ background: rgba(100, 255, 218, 0.1);
396
+ color: var(--gw-accent);
397
+ }
398
+
399
+ .gw-key-status {
400
+ padding: 2px 8px;
401
+ border-radius: 4px;
402
+ font-size: 0.7rem;
403
+ font-weight: 600;
404
+ font-family: var(--gw-font-mono);
405
+ }
406
+
407
+ .gw-key-active {
408
+ background: rgba(100, 255, 218, 0.1);
409
+ color: var(--gw-accent);
410
+ }
411
+
412
+ .gw-key-inactive {
413
+ background: rgba(204, 68, 68, 0.1);
414
+ color: var(--gw-danger);
415
+ }
416
+
417
+ .gw-key-value {
418
+ display: flex;
419
+ align-items: center;
420
+ gap: 10px;
421
+ padding: 8px 12px;
422
+ background: var(--gw-bg-primary);
423
+ border: 1px solid var(--gw-border);
424
+ border-radius: var(--gw-radius-sm);
425
+ margin-bottom: 10px;
426
+ }
427
+
428
+ .gw-key-value code {
429
+ flex: 1;
430
+ font-family: var(--gw-font-mono);
431
+ font-size: 0.8rem;
432
+ color: var(--gw-warning);
433
+ word-break: break-all;
434
+ }
435
+
436
+ .gw-key-copy {
437
+ padding: 4px 12px;
438
+ background: transparent;
439
+ border: 1px solid var(--gw-border);
440
+ border-radius: 4px;
441
+ color: var(--gw-text-secondary);
442
+ font-size: 0.75rem;
443
+ cursor: pointer;
444
+ white-space: nowrap;
445
+ transition: all var(--gw-transition);
446
+ }
447
+
448
+ .gw-key-copy:hover {
449
+ border-color: var(--gw-accent);
450
+ color: var(--gw-accent);
451
+ }
452
+
453
+ .gw-key-bottom {
454
+ display: flex;
455
+ justify-content: space-between;
456
+ align-items: center;
457
+ }
458
+
459
+ .gw-key-id {
460
+ font-family: var(--gw-font-mono);
461
+ font-size: 0.75rem;
462
+ color: var(--gw-text-muted);
463
+ }
464
+
465
+ .gw-key-revoke {
466
+ padding: 4px 12px;
467
+ background: transparent;
468
+ border: 1px solid var(--gw-danger);
469
+ border-radius: 4px;
470
+ color: var(--gw-danger);
471
+ font-size: 0.75rem;
472
+ cursor: pointer;
473
+ transition: all var(--gw-transition);
474
+ }
475
+
476
+ .gw-key-revoke:hover {
477
+ background: rgba(204, 68, 68, 0.1);
478
+ }
479
+
480
+ .gw-key-usage {
481
+ margin-top: 10px;
482
+ padding: 10px 12px;
483
+ background: rgba(100, 255, 218, 0.05);
484
+ border: 1px solid rgba(100, 255, 218, 0.15);
485
+ border-radius: var(--gw-radius-sm);
486
+ }
487
+
488
+ .gw-key-usage-label {
489
+ display: block;
490
+ font-size: 0.7rem;
491
+ color: var(--gw-accent);
492
+ text-transform: uppercase;
493
+ letter-spacing: 0.5px;
494
+ margin-bottom: 6px;
495
+ }
496
+
497
+ .gw-key-usage code {
498
+ font-family: var(--gw-font-mono);
499
+ font-size: 0.75rem;
500
+ color: var(--gw-text-secondary);
501
+ word-break: break-all;
502
+ }
503
+
253
504
  @media (max-width: 900px) {
254
505
  .gw-stats-grid {
255
506
  grid-template-columns: repeat(2, 1fr);
@@ -42,6 +42,54 @@ function GatewayDashboard({ apiBaseUrl }) {
42
42
  const { data: config } = useGatewayApi(apiBaseUrl, "/gateway/config");
43
43
  const { data: logsData } = useGatewayApi(apiBaseUrl, "/gateway/logs?limit=20");
44
44
  const eventSourceRef = (0, import_react.useRef)(null);
45
+ const [keyName, setKeyName] = (0, import_react.useState)("");
46
+ const [keyTier, setKeyTier] = (0, import_react.useState)("free");
47
+ const [createdKeys, setCreatedKeys] = (0, import_react.useState)([]);
48
+ const [keyError, setKeyError] = (0, import_react.useState)("");
49
+ const [keyLoading, setKeyLoading] = (0, import_react.useState)(false);
50
+ const [copiedKeyId, setCopiedKeyId] = (0, import_react.useState)(null);
51
+ const handleCreateKey = async () => {
52
+ if (!keyName.trim()) {
53
+ setKeyError("Name is required");
54
+ return;
55
+ }
56
+ setKeyError("");
57
+ setKeyLoading(true);
58
+ try {
59
+ const res = await fetch(`${apiBaseUrl}/gateway/keys`, {
60
+ method: "POST",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ name: keyName.trim(), tier: keyTier })
63
+ });
64
+ if (!res.ok) {
65
+ const err = await res.json();
66
+ setKeyError(err.error || "Failed to create key");
67
+ return;
68
+ }
69
+ const newKey = await res.json();
70
+ setCreatedKeys((prev) => [{ ...newKey, justCreated: true }, ...prev]);
71
+ setKeyName("");
72
+ } catch {
73
+ setKeyError("Network error");
74
+ } finally {
75
+ setKeyLoading(false);
76
+ }
77
+ };
78
+ const handleRevokeKey = async (keyId) => {
79
+ try {
80
+ const res = await fetch(`${apiBaseUrl}/gateway/keys/${keyId}`, { method: "DELETE" });
81
+ if (res.ok) {
82
+ setCreatedKeys((prev) => prev.map((k) => k.id === keyId ? { ...k, active: false } : k));
83
+ }
84
+ } catch {
85
+ }
86
+ };
87
+ const handleCopyKey = (key, keyId) => {
88
+ navigator.clipboard.writeText(key).then(() => {
89
+ setCopiedKeyId(keyId);
90
+ setTimeout(() => setCopiedKeyId(null), 2e3);
91
+ });
92
+ };
45
93
  (0, import_react.useEffect)(() => {
46
94
  const es = new EventSource(`${apiBaseUrl}/gateway/analytics/live`);
47
95
  eventSourceRef.current = es;
@@ -260,6 +308,87 @@ function GatewayDashboard({ apiBaseUrl }) {
260
308
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gw-tier-detail", children: config.activeKeyUses })
261
309
  ] })
262
310
  ] })
311
+ ] }),
312
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-keys-section", children: [
313
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-keys-header", children: [
314
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: "API Keys" }),
315
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: "Create keys to authenticate API requests. Each key is tied to a rate limit tier." })
316
+ ] }),
317
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-keys-create", children: [
318
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-keys-form", children: [
319
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
320
+ "input",
321
+ {
322
+ type: "text",
323
+ className: "gw-keys-input",
324
+ placeholder: "Key name (e.g. My App)",
325
+ value: keyName,
326
+ onChange: (e) => setKeyName(e.target.value),
327
+ onKeyDown: (e) => e.key === "Enter" && handleCreateKey()
328
+ }
329
+ ),
330
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
331
+ "select",
332
+ {
333
+ className: "gw-keys-select",
334
+ value: keyTier,
335
+ onChange: (e) => setKeyTier(e.target.value),
336
+ children: config && Object.keys(config.rateLimits.tiers).map((tier) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: tier, children: tier }, tier))
337
+ }
338
+ ),
339
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
340
+ "button",
341
+ {
342
+ className: "gw-keys-btn",
343
+ onClick: handleCreateKey,
344
+ disabled: keyLoading,
345
+ children: keyLoading ? "Creating..." : "Create Key"
346
+ }
347
+ )
348
+ ] }),
349
+ keyError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gw-keys-error", children: keyError })
350
+ ] }),
351
+ createdKeys.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "gw-keys-list", children: createdKeys.map((k) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `gw-key-card ${!k.active ? "gw-key-revoked" : ""}`, children: [
352
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-key-top", children: [
353
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gw-key-name", children: k.name }),
354
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-key-badges", children: [
355
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gw-key-tier", children: k.tier }),
356
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `gw-key-status ${k.active ? "gw-key-active" : "gw-key-inactive"}`, children: k.active ? "active" : "revoked" })
357
+ ] })
358
+ ] }),
359
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-key-value", children: [
360
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: k.key }),
361
+ k.active && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
362
+ "button",
363
+ {
364
+ className: "gw-key-copy",
365
+ onClick: () => handleCopyKey(k.key, k.id),
366
+ children: copiedKeyId === k.id ? "Copied!" : "Copy"
367
+ }
368
+ )
369
+ ] }),
370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-key-bottom", children: [
371
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gw-key-id", children: k.id }),
372
+ k.active && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
373
+ "button",
374
+ {
375
+ className: "gw-key-revoke",
376
+ onClick: () => handleRevokeKey(k.id),
377
+ children: "Revoke"
378
+ }
379
+ )
380
+ ] }),
381
+ k.justCreated && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "gw-key-usage", children: [
382
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "gw-key-usage-label", children: "Usage:" }),
383
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", { children: [
384
+ 'curl -H "X-API-Key: ',
385
+ k.key,
386
+ '" ',
387
+ apiBaseUrl,
388
+ "/health"
389
+ ] })
390
+ ] })
391
+ ] }, k.id)) })
263
392
  ] })
264
393
  ] });
265
394
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/frontend/index.ts","../../src/frontend/GatewayDashboard.tsx"],"sourcesContent":["export { GatewayDashboard } from './GatewayDashboard';\nexport type { GatewayDashboardProps } from './GatewayDashboard';\n","import { useState, useEffect, useRef } from 'react';\nimport { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';\nimport type { GatewayAnalytics, GatewayConfig, RequestLog } from '../types';\n\nexport interface GatewayDashboardProps {\n apiBaseUrl: string;\n}\n\ninterface LogsResponse {\n logs: RequestLog[];\n limit: number;\n offset: number;\n}\n\nfunction useGatewayApi<T>(apiBaseUrl: string, path: string): { data: T | null } {\n const [data, setData] = useState<T | null>(null);\n useEffect(() => {\n fetch(`${apiBaseUrl}${path}`)\n .then(r => r.json())\n .then(setData)\n .catch(() => {});\n }, [apiBaseUrl, path]);\n return { data };\n}\n\nexport function GatewayDashboard({ apiBaseUrl }: GatewayDashboardProps) {\n const [analytics, setAnalytics] = useState<GatewayAnalytics | null>(null);\n const [rpmHistory, setRpmHistory] = useState<{ time: string; rpm: number }[]>([]);\n const { data: config } = useGatewayApi<GatewayConfig>(apiBaseUrl, '/gateway/config');\n const { data: logsData } = useGatewayApi<LogsResponse>(apiBaseUrl, '/gateway/logs?limit=20');\n const eventSourceRef = useRef<EventSource | null>(null);\n\n useEffect(() => {\n const es = new EventSource(`${apiBaseUrl}/gateway/analytics/live`);\n eventSourceRef.current = es;\n\n es.onmessage = (event) => {\n const data: GatewayAnalytics = JSON.parse(event.data);\n setAnalytics(data);\n setRpmHistory(prev => {\n const next = [\n ...prev,\n { time: new Date().toLocaleTimeString(), rpm: data.requestsPerMinute },\n ];\n return next.slice(-20);\n });\n };\n\n return () => {\n es.close();\n };\n }, [apiBaseUrl]);\n\n const getMethodClass = (method: string) => {\n switch (method) {\n case 'GET': return 'gw-method-get';\n case 'POST': return 'gw-method-post';\n case 'DELETE': return 'gw-method-delete';\n default: return 'gw-method-get';\n }\n };\n\n const getStatusClass = (code: number) => {\n if (code === 429) return 'gw-status-rate-limit';\n if (code >= 400) return 'gw-status-error';\n return 'gw-status-ok';\n };\n\n return (\n <div className=\"gw-dashboard\">\n <div className=\"gw-header\">\n <h1>API Gateway Dashboard</h1>\n <p>Real-time monitoring for the API gateway and rate limiter</p>\n <div className=\"gw-status-badge\">\n <span className=\"gw-status-dot\" />\n Live\n </div>\n </div>\n\n {/* Stats Grid */}\n <div className=\"gw-stats-grid\">\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Total Requests</div>\n <div className=\"gw-stat-value\">{analytics?.totalRequests ?? 0}</div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Requests / Min</div>\n <div className=\"gw-stat-value gw-accent\">\n {analytics?.requestsPerMinute ?? 0}\n </div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Error Rate</div>\n <div className={`gw-stat-value ${analytics && analytics.errorRate > 5 ? 'gw-danger' : ''}`}>\n {analytics?.errorRate ?? 0}%\n </div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Avg Response Time</div>\n <div className=\"gw-stat-value\">{analytics?.avgResponseTime ?? 0}ms</div>\n </div>\n </div>\n\n {/* Second Row Stats */}\n <div className=\"gw-stats-grid\" style={{ marginBottom: 32 }}>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Rate Limit Hits</div>\n <div className=\"gw-stat-value gw-warning\">\n {analytics?.rateLimitHits ?? 0}\n </div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Active IPs</div>\n <div className=\"gw-stat-value\">{analytics?.activeClients ?? 0}</div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Active Key Sessions</div>\n <div className=\"gw-stat-value\">{analytics?.activeKeyUses ?? 0}</div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Rate Limit Tiers</div>\n <div className=\"gw-stat-value\">\n {config ? Object.keys(config.rateLimits.tiers).length : 0}\n </div>\n </div>\n </div>\n\n {/* Charts Row */}\n <div className=\"gw-charts-row\">\n <div className=\"gw-chart-card\">\n <div className=\"gw-chart-title\">Requests Per Minute</div>\n <ResponsiveContainer width=\"100%\" height={250}>\n <LineChart data={rpmHistory}>\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--gw-border, #2a2a2a)\" />\n <XAxis dataKey=\"time\" tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 11 }} />\n <YAxis tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 11 }} />\n <Tooltip\n contentStyle={{ background: 'var(--gw-bg-card, #1a1a1a)', border: '1px solid var(--gw-border, #2a2a2a)', borderRadius: 8 }}\n labelStyle={{ color: 'var(--gw-text-muted, #888)' }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"rpm\"\n stroke=\"var(--gw-accent, #64ffda)\"\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 4, fill: 'var(--gw-accent, #64ffda)' }}\n />\n </LineChart>\n </ResponsiveContainer>\n </div>\n\n <div className=\"gw-chart-card\">\n <div className=\"gw-chart-title\">Top Endpoints</div>\n <ResponsiveContainer width=\"100%\" height={250}>\n <BarChart\n data={analytics?.topEndpoints ?? []}\n layout=\"vertical\"\n >\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--gw-border, #2a2a2a)\" />\n <XAxis type=\"number\" tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 11 }} />\n <YAxis\n dataKey=\"path\"\n type=\"category\"\n tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 10 }}\n width={120}\n />\n <Tooltip\n contentStyle={{ background: 'var(--gw-bg-card, #1a1a1a)', border: '1px solid var(--gw-border, #2a2a2a)', borderRadius: 8 }}\n />\n <Bar dataKey=\"count\" fill=\"var(--gw-accent, #64ffda)\" radius={[0, 4, 4, 0]} />\n </BarChart>\n </ResponsiveContainer>\n </div>\n </div>\n\n {/* Recent Logs */}\n <div className=\"gw-logs-section\">\n <div className=\"gw-logs-title\">Recent Requests</div>\n <table className=\"gw-logs-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Method</th>\n <th>Path</th>\n <th>Status</th>\n <th>Duration</th>\n <th>IP</th>\n </tr>\n </thead>\n <tbody>\n {(logsData?.logs ?? []).map((log, i) => (\n <tr key={i}>\n <td>{new Date(log.timestamp).toLocaleTimeString()}</td>\n <td>\n <span className={`gw-method-badge ${getMethodClass(log.method)}`}>\n {log.method}\n </span>\n </td>\n <td>{log.path}</td>\n <td className={getStatusClass(log.statusCode)}>{log.statusCode}</td>\n <td>{log.responseTime}ms</td>\n <td>{log.ip}</td>\n </tr>\n ))}\n {(!logsData?.logs || logsData.logs.length === 0) && (\n <tr>\n <td colSpan={6} style={{ textAlign: 'center', color: 'var(--gw-text-muted, #666)' }}>\n No requests logged yet. Make some API calls to see data here.\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n\n {/* Config Section */}\n {config && (\n <div className=\"gw-config-section\">\n <div className=\"gw-config-card\">\n <h3>Rate Limit Tiers</h3>\n {Object.entries(config.rateLimits.tiers).map(([name, tier]) => (\n <div key={name} className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">{name}</span>\n <span className=\"gw-tier-detail\">\n {tier.algorithm === 'none'\n ? 'unlimited'\n : `${tier.maxRequests} req / ${(tier.windowMs || 60000) / 1000}s`}\n </span>\n </div>\n ))}\n </div>\n\n <div className=\"gw-config-card\">\n <h3>IP Rules</h3>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Mode</span>\n <span className=\"gw-tier-detail\">{config.ipRules.mode}</span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Allowlist</span>\n <span className=\"gw-tier-detail\">\n {config.ipRules.allowlist.length === 0 ? 'empty' : config.ipRules.allowlist.length + ' IPs'}\n </span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Blocklist</span>\n <span className=\"gw-tier-detail\">\n {config.ipRules.blocklist.length === 0 ? 'empty' : config.ipRules.blocklist.length + ' IPs'}\n </span>\n </div>\n </div>\n\n <div className=\"gw-config-card\">\n <h3>Global Limit</h3>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Max Requests</span>\n <span className=\"gw-tier-detail\">\n {config.rateLimits.globalLimit.maxRequests} / {config.rateLimits.globalLimit.windowMs / 1000}s\n </span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Default Tier</span>\n <span className=\"gw-tier-detail\">{config.rateLimits.defaultTier}</span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Active Keys</span>\n <span className=\"gw-tier-detail\">{config.activeKeys}</span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Active Key Sessions</span>\n <span className=\"gw-tier-detail\">{config.activeKeyUses}</span>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/frontend/index.ts","../../src/frontend/GatewayDashboard.tsx"],"sourcesContent":["export { GatewayDashboard } from './GatewayDashboard';\nexport type { GatewayDashboardProps } from './GatewayDashboard';\n","import { useState, useEffect, useRef } from 'react';\nimport { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';\nimport type { GatewayAnalytics, GatewayConfig, RequestLog, ApiKey } from '../types';\n\nexport interface GatewayDashboardProps {\n apiBaseUrl: string;\n}\n\ninterface LogsResponse {\n logs: RequestLog[];\n limit: number;\n offset: number;\n}\n\ninterface CreatedKey extends ApiKey {\n justCreated?: boolean;\n}\n\nfunction useGatewayApi<T>(apiBaseUrl: string, path: string): { data: T | null } {\n const [data, setData] = useState<T | null>(null);\n useEffect(() => {\n fetch(`${apiBaseUrl}${path}`)\n .then(r => r.json())\n .then(setData)\n .catch(() => {});\n }, [apiBaseUrl, path]);\n return { data };\n}\n\nexport function GatewayDashboard({ apiBaseUrl }: GatewayDashboardProps) {\n const [analytics, setAnalytics] = useState<GatewayAnalytics | null>(null);\n const [rpmHistory, setRpmHistory] = useState<{ time: string; rpm: number }[]>([]);\n const { data: config } = useGatewayApi<GatewayConfig>(apiBaseUrl, '/gateway/config');\n const { data: logsData } = useGatewayApi<LogsResponse>(apiBaseUrl, '/gateway/logs?limit=20');\n const eventSourceRef = useRef<EventSource | null>(null);\n const [keyName, setKeyName] = useState('');\n const [keyTier, setKeyTier] = useState('free');\n const [createdKeys, setCreatedKeys] = useState<CreatedKey[]>([]);\n const [keyError, setKeyError] = useState('');\n const [keyLoading, setKeyLoading] = useState(false);\n const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);\n\n const handleCreateKey = async () => {\n if (!keyName.trim()) {\n setKeyError('Name is required');\n return;\n }\n setKeyError('');\n setKeyLoading(true);\n try {\n const res = await fetch(`${apiBaseUrl}/gateway/keys`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name: keyName.trim(), tier: keyTier }),\n });\n if (!res.ok) {\n const err = await res.json();\n setKeyError(err.error || 'Failed to create key');\n return;\n }\n const newKey: ApiKey = await res.json();\n setCreatedKeys(prev => [{ ...newKey, justCreated: true }, ...prev]);\n setKeyName('');\n } catch {\n setKeyError('Network error');\n } finally {\n setKeyLoading(false);\n }\n };\n\n const handleRevokeKey = async (keyId: string) => {\n try {\n const res = await fetch(`${apiBaseUrl}/gateway/keys/${keyId}`, { method: 'DELETE' });\n if (res.ok) {\n setCreatedKeys(prev => prev.map(k => k.id === keyId ? { ...k, active: false } : k));\n }\n } catch {}\n };\n\n const handleCopyKey = (key: string, keyId: string) => {\n navigator.clipboard.writeText(key).then(() => {\n setCopiedKeyId(keyId);\n setTimeout(() => setCopiedKeyId(null), 2000);\n });\n };\n\n useEffect(() => {\n const es = new EventSource(`${apiBaseUrl}/gateway/analytics/live`);\n eventSourceRef.current = es;\n\n es.onmessage = (event) => {\n const data: GatewayAnalytics = JSON.parse(event.data);\n setAnalytics(data);\n setRpmHistory(prev => {\n const next = [\n ...prev,\n { time: new Date().toLocaleTimeString(), rpm: data.requestsPerMinute },\n ];\n return next.slice(-20);\n });\n };\n\n return () => {\n es.close();\n };\n }, [apiBaseUrl]);\n\n const getMethodClass = (method: string) => {\n switch (method) {\n case 'GET': return 'gw-method-get';\n case 'POST': return 'gw-method-post';\n case 'DELETE': return 'gw-method-delete';\n default: return 'gw-method-get';\n }\n };\n\n const getStatusClass = (code: number) => {\n if (code === 429) return 'gw-status-rate-limit';\n if (code >= 400) return 'gw-status-error';\n return 'gw-status-ok';\n };\n\n return (\n <div className=\"gw-dashboard\">\n <div className=\"gw-header\">\n <h1>API Gateway Dashboard</h1>\n <p>Real-time monitoring for the API gateway and rate limiter</p>\n <div className=\"gw-status-badge\">\n <span className=\"gw-status-dot\" />\n Live\n </div>\n </div>\n\n {/* Stats Grid */}\n <div className=\"gw-stats-grid\">\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Total Requests</div>\n <div className=\"gw-stat-value\">{analytics?.totalRequests ?? 0}</div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Requests / Min</div>\n <div className=\"gw-stat-value gw-accent\">\n {analytics?.requestsPerMinute ?? 0}\n </div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Error Rate</div>\n <div className={`gw-stat-value ${analytics && analytics.errorRate > 5 ? 'gw-danger' : ''}`}>\n {analytics?.errorRate ?? 0}%\n </div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Avg Response Time</div>\n <div className=\"gw-stat-value\">{analytics?.avgResponseTime ?? 0}ms</div>\n </div>\n </div>\n\n {/* Second Row Stats */}\n <div className=\"gw-stats-grid\" style={{ marginBottom: 32 }}>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Rate Limit Hits</div>\n <div className=\"gw-stat-value gw-warning\">\n {analytics?.rateLimitHits ?? 0}\n </div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Active IPs</div>\n <div className=\"gw-stat-value\">{analytics?.activeClients ?? 0}</div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Active Key Sessions</div>\n <div className=\"gw-stat-value\">{analytics?.activeKeyUses ?? 0}</div>\n </div>\n <div className=\"gw-stat-card\">\n <div className=\"gw-stat-label\">Rate Limit Tiers</div>\n <div className=\"gw-stat-value\">\n {config ? Object.keys(config.rateLimits.tiers).length : 0}\n </div>\n </div>\n </div>\n\n {/* Charts Row */}\n <div className=\"gw-charts-row\">\n <div className=\"gw-chart-card\">\n <div className=\"gw-chart-title\">Requests Per Minute</div>\n <ResponsiveContainer width=\"100%\" height={250}>\n <LineChart data={rpmHistory}>\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--gw-border, #2a2a2a)\" />\n <XAxis dataKey=\"time\" tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 11 }} />\n <YAxis tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 11 }} />\n <Tooltip\n contentStyle={{ background: 'var(--gw-bg-card, #1a1a1a)', border: '1px solid var(--gw-border, #2a2a2a)', borderRadius: 8 }}\n labelStyle={{ color: 'var(--gw-text-muted, #888)' }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"rpm\"\n stroke=\"var(--gw-accent, #64ffda)\"\n strokeWidth={2}\n dot={false}\n activeDot={{ r: 4, fill: 'var(--gw-accent, #64ffda)' }}\n />\n </LineChart>\n </ResponsiveContainer>\n </div>\n\n <div className=\"gw-chart-card\">\n <div className=\"gw-chart-title\">Top Endpoints</div>\n <ResponsiveContainer width=\"100%\" height={250}>\n <BarChart\n data={analytics?.topEndpoints ?? []}\n layout=\"vertical\"\n >\n <CartesianGrid strokeDasharray=\"3 3\" stroke=\"var(--gw-border, #2a2a2a)\" />\n <XAxis type=\"number\" tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 11 }} />\n <YAxis\n dataKey=\"path\"\n type=\"category\"\n tick={{ fill: 'var(--gw-text-muted, #888)', fontSize: 10 }}\n width={120}\n />\n <Tooltip\n contentStyle={{ background: 'var(--gw-bg-card, #1a1a1a)', border: '1px solid var(--gw-border, #2a2a2a)', borderRadius: 8 }}\n />\n <Bar dataKey=\"count\" fill=\"var(--gw-accent, #64ffda)\" radius={[0, 4, 4, 0]} />\n </BarChart>\n </ResponsiveContainer>\n </div>\n </div>\n\n {/* Recent Logs */}\n <div className=\"gw-logs-section\">\n <div className=\"gw-logs-title\">Recent Requests</div>\n <table className=\"gw-logs-table\">\n <thead>\n <tr>\n <th>Time</th>\n <th>Method</th>\n <th>Path</th>\n <th>Status</th>\n <th>Duration</th>\n <th>IP</th>\n </tr>\n </thead>\n <tbody>\n {(logsData?.logs ?? []).map((log, i) => (\n <tr key={i}>\n <td>{new Date(log.timestamp).toLocaleTimeString()}</td>\n <td>\n <span className={`gw-method-badge ${getMethodClass(log.method)}`}>\n {log.method}\n </span>\n </td>\n <td>{log.path}</td>\n <td className={getStatusClass(log.statusCode)}>{log.statusCode}</td>\n <td>{log.responseTime}ms</td>\n <td>{log.ip}</td>\n </tr>\n ))}\n {(!logsData?.logs || logsData.logs.length === 0) && (\n <tr>\n <td colSpan={6} style={{ textAlign: 'center', color: 'var(--gw-text-muted, #666)' }}>\n No requests logged yet. Make some API calls to see data here.\n </td>\n </tr>\n )}\n </tbody>\n </table>\n </div>\n\n {/* Config Section */}\n {config && (\n <div className=\"gw-config-section\">\n <div className=\"gw-config-card\">\n <h3>Rate Limit Tiers</h3>\n {Object.entries(config.rateLimits.tiers).map(([name, tier]) => (\n <div key={name} className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">{name}</span>\n <span className=\"gw-tier-detail\">\n {tier.algorithm === 'none'\n ? 'unlimited'\n : `${tier.maxRequests} req / ${(tier.windowMs || 60000) / 1000}s`}\n </span>\n </div>\n ))}\n </div>\n\n <div className=\"gw-config-card\">\n <h3>IP Rules</h3>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Mode</span>\n <span className=\"gw-tier-detail\">{config.ipRules.mode}</span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Allowlist</span>\n <span className=\"gw-tier-detail\">\n {config.ipRules.allowlist.length === 0 ? 'empty' : config.ipRules.allowlist.length + ' IPs'}\n </span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Blocklist</span>\n <span className=\"gw-tier-detail\">\n {config.ipRules.blocklist.length === 0 ? 'empty' : config.ipRules.blocklist.length + ' IPs'}\n </span>\n </div>\n </div>\n\n <div className=\"gw-config-card\">\n <h3>Global Limit</h3>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Max Requests</span>\n <span className=\"gw-tier-detail\">\n {config.rateLimits.globalLimit.maxRequests} / {config.rateLimits.globalLimit.windowMs / 1000}s\n </span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Default Tier</span>\n <span className=\"gw-tier-detail\">{config.rateLimits.defaultTier}</span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Active Keys</span>\n <span className=\"gw-tier-detail\">{config.activeKeys}</span>\n </div>\n <div className=\"gw-tier-item\">\n <span className=\"gw-tier-name\">Active Key Sessions</span>\n <span className=\"gw-tier-detail\">{config.activeKeyUses}</span>\n </div>\n </div>\n </div>\n )}\n\n {/* API Key Management */}\n <div className=\"gw-keys-section\">\n <div className=\"gw-keys-header\">\n <h2>API Keys</h2>\n <p>Create keys to authenticate API requests. Each key is tied to a rate limit tier.</p>\n </div>\n\n <div className=\"gw-keys-create\">\n <div className=\"gw-keys-form\">\n <input\n type=\"text\"\n className=\"gw-keys-input\"\n placeholder=\"Key name (e.g. My App)\"\n value={keyName}\n onChange={e => setKeyName(e.target.value)}\n onKeyDown={e => e.key === 'Enter' && handleCreateKey()}\n />\n <select\n className=\"gw-keys-select\"\n value={keyTier}\n onChange={e => setKeyTier(e.target.value)}\n >\n {config && Object.keys(config.rateLimits.tiers).map(tier => (\n <option key={tier} value={tier}>{tier}</option>\n ))}\n </select>\n <button\n className=\"gw-keys-btn\"\n onClick={handleCreateKey}\n disabled={keyLoading}\n >\n {keyLoading ? 'Creating...' : 'Create Key'}\n </button>\n </div>\n {keyError && <div className=\"gw-keys-error\">{keyError}</div>}\n </div>\n\n {createdKeys.length > 0 && (\n <div className=\"gw-keys-list\">\n {createdKeys.map(k => (\n <div key={k.id} className={`gw-key-card ${!k.active ? 'gw-key-revoked' : ''}`}>\n <div className=\"gw-key-top\">\n <span className=\"gw-key-name\">{k.name}</span>\n <div className=\"gw-key-badges\">\n <span className=\"gw-key-tier\">{k.tier}</span>\n <span className={`gw-key-status ${k.active ? 'gw-key-active' : 'gw-key-inactive'}`}>\n {k.active ? 'active' : 'revoked'}\n </span>\n </div>\n </div>\n <div className=\"gw-key-value\">\n <code>{k.key}</code>\n {k.active && (\n <button\n className=\"gw-key-copy\"\n onClick={() => handleCopyKey(k.key, k.id)}\n >\n {copiedKeyId === k.id ? 'Copied!' : 'Copy'}\n </button>\n )}\n </div>\n <div className=\"gw-key-bottom\">\n <span className=\"gw-key-id\">{k.id}</span>\n {k.active && (\n <button\n className=\"gw-key-revoke\"\n onClick={() => handleRevokeKey(k.id)}\n >\n Revoke\n </button>\n )}\n </div>\n {k.justCreated && (\n <div className=\"gw-key-usage\">\n <span className=\"gw-key-usage-label\">Usage:</span>\n <code>curl -H \"X-API-Key: {k.key}\" {apiBaseUrl}/health</code>\n </div>\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -16,6 +16,54 @@ function GatewayDashboard({ apiBaseUrl }) {
16
16
  const { data: config } = useGatewayApi(apiBaseUrl, "/gateway/config");
17
17
  const { data: logsData } = useGatewayApi(apiBaseUrl, "/gateway/logs?limit=20");
18
18
  const eventSourceRef = useRef(null);
19
+ const [keyName, setKeyName] = useState("");
20
+ const [keyTier, setKeyTier] = useState("free");
21
+ const [createdKeys, setCreatedKeys] = useState([]);
22
+ const [keyError, setKeyError] = useState("");
23
+ const [keyLoading, setKeyLoading] = useState(false);
24
+ const [copiedKeyId, setCopiedKeyId] = useState(null);
25
+ const handleCreateKey = async () => {
26
+ if (!keyName.trim()) {
27
+ setKeyError("Name is required");
28
+ return;
29
+ }
30
+ setKeyError("");
31
+ setKeyLoading(true);
32
+ try {
33
+ const res = await fetch(`${apiBaseUrl}/gateway/keys`, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({ name: keyName.trim(), tier: keyTier })
37
+ });
38
+ if (!res.ok) {
39
+ const err = await res.json();
40
+ setKeyError(err.error || "Failed to create key");
41
+ return;
42
+ }
43
+ const newKey = await res.json();
44
+ setCreatedKeys((prev) => [{ ...newKey, justCreated: true }, ...prev]);
45
+ setKeyName("");
46
+ } catch {
47
+ setKeyError("Network error");
48
+ } finally {
49
+ setKeyLoading(false);
50
+ }
51
+ };
52
+ const handleRevokeKey = async (keyId) => {
53
+ try {
54
+ const res = await fetch(`${apiBaseUrl}/gateway/keys/${keyId}`, { method: "DELETE" });
55
+ if (res.ok) {
56
+ setCreatedKeys((prev) => prev.map((k) => k.id === keyId ? { ...k, active: false } : k));
57
+ }
58
+ } catch {
59
+ }
60
+ };
61
+ const handleCopyKey = (key, keyId) => {
62
+ navigator.clipboard.writeText(key).then(() => {
63
+ setCopiedKeyId(keyId);
64
+ setTimeout(() => setCopiedKeyId(null), 2e3);
65
+ });
66
+ };
19
67
  useEffect(() => {
20
68
  const es = new EventSource(`${apiBaseUrl}/gateway/analytics/live`);
21
69
  eventSourceRef.current = es;
@@ -234,6 +282,87 @@ function GatewayDashboard({ apiBaseUrl }) {
234
282
  /* @__PURE__ */ jsx("span", { className: "gw-tier-detail", children: config.activeKeyUses })
235
283
  ] })
236
284
  ] })
285
+ ] }),
286
+ /* @__PURE__ */ jsxs("div", { className: "gw-keys-section", children: [
287
+ /* @__PURE__ */ jsxs("div", { className: "gw-keys-header", children: [
288
+ /* @__PURE__ */ jsx("h2", { children: "API Keys" }),
289
+ /* @__PURE__ */ jsx("p", { children: "Create keys to authenticate API requests. Each key is tied to a rate limit tier." })
290
+ ] }),
291
+ /* @__PURE__ */ jsxs("div", { className: "gw-keys-create", children: [
292
+ /* @__PURE__ */ jsxs("div", { className: "gw-keys-form", children: [
293
+ /* @__PURE__ */ jsx(
294
+ "input",
295
+ {
296
+ type: "text",
297
+ className: "gw-keys-input",
298
+ placeholder: "Key name (e.g. My App)",
299
+ value: keyName,
300
+ onChange: (e) => setKeyName(e.target.value),
301
+ onKeyDown: (e) => e.key === "Enter" && handleCreateKey()
302
+ }
303
+ ),
304
+ /* @__PURE__ */ jsx(
305
+ "select",
306
+ {
307
+ className: "gw-keys-select",
308
+ value: keyTier,
309
+ onChange: (e) => setKeyTier(e.target.value),
310
+ children: config && Object.keys(config.rateLimits.tiers).map((tier) => /* @__PURE__ */ jsx("option", { value: tier, children: tier }, tier))
311
+ }
312
+ ),
313
+ /* @__PURE__ */ jsx(
314
+ "button",
315
+ {
316
+ className: "gw-keys-btn",
317
+ onClick: handleCreateKey,
318
+ disabled: keyLoading,
319
+ children: keyLoading ? "Creating..." : "Create Key"
320
+ }
321
+ )
322
+ ] }),
323
+ keyError && /* @__PURE__ */ jsx("div", { className: "gw-keys-error", children: keyError })
324
+ ] }),
325
+ createdKeys.length > 0 && /* @__PURE__ */ jsx("div", { className: "gw-keys-list", children: createdKeys.map((k) => /* @__PURE__ */ jsxs("div", { className: `gw-key-card ${!k.active ? "gw-key-revoked" : ""}`, children: [
326
+ /* @__PURE__ */ jsxs("div", { className: "gw-key-top", children: [
327
+ /* @__PURE__ */ jsx("span", { className: "gw-key-name", children: k.name }),
328
+ /* @__PURE__ */ jsxs("div", { className: "gw-key-badges", children: [
329
+ /* @__PURE__ */ jsx("span", { className: "gw-key-tier", children: k.tier }),
330
+ /* @__PURE__ */ jsx("span", { className: `gw-key-status ${k.active ? "gw-key-active" : "gw-key-inactive"}`, children: k.active ? "active" : "revoked" })
331
+ ] })
332
+ ] }),
333
+ /* @__PURE__ */ jsxs("div", { className: "gw-key-value", children: [
334
+ /* @__PURE__ */ jsx("code", { children: k.key }),
335
+ k.active && /* @__PURE__ */ jsx(
336
+ "button",
337
+ {
338
+ className: "gw-key-copy",
339
+ onClick: () => handleCopyKey(k.key, k.id),
340
+ children: copiedKeyId === k.id ? "Copied!" : "Copy"
341
+ }
342
+ )
343
+ ] }),
344
+ /* @__PURE__ */ jsxs("div", { className: "gw-key-bottom", children: [
345
+ /* @__PURE__ */ jsx("span", { className: "gw-key-id", children: k.id }),
346
+ k.active && /* @__PURE__ */ jsx(
347
+ "button",
348
+ {
349
+ className: "gw-key-revoke",
350
+ onClick: () => handleRevokeKey(k.id),
351
+ children: "Revoke"
352
+ }
353
+ )
354
+ ] }),
355
+ k.justCreated && /* @__PURE__ */ jsxs("div", { className: "gw-key-usage", children: [
356
+ /* @__PURE__ */ jsx("span", { className: "gw-key-usage-label", children: "Usage:" }),
357
+ /* @__PURE__ */ jsxs("code", { children: [
358
+ 'curl -H "X-API-Key: ',
359
+ k.key,
360
+ '" ',
361
+ apiBaseUrl,
362
+ "/health"
363
+ ] })
364
+ ] })
365
+ ] }, k.id)) })
237
366
  ] })
238
367
  ] });
239
368
  }