@growthub/cli 0.12.2 → 0.13.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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +50 -25
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +556 -248
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +242 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +52 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +1203 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +163 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +190 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +64 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +376 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/dm-shared.jsx +8 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +2897 -934
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/views/[viewId]/page.jsx +206 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +493 -28
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +1363 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +13 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +96 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +122 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
- package/package.json +1 -1
|
@@ -25,18 +25,17 @@ import {
|
|
|
25
25
|
Layers,
|
|
26
26
|
Link2,
|
|
27
27
|
Lock,
|
|
28
|
-
List,
|
|
29
28
|
Mail,
|
|
30
29
|
Maximize2,
|
|
31
30
|
MoreHorizontal,
|
|
32
31
|
Plus,
|
|
33
|
-
Pin,
|
|
34
32
|
Pencil,
|
|
35
33
|
Search,
|
|
36
34
|
ShoppingCart,
|
|
37
35
|
Tag,
|
|
38
36
|
Terminal,
|
|
39
37
|
ToggleLeft,
|
|
38
|
+
Trash2,
|
|
40
39
|
Type,
|
|
41
40
|
Users,
|
|
42
41
|
X,
|
|
@@ -62,7 +61,6 @@ import {
|
|
|
62
61
|
parseSandboxAllowList,
|
|
63
62
|
parseSandboxEnvRefs,
|
|
64
63
|
replaceTableContent,
|
|
65
|
-
snapshotTableViewState,
|
|
66
64
|
transformTableSchema,
|
|
67
65
|
updateTableFieldSettings,
|
|
68
66
|
updateTableCell,
|
|
@@ -72,6 +70,17 @@ import { SandboxRunPanel } from "./SandboxRunPanel.jsx";
|
|
|
72
70
|
import { StatusPill } from "./StatusPill.jsx";
|
|
73
71
|
import { SegmentedToggle, ToggleField } from "./ToggleField.jsx";
|
|
74
72
|
import { SourceTestPanel } from "./SourceTestPanel.jsx";
|
|
73
|
+
import { ApiRegistryActionCard } from "./ApiRegistryActionCard.jsx";
|
|
74
|
+
import { SandboxToolDraftPanel } from "./SandboxToolDraftPanel.jsx";
|
|
75
|
+
import { SandboxToolConfirmModal } from "./SandboxToolConfirmModal.jsx";
|
|
76
|
+
import { SandboxOrchestrationEditorPanel } from "./SandboxOrchestrationEditorPanel.jsx";
|
|
77
|
+
import { OrchestrationRunTracePanel } from "./OrchestrationRunTracePanel.jsx";
|
|
78
|
+
import {
|
|
79
|
+
buildSandboxRowFromApiRegistry,
|
|
80
|
+
findSandboxRowsForRegistry,
|
|
81
|
+
getOrchestrationGraphUiState,
|
|
82
|
+
redactSecretsFromText
|
|
83
|
+
} from "@/lib/orchestration-graph";
|
|
75
84
|
import {
|
|
76
85
|
FIELD_TYPE_ICON_NAMES,
|
|
77
86
|
ICON_PICKER_SET,
|
|
@@ -199,88 +208,20 @@ function applyRowsView(rows, settings) {
|
|
|
199
208
|
});
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
function ObjectViewPicker({ tables, selectedTable,
|
|
211
|
+
function ObjectViewPicker({ tables, selectedTable, onSelectSource }) {
|
|
203
212
|
const pickerRef = useRef(null);
|
|
204
213
|
const [open, setOpen] = useState(false);
|
|
205
|
-
const [mode, setMode] = useState("all");
|
|
206
|
-
const [newViewName, setNewViewName] = useState("");
|
|
207
|
-
const [viewMenuId, setViewMenuId] = useState("");
|
|
208
|
-
const currentViews = selectedTable?.fieldSettings?.views || [];
|
|
209
|
-
const favoriteObjects = tables.filter((table) => table.fieldSettings?.favorite);
|
|
210
214
|
|
|
211
215
|
useEffect(() => {
|
|
212
216
|
function handlePointer(event) {
|
|
213
217
|
if (!pickerRef.current?.contains(event.target)) {
|
|
214
|
-
|
|
218
|
+
setOpen(false);
|
|
215
219
|
}
|
|
216
220
|
}
|
|
217
221
|
document.addEventListener("pointerdown", handlePointer);
|
|
218
222
|
return () => document.removeEventListener("pointerdown", handlePointer);
|
|
219
223
|
}, []);
|
|
220
224
|
|
|
221
|
-
function applyView(view) {
|
|
222
|
-
if (!selectedTable) return;
|
|
223
|
-
const nextState = view
|
|
224
|
-
? { ...snapshotTableViewState(view), activeViewId: view.id }
|
|
225
|
-
: { activeViewId: "", hidden: [], order: selectedTable.columns, sort: [], filter: null };
|
|
226
|
-
onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
|
|
227
|
-
...settings,
|
|
228
|
-
...nextState
|
|
229
|
-
})));
|
|
230
|
-
setOpen(false);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function createView() {
|
|
234
|
-
const name = newViewName.trim();
|
|
235
|
-
if (!selectedTable || !name) return;
|
|
236
|
-
const viewId = `view_${Date.now().toString(36)}`;
|
|
237
|
-
onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
|
|
238
|
-
...settings,
|
|
239
|
-
activeViewId: viewId,
|
|
240
|
-
views: [...(settings.views || []), {
|
|
241
|
-
id: viewId,
|
|
242
|
-
name,
|
|
243
|
-
favorite: false,
|
|
244
|
-
locked: false,
|
|
245
|
-
...snapshotTableViewState(settings)
|
|
246
|
-
}]
|
|
247
|
-
})));
|
|
248
|
-
setNewViewName("");
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function toggleViewFavorite(viewId) {
|
|
252
|
-
if (!selectedTable) return;
|
|
253
|
-
onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
|
|
254
|
-
...settings,
|
|
255
|
-
views: (settings.views || []).map((view) => view.id === viewId ? { ...view, favorite: !view.favorite } : view)
|
|
256
|
-
})));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function deleteView(viewId) {
|
|
260
|
-
if (!selectedTable) return;
|
|
261
|
-
onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
|
|
262
|
-
...settings,
|
|
263
|
-
activeViewId: settings.activeViewId === viewId ? "" : settings.activeViewId,
|
|
264
|
-
views: (settings.views || []).filter((view) => view.id !== viewId)
|
|
265
|
-
})));
|
|
266
|
-
setViewMenuId("");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function renameView(view) {
|
|
270
|
-
if (!selectedTable) return;
|
|
271
|
-
const nextName = window.prompt("Rename view", view.name);
|
|
272
|
-
if (!nextName?.trim()) return;
|
|
273
|
-
onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
|
|
274
|
-
...settings,
|
|
275
|
-
views: (settings.views || []).map((candidate) => candidate.id === view.id ? { ...candidate, name: nextName.trim() } : candidate)
|
|
276
|
-
})));
|
|
277
|
-
setViewMenuId("");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const activeView = currentViews.find((view) => view.id === selectedTable?.fieldSettings?.activeViewId) || null;
|
|
281
|
-
const objects = mode === "views" ? [] : tables;
|
|
282
|
-
const views = mode === "objects" ? [] : currentViews;
|
|
283
|
-
|
|
284
225
|
return (
|
|
285
226
|
<div
|
|
286
227
|
ref={pickerRef}
|
|
@@ -288,125 +229,36 @@ function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSav
|
|
|
288
229
|
onBlur={(event) => {
|
|
289
230
|
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
290
231
|
setOpen(false);
|
|
291
|
-
setViewMenuId("");
|
|
292
232
|
}
|
|
293
233
|
}}
|
|
294
234
|
>
|
|
295
235
|
<button type="button" className="dm-picker-trigger" onClick={() => setOpen((current) => !current)}>
|
|
296
236
|
<LucideIcon name={selectedTable?.icon || OBJECT_TYPE_PRESETS[selectedTable?.objectType]?.icon || "Database"} size={14} />
|
|
297
237
|
<span className="dm-picker-trigger-copy">
|
|
298
|
-
<strong>{
|
|
238
|
+
<strong>{selectedTable?.label || "Object"}</strong>
|
|
299
239
|
<em>{pluralize(selectedTable?.columns?.length || 0, "field")} · {pluralize(selectedTable?.rows?.length || 0, "record")}</em>
|
|
300
240
|
</span>
|
|
301
241
|
<ChevronDown size={14} />
|
|
302
242
|
</button>
|
|
303
243
|
{open && (
|
|
304
244
|
<div className="dm-picker-popover">
|
|
305
|
-
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
{
|
|
309
|
-
<
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
245
|
+
<div className="dm-picker-section">
|
|
246
|
+
<p>Objects</p>
|
|
247
|
+
<div className="dm-picker-scroll">
|
|
248
|
+
{tables.map((table, objIdx) => (
|
|
249
|
+
<div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
|
|
250
|
+
<button type="button" className="dm-picker-row" onClick={() => {
|
|
251
|
+
onSelectSource(table.source);
|
|
252
|
+
setOpen(false);
|
|
253
|
+
}}>
|
|
254
|
+
<LucideIcon name={table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database"} size={14} />
|
|
255
|
+
<span>{table.label}</span>
|
|
256
|
+
{isLockedObject(table) && <Lock size={12} className="dm-picker-lock" />}
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
313
259
|
))}
|
|
314
260
|
</div>
|
|
315
|
-
)}
|
|
316
|
-
<div className="dm-picker-tabs">
|
|
317
|
-
{[
|
|
318
|
-
{ id: "all", label: "All" },
|
|
319
|
-
{ id: "objects", label: "Objects" },
|
|
320
|
-
{ id: "views", label: "Views" },
|
|
321
|
-
].map((item) => (
|
|
322
|
-
<button key={item.id} type="button" className={mode === item.id ? "active" : ""} onClick={() => setMode(item.id)}>
|
|
323
|
-
{item.label}
|
|
324
|
-
</button>
|
|
325
|
-
))}
|
|
326
261
|
</div>
|
|
327
|
-
{objects.length > 0 && (
|
|
328
|
-
<div className="dm-picker-section">
|
|
329
|
-
<p>Objects</p>
|
|
330
|
-
<div className="dm-picker-scroll">
|
|
331
|
-
{objects.map((table, objIdx) => (
|
|
332
|
-
<div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
|
|
333
|
-
<button type="button" className="dm-picker-row" onClick={() => {
|
|
334
|
-
onSelectSource(table.source);
|
|
335
|
-
setOpen(false);
|
|
336
|
-
}}>
|
|
337
|
-
<LucideIcon name={table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database"} size={14} />
|
|
338
|
-
<span>{table.label}</span>
|
|
339
|
-
{isLockedObject(table) && <Lock size={12} className="dm-picker-lock" />}
|
|
340
|
-
</button>
|
|
341
|
-
</div>
|
|
342
|
-
))}
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
)}
|
|
346
|
-
{selectedTable && (
|
|
347
|
-
<div className="dm-picker-section">
|
|
348
|
-
<p>Views</p>
|
|
349
|
-
<button type="button" className={`dm-picker-row${!activeView ? " active" : ""}`} onClick={() => applyView(null)}>
|
|
350
|
-
<List size={14} />
|
|
351
|
-
<span>{selectedTable.label}</span>
|
|
352
|
-
{isLockedObject(selectedTable) && <Lock size={12} className="dm-picker-lock" />}
|
|
353
|
-
</button>
|
|
354
|
-
<div className="dm-picker-scroll">
|
|
355
|
-
{views.map((view) => (
|
|
356
|
-
<div key={view.id} className={`dm-picker-item${activeView?.id === view.id ? " active" : ""}`}>
|
|
357
|
-
<button type="button" className="dm-picker-row" onClick={() => applyView(view)}>
|
|
358
|
-
<List size={14} />
|
|
359
|
-
<span>{view.name}</span>
|
|
360
|
-
</button>
|
|
361
|
-
<div className="dm-picker-actions">
|
|
362
|
-
<button
|
|
363
|
-
type="button"
|
|
364
|
-
className="dm-picker-icon-btn"
|
|
365
|
-
aria-label="View actions"
|
|
366
|
-
onClick={(event) => {
|
|
367
|
-
event.stopPropagation();
|
|
368
|
-
setViewMenuId((current) => current === view.id ? "" : view.id);
|
|
369
|
-
}}
|
|
370
|
-
>
|
|
371
|
-
<MoreHorizontal size={12} style={{ transform: "rotate(90deg)" }} />
|
|
372
|
-
</button>
|
|
373
|
-
{viewMenuId === view.id && (
|
|
374
|
-
<div className="dm-picker-menu">
|
|
375
|
-
<button type="button" onClick={() => toggleViewFavorite(view.id)}>
|
|
376
|
-
<Pin size={13} />
|
|
377
|
-
{view.favorite ? "Unpin" : "Pin"}
|
|
378
|
-
</button>
|
|
379
|
-
<button type="button" onClick={() => renameView(view)}>
|
|
380
|
-
<Type size={13} />
|
|
381
|
-
Rename
|
|
382
|
-
</button>
|
|
383
|
-
{!view.locked && (
|
|
384
|
-
<button type="button" className="danger" onClick={() => deleteView(view.id)}>
|
|
385
|
-
<X size={13} />
|
|
386
|
-
Delete
|
|
387
|
-
</button>
|
|
388
|
-
)}
|
|
389
|
-
</div>
|
|
390
|
-
)}
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
))}
|
|
394
|
-
</div>
|
|
395
|
-
<div className="dm-picker-create">
|
|
396
|
-
<input
|
|
397
|
-
value={newViewName}
|
|
398
|
-
placeholder="New view name"
|
|
399
|
-
onChange={(event) => setNewViewName(event.target.value)}
|
|
400
|
-
onKeyDown={(event) => {
|
|
401
|
-
if (event.key === "Enter") createView();
|
|
402
|
-
}}
|
|
403
|
-
/>
|
|
404
|
-
<button type="button" className="dm-btn-outline" disabled={saving || !newViewName.trim()} onClick={createView}>
|
|
405
|
-
<Plus size={13} />Add view
|
|
406
|
-
</button>
|
|
407
|
-
</div>
|
|
408
|
-
</div>
|
|
409
|
-
)}
|
|
410
262
|
</div>
|
|
411
263
|
)}
|
|
412
264
|
</div>
|
|
@@ -711,6 +563,24 @@ function RecordFieldEditor({ table, tables, column, value, saving, editable, onD
|
|
|
711
563
|
);
|
|
712
564
|
}
|
|
713
565
|
|
|
566
|
+
function SandboxTraceFieldButton({ label, value, disabled, onOpen }) {
|
|
567
|
+
const hasValue = value !== null && value !== undefined && String(value).trim() !== "";
|
|
568
|
+
return (
|
|
569
|
+
<label className="dm-record-field dm-field-link">
|
|
570
|
+
<span>{label}</span>
|
|
571
|
+
<button
|
|
572
|
+
type="button"
|
|
573
|
+
className="dm-field-link__btn"
|
|
574
|
+
disabled={disabled || !hasValue}
|
|
575
|
+
onClick={() => onOpen?.()}
|
|
576
|
+
>
|
|
577
|
+
{hasValue ? String(value).slice(0, 80) + (String(value).length > 80 ? "…" : "") : "—"}
|
|
578
|
+
</button>
|
|
579
|
+
<span className="dm-field-link__hint">Opens run trace viewer</span>
|
|
580
|
+
</label>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
714
584
|
function SandboxRecordFields({
|
|
715
585
|
draft,
|
|
716
586
|
setDraft,
|
|
@@ -724,7 +594,8 @@ function SandboxRecordFields({
|
|
|
724
594
|
sandboxHistoryMessage,
|
|
725
595
|
loadingSandboxHistory,
|
|
726
596
|
onLoadSandboxHistory,
|
|
727
|
-
|
|
597
|
+
onOpenGraphSidecar,
|
|
598
|
+
onOpenTraceSidecar
|
|
728
599
|
}) {
|
|
729
600
|
const [sandboxAdapters, setSandboxAdapters] = useState([]);
|
|
730
601
|
useEffect(() => {
|
|
@@ -990,32 +861,72 @@ function SandboxRecordFields({
|
|
|
990
861
|
</label>
|
|
991
862
|
</DrawerSection>
|
|
992
863
|
|
|
864
|
+
<DrawerSection title="Orchestration">
|
|
865
|
+
<div className="dm-record-field">
|
|
866
|
+
<span>{draft.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph"}</span>
|
|
867
|
+
<button
|
|
868
|
+
type="button"
|
|
869
|
+
className="dm-btn-outline"
|
|
870
|
+
disabled={saving}
|
|
871
|
+
onClick={() => onOpenGraphSidecar?.()}
|
|
872
|
+
>
|
|
873
|
+
{getOrchestrationGraphUiState(draft.orchestrationGraph ?? draft.orchestrationConfig) === "populated" ? "Edit orchestration graph" : "Start orchestration graph"}
|
|
874
|
+
</button>
|
|
875
|
+
</div>
|
|
876
|
+
</DrawerSection>
|
|
877
|
+
|
|
993
878
|
<DrawerSection title="Response & History">
|
|
994
|
-
<
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
879
|
+
<SandboxTraceFieldButton
|
|
880
|
+
label="lastRunId"
|
|
881
|
+
value={draft.lastRunId}
|
|
882
|
+
disabled={saving}
|
|
883
|
+
onOpen={() => onOpenTraceSidecar?.({ field: "lastRunId", runId: draft.lastRunId })}
|
|
884
|
+
/>
|
|
998
885
|
|
|
999
|
-
<
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
886
|
+
<SandboxTraceFieldButton
|
|
887
|
+
label="lastSourceId"
|
|
888
|
+
value={draft.lastSourceId}
|
|
889
|
+
disabled={saving}
|
|
890
|
+
onOpen={() => onOpenTraceSidecar?.({ field: "lastSourceId" })}
|
|
891
|
+
/>
|
|
1003
892
|
|
|
1004
|
-
<label className="dm-record-field dm-
|
|
893
|
+
<label className="dm-record-field dm-field-link">
|
|
1005
894
|
<span>lastResponse</span>
|
|
1006
895
|
<button
|
|
1007
896
|
type="button"
|
|
1008
|
-
className="dm-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
disabled={!draft.lastResponse}
|
|
1012
|
-
onClick={onExpandLastResponse}
|
|
897
|
+
className="dm-field-link__btn"
|
|
898
|
+
disabled={saving || !draft.lastResponse}
|
|
899
|
+
onClick={() => onOpenTraceSidecar?.({ field: "lastResponse" })}
|
|
1013
900
|
>
|
|
1014
|
-
|
|
901
|
+
{draft.lastResponse ? "View run trace" : "—"}
|
|
1015
902
|
</button>
|
|
1016
|
-
<
|
|
903
|
+
<span className="dm-field-link__hint">Run output — not the graph builder</span>
|
|
1017
904
|
</label>
|
|
1018
905
|
|
|
906
|
+
<label className="dm-record-field">
|
|
907
|
+
<span>status</span>
|
|
908
|
+
<div className="dm-field-link__row">
|
|
909
|
+
<StatusPill value={draft.status} />
|
|
910
|
+
{(draft.lastRunId || draft.lastResponse) && (
|
|
911
|
+
<button
|
|
912
|
+
type="button"
|
|
913
|
+
className="dm-btn-ghost"
|
|
914
|
+
disabled={saving}
|
|
915
|
+
onClick={() => onOpenTraceSidecar?.({ field: "lastResponse", runId: draft.lastRunId })}
|
|
916
|
+
>
|
|
917
|
+
View latest run
|
|
918
|
+
</button>
|
|
919
|
+
)}
|
|
920
|
+
</div>
|
|
921
|
+
</label>
|
|
922
|
+
|
|
923
|
+
{draft.lastTested && (
|
|
924
|
+
<label className="dm-record-field">
|
|
925
|
+
<span>lastTested</span>
|
|
926
|
+
<input readOnly value={draft.lastTested} />
|
|
927
|
+
</label>
|
|
928
|
+
)}
|
|
929
|
+
|
|
1019
930
|
<div className="dm-record-field">
|
|
1020
931
|
<span>Run history</span>
|
|
1021
932
|
<button type="button" className="dm-btn-ghost" disabled={loadingSandboxHistory} onClick={onLoadSandboxHistory}>
|
|
@@ -1045,7 +956,19 @@ function SandboxRecordFields({
|
|
|
1045
956
|
);
|
|
1046
957
|
}
|
|
1047
958
|
|
|
1048
|
-
function DataModelRecordDrawer({
|
|
959
|
+
function DataModelRecordDrawer({
|
|
960
|
+
table,
|
|
961
|
+
tables,
|
|
962
|
+
workspaceConfig,
|
|
963
|
+
rowIndex,
|
|
964
|
+
row,
|
|
965
|
+
saving,
|
|
966
|
+
onClose,
|
|
967
|
+
onSave,
|
|
968
|
+
onFocusSandboxRow,
|
|
969
|
+
initialSidecar,
|
|
970
|
+
onClearInitialSidecar,
|
|
971
|
+
}) {
|
|
1049
972
|
const [draft, setDraft] = useState(row || {});
|
|
1050
973
|
const [editMode, setEditMode] = useState(false);
|
|
1051
974
|
const [pendingColumns, setPendingColumns] = useState(table.columns || []);
|
|
@@ -1058,6 +981,15 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1058
981
|
const [sandboxHistoryMessage, setSandboxHistoryMessage] = useState("");
|
|
1059
982
|
const [loadingSandboxHistory, setLoadingSandboxHistory] = useState(false);
|
|
1060
983
|
const [expandedJson, setExpandedJson] = useState(null);
|
|
984
|
+
const [sandboxToolFlow, setSandboxToolFlow] = useState(null);
|
|
985
|
+
const [sandboxToolDraft, setSandboxToolDraft] = useState({});
|
|
986
|
+
const [sandboxToolCreating, setSandboxToolCreating] = useState(false);
|
|
987
|
+
const [createdSandboxMeta, setCreatedSandboxMeta] = useState(null);
|
|
988
|
+
const [createdSandboxTesting, setCreatedSandboxTesting] = useState(false);
|
|
989
|
+
const [createdSandboxTestMessage, setCreatedSandboxTestMessage] = useState("");
|
|
990
|
+
const [sidecarMode, setSidecarMode] = useState(null);
|
|
991
|
+
const [traceField, setTraceField] = useState(null);
|
|
992
|
+
const [traceRunId, setTraceRunId] = useState("");
|
|
1061
993
|
|
|
1062
994
|
useEffect(() => {
|
|
1063
995
|
setDraft(row || {});
|
|
@@ -1069,10 +1001,28 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1069
1001
|
setSandboxHistory([]);
|
|
1070
1002
|
setSandboxHistoryMessage("");
|
|
1071
1003
|
setExpandedJson(null);
|
|
1072
|
-
|
|
1004
|
+
setSandboxToolFlow(null);
|
|
1005
|
+
setSandboxToolDraft({});
|
|
1006
|
+
setCreatedSandboxMeta(null);
|
|
1007
|
+
setCreatedSandboxTestMessage("");
|
|
1008
|
+
if (initialSidecar?.mode === "graph") {
|
|
1009
|
+
setSidecarMode("graph");
|
|
1010
|
+
setTraceField(null);
|
|
1011
|
+
setTraceRunId("");
|
|
1012
|
+
} else if (initialSidecar?.mode === "trace") {
|
|
1013
|
+
setSidecarMode("trace");
|
|
1014
|
+
setTraceField(initialSidecar.field || "lastResponse");
|
|
1015
|
+
setTraceRunId(String(initialSidecar.runId || row?.lastRunId || "").trim());
|
|
1016
|
+
} else {
|
|
1017
|
+
setSidecarMode(null);
|
|
1018
|
+
setTraceField(null);
|
|
1019
|
+
setTraceRunId("");
|
|
1020
|
+
}
|
|
1021
|
+
}, [row, rowIndex, initialSidecar]);
|
|
1073
1022
|
|
|
1074
1023
|
if (rowIndex === null || rowIndex === undefined || !row) return null;
|
|
1075
1024
|
|
|
1025
|
+
const isApiRegistry = table.objectType === "api-registry";
|
|
1076
1026
|
const isSandbox = table.objectType === "sandbox-environment";
|
|
1077
1027
|
const isDirty = JSON.stringify(draft || {}) !== JSON.stringify(row || {}) || JSON.stringify(pendingColumns) !== JSON.stringify(table.columns || []) || JSON.stringify(pendingHidden) !== JSON.stringify(table.fieldSettings?.hidden || []);
|
|
1078
1028
|
|
|
@@ -1164,6 +1114,147 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1164
1114
|
}
|
|
1165
1115
|
}
|
|
1166
1116
|
|
|
1117
|
+
function ensureSandboxColumns(config, sandboxTable) {
|
|
1118
|
+
let next = config;
|
|
1119
|
+
let current = sandboxTable;
|
|
1120
|
+
for (const field of ["orchestrationGraph", "description"]) {
|
|
1121
|
+
if (!current.columns.includes(field)) {
|
|
1122
|
+
next = addTableField(next, current, field);
|
|
1123
|
+
const tables = listWorkspaceDataModelTables(next);
|
|
1124
|
+
current = tables.find((t) => t.objectId === sandboxTable.objectId) || current;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return { config: next, sandboxTable: current };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function createSandboxToolFromRegistry() {
|
|
1131
|
+
if (!sandboxToolDraft?.name?.trim()) return;
|
|
1132
|
+
const integrationId = String(draft?.integrationId || "").trim();
|
|
1133
|
+
if (integrationId && findSandboxRowsForRegistry(workspaceConfig, integrationId).length > 0) {
|
|
1134
|
+
setCreatedSandboxTestMessage("A sandbox tool already exists for this API Registry entry. Open it instead of creating a duplicate.");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
setSandboxToolCreating(true);
|
|
1138
|
+
try {
|
|
1139
|
+
onSave((config) => {
|
|
1140
|
+
let next = config;
|
|
1141
|
+
if (integrationId && findSandboxRowsForRegistry(next, integrationId).length > 0) {
|
|
1142
|
+
return next;
|
|
1143
|
+
}
|
|
1144
|
+
let sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
|
|
1145
|
+
if (!sandboxTable) {
|
|
1146
|
+
next = createTypedBusinessObject(next, {
|
|
1147
|
+
name: "Sandbox Environments",
|
|
1148
|
+
objectType: "sandbox-environment"
|
|
1149
|
+
});
|
|
1150
|
+
sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
|
|
1151
|
+
}
|
|
1152
|
+
if (!sandboxTable) return next;
|
|
1153
|
+
const ensured = ensureSandboxColumns(next, sandboxTable);
|
|
1154
|
+
next = ensured.config;
|
|
1155
|
+
sandboxTable = ensured.sandboxTable;
|
|
1156
|
+
const newRow = buildSandboxRowFromApiRegistry(next, draft, {
|
|
1157
|
+
name: sandboxToolDraft.name,
|
|
1158
|
+
description: sandboxToolDraft.description,
|
|
1159
|
+
runLocality: sandboxToolDraft.runLocality,
|
|
1160
|
+
adapter: sandboxToolDraft.adapter,
|
|
1161
|
+
authRef: sandboxToolDraft.authRef,
|
|
1162
|
+
envRefs: sandboxToolDraft.envRefs,
|
|
1163
|
+
networkAllow: sandboxToolDraft.networkAllow,
|
|
1164
|
+
timeoutMs: sandboxToolDraft.timeoutMs,
|
|
1165
|
+
rootPath: sandboxToolDraft.rootPath,
|
|
1166
|
+
instructions: sandboxToolDraft.instructions,
|
|
1167
|
+
agentHost: sandboxToolDraft.agentHost,
|
|
1168
|
+
schedulerRegistryId: sandboxToolDraft.schedulerRegistryId,
|
|
1169
|
+
orchestrationGraph: sandboxToolDraft.orchestrationGraph
|
|
1170
|
+
});
|
|
1171
|
+
next = appendRowsToTable(next, sandboxTable, [newRow]);
|
|
1172
|
+
setCreatedSandboxMeta({
|
|
1173
|
+
objectId: sandboxTable.objectId,
|
|
1174
|
+
name: newRow.Name,
|
|
1175
|
+
authRef: newRow.authRef || sandboxToolDraft.authRef
|
|
1176
|
+
});
|
|
1177
|
+
setSandboxToolFlow("created");
|
|
1178
|
+
onFocusSandboxRow?.({ rowName: newRow.Name, deferOpen: true });
|
|
1179
|
+
return next;
|
|
1180
|
+
});
|
|
1181
|
+
} finally {
|
|
1182
|
+
setSandboxToolCreating(false);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async function runSandboxToolByName({ objectId, name }) {
|
|
1187
|
+
const rowName = String(name || "").trim();
|
|
1188
|
+
const objectIdValue = String(objectId || "").trim();
|
|
1189
|
+
if (!rowName || !objectIdValue) return;
|
|
1190
|
+
setCreatedSandboxTesting(true);
|
|
1191
|
+
setCreatedSandboxTestMessage("");
|
|
1192
|
+
try {
|
|
1193
|
+
const res = await fetch("/api/workspace/sandbox-run", {
|
|
1194
|
+
method: "POST",
|
|
1195
|
+
headers: { "content-type": "application/json" },
|
|
1196
|
+
body: JSON.stringify({ objectId: objectIdValue, name: rowName }),
|
|
1197
|
+
});
|
|
1198
|
+
const payload = await res.json();
|
|
1199
|
+
const responseText = redactSecretsFromText(JSON.stringify(payload.response ?? payload, null, 2));
|
|
1200
|
+
const status = payload.ok && String(payload.status || "").toLowerCase() === "connected" ? "connected" : "failed";
|
|
1201
|
+
const testedAt = payload.response?.ranAt || new Date().toISOString();
|
|
1202
|
+
const lastRunId = payload.runId || payload.response?.runId || "";
|
|
1203
|
+
const lastSourceId = payload.sourceId || payload.response?.sourceId || "";
|
|
1204
|
+
onSave((config) => {
|
|
1205
|
+
const sandboxTable = listWorkspaceDataModelTables(config).find((t) => t.objectType === "sandbox-environment");
|
|
1206
|
+
if (!sandboxTable) return config;
|
|
1207
|
+
const idx = (sandboxTable.rows || []).findIndex((r) => String(r?.Name || "").trim() === rowName);
|
|
1208
|
+
if (idx < 0) return config;
|
|
1209
|
+
let next = updateTableCell(config, sandboxTable, idx, "status", status);
|
|
1210
|
+
next = updateTableCell(next, sandboxTable, idx, "lastTested", testedAt);
|
|
1211
|
+
next = updateTableCell(next, sandboxTable, idx, "lastRunId", lastRunId);
|
|
1212
|
+
next = updateTableCell(next, sandboxTable, idx, "lastSourceId", lastSourceId);
|
|
1213
|
+
next = updateTableCell(next, sandboxTable, idx, "lastResponse", responseText);
|
|
1214
|
+
return next;
|
|
1215
|
+
});
|
|
1216
|
+
const safeError = redactSecretsFromText(
|
|
1217
|
+
payload.response?.error || payload.error || "Sandbox run failed"
|
|
1218
|
+
);
|
|
1219
|
+
setCreatedSandboxTestMessage(
|
|
1220
|
+
status === "connected"
|
|
1221
|
+
? "Sandbox run succeeded — lastResponse and source record saved."
|
|
1222
|
+
: safeError
|
|
1223
|
+
);
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
setCreatedSandboxTestMessage(redactSecretsFromText(err.message || "Sandbox run failed"));
|
|
1226
|
+
} finally {
|
|
1227
|
+
setCreatedSandboxTesting(false);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function resolveSandboxTableMeta() {
|
|
1232
|
+
const sandboxTable = tables.find((t) => t.objectType === "sandbox-environment")
|
|
1233
|
+
|| (workspaceConfig ? listWorkspaceDataModelTables(workspaceConfig).find((t) => t.objectType === "sandbox-environment") : null);
|
|
1234
|
+
return sandboxTable?.objectId ? { objectId: sandboxTable.objectId, table: sandboxTable } : null;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
async function runCreatedSandboxTest() {
|
|
1238
|
+
if (!createdSandboxMeta?.objectId || !createdSandboxMeta?.name) return;
|
|
1239
|
+
await runSandboxToolByName(createdSandboxMeta);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function runExistingSandboxTool({ name }) {
|
|
1243
|
+
const meta = resolveSandboxTableMeta();
|
|
1244
|
+
if (!meta) {
|
|
1245
|
+
setCreatedSandboxTestMessage("No sandbox-environment table in this workspace.");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
await runSandboxToolByName({ objectId: meta.objectId, name });
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function openSandboxToolRow({ name }) {
|
|
1252
|
+
const rowName = String(name || "").trim();
|
|
1253
|
+
if (!rowName) return;
|
|
1254
|
+
onFocusSandboxRow?.({ rowName });
|
|
1255
|
+
onClose();
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1167
1258
|
async function runSandbox() {
|
|
1168
1259
|
if (!table.objectId) {
|
|
1169
1260
|
setSandboxMessage("Missing object id for this sandbox table.");
|
|
@@ -1238,17 +1329,49 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1238
1329
|
}
|
|
1239
1330
|
}
|
|
1240
1331
|
|
|
1332
|
+
function closeSidecar() {
|
|
1333
|
+
setSidecarMode(null);
|
|
1334
|
+
setTraceField(null);
|
|
1335
|
+
setTraceRunId("");
|
|
1336
|
+
onClearInitialSidecar?.();
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function openGraphSidecar() {
|
|
1340
|
+
setSidecarMode("graph");
|
|
1341
|
+
onClearInitialSidecar?.();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function openTraceSidecar({ field, runId } = {}) {
|
|
1345
|
+
setSidecarMode("trace");
|
|
1346
|
+
setTraceField(field || "lastResponse");
|
|
1347
|
+
setTraceRunId(String(runId || draft?.lastRunId || "").trim());
|
|
1348
|
+
onClearInitialSidecar?.();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function saveOrchestrationGraph(serialized) {
|
|
1352
|
+
if (rowIndex === null || rowIndex === undefined) return;
|
|
1353
|
+
const graphField = draft.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph";
|
|
1354
|
+
onSave((config) => updateTableCell(config, table, rowIndex, graphField, serialized));
|
|
1355
|
+
setDraft((current) => ({ ...current, [graphField]: serialized }));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const drawerWide = sandboxToolFlow === "draft" || sidecarMode === "graph" || sidecarMode === "trace";
|
|
1359
|
+
const hideRecordFields = isSandbox && (sidecarMode === "graph" || sidecarMode === "trace");
|
|
1360
|
+
|
|
1241
1361
|
return (
|
|
1242
1362
|
<>
|
|
1243
1363
|
<div className="dm-record-backdrop" onClick={onClose} />
|
|
1244
|
-
<aside
|
|
1364
|
+
<aside
|
|
1365
|
+
className={`dm-record-drawer${drawerWide ? " dm-record-drawer-wide" : ""}`}
|
|
1366
|
+
aria-label="Record details"
|
|
1367
|
+
>
|
|
1245
1368
|
<header className="dm-record-drawer-head">
|
|
1246
1369
|
<div>
|
|
1247
1370
|
<p>Record</p>
|
|
1248
1371
|
<h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
|
|
1249
1372
|
</div>
|
|
1250
1373
|
<div className="dm-record-drawer-actions">
|
|
1251
|
-
{!isSandbox && (
|
|
1374
|
+
{!isSandbox && sandboxToolFlow !== "draft" && (
|
|
1252
1375
|
<button type="button" className="dm-sidebar-close" onClick={() => setEditMode((current) => !current)} aria-label="Toggle edit mode">
|
|
1253
1376
|
<Pencil size={16} />
|
|
1254
1377
|
</button>
|
|
@@ -1258,7 +1381,7 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1258
1381
|
</button>
|
|
1259
1382
|
</div>
|
|
1260
1383
|
</header>
|
|
1261
|
-
{(table.objectType === "api-registry" || table.objectType === "data-source") && (
|
|
1384
|
+
{(table.objectType === "api-registry" || table.objectType === "data-source") && sandboxToolFlow !== "draft" && (
|
|
1262
1385
|
<SourceTestPanel
|
|
1263
1386
|
status={draft.status}
|
|
1264
1387
|
testing={testing}
|
|
@@ -1267,7 +1390,67 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1267
1390
|
onTest={testApiRecord}
|
|
1268
1391
|
/>
|
|
1269
1392
|
)}
|
|
1270
|
-
{
|
|
1393
|
+
{isApiRegistry && sandboxToolFlow !== "draft" && sandboxToolFlow !== "created" && (
|
|
1394
|
+
<ApiRegistryActionCard
|
|
1395
|
+
registryRow={draft}
|
|
1396
|
+
workspaceConfig={workspaceConfig}
|
|
1397
|
+
disabled={saving || sandboxToolCreating}
|
|
1398
|
+
testing={testing}
|
|
1399
|
+
sandboxRunning={createdSandboxTesting}
|
|
1400
|
+
onTestConnection={testApiRecord}
|
|
1401
|
+
onCreateSandboxTool={() => setSandboxToolFlow("draft")}
|
|
1402
|
+
onOpenSandboxTool={openSandboxToolRow}
|
|
1403
|
+
onRunSandboxTool={runExistingSandboxTool}
|
|
1404
|
+
/>
|
|
1405
|
+
)}
|
|
1406
|
+
{isApiRegistry && sandboxToolFlow === "created" && createdSandboxMeta && (
|
|
1407
|
+
<section className="dm-api-action-card dm-api-action-card-success" aria-label="Sandbox tool created">
|
|
1408
|
+
<div className="dm-api-action-card-body">
|
|
1409
|
+
<p className="dm-api-action-card-eyebrow">Sandbox tool created</p>
|
|
1410
|
+
<h3>{createdSandboxMeta.name}</h3>
|
|
1411
|
+
<p>Governed sandbox row saved with orchestrationGraph. Run test to persist lastResponse — nothing auto-runs.</p>
|
|
1412
|
+
{createdSandboxTestMessage && <p className="dm-sandbox-tool-test-msg">{createdSandboxTestMessage}</p>}
|
|
1413
|
+
</div>
|
|
1414
|
+
<div className="dm-api-action-card-actions">
|
|
1415
|
+
<button
|
|
1416
|
+
type="button"
|
|
1417
|
+
className="dm-btn-outline dm-api-action-card-cta"
|
|
1418
|
+
disabled={saving}
|
|
1419
|
+
onClick={() => openSandboxToolRow({ name: createdSandboxMeta.name })}
|
|
1420
|
+
>
|
|
1421
|
+
Open sandbox tool
|
|
1422
|
+
</button>
|
|
1423
|
+
<button
|
|
1424
|
+
type="button"
|
|
1425
|
+
className="dm-btn-primary-sm dm-api-action-card-cta"
|
|
1426
|
+
disabled={createdSandboxTesting || saving}
|
|
1427
|
+
onClick={runCreatedSandboxTest}
|
|
1428
|
+
>
|
|
1429
|
+
{createdSandboxTesting ? "Running…" : "Run sandbox"}
|
|
1430
|
+
</button>
|
|
1431
|
+
</div>
|
|
1432
|
+
</section>
|
|
1433
|
+
)}
|
|
1434
|
+
{isApiRegistry && sandboxToolFlow === "draft" && (
|
|
1435
|
+
<SandboxToolDraftPanel
|
|
1436
|
+
registryRow={draft}
|
|
1437
|
+
draftOptions={sandboxToolDraft}
|
|
1438
|
+
disabled={saving || sandboxToolCreating}
|
|
1439
|
+
onDraftChange={setSandboxToolDraft}
|
|
1440
|
+
onRequestConfirm={() => setSandboxToolFlow("confirm")}
|
|
1441
|
+
onCancel={() => setSandboxToolFlow(null)}
|
|
1442
|
+
/>
|
|
1443
|
+
)}
|
|
1444
|
+
<SandboxToolConfirmModal
|
|
1445
|
+
open={isApiRegistry && sandboxToolFlow === "confirm"}
|
|
1446
|
+
toolName={sandboxToolDraft?.name || ""}
|
|
1447
|
+
authRef={sandboxToolDraft?.authRef || draft.authRef}
|
|
1448
|
+
orchestrationGraph={sandboxToolDraft?.orchestrationGraph}
|
|
1449
|
+
creating={sandboxToolCreating}
|
|
1450
|
+
onConfirm={createSandboxToolFromRegistry}
|
|
1451
|
+
onCancel={() => setSandboxToolFlow("draft")}
|
|
1452
|
+
/>
|
|
1453
|
+
{isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
|
|
1271
1454
|
<SandboxRunPanel
|
|
1272
1455
|
status={draft.status}
|
|
1273
1456
|
sandboxRunning={sandboxRunning}
|
|
@@ -1277,8 +1460,27 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1277
1460
|
onRun={runSandbox}
|
|
1278
1461
|
/>
|
|
1279
1462
|
)}
|
|
1463
|
+
{isSandbox && sidecarMode === "graph" && (
|
|
1464
|
+
<SandboxOrchestrationEditorPanel
|
|
1465
|
+
sandboxRow={draft}
|
|
1466
|
+
workspaceConfig={workspaceConfig}
|
|
1467
|
+
disabled={saving}
|
|
1468
|
+
onSaveGraph={saveOrchestrationGraph}
|
|
1469
|
+
onBack={closeSidecar}
|
|
1470
|
+
/>
|
|
1471
|
+
)}
|
|
1472
|
+
{isSandbox && sidecarMode === "trace" && (
|
|
1473
|
+
<OrchestrationRunTracePanel
|
|
1474
|
+
row={draft}
|
|
1475
|
+
objectId={table.objectId}
|
|
1476
|
+
fieldName={traceField || "lastResponse"}
|
|
1477
|
+
selectedRunId={traceRunId}
|
|
1478
|
+
onBack={closeSidecar}
|
|
1479
|
+
onOpenGraph={openGraphSidecar}
|
|
1480
|
+
/>
|
|
1481
|
+
)}
|
|
1280
1482
|
<div className="dm-record-fields">
|
|
1281
|
-
{isSandbox ? (
|
|
1483
|
+
{isApiRegistry && sandboxToolFlow === "draft" ? null : hideRecordFields ? null : isSandbox ? (
|
|
1282
1484
|
<SandboxRecordFields
|
|
1283
1485
|
draft={draft}
|
|
1284
1486
|
setDraft={setDraft}
|
|
@@ -1292,7 +1494,8 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1292
1494
|
sandboxHistoryMessage={sandboxHistoryMessage}
|
|
1293
1495
|
loadingSandboxHistory={loadingSandboxHistory}
|
|
1294
1496
|
onLoadSandboxHistory={loadSandboxHistory}
|
|
1295
|
-
|
|
1497
|
+
onOpenGraphSidecar={openGraphSidecar}
|
|
1498
|
+
onOpenTraceSidecar={openTraceSidecar}
|
|
1296
1499
|
/>
|
|
1297
1500
|
) : groupRecordColumns(table.columns || []).map((section) => (
|
|
1298
1501
|
<DrawerSection key={section.title} title={section.title}>
|
|
@@ -1369,8 +1572,34 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1369
1572
|
);
|
|
1370
1573
|
}
|
|
1371
1574
|
|
|
1372
|
-
|
|
1575
|
+
const SANDBOX_SIDECAR_COLUMNS = new Set(["orchestrationGraph", "orchestrationConfig", "lastResponse", "lastRunId", "lastSourceId"]);
|
|
1576
|
+
|
|
1577
|
+
function sandboxSidecarForColumn(column, row) {
|
|
1578
|
+
if (column === "orchestrationGraph" || column === "orchestrationConfig") return { mode: "graph" };
|
|
1579
|
+
if (column === "lastResponse") return { mode: "trace", field: "lastResponse" };
|
|
1580
|
+
if (column === "lastRunId") return { mode: "trace", field: "lastRunId", runId: row?.lastRunId };
|
|
1581
|
+
if (column === "lastSourceId") return { mode: "trace", field: "lastSourceId" };
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function isSandboxSidecarCell(table, column) {
|
|
1586
|
+
return table?.objectType === "sandbox-environment" && SANDBOX_SIDECAR_COLUMNS.has(column);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function DataModelTableSurface({
|
|
1590
|
+
table,
|
|
1591
|
+
tables,
|
|
1592
|
+
workspaceConfig,
|
|
1593
|
+
saving,
|
|
1594
|
+
onSave,
|
|
1595
|
+
onOpenThread,
|
|
1596
|
+
focusSandboxRowName,
|
|
1597
|
+
onFocusSandboxRowConsumed,
|
|
1598
|
+
onFocusSandboxRow,
|
|
1599
|
+
}) {
|
|
1600
|
+
const router = useRouter();
|
|
1373
1601
|
const [selectedRow, setSelectedRow] = useState(null);
|
|
1602
|
+
const [initialSidecar, setInitialSidecar] = useState(null);
|
|
1374
1603
|
const [fieldName, setFieldName] = useState("");
|
|
1375
1604
|
const [fieldType, setFieldType] = useState("text");
|
|
1376
1605
|
const [addingField, setAddingField] = useState(false);
|
|
@@ -1397,13 +1626,14 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1397
1626
|
setSelectMenuOpen(false);
|
|
1398
1627
|
setPageIndex(0);
|
|
1399
1628
|
}, [table.id]);
|
|
1629
|
+
|
|
1400
1630
|
useEffect(() => {
|
|
1401
1631
|
setFieldName("");
|
|
1402
1632
|
setFieldType("text");
|
|
1403
1633
|
setFilterDraft({ fieldId: table.columns[0] || "", operator: "eq", value: "" });
|
|
1404
1634
|
}, [table.id, table.columns]);
|
|
1405
1635
|
|
|
1406
|
-
const settings = table.fieldSettings || { hidden: [], order: table.columns, sort: [], filter: null
|
|
1636
|
+
const settings = table.fieldSettings || { hidden: [], order: table.columns, sort: [], filter: null };
|
|
1407
1637
|
const orderedColumns = useMemo(() => mergeColumnOrder(settings.order, table.columns), [settings.order, table.columns]);
|
|
1408
1638
|
const visibleColumns = useMemo(() => orderedColumns.filter((column) => !settings.hidden.includes(column)), [orderedColumns, settings.hidden]);
|
|
1409
1639
|
const rowEntries = useMemo(() => {
|
|
@@ -1420,10 +1650,6 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1420
1650
|
return 0;
|
|
1421
1651
|
});
|
|
1422
1652
|
}, [table.rows, settings]);
|
|
1423
|
-
const activeView = useMemo(
|
|
1424
|
-
() => (settings.views || []).find((view) => view.id === settings.activeViewId) || null,
|
|
1425
|
-
[settings.views, settings.activeViewId]
|
|
1426
|
-
);
|
|
1427
1653
|
const selectedRowCount = selectedRows.size;
|
|
1428
1654
|
const pageCount = Math.max(1, Math.ceil(rowEntries.length / pageSize));
|
|
1429
1655
|
const safePageIndex = Math.min(pageIndex, pageCount - 1);
|
|
@@ -1433,6 +1659,20 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1433
1659
|
const pageSelectedCount = pageEntries.filter((entry) => selectedRows.has(entry.originalIndex)).length;
|
|
1434
1660
|
const allPageSelected = pageEntries.length > 0 && pageSelectedCount === pageEntries.length;
|
|
1435
1661
|
|
|
1662
|
+
useEffect(() => {
|
|
1663
|
+
if (!focusSandboxRowName || table.objectType !== "sandbox-environment") return;
|
|
1664
|
+
const wanted = String(focusSandboxRowName).trim();
|
|
1665
|
+
if (!wanted) return;
|
|
1666
|
+
const originalIndex = (table.rows || []).findIndex((r) => String(r?.Name || "").trim() === wanted);
|
|
1667
|
+
if (originalIndex < 0) return;
|
|
1668
|
+
const visibleIndex = rowEntries.findIndex((entry) => entry.originalIndex === originalIndex);
|
|
1669
|
+
if (visibleIndex < 0) return;
|
|
1670
|
+
const pageForRow = Math.floor(visibleIndex / pageSize);
|
|
1671
|
+
setPageIndex(pageForRow);
|
|
1672
|
+
setSelectedRow(visibleIndex - pageForRow * pageSize);
|
|
1673
|
+
onFocusSandboxRowConsumed?.();
|
|
1674
|
+
}, [focusSandboxRowName, table.id, table.objectType, table.rows, rowEntries, pageSize, onFocusSandboxRowConsumed]);
|
|
1675
|
+
|
|
1436
1676
|
useEffect(() => {
|
|
1437
1677
|
setPageIndex((current) => Math.min(current, pageCount - 1));
|
|
1438
1678
|
}, [pageCount]);
|
|
@@ -1516,6 +1756,15 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1516
1756
|
setFilterTarget("");
|
|
1517
1757
|
}
|
|
1518
1758
|
|
|
1759
|
+
function openSandboxGraph(column, row) {
|
|
1760
|
+
const rowId = String(row?.Name || row?.name || row?.slug || row?.id || "").trim();
|
|
1761
|
+
const field = String(column || "orchestrationConfig").trim();
|
|
1762
|
+
if (!table.objectId || !rowId) return;
|
|
1763
|
+
router.push(
|
|
1764
|
+
`/workflows?object=${encodeURIComponent(table.objectId)}&row=${encodeURIComponent(rowId)}&field=${encodeURIComponent(field)}`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1519
1768
|
function removeFilter(fieldId) {
|
|
1520
1769
|
updateSettings((current) => {
|
|
1521
1770
|
const clauses = (current.filter?.clauses || []).filter((clause) => clause.fieldId !== fieldId);
|
|
@@ -1529,27 +1778,7 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1529
1778
|
hidden: [],
|
|
1530
1779
|
order: table.columns,
|
|
1531
1780
|
sort: [],
|
|
1532
|
-
filter: null
|
|
1533
|
-
activeViewId: ""
|
|
1534
|
-
}));
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function saveCurrentAsNewView() {
|
|
1538
|
-
const name = window.prompt("View name");
|
|
1539
|
-
if (!name?.trim()) return;
|
|
1540
|
-
const viewId = `view_${Date.now().toString(36)}`;
|
|
1541
|
-
updateSettings((current) => ({
|
|
1542
|
-
...current,
|
|
1543
|
-
activeViewId: viewId,
|
|
1544
|
-
views: [...(current.views || []), { id: viewId, name: name.trim(), favorite: false, locked: false, ...snapshotTableViewState(current) }]
|
|
1545
|
-
}));
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
function updateCurrentView() {
|
|
1549
|
-
if (!activeView) return;
|
|
1550
|
-
updateSettings((current) => ({
|
|
1551
|
-
...current,
|
|
1552
|
-
views: (current.views || []).map((view) => view.id === activeView.id ? { ...view, ...snapshotTableViewState(current) } : view)
|
|
1781
|
+
filter: null
|
|
1553
1782
|
}));
|
|
1554
1783
|
}
|
|
1555
1784
|
|
|
@@ -1615,13 +1844,10 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1615
1844
|
|
|
1616
1845
|
function deleteSelectedRows() {
|
|
1617
1846
|
if (!selectedRows.size) return;
|
|
1618
|
-
if (!confirmDeleteSelection) {
|
|
1619
|
-
setConfirmDeleteSelection(true);
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
1847
|
const rowIndexes = Array.from(selectedRows).sort((a, b) => b - a);
|
|
1623
1848
|
onSave((config) => rowIndexes.reduce((nextConfig, rowIndex) => deleteTableRow(nextConfig, table, rowIndex), config));
|
|
1624
1849
|
setSelectedRow(null);
|
|
1850
|
+
setConfirmDeleteSelection(false);
|
|
1625
1851
|
clearRowSelection();
|
|
1626
1852
|
}
|
|
1627
1853
|
|
|
@@ -1670,15 +1896,6 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1670
1896
|
</div>
|
|
1671
1897
|
)}
|
|
1672
1898
|
</span>
|
|
1673
|
-
{activeView ? (
|
|
1674
|
-
<button type="button" className="dm-btn-ghost" onClick={updateCurrentView}>
|
|
1675
|
-
Update view
|
|
1676
|
-
</button>
|
|
1677
|
-
) : (
|
|
1678
|
-
<button type="button" className="dm-btn-ghost" onClick={saveCurrentAsNewView}>
|
|
1679
|
-
Save as new view
|
|
1680
|
-
</button>
|
|
1681
|
-
)}
|
|
1682
1899
|
{table.rows.length > 0 && (
|
|
1683
1900
|
<button type="button" className="dm-btn-ghost" onClick={() => {
|
|
1684
1901
|
const blob = new Blob([exportTableAsCsv(table)], { type: "text/csv" });
|
|
@@ -1697,8 +1914,8 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1697
1914
|
{table.mutable && selectedRowCount > 0 && (
|
|
1698
1915
|
<>
|
|
1699
1916
|
<button type="button" className="dm-btn-ghost" disabled={saving} onClick={clearRowSelection}>Cancel selection</button>
|
|
1700
|
-
<button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={
|
|
1701
|
-
|
|
1917
|
+
<button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={() => setConfirmDeleteSelection(true)}>
|
|
1918
|
+
<Trash2 size={13} />Delete
|
|
1702
1919
|
</button>
|
|
1703
1920
|
</>
|
|
1704
1921
|
)}
|
|
@@ -1860,6 +2077,26 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1860
2077
|
/>
|
|
1861
2078
|
) : column.toLowerCase() === "status" ? (
|
|
1862
2079
|
<StatusPill value={row?.[column]} />
|
|
2080
|
+
) : isSandboxSidecarCell(table, column) ? (
|
|
2081
|
+
<button
|
|
2082
|
+
type="button"
|
|
2083
|
+
className={`dm-cell-link${row?.[column] ? "" : " dm-cell-empty"}`}
|
|
2084
|
+
disabled={column !== "orchestrationGraph" && column !== "orchestrationConfig" && !row?.[column]}
|
|
2085
|
+
onClick={(event) => {
|
|
2086
|
+
event.stopPropagation();
|
|
2087
|
+
if (column === "orchestrationGraph" || column === "orchestrationConfig") {
|
|
2088
|
+
openSandboxGraph(column, row);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const sidecar = sandboxSidecarForColumn(column, row);
|
|
2092
|
+
setSelectedRow(visibleIndex);
|
|
2093
|
+
setInitialSidecar(sidecar);
|
|
2094
|
+
}}
|
|
2095
|
+
>
|
|
2096
|
+
{column === "orchestrationGraph" || column === "orchestrationConfig"
|
|
2097
|
+
? (getOrchestrationGraphUiState(row?.[column]) === "populated" ? "Edit graph" : "Start graph")
|
|
2098
|
+
: (formatCellValue(row?.[column], column) || "View trace")}
|
|
2099
|
+
</button>
|
|
1863
2100
|
) : (
|
|
1864
2101
|
<span className={row?.[column] ? "" : "dm-cell-empty"}>
|
|
1865
2102
|
{formatCellValue(row?.[column], column) || "—"}
|
|
@@ -1904,9 +2141,36 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1904
2141
|
rowIndex={selectedEntry?.originalIndex ?? null}
|
|
1905
2142
|
row={selectedRecord}
|
|
1906
2143
|
saving={saving}
|
|
1907
|
-
onClose={() => setSelectedRow(null)}
|
|
2144
|
+
onClose={() => { setSelectedRow(null); setInitialSidecar(null); }}
|
|
1908
2145
|
onSave={onSave}
|
|
2146
|
+
onFocusSandboxRow={onFocusSandboxRow}
|
|
2147
|
+
initialSidecar={initialSidecar}
|
|
2148
|
+
onClearInitialSidecar={() => setInitialSidecar(null)}
|
|
1909
2149
|
/>
|
|
2150
|
+
{confirmDeleteSelection && selectedRowCount > 0 && (
|
|
2151
|
+
<div className="dm-orch-modal-backdrop" onClick={() => setConfirmDeleteSelection(false)}>
|
|
2152
|
+
<section className="dm-orch-modal" role="dialog" aria-modal="true" aria-label="Confirm row deletion" onClick={(event) => event.stopPropagation()}>
|
|
2153
|
+
<header className="dm-orch-modal-head">
|
|
2154
|
+
<div>
|
|
2155
|
+
<p>Confirm deletion</p>
|
|
2156
|
+
<h2>Delete selected records?</h2>
|
|
2157
|
+
</div>
|
|
2158
|
+
<button type="button" className="dm-icon-btn" onClick={() => setConfirmDeleteSelection(false)} aria-label="Close delete confirmation">
|
|
2159
|
+
<X size={15} />
|
|
2160
|
+
</button>
|
|
2161
|
+
</header>
|
|
2162
|
+
<div className="dm-orch-modal-body">
|
|
2163
|
+
<p>This will permanently remove {pluralize(selectedRowCount, "selected record")} from {table.label || table.source}.</p>
|
|
2164
|
+
</div>
|
|
2165
|
+
<footer className="dm-orch-modal-foot">
|
|
2166
|
+
<button type="button" className="dm-btn-outline" onClick={() => setConfirmDeleteSelection(false)}>Cancel</button>
|
|
2167
|
+
<button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={deleteSelectedRows}>
|
|
2168
|
+
<Trash2 size={13} />Delete {selectedRowCount}
|
|
2169
|
+
</button>
|
|
2170
|
+
</footer>
|
|
2171
|
+
</section>
|
|
2172
|
+
</div>
|
|
2173
|
+
)}
|
|
1910
2174
|
</div>
|
|
1911
2175
|
);
|
|
1912
2176
|
}
|
|
@@ -2184,6 +2448,7 @@ export default function DataModelShell() {
|
|
|
2184
2448
|
const [helperInitialPrompt, setHelperInitialPrompt] = useState("");
|
|
2185
2449
|
const [helperInitialThread, setHelperInitialThread] = useState(null);
|
|
2186
2450
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
2451
|
+
const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
|
|
2187
2452
|
const pendingPatchRef = useRef({});
|
|
2188
2453
|
const saveTimerRef = useRef(null);
|
|
2189
2454
|
|
|
@@ -2276,10 +2541,43 @@ export default function DataModelShell() {
|
|
|
2276
2541
|
|
|
2277
2542
|
const selectedTable = tables.find((t) => t.source === selectedSource) || tables[0] || null;
|
|
2278
2543
|
|
|
2544
|
+
const focusSandboxEnvironmentRow = useCallback(({ rowName, deferOpen = false } = {}) => {
|
|
2545
|
+
const wanted = String(rowName || "").trim();
|
|
2546
|
+
if (!wanted) return;
|
|
2547
|
+
const sandboxTable = tables.find((t) => t.objectType === "sandbox-environment");
|
|
2548
|
+
if (!sandboxTable?.source) return;
|
|
2549
|
+
setSelectedSource(sandboxTable.source);
|
|
2550
|
+
if (!deferOpen) {
|
|
2551
|
+
setFocusSandboxRowName(wanted);
|
|
2552
|
+
} else {
|
|
2553
|
+
requestAnimationFrame(() => setFocusSandboxRowName(wanted));
|
|
2554
|
+
}
|
|
2555
|
+
}, [tables]);
|
|
2556
|
+
|
|
2279
2557
|
useEffect(() => {
|
|
2280
2558
|
if (!selectedSource && tables[0]) setSelectedSource(tables[0].source);
|
|
2281
2559
|
}, [selectedSource, tables]);
|
|
2282
2560
|
|
|
2561
|
+
useEffect(() => {
|
|
2562
|
+
const objectParam = searchParams?.get("object");
|
|
2563
|
+
if (!objectParam || !tables.length) return;
|
|
2564
|
+
const target = tables.find((table) => (
|
|
2565
|
+
table.objectId === objectParam
|
|
2566
|
+
|| table.id === objectParam
|
|
2567
|
+
|| table.source === objectParam
|
|
2568
|
+
|| table.label === objectParam
|
|
2569
|
+
));
|
|
2570
|
+
if (target && target.source !== selectedSource) {
|
|
2571
|
+
setSelectedSource(target.source);
|
|
2572
|
+
}
|
|
2573
|
+
}, [searchParams, selectedSource, tables]);
|
|
2574
|
+
|
|
2575
|
+
useEffect(() => {
|
|
2576
|
+
const rowParam = searchParams?.get("row");
|
|
2577
|
+
if (!rowParam || !tables.length) return;
|
|
2578
|
+
focusSandboxEnvironmentRow({ rowName: rowParam, deferOpen: true });
|
|
2579
|
+
}, [focusSandboxEnvironmentRow, searchParams, tables]);
|
|
2580
|
+
|
|
2283
2581
|
// Flush any accumulated patch keys to the server. Called by the debounce
|
|
2284
2582
|
// timer and on visibilitychange/beforeunload so no local edit is lost.
|
|
2285
2583
|
const flushPendingPatch = useCallback(async () => {
|
|
@@ -2439,7 +2737,7 @@ export default function DataModelShell() {
|
|
|
2439
2737
|
},
|
|
2440
2738
|
{
|
|
2441
2739
|
id: "helper.repair", group: "Ask helper", label: "Ask helper — repair workspace",
|
|
2442
|
-
run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete
|
|
2740
|
+
run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete object configuration. Propose the smallest fix for each issue.")
|
|
2443
2741
|
},
|
|
2444
2742
|
{
|
|
2445
2743
|
id: "helper.explain", group: "Ask helper", label: "Ask helper — explain this workspace",
|
|
@@ -2450,7 +2748,7 @@ export default function DataModelShell() {
|
|
|
2450
2748
|
run: () => setAddOpen(true)
|
|
2451
2749
|
},
|
|
2452
2750
|
{
|
|
2453
|
-
id: "nav.
|
|
2751
|
+
id: "nav.builder", group: "Navigation", label: "Go to Builder",
|
|
2454
2752
|
run: () => { window.location.href = "/"; }
|
|
2455
2753
|
},
|
|
2456
2754
|
{
|
|
@@ -2595,7 +2893,17 @@ export default function DataModelShell() {
|
|
|
2595
2893
|
selectedTable && (
|
|
2596
2894
|
<section className="dm-detail-v2 dm-detail-v3">
|
|
2597
2895
|
<SourceValidationBanner table={selectedTable} />
|
|
2598
|
-
<DataModelTableSurface
|
|
2896
|
+
<DataModelTableSurface
|
|
2897
|
+
workspaceConfig={workspaceConfig}
|
|
2898
|
+
table={selectedTable}
|
|
2899
|
+
tables={tables}
|
|
2900
|
+
saving={saving}
|
|
2901
|
+
onSave={save}
|
|
2902
|
+
onOpenThread={openHelperThreadFromRow}
|
|
2903
|
+
focusSandboxRowName={focusSandboxRowName}
|
|
2904
|
+
onFocusSandboxRowConsumed={() => setFocusSandboxRowName(null)}
|
|
2905
|
+
onFocusSandboxRow={focusSandboxEnvironmentRow}
|
|
2906
|
+
/>
|
|
2599
2907
|
</section>
|
|
2600
2908
|
)
|
|
2601
2909
|
)}
|