@growthub/cli 0.9.14 → 0.9.17
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/README.md +17 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-adapters/route.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +634 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +712 -54
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +55 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/data-sources-api-registry.md +2 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapter-loader.js +58 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/README.md +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +284 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +194 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +33 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +113 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +107 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +103 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +9 -0
- package/dist/index.js +41066 -1761
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Building2,
|
|
11
11
|
Calendar,
|
|
12
12
|
CheckSquare,
|
|
13
|
+
ChevronDown,
|
|
13
14
|
ChevronRight,
|
|
14
15
|
Code2,
|
|
15
16
|
Database,
|
|
@@ -20,10 +21,14 @@ import {
|
|
|
20
21
|
Link2,
|
|
21
22
|
List,
|
|
22
23
|
Mail,
|
|
24
|
+
Maximize2,
|
|
23
25
|
Plus,
|
|
26
|
+
Play,
|
|
27
|
+
Search,
|
|
24
28
|
ShoppingCart,
|
|
25
29
|
Star,
|
|
26
30
|
Tag,
|
|
31
|
+
Terminal,
|
|
27
32
|
ToggleLeft,
|
|
28
33
|
Type,
|
|
29
34
|
Users,
|
|
@@ -40,7 +45,10 @@ import {
|
|
|
40
45
|
describeBindingLane,
|
|
41
46
|
exportTableAsCsv,
|
|
42
47
|
importTableFromCsv,
|
|
48
|
+
listSavedEnvRefs,
|
|
43
49
|
listWorkspaceDataModelTables,
|
|
50
|
+
parseSandboxAllowList,
|
|
51
|
+
parseSandboxEnvRefs,
|
|
44
52
|
replaceTableContent,
|
|
45
53
|
updateTableCell,
|
|
46
54
|
} from "@/lib/workspace-data-model";
|
|
@@ -50,13 +58,13 @@ import {
|
|
|
50
58
|
const LUCIDE_MAP = {
|
|
51
59
|
Activity, BarChart2, Box, Building2, Calendar, CheckSquare, Code2,
|
|
52
60
|
Database, FileText, Globe, Hash, Layers, Link2, List, Mail, Plus,
|
|
53
|
-
ShoppingCart, Star, Tag, Type, Users, Zap,
|
|
61
|
+
ShoppingCart, Star, Tag, Terminal, Type, Users, Zap,
|
|
54
62
|
};
|
|
55
63
|
|
|
56
64
|
const ICON_PICKER_SET = [
|
|
57
65
|
"Database", "Globe", "Code2", "Users", "CheckSquare", "Building2",
|
|
58
66
|
"Tag", "Star", "Zap", "FileText", "Mail", "BarChart2",
|
|
59
|
-
"Layers", "Box", "Activity", "ShoppingCart",
|
|
67
|
+
"Layers", "Box", "Activity", "ShoppingCart", "Terminal",
|
|
60
68
|
];
|
|
61
69
|
|
|
62
70
|
function LucideIcon({ name, size = 14, className, style }) {
|
|
@@ -91,6 +99,12 @@ const OBJECT_TYPE_DEFS = [
|
|
|
91
99
|
label: "Tasks",
|
|
92
100
|
description: "Action items, to-dos, and work tracking.",
|
|
93
101
|
},
|
|
102
|
+
{
|
|
103
|
+
type: "sandbox-environment",
|
|
104
|
+
icon: Terminal,
|
|
105
|
+
label: "Sandbox Environment",
|
|
106
|
+
description: "Localized py/node/bash terminal sandbox or local agent host (Claude / Codex / Cursor / Gemini / Hermes). Server-side execution with versioned run history. Cannot bind directly to a widget.",
|
|
107
|
+
},
|
|
94
108
|
{
|
|
95
109
|
type: "custom",
|
|
96
110
|
icon: Plus,
|
|
@@ -102,13 +116,38 @@ const OBJECT_TYPE_DEFS = [
|
|
|
102
116
|
// ─── Lane / badge meta ────────────────────────────────────────────────────────
|
|
103
117
|
|
|
104
118
|
const OBJECT_TYPE_BADGE = {
|
|
105
|
-
"data-source":
|
|
106
|
-
"api-registry":
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
"data-source": { label: "Data Source", cls: "dm-badge-datasource" },
|
|
120
|
+
"api-registry": { label: "API Registry", cls: "dm-badge-registry" },
|
|
121
|
+
"sandbox-environment": { label: "Sandbox Environment", cls: "dm-badge-sandbox" },
|
|
122
|
+
people: { label: "People", cls: "dm-badge-people" },
|
|
123
|
+
tasks: { label: "Tasks", cls: "dm-badge-tasks" },
|
|
124
|
+
custom: { label: "Custom", cls: "dm-badge-manual" },
|
|
110
125
|
};
|
|
111
126
|
|
|
127
|
+
const SANDBOX_ROW_FIELDS = new Set([
|
|
128
|
+
"Name",
|
|
129
|
+
"lifecycleStatus",
|
|
130
|
+
"version",
|
|
131
|
+
"runLocality",
|
|
132
|
+
"schedulerRegistryId",
|
|
133
|
+
"runtime",
|
|
134
|
+
"adapter",
|
|
135
|
+
"agentHost",
|
|
136
|
+
"envRefs",
|
|
137
|
+
"networkAllow",
|
|
138
|
+
"allowList",
|
|
139
|
+
"instructions",
|
|
140
|
+
"command",
|
|
141
|
+
"timeoutMs",
|
|
142
|
+
"status",
|
|
143
|
+
"lastTested",
|
|
144
|
+
"lastRunId",
|
|
145
|
+
"lastSourceId",
|
|
146
|
+
"lastResponse"
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
const SANDBOX_RUNTIME_OPTIONS = ["python", "node", "bash"];
|
|
150
|
+
|
|
112
151
|
const FIELD_TYPE_ICON_NAMES = {
|
|
113
152
|
text: "Type", number: "Hash", date: "Calendar", url: "Link2", select: "List", boolean: "ToggleLeft",
|
|
114
153
|
};
|
|
@@ -245,36 +284,556 @@ function referenceOptions(tables, relation) {
|
|
|
245
284
|
}
|
|
246
285
|
|
|
247
286
|
function ReferenceSelect({ value, options, disabled, onChange }) {
|
|
287
|
+
const normalizedOptions = useMemo(() => options.map((option) => ({
|
|
288
|
+
value: String(option.value ?? ""),
|
|
289
|
+
label: String(option.label ?? option.value ?? ""),
|
|
290
|
+
source: option.source ? String(option.source) : ""
|
|
291
|
+
})), [options]);
|
|
248
292
|
return (
|
|
249
|
-
<
|
|
250
|
-
className="dm-reference-select"
|
|
293
|
+
<SearchableSelect
|
|
251
294
|
value={value || ""}
|
|
295
|
+
options={normalizedOptions}
|
|
252
296
|
disabled={disabled}
|
|
297
|
+
placeholder="Select reference..."
|
|
298
|
+
onChange={onChange}
|
|
299
|
+
/>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function SearchableSelect({ value, options, disabled, placeholder = "Select...", onChange, pageSize = 8 }) {
|
|
304
|
+
const [open, setOpen] = useState(false);
|
|
305
|
+
const [query, setQuery] = useState("");
|
|
306
|
+
const [page, setPage] = useState(0);
|
|
307
|
+
const selected = options.find((option) => option.value === String(value || ""));
|
|
308
|
+
const filtered = useMemo(() => {
|
|
309
|
+
const needle = query.trim().toLowerCase();
|
|
310
|
+
if (!needle) return options;
|
|
311
|
+
return options.filter((option) => `${option.label} ${option.value} ${option.source}`.toLowerCase().includes(needle));
|
|
312
|
+
}, [options, query]);
|
|
313
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
314
|
+
const currentPage = Math.min(page, pageCount - 1);
|
|
315
|
+
const visibleOptions = filtered.slice(currentPage * pageSize, currentPage * pageSize + pageSize);
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
setPage(0);
|
|
319
|
+
}, [query, options.length]);
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div
|
|
323
|
+
className={`dm-select${open ? " open" : ""}${disabled ? " disabled" : ""}`}
|
|
253
324
|
onClick={(event) => event.stopPropagation()}
|
|
254
|
-
|
|
325
|
+
onBlur={(event) => {
|
|
326
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
|
327
|
+
}}
|
|
255
328
|
>
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
className="dm-select-trigger"
|
|
332
|
+
disabled={disabled}
|
|
333
|
+
aria-haspopup="listbox"
|
|
334
|
+
aria-expanded={open}
|
|
335
|
+
onClick={() => setOpen((current) => !current)}
|
|
336
|
+
>
|
|
337
|
+
<span className={selected ? "" : "empty"}>{selected?.label || placeholder}</span>
|
|
338
|
+
<ChevronDown size={15} aria-hidden="true" />
|
|
339
|
+
</button>
|
|
340
|
+
{open && (
|
|
341
|
+
<div className="dm-select-popover">
|
|
342
|
+
<label className="dm-select-search">
|
|
343
|
+
<Search size={14} aria-hidden="true" />
|
|
344
|
+
<input
|
|
345
|
+
autoFocus
|
|
346
|
+
value={query}
|
|
347
|
+
placeholder="Search..."
|
|
348
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
349
|
+
/>
|
|
350
|
+
</label>
|
|
351
|
+
<div className="dm-select-list" role="listbox">
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
className={`dm-select-option${!value ? " selected" : ""}`}
|
|
355
|
+
role="option"
|
|
356
|
+
aria-selected={!value}
|
|
357
|
+
onClick={() => {
|
|
358
|
+
onChange("");
|
|
359
|
+
setOpen(false);
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
<span>{placeholder}</span>
|
|
363
|
+
</button>
|
|
364
|
+
{visibleOptions.map((option) => (
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
key={`${option.source}:${option.value}`}
|
|
368
|
+
className={`dm-select-option${option.value === String(value || "") ? " selected" : ""}`}
|
|
369
|
+
role="option"
|
|
370
|
+
aria-selected={option.value === String(value || "")}
|
|
371
|
+
onClick={() => {
|
|
372
|
+
onChange(option.value);
|
|
373
|
+
setOpen(false);
|
|
374
|
+
setQuery("");
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
<span>{option.label}</span>
|
|
378
|
+
{option.source && <em>{option.source}</em>}
|
|
379
|
+
</button>
|
|
380
|
+
))}
|
|
381
|
+
{visibleOptions.length === 0 && <p className="dm-select-empty">No matches</p>}
|
|
382
|
+
</div>
|
|
383
|
+
{filtered.length > pageSize && (
|
|
384
|
+
<div className="dm-select-pager">
|
|
385
|
+
<button type="button" disabled={currentPage === 0} onClick={() => setPage((next) => Math.max(0, next - 1))}>Prev</button>
|
|
386
|
+
<span>{currentPage + 1} / {pageCount}</span>
|
|
387
|
+
<button type="button" disabled={currentPage >= pageCount - 1} onClick={() => setPage((next) => Math.min(pageCount - 1, next + 1))}>Next</button>
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function StaticSelect({ value, options, disabled, onChange, placeholder = "Select..." }) {
|
|
397
|
+
const normalizedOptions = useMemo(() => options.map((option) => (
|
|
398
|
+
typeof option === "string" ? { value: option, label: option } : option
|
|
399
|
+
)), [options]);
|
|
400
|
+
return (
|
|
401
|
+
<SearchableSelect
|
|
402
|
+
value={value || ""}
|
|
403
|
+
options={normalizedOptions}
|
|
404
|
+
disabled={disabled}
|
|
405
|
+
placeholder={placeholder}
|
|
406
|
+
onChange={onChange}
|
|
407
|
+
/>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function DrawerSection({ title, children, defaultOpen = false }) {
|
|
412
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
413
|
+
return (
|
|
414
|
+
<section className={`dm-drawer-section${open ? " open" : ""}`}>
|
|
415
|
+
<button type="button" className="dm-drawer-section-toggle" onClick={() => setOpen((current) => !current)}>
|
|
416
|
+
<ChevronRight size={14} aria-hidden="true" />
|
|
417
|
+
<span>{title}</span>
|
|
418
|
+
</button>
|
|
419
|
+
{open && <div className="dm-drawer-section-body">{children}</div>}
|
|
420
|
+
</section>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const GENERIC_FIELD_SECTIONS = [
|
|
425
|
+
{
|
|
426
|
+
title: "Identity",
|
|
427
|
+
columns: new Set(["Name", "name", "id", "integrationId", "registryId", "authRef"])
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
title: "Connection",
|
|
431
|
+
columns: new Set(["baseUrl", "endpoint", "method", "schedulerRegistryId"])
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
title: "Status & Response",
|
|
435
|
+
columns: new Set(["status", "lastTested", "lastRunId", "lastSourceId", "lastResponse"])
|
|
436
|
+
}
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
function groupRecordColumns(columns) {
|
|
440
|
+
const groups = GENERIC_FIELD_SECTIONS.map((section) => ({
|
|
441
|
+
title: section.title,
|
|
442
|
+
columns: columns.filter((column) => section.columns.has(column))
|
|
443
|
+
})).filter((section) => section.columns.length > 0);
|
|
444
|
+
const grouped = new Set(groups.flatMap((section) => section.columns));
|
|
445
|
+
const otherColumns = columns.filter((column) => !grouped.has(column));
|
|
446
|
+
if (otherColumns.length) groups.push({ title: "Details", columns: otherColumns });
|
|
447
|
+
return groups;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function RecordFieldEditor({ table, tables, column, value, saving, onDraft, onCommit, onExpandJson }) {
|
|
451
|
+
const relation = relationForColumn(table, column);
|
|
452
|
+
const options = referenceOptions(tables, relation);
|
|
453
|
+
const large = column === "lastResponse" || value.length > 120;
|
|
454
|
+
if (relation) {
|
|
455
|
+
return (
|
|
456
|
+
<label className="dm-record-field">
|
|
457
|
+
<span>{column}</span>
|
|
458
|
+
<ReferenceSelect
|
|
459
|
+
value={value}
|
|
460
|
+
options={options}
|
|
461
|
+
disabled={!table.mutable || saving}
|
|
462
|
+
onChange={(nextValue) => onCommit(column, nextValue)}
|
|
463
|
+
/>
|
|
464
|
+
</label>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
if (column === "lastResponse") {
|
|
468
|
+
return (
|
|
469
|
+
<label className="dm-record-field dm-json-field">
|
|
470
|
+
<span>{column}</span>
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
className="dm-json-expand"
|
|
474
|
+
aria-label="Expand lastResponse JSON"
|
|
475
|
+
title="Expand JSON"
|
|
476
|
+
disabled={!value}
|
|
477
|
+
onClick={onExpandJson}
|
|
478
|
+
>
|
|
479
|
+
<Maximize2 size={14} aria-hidden="true" />
|
|
480
|
+
</button>
|
|
481
|
+
<textarea
|
|
482
|
+
value={value}
|
|
483
|
+
rows={10}
|
|
484
|
+
disabled={!table.mutable || saving}
|
|
485
|
+
onChange={(event) => onDraft(column, event.target.value)}
|
|
486
|
+
onBlur={(event) => onCommit(column, event.target.value)}
|
|
487
|
+
/>
|
|
488
|
+
</label>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return (
|
|
492
|
+
<label className="dm-record-field">
|
|
493
|
+
<span>{column}</span>
|
|
494
|
+
{large ? (
|
|
495
|
+
<textarea
|
|
496
|
+
value={value}
|
|
497
|
+
rows={4}
|
|
498
|
+
disabled={!table.mutable || saving}
|
|
499
|
+
onChange={(event) => onDraft(column, event.target.value)}
|
|
500
|
+
onBlur={(event) => onCommit(column, event.target.value)}
|
|
501
|
+
/>
|
|
502
|
+
) : (
|
|
503
|
+
<input
|
|
504
|
+
value={value}
|
|
505
|
+
disabled={!table.mutable || saving}
|
|
506
|
+
onChange={(event) => onDraft(column, event.target.value)}
|
|
507
|
+
onBlur={(event) => onCommit(column, event.target.value)}
|
|
508
|
+
/>
|
|
509
|
+
)}
|
|
510
|
+
</label>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function SandboxRecordFields({
|
|
515
|
+
draft,
|
|
516
|
+
setDraft,
|
|
517
|
+
table,
|
|
518
|
+
tables,
|
|
519
|
+
workspaceConfig,
|
|
520
|
+
saving,
|
|
521
|
+
onSave,
|
|
522
|
+
rowIndex,
|
|
523
|
+
sandboxHistory,
|
|
524
|
+
sandboxHistoryMessage,
|
|
525
|
+
loadingSandboxHistory,
|
|
526
|
+
onLoadSandboxHistory,
|
|
527
|
+
onExpandLastResponse
|
|
528
|
+
}) {
|
|
529
|
+
const [sandboxAdapters, setSandboxAdapters] = useState([]);
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
fetch("/api/workspace/sandbox-adapters", { cache: "no-store" })
|
|
532
|
+
.then((res) => res.json())
|
|
533
|
+
.then((payload) => setSandboxAdapters(Array.isArray(payload.adapters) ? payload.adapters : []))
|
|
534
|
+
.catch(() => setSandboxAdapters([]));
|
|
535
|
+
}, []);
|
|
536
|
+
|
|
537
|
+
const locality = String(draft.runLocality || "local").trim().toLowerCase() === "serverless" ? "serverless" : "local";
|
|
538
|
+
const savedEnvRefs = useMemo(() => listSavedEnvRefs(workspaceConfig || {}), [workspaceConfig]);
|
|
539
|
+
const selectedEnvSlugs = useMemo(() => new Set(parseSandboxEnvRefs(draft.envRefs)), [draft.envRefs]);
|
|
540
|
+
const schedulerRelation = relationForColumn(table, "schedulerRegistryId");
|
|
541
|
+
const schedulerOptions = referenceOptions(tables, schedulerRelation);
|
|
542
|
+
const selectedAdapterMeta = sandboxAdapters.find((a) => a.id === String(draft.adapter || "").trim());
|
|
543
|
+
|
|
544
|
+
function patchFields(fields) {
|
|
545
|
+
setDraft((c) => ({ ...c, ...fields }));
|
|
546
|
+
onSave((cfg) => Object.entries(fields).reduce(
|
|
547
|
+
(acc, [column, value]) => updateTableCell(acc, table, rowIndex, column, value),
|
|
548
|
+
cfg
|
|
549
|
+
));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function setRunLocality(next) {
|
|
553
|
+
const fields = { runLocality: next };
|
|
554
|
+
if (next === "serverless" && String(draft.adapter || "").trim() === "local-agent-host") {
|
|
555
|
+
fields.adapter = "local-process";
|
|
556
|
+
}
|
|
557
|
+
patchFields(fields);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function toggleEnvRef(slug) {
|
|
561
|
+
const next = new Set(selectedEnvSlugs);
|
|
562
|
+
if (next.has(slug)) next.delete(slug);
|
|
563
|
+
else next.add(slug);
|
|
564
|
+
patchFields({ envRefs: [...next].join(",") });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const netOn = ["true", "1", "on", "yes"].includes(String(draft.networkAllow || "").trim().toLowerCase());
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<div className="dm-sandbox-config">
|
|
571
|
+
<DrawerSection title="Identity & Mode">
|
|
572
|
+
<label className="dm-record-field">
|
|
573
|
+
<span>Name</span>
|
|
574
|
+
<input
|
|
575
|
+
value={draft.Name ?? ""}
|
|
576
|
+
disabled={!table.mutable || saving}
|
|
577
|
+
onChange={(event) => setDraft((c) => ({ ...c, Name: event.target.value }))}
|
|
578
|
+
onBlur={(event) => patchFields({ Name: event.target.value })}
|
|
579
|
+
/>
|
|
580
|
+
</label>
|
|
581
|
+
|
|
582
|
+
<label className="dm-record-field">
|
|
583
|
+
<span>Status mode</span>
|
|
584
|
+
<StaticSelect
|
|
585
|
+
value={String(draft.lifecycleStatus || "draft").trim().toLowerCase() === "live" ? "live" : "draft"}
|
|
586
|
+
disabled={!table.mutable || saving}
|
|
587
|
+
options={["draft", "live"]}
|
|
588
|
+
onChange={(nextValue) => patchFields({ lifecycleStatus: nextValue })}
|
|
589
|
+
/>
|
|
590
|
+
</label>
|
|
591
|
+
|
|
592
|
+
<label className="dm-record-field">
|
|
593
|
+
<span>Version</span>
|
|
594
|
+
<input
|
|
595
|
+
value={draft.version ?? ""}
|
|
596
|
+
disabled={!table.mutable || saving}
|
|
597
|
+
onChange={(event) => setDraft((c) => ({ ...c, version: event.target.value }))}
|
|
598
|
+
onBlur={(event) => patchFields({ version: event.target.value })}
|
|
599
|
+
/>
|
|
600
|
+
</label>
|
|
601
|
+
</DrawerSection>
|
|
602
|
+
|
|
603
|
+
<DrawerSection title="Execution Target">
|
|
604
|
+
<div className="dm-record-field">
|
|
605
|
+
<span>Where it runs</span>
|
|
606
|
+
<div className="dm-radio-row">
|
|
607
|
+
<label>
|
|
608
|
+
<input
|
|
609
|
+
type="radio"
|
|
610
|
+
name="sandbox-run-locality"
|
|
611
|
+
checked={locality === "local"}
|
|
612
|
+
disabled={!table.mutable || saving}
|
|
613
|
+
onChange={() => setRunLocality("local")}
|
|
614
|
+
/>
|
|
615
|
+
<span>Local - process sandbox or Paperclip local agent host on this machine</span>
|
|
616
|
+
</label>
|
|
617
|
+
<label>
|
|
618
|
+
<input
|
|
619
|
+
type="radio"
|
|
620
|
+
name="sandbox-run-locality"
|
|
621
|
+
checked={locality === "serverless"}
|
|
622
|
+
disabled={!table.mutable || saving}
|
|
623
|
+
onChange={() => setRunLocality("serverless")}
|
|
624
|
+
/>
|
|
625
|
+
<span>Serverless - delegate to scheduler URL (API Registry); no local agent CLI</span>
|
|
626
|
+
</label>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
{locality === "serverless" && (
|
|
631
|
+
<label className="dm-record-field">
|
|
632
|
+
<span>Scheduler (API Registry)</span>
|
|
633
|
+
<ReferenceSelect
|
|
634
|
+
value={draft.schedulerRegistryId || ""}
|
|
635
|
+
options={schedulerOptions}
|
|
636
|
+
disabled={!table.mutable || saving}
|
|
637
|
+
onChange={(nextValue) => patchFields({ schedulerRegistryId: nextValue })}
|
|
638
|
+
/>
|
|
639
|
+
<span className="dm-cell-empty" style={{ fontSize: 11, marginTop: 4, display: "block" }}>
|
|
640
|
+
POST sends <code>growthub-sandbox-run-v1</code> JSON; auth from registry <code>authRef</code> (server env only).
|
|
641
|
+
</span>
|
|
642
|
+
</label>
|
|
643
|
+
)}
|
|
644
|
+
|
|
645
|
+
<label className="dm-record-field">
|
|
646
|
+
<span>Execution adapter</span>
|
|
647
|
+
<StaticSelect
|
|
648
|
+
value={String(draft.adapter || "local-process").trim() || "local-process"}
|
|
649
|
+
disabled={!table.mutable || saving}
|
|
650
|
+
options={sandboxAdapters.length === 0 ? [{ value: "local-process", label: "local-process" }] : sandboxAdapters.map((a) => ({ value: a.id, label: a.label }))}
|
|
651
|
+
onChange={(nextValue) => patchFields({ adapter: nextValue })}
|
|
652
|
+
/>
|
|
653
|
+
</label>
|
|
654
|
+
|
|
655
|
+
{locality === "local" && String(draft.adapter || "").trim() === "local-agent-host" && (
|
|
656
|
+
<label className="dm-record-field">
|
|
657
|
+
<span>Agent host (Paperclip)</span>
|
|
658
|
+
<StaticSelect
|
|
659
|
+
value={draft.agentHost || ""}
|
|
660
|
+
disabled={!table.mutable || saving}
|
|
661
|
+
placeholder="Select host..."
|
|
662
|
+
options={(selectedAdapterMeta?.hostCatalog || []).map((h) => ({ value: h.slug, label: h.label }))}
|
|
663
|
+
onChange={(nextValue) => patchFields({ agentHost: nextValue })}
|
|
664
|
+
/>
|
|
665
|
+
</label>
|
|
666
|
+
)}
|
|
667
|
+
|
|
668
|
+
<label className="dm-record-field">
|
|
669
|
+
<span>Runtime</span>
|
|
670
|
+
<StaticSelect
|
|
671
|
+
value={draft.runtime || "node"}
|
|
672
|
+
disabled={!table.mutable || saving}
|
|
673
|
+
options={SANDBOX_RUNTIME_OPTIONS}
|
|
674
|
+
onChange={(nextValue) => patchFields({ runtime: nextValue })}
|
|
675
|
+
/>
|
|
676
|
+
</label>
|
|
677
|
+
</DrawerSection>
|
|
678
|
+
|
|
679
|
+
<DrawerSection title="Environment & Network">
|
|
680
|
+
<div className="dm-record-field">
|
|
681
|
+
<span>Env key references</span>
|
|
682
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
|
|
683
|
+
{savedEnvRefs.length === 0 ? (
|
|
684
|
+
<span className="dm-cell-empty">Add keys under Settings -> APIs & Webhooks.</span>
|
|
685
|
+
) : savedEnvRefs.map((ref) => (
|
|
686
|
+
<button
|
|
687
|
+
key={ref.endpointRef}
|
|
688
|
+
type="button"
|
|
689
|
+
className={`dm-btn-ghost${selectedEnvSlugs.has(ref.endpointRef) ? " dm-chip-active" : ""}`}
|
|
690
|
+
style={{ padding: "2px 8px", borderRadius: 999, fontSize: 11 }}
|
|
691
|
+
disabled={!table.mutable || saving}
|
|
692
|
+
onClick={() => toggleEnvRef(ref.endpointRef)}
|
|
693
|
+
>
|
|
694
|
+
{ref.endpointRef}
|
|
695
|
+
</button>
|
|
696
|
+
))}
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<label className="dm-check-row">
|
|
701
|
+
<input
|
|
702
|
+
type="checkbox"
|
|
703
|
+
checked={netOn}
|
|
704
|
+
disabled={!table.mutable || saving}
|
|
705
|
+
onChange={(event) => patchFields({ networkAllow: event.target.checked ? "true" : "false" })}
|
|
706
|
+
/>
|
|
707
|
+
<span>Network allow-list mode (locals see <code>GROWTHUB_SANDBOX_NET_*</code>)</span>
|
|
708
|
+
</label>
|
|
709
|
+
|
|
710
|
+
<label className="dm-record-field">
|
|
711
|
+
<span>Allow list (comma-separated hosts)</span>
|
|
712
|
+
<input
|
|
713
|
+
value={draft.allowList ?? ""}
|
|
714
|
+
disabled={!table.mutable || saving}
|
|
715
|
+
onChange={(event) => setDraft((c) => ({ ...c, allowList: event.target.value }))}
|
|
716
|
+
onBlur={(event) => patchFields({ allowList: event.target.value })}
|
|
717
|
+
/>
|
|
718
|
+
</label>
|
|
719
|
+
</DrawerSection>
|
|
720
|
+
|
|
721
|
+
<DrawerSection title="Prompt & Limits">
|
|
722
|
+
<label className="dm-record-field">
|
|
723
|
+
<span>Instructions</span>
|
|
724
|
+
<textarea
|
|
725
|
+
rows={5}
|
|
726
|
+
value={draft.instructions ?? ""}
|
|
727
|
+
disabled={!table.mutable || saving}
|
|
728
|
+
onChange={(event) => setDraft((c) => ({ ...c, instructions: event.target.value }))}
|
|
729
|
+
onBlur={(event) => patchFields({ instructions: event.target.value })}
|
|
730
|
+
/>
|
|
731
|
+
</label>
|
|
732
|
+
|
|
733
|
+
<label className="dm-record-field">
|
|
734
|
+
<span>Command / prompt</span>
|
|
735
|
+
<textarea
|
|
736
|
+
rows={6}
|
|
737
|
+
value={draft.command ?? ""}
|
|
738
|
+
disabled={!table.mutable || saving}
|
|
739
|
+
onChange={(event) => setDraft((c) => ({ ...c, command: event.target.value }))}
|
|
740
|
+
onBlur={(event) => patchFields({ command: event.target.value })}
|
|
741
|
+
/>
|
|
742
|
+
</label>
|
|
743
|
+
|
|
744
|
+
<label className="dm-record-field">
|
|
745
|
+
<span>timeoutMs</span>
|
|
746
|
+
<input
|
|
747
|
+
type="number"
|
|
748
|
+
min={1000}
|
|
749
|
+
max={600000}
|
|
750
|
+
value={draft.timeoutMs ?? ""}
|
|
751
|
+
disabled={!table.mutable || saving}
|
|
752
|
+
onChange={(event) => setDraft((c) => ({ ...c, timeoutMs: event.target.value }))}
|
|
753
|
+
onBlur={(event) => patchFields({ timeoutMs: event.target.value })}
|
|
754
|
+
/>
|
|
755
|
+
</label>
|
|
756
|
+
</DrawerSection>
|
|
757
|
+
|
|
758
|
+
<DrawerSection title="Response & History">
|
|
759
|
+
<label className="dm-record-field">
|
|
760
|
+
<span>lastRunId</span>
|
|
761
|
+
<input readOnly value={draft.lastRunId ?? ""} />
|
|
762
|
+
</label>
|
|
763
|
+
|
|
764
|
+
<label className="dm-record-field">
|
|
765
|
+
<span>lastSourceId</span>
|
|
766
|
+
<input readOnly value={draft.lastSourceId ?? ""} />
|
|
767
|
+
</label>
|
|
768
|
+
|
|
769
|
+
<label className="dm-record-field dm-json-field">
|
|
770
|
+
<span>lastResponse</span>
|
|
771
|
+
<button
|
|
772
|
+
type="button"
|
|
773
|
+
className="dm-json-expand"
|
|
774
|
+
aria-label="Expand lastResponse JSON"
|
|
775
|
+
title="Expand JSON"
|
|
776
|
+
disabled={!draft.lastResponse}
|
|
777
|
+
onClick={onExpandLastResponse}
|
|
778
|
+
>
|
|
779
|
+
<Maximize2 size={14} aria-hidden="true" />
|
|
780
|
+
</button>
|
|
781
|
+
<textarea rows={10} readOnly value={draft.lastResponse ?? ""} />
|
|
782
|
+
</label>
|
|
783
|
+
|
|
784
|
+
<div className="dm-record-field">
|
|
785
|
+
<span>Run history</span>
|
|
786
|
+
<button type="button" className="dm-btn-ghost" disabled={loadingSandboxHistory} onClick={onLoadSandboxHistory}>
|
|
787
|
+
{loadingSandboxHistory ? "Loading..." : "Load previous runs"}
|
|
788
|
+
</button>
|
|
789
|
+
{sandboxHistoryMessage && <span className="dm-cell-empty">{sandboxHistoryMessage}</span>}
|
|
790
|
+
{Array.isArray(sandboxHistory) && sandboxHistory.length > 0 && (
|
|
791
|
+
<div style={{ display: "grid", gap: 8, marginTop: 8 }}>
|
|
792
|
+
{sandboxHistory.slice(0, 8).map((record) => (
|
|
793
|
+
<pre key={record.runId || record.ranAt} className="dm-source-preview" style={{ margin: 0, maxHeight: 160, overflow: "auto" }}>
|
|
794
|
+
{JSON.stringify({
|
|
795
|
+
runId: record.runId,
|
|
796
|
+
ranAt: record.ranAt,
|
|
797
|
+
lifecycleStatus: record.lifecycleStatus,
|
|
798
|
+
version: record.version,
|
|
799
|
+
status: record.exitCode === 0 && !record.error ? "connected" : "failed",
|
|
800
|
+
stdout: record.stdout,
|
|
801
|
+
error: record.error
|
|
802
|
+
}, null, 2)}
|
|
803
|
+
</pre>
|
|
804
|
+
))}
|
|
805
|
+
</div>
|
|
806
|
+
)}
|
|
807
|
+
</div>
|
|
808
|
+
</DrawerSection>
|
|
809
|
+
</div>
|
|
263
810
|
);
|
|
264
811
|
}
|
|
265
812
|
|
|
266
|
-
function DataModelRecordDrawer({ table, tables, rowIndex, row, saving, onClose, onSave }) {
|
|
813
|
+
function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row, saving, onClose, onSave }) {
|
|
267
814
|
const [draft, setDraft] = useState(row || {});
|
|
268
815
|
const [testing, setTesting] = useState(false);
|
|
269
816
|
const [testMessage, setTestMessage] = useState("");
|
|
817
|
+
const [sandboxRunning, setSandboxRunning] = useState(false);
|
|
818
|
+
const [sandboxMessage, setSandboxMessage] = useState("");
|
|
819
|
+
const [sandboxHistory, setSandboxHistory] = useState([]);
|
|
820
|
+
const [sandboxHistoryMessage, setSandboxHistoryMessage] = useState("");
|
|
821
|
+
const [loadingSandboxHistory, setLoadingSandboxHistory] = useState(false);
|
|
822
|
+
const [expandedJson, setExpandedJson] = useState(null);
|
|
270
823
|
|
|
271
824
|
useEffect(() => {
|
|
272
825
|
setDraft(row || {});
|
|
273
826
|
setTestMessage("");
|
|
827
|
+
setSandboxMessage("");
|
|
828
|
+
setSandboxHistory([]);
|
|
829
|
+
setSandboxHistoryMessage("");
|
|
830
|
+
setExpandedJson(null);
|
|
274
831
|
}, [row, rowIndex]);
|
|
275
832
|
|
|
276
833
|
if (rowIndex === null || rowIndex === undefined || !row) return null;
|
|
277
834
|
|
|
835
|
+
const isSandbox = table.objectType === "sandbox-environment";
|
|
836
|
+
|
|
278
837
|
function updateField(column, value) {
|
|
279
838
|
setDraft((current) => ({ ...current, [column]: value }));
|
|
280
839
|
onSave((config) => updateTableCell(config, table, rowIndex, column, value));
|
|
@@ -314,6 +873,80 @@ function DataModelRecordDrawer({ table, tables, rowIndex, row, saving, onClose,
|
|
|
314
873
|
}
|
|
315
874
|
}
|
|
316
875
|
|
|
876
|
+
async function runSandbox() {
|
|
877
|
+
if (!table.objectId) {
|
|
878
|
+
setSandboxMessage("Missing object id for this sandbox table.");
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const rowName = String(draft?.Name ?? "").trim();
|
|
882
|
+
if (!rowName) {
|
|
883
|
+
setSandboxMessage("Row Name is required.");
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
setSandboxRunning(true);
|
|
887
|
+
setSandboxMessage("");
|
|
888
|
+
try {
|
|
889
|
+
const res = await fetch("/api/workspace/sandbox-run", {
|
|
890
|
+
method: "POST",
|
|
891
|
+
headers: { "content-type": "application/json" },
|
|
892
|
+
body: JSON.stringify({ objectId: table.objectId, name: rowName }),
|
|
893
|
+
});
|
|
894
|
+
const payload = await res.json();
|
|
895
|
+
const responseText = JSON.stringify(payload.response ?? payload, null, 2);
|
|
896
|
+
const status = String(payload.status || "").toLowerCase() === "connected" ? "connected" : "failed";
|
|
897
|
+
const testedAt = payload.response?.ranAt || new Date().toISOString();
|
|
898
|
+
const lastRunId = payload.runId || payload.response?.runId || "";
|
|
899
|
+
const lastSourceId = payload.sourceId || payload.response?.sourceId || "";
|
|
900
|
+
onSave((config) => {
|
|
901
|
+
let next = updateTableCell(config, table, rowIndex, "status", status);
|
|
902
|
+
next = updateTableCell(next, table, rowIndex, "lastTested", testedAt);
|
|
903
|
+
next = updateTableCell(next, table, rowIndex, "lastRunId", lastRunId);
|
|
904
|
+
next = updateTableCell(next, table, rowIndex, "lastSourceId", lastSourceId);
|
|
905
|
+
next = updateTableCell(next, table, rowIndex, "lastResponse", responseText);
|
|
906
|
+
return next;
|
|
907
|
+
});
|
|
908
|
+
setDraft((current) => ({ ...current, status, lastTested: testedAt, lastRunId, lastSourceId, lastResponse: responseText }));
|
|
909
|
+
setSandboxHistory((current) => payload.response ? [payload.response, ...current].slice(0, 25) : current);
|
|
910
|
+
setSandboxMessage(payload.ok ? "Sandbox run recorded" : (payload.response?.error || payload.error || "Run failed"));
|
|
911
|
+
} catch (err) {
|
|
912
|
+
setSandboxMessage(err.message || "Sandbox run failed");
|
|
913
|
+
} finally {
|
|
914
|
+
setSandboxRunning(false);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function loadSandboxHistory() {
|
|
919
|
+
if (!table.objectId || !String(draft?.Name || "").trim()) {
|
|
920
|
+
setSandboxHistoryMessage("Sandbox Name is required.");
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
setLoadingSandboxHistory(true);
|
|
924
|
+
setSandboxHistoryMessage("");
|
|
925
|
+
try {
|
|
926
|
+
const params = new URLSearchParams({ objectId: table.objectId, name: String(draft.Name || "").trim() });
|
|
927
|
+
const res = await fetch(`/api/workspace/sandbox-run?${params.toString()}`, { cache: "no-store" });
|
|
928
|
+
const payload = await res.json();
|
|
929
|
+
if (!payload.ok) throw new Error(payload.error || "Could not load run history");
|
|
930
|
+
setSandboxHistory(Array.isArray(payload.records) ? payload.records : []);
|
|
931
|
+
setSandboxHistoryMessage(`${payload.recordCount || 0} saved run${payload.recordCount === 1 ? "" : "s"} · ${payload.sourceId || ""}`);
|
|
932
|
+
} catch (err) {
|
|
933
|
+
setSandboxHistory([]);
|
|
934
|
+
setSandboxHistoryMessage(err.message || "Could not load run history");
|
|
935
|
+
} finally {
|
|
936
|
+
setLoadingSandboxHistory(false);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function expandLastResponse() {
|
|
941
|
+
const text = String(draft.lastResponse || "");
|
|
942
|
+
if (!text) return;
|
|
943
|
+
try {
|
|
944
|
+
setExpandedJson(JSON.stringify(JSON.parse(text), null, 2));
|
|
945
|
+
} catch {
|
|
946
|
+
setExpandedJson(text);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
317
950
|
return (
|
|
318
951
|
<>
|
|
319
952
|
<div className="dm-record-backdrop" onClick={onClose} />
|
|
@@ -336,48 +969,72 @@ function DataModelRecordDrawer({ table, tables, rowIndex, row, saving, onClose,
|
|
|
336
969
|
{testMessage && <span>{testMessage}</span>}
|
|
337
970
|
</div>
|
|
338
971
|
)}
|
|
972
|
+
{isSandbox && (
|
|
973
|
+
<div className="dm-record-testbar">
|
|
974
|
+
<ConnectionPill value={draft.status} />
|
|
975
|
+
<button type="button" className="dm-btn-primary-sm" disabled={sandboxRunning || saving || !String(draft.Name || "").trim()} onClick={runSandbox}>
|
|
976
|
+
{sandboxRunning ? "Running…" : (<><Play size={13} aria-hidden /> Run sandbox</>)}
|
|
977
|
+
</button>
|
|
978
|
+
{sandboxMessage && <span>{sandboxMessage}</span>}
|
|
979
|
+
</div>
|
|
980
|
+
)}
|
|
339
981
|
<div className="dm-record-fields">
|
|
340
|
-
{
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
})}
|
|
982
|
+
{isSandbox ? (
|
|
983
|
+
<SandboxRecordFields
|
|
984
|
+
draft={draft}
|
|
985
|
+
setDraft={setDraft}
|
|
986
|
+
table={table}
|
|
987
|
+
tables={tables}
|
|
988
|
+
workspaceConfig={workspaceConfig}
|
|
989
|
+
saving={saving}
|
|
990
|
+
onSave={onSave}
|
|
991
|
+
rowIndex={rowIndex}
|
|
992
|
+
sandboxHistory={sandboxHistory}
|
|
993
|
+
sandboxHistoryMessage={sandboxHistoryMessage}
|
|
994
|
+
loadingSandboxHistory={loadingSandboxHistory}
|
|
995
|
+
onLoadSandboxHistory={loadSandboxHistory}
|
|
996
|
+
onExpandLastResponse={expandLastResponse}
|
|
997
|
+
/>
|
|
998
|
+
) : groupRecordColumns(table.columns || []).map((section) => (
|
|
999
|
+
<DrawerSection key={section.title} title={section.title}>
|
|
1000
|
+
{section.columns.map((column) => (
|
|
1001
|
+
<RecordFieldEditor
|
|
1002
|
+
key={column}
|
|
1003
|
+
table={table}
|
|
1004
|
+
tables={tables}
|
|
1005
|
+
column={column}
|
|
1006
|
+
value={String(draft?.[column] ?? "")}
|
|
1007
|
+
saving={saving}
|
|
1008
|
+
onDraft={(field, nextValue) => setDraft((current) => ({ ...current, [field]: nextValue }))}
|
|
1009
|
+
onCommit={updateField}
|
|
1010
|
+
onExpandJson={expandLastResponse}
|
|
1011
|
+
/>
|
|
1012
|
+
))}
|
|
1013
|
+
</DrawerSection>
|
|
1014
|
+
))}
|
|
374
1015
|
</div>
|
|
375
1016
|
</aside>
|
|
1017
|
+
{expandedJson !== null && (
|
|
1018
|
+
<div className="dm-json-modal-backdrop" onClick={() => setExpandedJson(null)}>
|
|
1019
|
+
<section className="dm-json-modal" role="dialog" aria-modal="true" aria-label="lastResponse JSON" onClick={(event) => event.stopPropagation()}>
|
|
1020
|
+
<header>
|
|
1021
|
+
<div>
|
|
1022
|
+
<p>lastResponse</p>
|
|
1023
|
+
<h2>{draft.Name || draft.integrationId || "Record response"}</h2>
|
|
1024
|
+
</div>
|
|
1025
|
+
<button type="button" className="dm-sidebar-close" onClick={() => setExpandedJson(null)} aria-label="Close expanded JSON">
|
|
1026
|
+
<X size={16} />
|
|
1027
|
+
</button>
|
|
1028
|
+
</header>
|
|
1029
|
+
<pre>{expandedJson}</pre>
|
|
1030
|
+
</section>
|
|
1031
|
+
</div>
|
|
1032
|
+
)}
|
|
376
1033
|
</>
|
|
377
1034
|
);
|
|
378
1035
|
}
|
|
379
1036
|
|
|
380
|
-
function DataModelTableSurface({ table, tables, saving, onSave }) {
|
|
1037
|
+
function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave }) {
|
|
381
1038
|
const [selectedRow, setSelectedRow] = useState(null);
|
|
382
1039
|
const [addingField, setAddingField] = useState(false);
|
|
383
1040
|
const [fieldName, setFieldName] = useState("");
|
|
@@ -529,6 +1186,7 @@ function DataModelTableSurface({ table, tables, saving, onSave }) {
|
|
|
529
1186
|
<DataModelRecordDrawer
|
|
530
1187
|
table={table}
|
|
531
1188
|
tables={tables}
|
|
1189
|
+
workspaceConfig={workspaceConfig}
|
|
532
1190
|
rowIndex={selectedRow}
|
|
533
1191
|
row={selectedRecord}
|
|
534
1192
|
saving={saving}
|
|
@@ -853,7 +1511,7 @@ export default function DataModelPage() {
|
|
|
853
1511
|
</div>
|
|
854
1512
|
<SourceValidationBanner table={selectedTable} />
|
|
855
1513
|
</div>
|
|
856
|
-
<DataModelTableSurface table={selectedTable} tables={tables} saving={saving} onSave={save} />
|
|
1514
|
+
<DataModelTableSurface workspaceConfig={workspaceConfig} table={selectedTable} tables={tables} saving={saving} onSave={save} />
|
|
857
1515
|
</section>
|
|
858
1516
|
)}
|
|
859
1517
|
</div>
|