@rebasepro/studio 0.17.3 → 0.18.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 (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,413 +0,0 @@
1
-
2
- import React, { useState, useEffect, useRef } from "react";
3
- import {
4
- AlertCircleIcon,
5
- Button,
6
- CalendarIcon,
7
- Card,
8
- CheckCircleIcon,
9
- Chip,
10
- CircularProgress,
11
- cls,
12
- defaultBorderMixin,
13
- HistoryIcon,
14
- IconButton,
15
- iconSize,
16
- Paper,
17
- PauseIcon,
18
- PlayIcon,
19
- RefreshCwIcon,
20
- Typography
21
- } from "@rebasepro/ui";
22
- import { useRebaseClient, useSnackbarController } from "@rebasepro/app";
23
- import type { CronJobStatus, CronJobLogEntry } from "@rebasepro/types";
24
- import type { RebaseClient } from "@rebasepro/types";
25
-
26
- function formatDuration(ms: number): string {
27
- if (ms < 1000) return `${ms}ms`;
28
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
29
- return `${(ms / 60000).toFixed(1)}m`;
30
- }
31
-
32
- function formatRelative(iso: string | undefined): string {
33
- if (!iso) return "—";
34
- const d = new Date(iso);
35
- const now = Date.now();
36
- const diff = d.getTime() - now;
37
- const abs = Math.abs(diff);
38
- if (abs < 60000) return diff > 0 ? "in <1m" : "<1m ago";
39
- if (abs < 3600000) { const m = Math.round(abs / 60000); return diff > 0 ? `in ${m}m` : `${m}m ago`; }
40
- if (abs < 86400000) { const h = Math.round(abs / 3600000); return diff > 0 ? `in ${h}h` : `${h}h ago`; }
41
- return d.toLocaleString();
42
- }
43
-
44
- const stateColors: Record<string, string> = {
45
- idle: "bg-emerald-500",
46
- running: "bg-blue-500",
47
- success: "bg-emerald-500",
48
- error: "bg-red-500",
49
- disabled: "bg-surface-400"
50
- };
51
-
52
- export function CronJobsView() {
53
- const client = useRebaseClient<RebaseClient>();
54
- const snackbar = useSnackbarController();
55
- const [jobs, setJobs] = useState<CronJobStatus[]>([]);
56
- const [loading, setLoading] = useState(true);
57
- const [selectedId, setSelectedId] = useState<string | null>(null);
58
- const [logs, setLogs] = useState<CronJobLogEntry[]>([]);
59
- const [logsLoading, setLogsLoading] = useState(false);
60
- const [triggering, setTriggering] = useState<string | null>(null);
61
-
62
- // Refs so effects never re-fire due to identity changes
63
- const clientRef = useRef(client);
64
- clientRef.current = client;
65
- const snackbarRef = useRef(snackbar);
66
- snackbarRef.current = snackbar;
67
-
68
- // ── Fetch jobs on mount + poll every 15s ──
69
- useEffect(() => {
70
- let cancelled = false;
71
-
72
- async function load() {
73
- const c = clientRef.current;
74
- if (!c?.cron) {
75
- setLoading(false);
76
- return;
77
- }
78
- try {
79
- const res = await c.cron.listJobs();
80
- if (!cancelled) setJobs(res.jobs);
81
- } catch (e: unknown) {
82
- if (!cancelled) {
83
- snackbarRef.current.open({
84
- type: "error",
85
- message: e instanceof Error ? e.message : String(e)
86
- });
87
- }
88
- } finally {
89
- if (!cancelled) setLoading(false);
90
- }
91
- }
92
-
93
- load();
94
-
95
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
96
-
97
- const scheduleNext = () => {
98
- if (cancelled) return;
99
- timeoutId = setTimeout(async () => {
100
- if (document.visibilityState === "visible") {
101
- await load();
102
- }
103
- scheduleNext();
104
- }, 15_000);
105
- };
106
-
107
- scheduleNext();
108
-
109
- const handleVisibility = () => {
110
- if (document.visibilityState === "visible") {
111
- load();
112
- }
113
- };
114
- document.addEventListener("visibilitychange", handleVisibility);
115
-
116
- return () => {
117
- cancelled = true;
118
- if (timeoutId) clearTimeout(timeoutId);
119
- document.removeEventListener("visibilitychange", handleVisibility);
120
- };
121
- }, []); // runs once
122
-
123
- // ── Fetch logs when selection changes ──
124
- useEffect(() => {
125
- if (!selectedId) {
126
- setLogs([]);
127
- return;
128
- }
129
- let cancelled = false;
130
- const c = clientRef.current;
131
- if (!c?.cron) return;
132
-
133
- setLogsLoading(true);
134
- c.cron.getJobLogs(selectedId, { limit: 25 })
135
- .then(res => { if (!cancelled) setLogs(res.logs); })
136
- .catch((e: unknown) => {
137
- if (cancelled) return;
138
- // Same reasoning as `refreshLogs`: an empty log list is a claim.
139
- setLogs([]);
140
- snackbarRef.current.open({
141
- type: "error",
142
- message: e instanceof Error ? e.message : String(e)
143
- });
144
- })
145
- .finally(() => { if (!cancelled) setLogsLoading(false); });
146
-
147
- return () => { cancelled = true; };
148
- }, [selectedId]);
149
-
150
- // ── Imperative helpers (not in any dep array) ──
151
- async function refreshJobs() {
152
- const c = clientRef.current;
153
- if (!c?.cron) return;
154
- try {
155
- const res = await c.cron.listJobs();
156
- setJobs(res.jobs);
157
- } catch (e: unknown) {
158
- // Swallowed before, which left the list showing whatever it last
159
- // held — or nothing — after a failed refresh. "No cron jobs" and
160
- // "could not read the cron jobs" are not the same statement, and
161
- // this view has a snackbar precisely so they can be told apart; the
162
- // initial load already uses it.
163
- snackbarRef.current.open({
164
- type: "error",
165
- message: e instanceof Error ? e.message : String(e)
166
- });
167
- }
168
- }
169
-
170
- async function refreshLogs(id: string) {
171
- const c = clientRef.current;
172
- if (!c?.cron) return;
173
- setLogsLoading(true);
174
- try {
175
- const res = await c.cron.getJobLogs(id, { limit: 25 });
176
- setLogs(res.logs);
177
- } catch (e: unknown) {
178
- // Clearing the list silently reads as "this job has never run".
179
- setLogs([]);
180
- snackbarRef.current.open({
181
- type: "error",
182
- message: e instanceof Error ? e.message : String(e)
183
- });
184
- }
185
- finally { setLogsLoading(false); }
186
- }
187
-
188
- const handleTrigger = async (id: string) => {
189
- const c = clientRef.current;
190
- if (!c?.cron) return;
191
- setTriggering(id);
192
- try {
193
- await c.cron.triggerJob(id);
194
- snackbarRef.current.open({ type: "success",
195
- message: "Job triggered" });
196
- await refreshJobs();
197
- if (selectedId === id) refreshLogs(id);
198
- } catch (e: unknown) {
199
- snackbarRef.current.open({ type: "error",
200
- message: e instanceof Error ? e.message : String(e) });
201
- } finally { setTriggering(null); }
202
- };
203
-
204
- const handleToggle = async (id: string, enabled: boolean) => {
205
- const c = clientRef.current;
206
- if (!c?.cron) return;
207
- try {
208
- await c.cron.toggleJob(id, enabled);
209
- snackbarRef.current.open({ type: "success",
210
- message: enabled ? "Job enabled" : "Job paused" });
211
- await refreshJobs();
212
- } catch (e: unknown) {
213
- snackbarRef.current.open({ type: "error",
214
- message: e instanceof Error ? e.message : String(e) });
215
- }
216
- };
217
-
218
- const selectedJob = jobs.find(j => j.id === selectedId);
219
-
220
- if (loading) return <div className="flex items-center justify-center h-full"><CircularProgress/></div>;
221
-
222
- if (jobs.length === 0) return (
223
- <div className="flex flex-col items-center justify-center h-full gap-4 text-center p-8">
224
- <CalendarIcon size={iconSize.medium} className="text-surface-300 dark:text-surface-600"/>
225
- <Typography variant="h6" color="secondary">No Cron Jobs Registered</Typography>
226
- <Typography variant="body2" color="disabled" className="max-w-md">
227
- Create a file in your <code className="text-xs bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded font-mono">crons/</code> directory that default-exports a <code className="text-xs bg-surface-100 dark:bg-surface-950 px-1.5 py-0.5 rounded font-mono">CronJobDefinition</code>.
228
- </Typography>
229
- </div>
230
- );
231
-
232
- return (
233
- <div className="flex h-full w-full overflow-hidden bg-white dark:bg-surface-950">
234
- {/* ── Job List ── */}
235
- <div className={cls("flex flex-col w-[340px] min-w-[280px] border-r h-full", defaultBorderMixin)}>
236
- <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)}>
237
- <div className="flex items-center gap-2">
238
- <CalendarIcon size={iconSize.smallest} className="text-primary"/>
239
- <Typography variant="subtitle2" className="font-semibold">Cron Jobs</Typography>
240
- <Chip size="smallest" className="bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300">{jobs.length}</Chip>
241
- </div>
242
- <IconButton size="small" onClick={refreshJobs} title="Refresh"><RefreshCwIcon size={iconSize.smallest}/></IconButton>
243
- </div>
244
- <div className="flex-1 overflow-y-auto p-2 space-y-1">
245
- {jobs.map(job => (
246
- <div
247
- key={job.id}
248
- onClick={() => setSelectedId(job.id)}
249
- className={cls(
250
- "flex items-center gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all",
251
- selectedId === job.id
252
- ? "bg-primary/10 dark:bg-primary/15 ring-1 ring-primary/30"
253
- : "hover:bg-surface-100 dark:hover:bg-surface-950"
254
- )}
255
- >
256
- <div className={cls("w-2 h-2 rounded-full shrink-0", stateColors[job.state] || "bg-surface-400")}/>
257
- <div className="flex-1 min-w-0">
258
- <Typography variant="body2" className="truncate font-medium text-[13px]">{job.name}</Typography>
259
- <Typography variant="caption" color="secondary" className="truncate text-[11px] font-mono">{job.schedule}</Typography>
260
- </div>
261
- {job.state === "running" && <CircularProgress size="smallest"/>}
262
- </div>
263
- ))}
264
- </div>
265
- </div>
266
-
267
- {/* ── Detail Panel ── */}
268
- <div className="flex-1 flex flex-col min-w-0 h-full overflow-hidden">
269
- {!selectedJob ? (
270
- <div className="flex items-center justify-center h-full">
271
- <Typography variant="body2" color="disabled">Select a cron job to view details</Typography>
272
- </div>
273
- ) : (
274
- <>
275
- {/* Header */}
276
- <div className={cls("flex items-center justify-between px-5 py-3 border-b bg-white dark:bg-surface-950 min-h-[56px]", defaultBorderMixin)}>
277
- <div className="flex items-center gap-3 min-w-0">
278
- <div className={cls("w-2.5 h-2.5 rounded-full", stateColors[selectedJob.state])}/>
279
- <div className="min-w-0">
280
- <Typography variant="subtitle1" className="font-semibold truncate">{selectedJob.name}</Typography>
281
- {selectedJob.description && <Typography variant="caption" color="secondary" className="truncate">{selectedJob.description}</Typography>}
282
- </div>
283
- </div>
284
- <div className="flex items-center gap-2 shrink-0">
285
- <IconButton title={selectedJob.enabled ? "Pause job" : "Enable job"} size="small" onClick={() => handleToggle(selectedJob.id, !selectedJob.enabled)}>
286
- {selectedJob.enabled ? <PauseIcon size={iconSize.small}/> : <PlayIcon size={iconSize.smallest}/>}
287
- </IconButton>
288
- <Button
289
- size="small"
290
- color="primary"
291
- onClick={() => handleTrigger(selectedJob.id)}
292
- disabled={triggering === selectedJob.id}
293
- startIcon={triggering === selectedJob.id ? <CircularProgress size="smallest"/> : <PlayIcon size={iconSize.smallest}/>}
294
- >
295
- Run Now
296
- </Button>
297
- </div>
298
- </div>
299
-
300
- {/* Stats Cards */}
301
- <div className="px-5 py-4 bg-surface-50 dark:bg-surface-900/50">
302
- <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
303
- <StatCard label="Schedule" value={selectedJob.schedule} mono/>
304
- <StatCard label="Last Run" value={formatRelative(selectedJob.lastRunAt)}/>
305
- <StatCard label="Next Run" value={selectedJob.enabled ? formatRelative(selectedJob.nextRunAt) : "Paused"}/>
306
- <StatCard label="Duration" value={selectedJob.lastDurationMs !== undefined ? formatDuration(selectedJob.lastDurationMs) : "—"}/>
307
- </div>
308
- <div className="grid grid-cols-3 gap-3 mt-3">
309
- <StatCard label="Status" value={selectedJob.state.toUpperCase()} chipColor={selectedJob.state === "error" ? "red" : selectedJob.state === "disabled" ? "gray" : "green"}/>
310
- <StatCard label="Total Runs" value={String(selectedJob.totalRuns)}/>
311
- <StatCard label="Failures" value={String(selectedJob.totalFailures)} highlight={selectedJob.totalFailures > 0}/>
312
- </div>
313
- {selectedJob.lastError && (
314
- <div className="mt-3 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50">
315
- <div className="flex items-center gap-2 mb-1">
316
- <AlertCircleIcon size={iconSize.smallest} className="text-red-500"/>
317
- <Typography variant="caption" className="font-semibold text-red-700 dark:text-red-400">Last Error</Typography>
318
- </div>
319
- <Typography variant="caption" className="font-mono text-red-600 dark:text-red-300 text-[11px] break-all">{selectedJob.lastError}</Typography>
320
- </div>
321
- )}
322
- </div>
323
-
324
- {/* Logs Section */}
325
- <div className={cls("flex items-center justify-between px-5 py-2 border-y bg-white dark:bg-surface-950", defaultBorderMixin)}>
326
- <div className="flex items-center gap-2">
327
- <HistoryIcon size={iconSize.smallest} className="text-surface-400"/>
328
- <Typography variant="subtitle2" className="font-semibold text-[13px]">Execution History</Typography>
329
- </div>
330
- <IconButton size="small" onClick={() => refreshLogs(selectedJob.id)} title="Refresh logs"><RefreshCwIcon size={iconSize.smallest}/></IconButton>
331
- </div>
332
- <div className="flex-1 overflow-y-auto">
333
- {logsLoading ? (
334
- <div className="flex justify-center p-8"><CircularProgress size="small"/></div>
335
- ) : logs.length === 0 ? (
336
- <div className="flex items-center justify-center h-32">
337
- <Typography variant="body2" color="disabled">No executions yet</Typography>
338
- </div>
339
- ) : (
340
- <div className="divide-y divide-surface-100 dark:divide-surface-950">
341
- {logs.map((log, idx) => (
342
- <LogRow key={idx} log={log}/>
343
- ))}
344
- </div>
345
- )}
346
- </div>
347
- </>
348
- )}
349
- </div>
350
- </div>
351
- );
352
- }
353
-
354
- function StatCard({ label, value, mono, chipColor, highlight }: {
355
- label: string; value: string; mono?: boolean; chipColor?: string; highlight?: boolean;
356
- }) {
357
- return (
358
- <div className={cls("px-3 py-2 rounded-lg border bg-white dark:bg-surface-900", defaultBorderMixin)}>
359
- <Typography variant="caption" color="secondary" className="text-[10px] uppercase tracking-wider font-medium">{label}</Typography>
360
- <Typography variant="body2" className={cls(
361
- "mt-0.5 font-semibold text-[13px]",
362
- mono && "font-mono",
363
- highlight && "text-red-500 dark:text-red-400",
364
- chipColor === "red" && "text-red-500",
365
- chipColor === "green" && "text-emerald-500",
366
- chipColor === "gray" && "text-surface-400"
367
- )}>{value}</Typography>
368
- </div>
369
- );
370
- }
371
-
372
- function LogRow({ log }: { log: CronJobLogEntry }) {
373
- const [expanded, setExpanded] = useState(false);
374
- return (
375
- <div className="px-5 py-2.5 hover:bg-surface-50 dark:hover:bg-surface-950/50 transition-colors">
376
- <div className="flex items-center gap-3 cursor-pointer" onClick={() => setExpanded(!expanded)}>
377
- {log.success
378
- ? <CheckCircleIcon size={iconSize.smallest} className="text-emerald-500 shrink-0"/>
379
- : <AlertCircleIcon size={iconSize.smallest} className="text-red-500 shrink-0"/>}
380
- <div className="flex-1 min-w-0">
381
- <Typography variant="caption" className="font-mono text-[11px] text-surface-500">{new Date(log.startedAt).toLocaleString()}</Typography>
382
- </div>
383
- <div className="flex items-center gap-2 shrink-0">
384
- {log.manual && <Chip size="smallest" className="bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300">manual</Chip>}
385
- <Typography variant="caption" className="font-mono text-[11px]">{formatDuration(log.durationMs)}</Typography>
386
- <svg className={cls("w-3 h-3 transition-transform text-surface-400", expanded && "rotate-180")} fill="currentColor" viewBox="0 0 20 20"><path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/></svg>
387
- </div>
388
- </div>
389
- {expanded && (
390
- <div className="mt-2 ml-6 space-y-2">
391
- {log.error && (
392
- <div className="p-2 rounded bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50">
393
- <Typography variant="caption" className="font-mono text-[11px] text-red-600 dark:text-red-300 break-all">{log.error}</Typography>
394
- </div>
395
- )}
396
- {log.logs.length > 0 && (
397
- <div className="p-2 rounded bg-surface-50 dark:bg-surface-900 border border-surface-200 dark:border-surface-700 max-h-40 overflow-auto">
398
- {log.logs.map((line, i) => (
399
- <div key={i} className="font-mono text-[11px] text-surface-600 dark:text-surface-400 leading-relaxed">{line}</div>
400
- ))}
401
- </div>
402
- )}
403
- {log.result !== undefined && (
404
- <div className="p-2 rounded bg-surface-50 dark:bg-surface-900 border border-surface-200 dark:border-surface-700">
405
- <Typography variant="caption" className="text-[10px] uppercase tracking-wider text-surface-400 mb-1 block">Result</Typography>
406
- <pre className="font-mono text-[11px] text-surface-600 dark:text-surface-400 whitespace-pre-wrap break-all">{JSON.stringify(log.result, null, 2)}</pre>
407
- </div>
408
- )}
409
- </div>
410
- )}
411
- </div>
412
- );
413
- }