@rebasepro/studio 0.17.3 → 0.18.1

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 (81) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +12 -4
  3. package/dist/{ApiKeysView-DiCurTEU.js → ApiKeysView-EIvegrUk.js} +54 -39
  4. package/dist/ApiKeysView-EIvegrUk.js.map +1 -0
  5. package/dist/{BackupsView-DNS6LdVg.js → BackupsView-DVkKzaPR.js} +29 -31
  6. package/dist/BackupsView-DVkKzaPR.js.map +1 -0
  7. package/dist/{BranchesView-CM3B-ER3.js → BranchesView-Df8hQ8wE.js} +85 -24
  8. package/dist/BranchesView-Df8hQ8wE.js.map +1 -0
  9. package/dist/{CronJobsView-BHtJZeJZ.js → CronJobsView-CLOGjb-L.js} +32 -27
  10. package/dist/CronJobsView-CLOGjb-L.js.map +1 -0
  11. package/dist/{RLSEditor-CBX4uD66.js → RLSEditor-Sa1D34ny.js} +724 -615
  12. package/dist/RLSEditor-Sa1D34ny.js.map +1 -0
  13. package/dist/{SQLEditor-ByecwX8B.js → SQLEditor-BfNA8xM3.js} +9 -4
  14. package/dist/{SQLEditor-ByecwX8B.js.map → SQLEditor-BfNA8xM3.js.map} +1 -1
  15. package/dist/{SchemaVisualizer-D6tMZgeC.js → SchemaVisualizer-DTDMYL6o.js} +84 -110
  16. package/dist/SchemaVisualizer-DTDMYL6o.js.map +1 -0
  17. package/dist/{StorageView-BGDeC5GI.js → StorageView-Ed9Q_VlB.js} +5 -32
  18. package/dist/StorageView-Ed9Q_VlB.js.map +1 -0
  19. package/dist/components/RLSEditor/saveRules.d.ts +38 -0
  20. package/dist/components/RebaseStudio.d.ts +1 -1
  21. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +5 -2
  22. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +0 -4
  23. package/dist/components/load-failure-view.d.ts +17 -0
  24. package/dist/components/load-failure.d.ts +47 -0
  25. package/dist/index.es.js +56 -36
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/load-failure-C6OMJSGC.js +31 -0
  28. package/dist/load-failure-C6OMJSGC.js.map +1 -0
  29. package/dist/load-failure-view-CBTB_h-h.js +48 -0
  30. package/dist/load-failure-view-CBTB_h-h.js.map +1 -0
  31. package/package.json +36 -21
  32. package/dist/ApiKeysView-DiCurTEU.js.map +0 -1
  33. package/dist/BackupsView-DNS6LdVg.js.map +0 -1
  34. package/dist/BranchesView-CM3B-ER3.js.map +0 -1
  35. package/dist/CronJobsView-BHtJZeJZ.js.map +0 -1
  36. package/dist/RLSEditor-CBX4uD66.js.map +0 -1
  37. package/dist/SchemaVisualizer-D6tMZgeC.js.map +0 -1
  38. package/dist/StorageView-BGDeC5GI.js.map +0 -1
  39. package/dist/components/StorageView/storage-failure.d.ts +0 -34
  40. package/src/components/ApiExplorer/ApiExplorer.tsx +0 -299
  41. package/src/components/ApiExplorer/EndpointDetail.tsx +0 -279
  42. package/src/components/ApiExplorer/TryItPanel.tsx +0 -499
  43. package/src/components/ApiExplorer/parseSpec.ts +0 -104
  44. package/src/components/ApiExplorer/types.ts +0 -84
  45. package/src/components/ApiKeys/ApiKeysView.tsx +0 -479
  46. package/src/components/ApiKeys/CreateApiKeyDialog.tsx +0 -711
  47. package/src/components/ApiKeys/permissions.ts +0 -145
  48. package/src/components/AuthSimulationSelector.tsx +0 -72
  49. package/src/components/Backups/BackupsView.tsx +0 -185
  50. package/src/components/Branches/BranchesView.tsx +0 -386
  51. package/src/components/CronJobs/CronJobsView.tsx +0 -413
  52. package/src/components/JSEditor/JSEditor.tsx +0 -1100
  53. package/src/components/JSEditor/JSEditorSidebar.tsx +0 -350
  54. package/src/components/JSEditor/JSMonacoEditor.tsx +0 -410
  55. package/src/components/LogsExplorer/LogsExplorer.tsx +0 -273
  56. package/src/components/LogsExplorer/useLogTail.ts +0 -356
  57. package/src/components/RLSEditor/PolicyEditor.tsx +0 -399
  58. package/src/components/RLSEditor/RLSEditor.tsx +0 -1128
  59. package/src/components/RLSEditor/index.ts +0 -1
  60. package/src/components/RLSEditor/policy-presets.ts +0 -176
  61. package/src/components/RLSEditor/saveSecurityRules.ts +0 -48
  62. package/src/components/RebaseStudio.tsx +0 -174
  63. package/src/components/SQLEditor/ExplainVisualizer.tsx +0 -128
  64. package/src/components/SQLEditor/MonacoEditor.tsx +0 -203
  65. package/src/components/SQLEditor/SQLEditor.tsx +0 -1518
  66. package/src/components/SQLEditor/SQLEditorSidebar.tsx +0 -173
  67. package/src/components/SQLEditor/SchemaBrowser.tsx +0 -168
  68. package/src/components/SQLEditor/sql_editor_types.ts +0 -18
  69. package/src/components/SchemaVisualizer/RelationEdge.tsx +0 -102
  70. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +0 -710
  71. package/src/components/SchemaVisualizer/TableNode.tsx +0 -257
  72. package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +0 -140
  73. package/src/components/SchemaVisualizer/useLiveRls.ts +0 -86
  74. package/src/components/SchemaVisualizer/useSchemaGraph.ts +0 -426
  75. package/src/components/StorageView/StorageView.tsx +0 -1588
  76. package/src/components/StorageView/storage-failure.ts +0 -63
  77. package/src/components/StudioHomePage.tsx +0 -430
  78. package/src/index.ts +0 -31
  79. package/src/utils/parseSpec.test.ts +0 -309
  80. package/src/utils/sql_utils.test.ts +0 -265
  81. package/src/utils/sql_utils.ts +0 -291
@@ -1,479 +0,0 @@
1
-
2
- import React, { useState, useEffect, useRef, useCallback } from "react";
3
- import {
4
- Button,
5
- Card,
6
- Chip,
7
- CircularProgress,
8
- cls,
9
- defaultBorderMixin,
10
- Dialog,
11
- DialogActions,
12
- DialogContent,
13
- DialogTitle,
14
- IconButton,
15
- iconSize,
16
- KeyRoundIcon,
17
- RefreshCwIcon,
18
- ShieldIcon,
19
- Tooltip,
20
- Typography,
21
- CopyIcon,
22
- PlusIcon as AddIcon,
23
- Trash2Icon as DeleteIcon,
24
- AlertCircleIcon,
25
- CheckCircleIcon
26
- } from "@rebasepro/ui";
27
- import { useRebaseClient, useSnackbarController } from "@rebasepro/app";
28
- import type { ApiKeyMasked, ApiKeyWithSecret, RebaseClient } from "@rebasepro/types";
29
-
30
- import { CreateApiKeyDialog } from "./CreateApiKeyDialog";
31
- import { permissionSummary, resourceLabel, resourcePhrase } from "./permissions";
32
-
33
- /* ═══════════════════════════════════════════════════════════════
34
- Helpers
35
-
36
- The row types come from `@rebasepro/types`: this view used to declare its
37
- own copies, and they had already drifted — neither carried `admin`, so the
38
- panel could not tell an admin key from a scoped one.
39
- ═══════════════════════════════════════════════════════════════ */
40
-
41
- function formatRelative(iso: string | null | undefined): string {
42
- if (!iso) return "—";
43
- const d = new Date(iso);
44
- const now = Date.now();
45
- const diff = d.getTime() - now;
46
- const abs = Math.abs(diff);
47
- if (abs < 60000) return diff > 0 ? "in <1m" : "<1m ago";
48
- if (abs < 3600000) { const m = Math.round(abs / 60000); return diff > 0 ? `in ${m}m` : `${m}m ago`; }
49
- if (abs < 86400000) { const h = Math.round(abs / 3600000); return diff > 0 ? `in ${h}h` : `${h}h ago`; }
50
- return d.toLocaleDateString();
51
- }
52
-
53
- function isExpired(key: ApiKeyMasked): boolean {
54
- return !!(key.expires_at && new Date(key.expires_at) < new Date());
55
- }
56
-
57
- function keyStatus(key: ApiKeyMasked): { label: string; color: string } {
58
- if (key.revoked_at) return { label: "Revoked", color: "text-red-500" };
59
- if (isExpired(key)) return { label: "Expired", color: "text-amber-500" };
60
- return { label: "Active", color: "text-emerald-500" };
61
- }
62
-
63
- /* ═══════════════════════════════════════════════════════════════
64
- Main Component
65
- ═══════════════════════════════════════════════════════════════ */
66
-
67
- export function ApiKeysView() {
68
- const client = useRebaseClient<RebaseClient>();
69
- const snackbar = useSnackbarController();
70
- const [keys, setKeys] = useState<ApiKeyMasked[]>([]);
71
- const [loading, setLoading] = useState(true);
72
- const [selectedId, setSelectedId] = useState<string | null>(null);
73
- const [showCreate, setShowCreate] = useState(false);
74
- const [showSecret, setShowSecret] = useState<ApiKeyWithSecret | null>(null);
75
- const [revoking, setRevoking] = useState<string | null>(null);
76
- const [confirmRevoke, setConfirmRevoke] = useState<ApiKeyMasked | null>(null);
77
-
78
- const clientRef = useRef(client);
79
- clientRef.current = client;
80
- const snackbarRef = useRef(snackbar);
81
- snackbarRef.current = snackbar;
82
-
83
- const loadKeys = useCallback(async () => {
84
- const c = clientRef.current;
85
- if (!c?.apiKeys) { setLoading(false); return; }
86
- try {
87
- const res = await c.apiKeys.listKeys();
88
- setKeys(res.keys);
89
- } catch (e: unknown) {
90
- snackbarRef.current.open({
91
- type: "error",
92
- message: e instanceof Error ? e.message : String(e)
93
- });
94
- } finally {
95
- setLoading(false);
96
- }
97
- }, []);
98
-
99
- useEffect(() => { loadKeys(); }, [loadKeys]);
100
-
101
- const handleRevoke = async (id: string) => {
102
- const c = clientRef.current;
103
- if (!c?.apiKeys) return;
104
- setRevoking(id);
105
- try {
106
- await c.apiKeys.revokeKey(id);
107
- snackbarRef.current.open({ type: "success", message: "API key revoked" });
108
- await loadKeys();
109
- if (selectedId === id) setSelectedId(null);
110
- } catch (e: unknown) {
111
- snackbarRef.current.open({ type: "error", message: e instanceof Error ? e.message : String(e) });
112
- } finally { setRevoking(null); }
113
- };
114
-
115
- const handleCreated = (keyWithSecret: ApiKeyWithSecret) => {
116
- setShowCreate(false);
117
- setShowSecret(keyWithSecret);
118
- loadKeys();
119
- };
120
-
121
- const selectedKey = keys.find(k => k.id === selectedId);
122
- const activeKeys = keys.filter(k => !k.revoked_at && !isExpired(k));
123
- const inactiveKeys = keys.filter(k => k.revoked_at || isExpired(k));
124
-
125
- if (loading) return <div className="flex items-center justify-center h-full"><CircularProgress/></div>;
126
-
127
- return (
128
- <>
129
- <div className="flex h-full w-full overflow-hidden bg-white dark:bg-surface-950">
130
- {/* ── Key List ── */}
131
- <div className={cls("flex flex-col w-[340px] min-w-[280px] border-r h-full", defaultBorderMixin)}>
132
- <div className={cls("flex items-center justify-between px-4 py-2.5 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]", defaultBorderMixin)}>
133
- <div className="flex items-center gap-2">
134
- <KeyRoundIcon size={iconSize.smallest} className="text-primary"/>
135
- <Typography variant="subtitle2" className="font-semibold">API Keys</Typography>
136
- <Chip size="smallest" className="bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300">{activeKeys.length}</Chip>
137
- </div>
138
- <div className="flex items-center gap-1">
139
- <IconButton size="small" onClick={loadKeys} title="Refresh"><RefreshCwIcon size={iconSize.smallest}/></IconButton>
140
- <Button size="small" color="primary" onClick={() => setShowCreate(true)} startIcon={<AddIcon size={iconSize.smallest}/>}>
141
- New
142
- </Button>
143
- </div>
144
- </div>
145
- <div className="flex-1 overflow-y-auto p-2 space-y-1">
146
- {activeKeys.length === 0 && inactiveKeys.length === 0 && (
147
- <div className="flex flex-col items-center justify-center h-full gap-3 text-center p-6">
148
- <KeyRoundIcon size={iconSize.medium} className="text-surface-300 dark:text-surface-600"/>
149
- <Typography variant="body2" color="secondary">No API keys yet</Typography>
150
- <Typography variant="caption" color="disabled">Create a key to enable machine-to-machine authentication</Typography>
151
- </div>
152
- )}
153
- {activeKeys.map(key => (
154
- <KeyListItem key={key.id} apiKey={key} selected={selectedId === key.id} onClick={() => setSelectedId(key.id)}/>
155
- ))}
156
- {inactiveKeys.length > 0 && (
157
- <>
158
- <div className="px-2 pt-3 pb-1">
159
- <Typography variant="caption" color="disabled" className="text-[10px] uppercase tracking-wider font-medium">Revoked / Expired</Typography>
160
- </div>
161
- {inactiveKeys.map(key => (
162
- <KeyListItem key={key.id} apiKey={key} selected={selectedId === key.id} onClick={() => setSelectedId(key.id)}/>
163
- ))}
164
- </>
165
- )}
166
- </div>
167
- </div>
168
-
169
- {/* ── Detail Panel ── */}
170
- <div className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
171
- {!selectedKey ? (
172
- <div className="flex items-center justify-center h-full">
173
- <Typography variant="body2" color="disabled">Select an API key to view details</Typography>
174
- </div>
175
- ) : (
176
- <>
177
- {/* Header */}
178
- <div className={cls("flex items-center justify-between px-5 py-3 border-b bg-white dark:bg-surface-950 min-h-[56px]", defaultBorderMixin)}>
179
- <div className="flex items-center gap-3 min-w-0">
180
- <KeyRoundIcon size={iconSize.small} className="text-primary shrink-0"/>
181
- <div className="min-w-0">
182
- <div className="flex items-center gap-2 min-w-0">
183
- <Typography variant="subtitle1" className="font-semibold truncate">{selectedKey.name}</Typography>
184
- {selectedKey.admin && <AdminChip/>}
185
- </div>
186
- <Typography variant="caption" color="secondary" className="font-mono text-[11px]">{selectedKey.key_prefix}•••</Typography>
187
- </div>
188
- </div>
189
- <div className="flex items-center gap-2 shrink-0">
190
- {!selectedKey.revoked_at && (
191
- <Button
192
- size="small"
193
- color="error"
194
- variant="outlined"
195
- onClick={() => setConfirmRevoke(selectedKey)}
196
- disabled={revoking === selectedKey.id}
197
- startIcon={revoking === selectedKey.id ? <CircularProgress size="smallest"/> : <DeleteIcon size={iconSize.smallest}/>}
198
- >
199
- Revoke
200
- </Button>
201
- )}
202
- </div>
203
- </div>
204
-
205
- {/* Stats */}
206
- <div className="px-5 py-4 bg-surface-50 dark:bg-surface-900/50">
207
- <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
208
- <StatCard label="Status" value={keyStatus(selectedKey).label} className={keyStatus(selectedKey).color}/>
209
- <StatCard label="Created" value={formatRelative(selectedKey.created_at)}/>
210
- <StatCard label="Last Used" value={formatRelative(selectedKey.last_used_at)}/>
211
- <StatCard label="Expires" value={selectedKey.expires_at ? formatRelative(selectedKey.expires_at) : "Never"}/>
212
- </div>
213
- <div className="grid grid-cols-2 md:grid-cols-3 gap-3 mt-3">
214
- <StatCard
215
- label="Role"
216
- value={selectedKey.admin ? "Admin" : "Service"}
217
- className={selectedKey.admin ? "text-amber-600 dark:text-amber-400" : undefined}
218
- />
219
- <StatCard label="Rate Limit" value={selectedKey.rate_limit ? `${selectedKey.rate_limit}/15min` : "Default (1000/15min)"}/>
220
- <StatCard label="Created By" value={selectedKey.created_by} mono/>
221
- </div>
222
- </div>
223
-
224
- {/* Permissions */}
225
- <div className={cls("flex items-center gap-2 px-5 py-2 border-y bg-white dark:bg-surface-950", defaultBorderMixin)}>
226
- <Typography variant="subtitle2" className="font-semibold text-[13px]">Permissions</Typography>
227
- <Chip size="smallest" className="bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300">
228
- {selectedKey.permissions.length}
229
- </Chip>
230
- </div>
231
- <div className="flex-1 overflow-y-auto px-5 py-3">
232
- {selectedKey.admin && (
233
- <div className="flex items-start gap-2 mb-3 px-3 py-2 rounded-lg border border-amber-500/40 bg-amber-500/[0.06]">
234
- <ShieldIcon size={iconSize.smallest} className="mt-[3px] shrink-0 text-amber-600 dark:text-amber-400"/>
235
- <Typography variant="caption" className="text-[12px] leading-snug text-amber-700 dark:text-amber-300">
236
- This key holds the <span className="font-semibold">admin role</span>: it also passes the
237
- admin-gated routes — users, roles, cron, backups, logs — and reads through the
238
- <span className="font-mono"> default_admin</span> RLS policies, beyond the resources listed here.
239
- </Typography>
240
- </div>
241
- )}
242
- {selectedKey.permissions.length === 0 ? (
243
- <Typography variant="body2" color="disabled">No permissions configured</Typography>
244
- ) : (
245
- <div className="space-y-2">
246
- {selectedKey.permissions.map((perm, idx) => (
247
- <div key={idx} className={cls("flex items-center gap-3 px-3 py-2 rounded-lg border", defaultBorderMixin)}>
248
- <div className="flex-1 min-w-0">
249
- <Typography variant="body2" className="text-[13px] font-medium truncate">
250
- {resourceLabel(perm.collection)}
251
- </Typography>
252
- <Typography variant="caption" color="secondary" className="text-[11px]">
253
- {resourcePhrase(perm.collection)}
254
- </Typography>
255
- </div>
256
- <div className="flex items-center gap-1 shrink-0">
257
- {perm.operations.map(op => (
258
- <Chip key={op} size="smallest" className={cls(
259
- op === "read" && "bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300",
260
- op === "write" && "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300",
261
- op === "delete" && "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300"
262
- )}>{op}</Chip>
263
- ))}
264
- </div>
265
- </div>
266
- ))}
267
- </div>
268
- )}
269
- </div>
270
- </>
271
- )}
272
- </div>
273
- </div>
274
-
275
- {/* Revoke Confirmation Dialog */}
276
- <Dialog
277
- open={confirmRevoke !== null}
278
- onOpenChange={(open) => {
279
- if (!open && !revoking) setConfirmRevoke(null);
280
- }}
281
- >
282
- <DialogTitle hidden>Revoke Confirmation</DialogTitle>
283
- <DialogContent>
284
- <Typography variant="subtitle1" className="font-semibold mb-2">
285
- Revoke &quot;{confirmRevoke?.name}&quot;?
286
- </Typography>
287
- <Typography variant="body2" color="secondary">
288
- Requests authenticated with this key will stop working immediately. This action cannot be undone.
289
- </Typography>
290
- </DialogContent>
291
- <DialogActions>
292
- <Button
293
- variant="text"
294
- onClick={() => setConfirmRevoke(null)}
295
- disabled={revoking !== null}
296
- >
297
- Cancel
298
- </Button>
299
- <Button
300
- color="error"
301
- disabled={revoking !== null}
302
- startIcon={revoking !== null ? <CircularProgress size="smallest"/> : <DeleteIcon size={iconSize.smallest}/>}
303
- onClick={async () => {
304
- if (!confirmRevoke) return;
305
- await handleRevoke(confirmRevoke.id);
306
- setConfirmRevoke(null);
307
- }}
308
- >
309
- Revoke
310
- </Button>
311
- </DialogActions>
312
- </Dialog>
313
-
314
- {/* Create Dialog */}
315
- {showCreate && (
316
- <CreateApiKeyDialog
317
- onClose={() => setShowCreate(false)}
318
- onCreated={handleCreated}
319
- />
320
- )}
321
-
322
- {/* Secret Display Dialog */}
323
- {showSecret && (
324
- <SecretDisplayDialog
325
- keyWithSecret={showSecret}
326
- onClose={() => setShowSecret(null)}
327
- />
328
- )}
329
- </>
330
- );
331
- }
332
-
333
- /* ═══════════════════════════════════════════════════════════════
334
- List item
335
- ═══════════════════════════════════════════════════════════════ */
336
-
337
- /**
338
- * Marks a key that carries the admin role.
339
- *
340
- * Not cosmetic: an admin key reaches the admin routes and the `default_admin`
341
- * RLS policies, and without this it is indistinguishable in the list from a
342
- * read-only one.
343
- */
344
- function AdminChip() {
345
- return (
346
- <Tooltip title="Carries the admin role: the admin-gated routes and the default_admin RLS policies">
347
- <Chip
348
- size="smallest"
349
- className="shrink-0 bg-amber-500/12 dark:bg-amber-500/12 text-amber-700 dark:text-amber-300 border-amber-500/30 dark:border-amber-500/30"
350
- >
351
- <ShieldIcon size={10}/>
352
- admin
353
- </Chip>
354
- </Tooltip>
355
- );
356
- }
357
-
358
- function KeyListItem({ apiKey, selected, onClick }: { apiKey: ApiKeyMasked; selected: boolean; onClick: () => void }) {
359
- const status = keyStatus(apiKey);
360
- return (
361
- <div
362
- onClick={onClick}
363
- className={cls(
364
- "flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all",
365
- selected
366
- ? "bg-primary/10 dark:bg-primary/15 ring-1 ring-primary/30"
367
- : "hover:bg-surface-100 dark:hover:bg-surface-950"
368
- )}
369
- >
370
- <div className={cls("w-2 h-2 rounded-full shrink-0",
371
- status.label === "Active" ? "bg-emerald-400" :
372
- status.label === "Expired" ? "bg-amber-400" : "bg-red-400"
373
- )}/>
374
- <div className="flex-1 min-w-0">
375
- <div className="flex items-center gap-1.5 min-w-0">
376
- <Typography variant="body2" className="truncate font-medium text-[13px]">{apiKey.name}</Typography>
377
- {apiKey.admin && <AdminChip/>}
378
- </div>
379
- <Typography variant="caption" color="secondary" className="truncate text-[11px] font-mono">{apiKey.key_prefix}•••</Typography>
380
- </div>
381
- <div className="shrink-0">
382
- <Typography variant="caption" color="disabled" className="text-[10px]">{permissionSummary(apiKey.permissions)}</Typography>
383
- </div>
384
- </div>
385
- );
386
- }
387
-
388
- /* ═══════════════════════════════════════════════════════════════
389
- Stat card
390
- ═══════════════════════════════════════════════════════════════ */
391
-
392
- function StatCard({ label, value, mono, className }: { label: string; value: string; mono?: boolean; className?: string }) {
393
- return (
394
- <div className={cls("px-3 py-2 rounded-lg border bg-white dark:bg-surface-900", defaultBorderMixin)}>
395
- <Typography variant="caption" color="secondary" className="text-[10px] uppercase tracking-wider font-medium">{label}</Typography>
396
- <Typography variant="body2" className={cls(
397
- "mt-0.5 font-semibold text-[13px]",
398
- mono && "font-mono",
399
- className
400
- )}>{value}</Typography>
401
- </div>
402
- );
403
- }
404
-
405
- /* ═══════════════════════════════════════════════════════════════
406
- Secret Display Dialog — shown exactly once after creation
407
- ═══════════════════════════════════════════════════════════════ */
408
-
409
- function SecretDisplayDialog({ keyWithSecret, onClose }: { keyWithSecret: ApiKeyWithSecret; onClose: () => void }) {
410
- const snackbar = useSnackbarController();
411
- const [copied, setCopied] = useState(false);
412
-
413
- const handleCopy = async () => {
414
- try {
415
- await navigator.clipboard.writeText(keyWithSecret.key);
416
- setCopied(true);
417
- snackbar.open({ type: "success", message: "API key copied to clipboard" });
418
- setTimeout(() => setCopied(false), 2000);
419
- } catch {
420
- snackbar.open({ type: "error", message: "Failed to copy" });
421
- }
422
- };
423
-
424
- return (
425
- <Dialog open onOpenChange={(open) => { if (!open) onClose(); }} maxWidth="md">
426
- <DialogTitle>
427
- <div className="flex items-center gap-2">
428
- <CheckCircleIcon size={iconSize.small} className="text-emerald-500"/>
429
- API Key Created
430
- </div>
431
- </DialogTitle>
432
- <DialogContent>
433
- <div className="p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50 mb-4">
434
- <div className="flex items-center gap-2 mb-1">
435
- <AlertCircleIcon size={iconSize.smallest} className="text-amber-600 dark:text-amber-400"/>
436
- <Typography variant="caption" className="font-semibold text-amber-700 dark:text-amber-400">
437
- Copy your key now — it won&apos;t be shown again
438
- </Typography>
439
- </div>
440
- <Typography variant="caption" className="text-amber-600 dark:text-amber-300">
441
- This is the only time the full API key will be displayed. Store it securely.
442
- </Typography>
443
- </div>
444
-
445
- <div className={cls("flex items-center gap-2 p-3 rounded-lg border bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
446
- <code className="flex-1 text-[12px] font-mono break-all text-surface-700 dark:text-surface-300 select-all">
447
- {keyWithSecret.key}
448
- </code>
449
- <Tooltip title={copied ? "Copied!" : "Copy"}>
450
- <IconButton size="small" onClick={handleCopy}>
451
- {copied
452
- ? <CheckCircleIcon size={iconSize.smallest} className="text-emerald-500"/>
453
- : <CopyIcon size={iconSize.smallest}/>
454
- }
455
- </IconButton>
456
- </Tooltip>
457
- </div>
458
-
459
- <div className="mt-4 space-y-1">
460
- <Typography variant="caption" color="secondary">
461
- <strong>Name:</strong> {keyWithSecret.name}
462
- </Typography>
463
- <Typography variant="caption" color="secondary">
464
- <strong>Access:</strong> {permissionSummary(keyWithSecret.permissions)}
465
- </Typography>
466
- {keyWithSecret.admin && (
467
- <Typography variant="caption" className="flex items-center gap-1.5 text-amber-700 dark:text-amber-300">
468
- <ShieldIcon size={iconSize.smallest} className="shrink-0"/>
469
- <span><strong>Admin role granted</strong> — the admin routes and the default_admin policies</span>
470
- </Typography>
471
- )}
472
- </div>
473
- </DialogContent>
474
- <DialogActions>
475
- <Button color="primary" onClick={onClose}>Done</Button>
476
- </DialogActions>
477
- </Dialog>
478
- );
479
- }