@growthub/cli 0.13.0 → 0.13.2
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 +522 -35
- 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/page.jsx +6 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1062 -2
- 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/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 +492 -28
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +114 -30
- 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 +2 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +21 -1
- package/package.json +1 -1
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
Tag,
|
|
36
36
|
Terminal,
|
|
37
37
|
ToggleLeft,
|
|
38
|
+
Trash2,
|
|
38
39
|
Type,
|
|
39
40
|
Users,
|
|
40
41
|
X,
|
|
@@ -69,6 +70,17 @@ import { SandboxRunPanel } from "./SandboxRunPanel.jsx";
|
|
|
69
70
|
import { StatusPill } from "./StatusPill.jsx";
|
|
70
71
|
import { SegmentedToggle, ToggleField } from "./ToggleField.jsx";
|
|
71
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";
|
|
72
84
|
import {
|
|
73
85
|
FIELD_TYPE_ICON_NAMES,
|
|
74
86
|
ICON_PICKER_SET,
|
|
@@ -551,6 +563,24 @@ function RecordFieldEditor({ table, tables, column, value, saving, editable, onD
|
|
|
551
563
|
);
|
|
552
564
|
}
|
|
553
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
|
+
|
|
554
584
|
function SandboxRecordFields({
|
|
555
585
|
draft,
|
|
556
586
|
setDraft,
|
|
@@ -564,7 +594,8 @@ function SandboxRecordFields({
|
|
|
564
594
|
sandboxHistoryMessage,
|
|
565
595
|
loadingSandboxHistory,
|
|
566
596
|
onLoadSandboxHistory,
|
|
567
|
-
|
|
597
|
+
onOpenGraphSidecar,
|
|
598
|
+
onOpenTraceSidecar
|
|
568
599
|
}) {
|
|
569
600
|
const [sandboxAdapters, setSandboxAdapters] = useState([]);
|
|
570
601
|
useEffect(() => {
|
|
@@ -830,32 +861,72 @@ function SandboxRecordFields({
|
|
|
830
861
|
</label>
|
|
831
862
|
</DrawerSection>
|
|
832
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
|
+
|
|
833
878
|
<DrawerSection title="Response & History">
|
|
834
|
-
<
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
879
|
+
<SandboxTraceFieldButton
|
|
880
|
+
label="lastRunId"
|
|
881
|
+
value={draft.lastRunId}
|
|
882
|
+
disabled={saving}
|
|
883
|
+
onOpen={() => onOpenTraceSidecar?.({ field: "lastRunId", runId: draft.lastRunId })}
|
|
884
|
+
/>
|
|
838
885
|
|
|
839
|
-
<
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
886
|
+
<SandboxTraceFieldButton
|
|
887
|
+
label="lastSourceId"
|
|
888
|
+
value={draft.lastSourceId}
|
|
889
|
+
disabled={saving}
|
|
890
|
+
onOpen={() => onOpenTraceSidecar?.({ field: "lastSourceId" })}
|
|
891
|
+
/>
|
|
843
892
|
|
|
844
|
-
<label className="dm-record-field dm-
|
|
893
|
+
<label className="dm-record-field dm-field-link">
|
|
845
894
|
<span>lastResponse</span>
|
|
846
895
|
<button
|
|
847
896
|
type="button"
|
|
848
|
-
className="dm-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
disabled={!draft.lastResponse}
|
|
852
|
-
onClick={onExpandLastResponse}
|
|
897
|
+
className="dm-field-link__btn"
|
|
898
|
+
disabled={saving || !draft.lastResponse}
|
|
899
|
+
onClick={() => onOpenTraceSidecar?.({ field: "lastResponse" })}
|
|
853
900
|
>
|
|
854
|
-
|
|
901
|
+
{draft.lastResponse ? "View run trace" : "—"}
|
|
855
902
|
</button>
|
|
856
|
-
<
|
|
903
|
+
<span className="dm-field-link__hint">Run output — not the graph builder</span>
|
|
857
904
|
</label>
|
|
858
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
|
+
|
|
859
930
|
<div className="dm-record-field">
|
|
860
931
|
<span>Run history</span>
|
|
861
932
|
<button type="button" className="dm-btn-ghost" disabled={loadingSandboxHistory} onClick={onLoadSandboxHistory}>
|
|
@@ -885,7 +956,19 @@ function SandboxRecordFields({
|
|
|
885
956
|
);
|
|
886
957
|
}
|
|
887
958
|
|
|
888
|
-
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
|
+
}) {
|
|
889
972
|
const [draft, setDraft] = useState(row || {});
|
|
890
973
|
const [editMode, setEditMode] = useState(false);
|
|
891
974
|
const [pendingColumns, setPendingColumns] = useState(table.columns || []);
|
|
@@ -898,6 +981,15 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
898
981
|
const [sandboxHistoryMessage, setSandboxHistoryMessage] = useState("");
|
|
899
982
|
const [loadingSandboxHistory, setLoadingSandboxHistory] = useState(false);
|
|
900
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("");
|
|
901
993
|
|
|
902
994
|
useEffect(() => {
|
|
903
995
|
setDraft(row || {});
|
|
@@ -909,10 +1001,28 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
909
1001
|
setSandboxHistory([]);
|
|
910
1002
|
setSandboxHistoryMessage("");
|
|
911
1003
|
setExpandedJson(null);
|
|
912
|
-
|
|
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]);
|
|
913
1022
|
|
|
914
1023
|
if (rowIndex === null || rowIndex === undefined || !row) return null;
|
|
915
1024
|
|
|
1025
|
+
const isApiRegistry = table.objectType === "api-registry";
|
|
916
1026
|
const isSandbox = table.objectType === "sandbox-environment";
|
|
917
1027
|
const isDirty = JSON.stringify(draft || {}) !== JSON.stringify(row || {}) || JSON.stringify(pendingColumns) !== JSON.stringify(table.columns || []) || JSON.stringify(pendingHidden) !== JSON.stringify(table.fieldSettings?.hidden || []);
|
|
918
1028
|
|
|
@@ -1004,6 +1114,147 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1004
1114
|
}
|
|
1005
1115
|
}
|
|
1006
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
|
+
|
|
1007
1258
|
async function runSandbox() {
|
|
1008
1259
|
if (!table.objectId) {
|
|
1009
1260
|
setSandboxMessage("Missing object id for this sandbox table.");
|
|
@@ -1078,17 +1329,49 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1078
1329
|
}
|
|
1079
1330
|
}
|
|
1080
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
|
+
|
|
1081
1361
|
return (
|
|
1082
1362
|
<>
|
|
1083
1363
|
<div className="dm-record-backdrop" onClick={onClose} />
|
|
1084
|
-
<aside
|
|
1364
|
+
<aside
|
|
1365
|
+
className={`dm-record-drawer${drawerWide ? " dm-record-drawer-wide" : ""}`}
|
|
1366
|
+
aria-label="Record details"
|
|
1367
|
+
>
|
|
1085
1368
|
<header className="dm-record-drawer-head">
|
|
1086
1369
|
<div>
|
|
1087
1370
|
<p>Record</p>
|
|
1088
1371
|
<h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
|
|
1089
1372
|
</div>
|
|
1090
1373
|
<div className="dm-record-drawer-actions">
|
|
1091
|
-
{!isSandbox && (
|
|
1374
|
+
{!isSandbox && sandboxToolFlow !== "draft" && (
|
|
1092
1375
|
<button type="button" className="dm-sidebar-close" onClick={() => setEditMode((current) => !current)} aria-label="Toggle edit mode">
|
|
1093
1376
|
<Pencil size={16} />
|
|
1094
1377
|
</button>
|
|
@@ -1098,7 +1381,7 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1098
1381
|
</button>
|
|
1099
1382
|
</div>
|
|
1100
1383
|
</header>
|
|
1101
|
-
{(table.objectType === "api-registry" || table.objectType === "data-source") && (
|
|
1384
|
+
{(table.objectType === "api-registry" || table.objectType === "data-source") && sandboxToolFlow !== "draft" && (
|
|
1102
1385
|
<SourceTestPanel
|
|
1103
1386
|
status={draft.status}
|
|
1104
1387
|
testing={testing}
|
|
@@ -1107,7 +1390,67 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1107
1390
|
onTest={testApiRecord}
|
|
1108
1391
|
/>
|
|
1109
1392
|
)}
|
|
1110
|
-
{
|
|
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" && (
|
|
1111
1454
|
<SandboxRunPanel
|
|
1112
1455
|
status={draft.status}
|
|
1113
1456
|
sandboxRunning={sandboxRunning}
|
|
@@ -1117,8 +1460,27 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1117
1460
|
onRun={runSandbox}
|
|
1118
1461
|
/>
|
|
1119
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
|
+
)}
|
|
1120
1482
|
<div className="dm-record-fields">
|
|
1121
|
-
{isSandbox ? (
|
|
1483
|
+
{isApiRegistry && sandboxToolFlow === "draft" ? null : hideRecordFields ? null : isSandbox ? (
|
|
1122
1484
|
<SandboxRecordFields
|
|
1123
1485
|
draft={draft}
|
|
1124
1486
|
setDraft={setDraft}
|
|
@@ -1132,7 +1494,8 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1132
1494
|
sandboxHistoryMessage={sandboxHistoryMessage}
|
|
1133
1495
|
loadingSandboxHistory={loadingSandboxHistory}
|
|
1134
1496
|
onLoadSandboxHistory={loadSandboxHistory}
|
|
1135
|
-
|
|
1497
|
+
onOpenGraphSidecar={openGraphSidecar}
|
|
1498
|
+
onOpenTraceSidecar={openTraceSidecar}
|
|
1136
1499
|
/>
|
|
1137
1500
|
) : groupRecordColumns(table.columns || []).map((section) => (
|
|
1138
1501
|
<DrawerSection key={section.title} title={section.title}>
|
|
@@ -1209,8 +1572,34 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1209
1572
|
);
|
|
1210
1573
|
}
|
|
1211
1574
|
|
|
1212
|
-
|
|
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();
|
|
1213
1601
|
const [selectedRow, setSelectedRow] = useState(null);
|
|
1602
|
+
const [initialSidecar, setInitialSidecar] = useState(null);
|
|
1214
1603
|
const [fieldName, setFieldName] = useState("");
|
|
1215
1604
|
const [fieldType, setFieldType] = useState("text");
|
|
1216
1605
|
const [addingField, setAddingField] = useState(false);
|
|
@@ -1237,6 +1626,7 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1237
1626
|
setSelectMenuOpen(false);
|
|
1238
1627
|
setPageIndex(0);
|
|
1239
1628
|
}, [table.id]);
|
|
1629
|
+
|
|
1240
1630
|
useEffect(() => {
|
|
1241
1631
|
setFieldName("");
|
|
1242
1632
|
setFieldType("text");
|
|
@@ -1269,6 +1659,20 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1269
1659
|
const pageSelectedCount = pageEntries.filter((entry) => selectedRows.has(entry.originalIndex)).length;
|
|
1270
1660
|
const allPageSelected = pageEntries.length > 0 && pageSelectedCount === pageEntries.length;
|
|
1271
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
|
+
|
|
1272
1676
|
useEffect(() => {
|
|
1273
1677
|
setPageIndex((current) => Math.min(current, pageCount - 1));
|
|
1274
1678
|
}, [pageCount]);
|
|
@@ -1352,6 +1756,15 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1352
1756
|
setFilterTarget("");
|
|
1353
1757
|
}
|
|
1354
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
|
+
|
|
1355
1768
|
function removeFilter(fieldId) {
|
|
1356
1769
|
updateSettings((current) => {
|
|
1357
1770
|
const clauses = (current.filter?.clauses || []).filter((clause) => clause.fieldId !== fieldId);
|
|
@@ -1431,13 +1844,10 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1431
1844
|
|
|
1432
1845
|
function deleteSelectedRows() {
|
|
1433
1846
|
if (!selectedRows.size) return;
|
|
1434
|
-
if (!confirmDeleteSelection) {
|
|
1435
|
-
setConfirmDeleteSelection(true);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
1847
|
const rowIndexes = Array.from(selectedRows).sort((a, b) => b - a);
|
|
1439
1848
|
onSave((config) => rowIndexes.reduce((nextConfig, rowIndex) => deleteTableRow(nextConfig, table, rowIndex), config));
|
|
1440
1849
|
setSelectedRow(null);
|
|
1850
|
+
setConfirmDeleteSelection(false);
|
|
1441
1851
|
clearRowSelection();
|
|
1442
1852
|
}
|
|
1443
1853
|
|
|
@@ -1504,8 +1914,8 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1504
1914
|
{table.mutable && selectedRowCount > 0 && (
|
|
1505
1915
|
<>
|
|
1506
1916
|
<button type="button" className="dm-btn-ghost" disabled={saving} onClick={clearRowSelection}>Cancel selection</button>
|
|
1507
|
-
<button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={
|
|
1508
|
-
|
|
1917
|
+
<button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={() => setConfirmDeleteSelection(true)}>
|
|
1918
|
+
<Trash2 size={13} />Delete
|
|
1509
1919
|
</button>
|
|
1510
1920
|
</>
|
|
1511
1921
|
)}
|
|
@@ -1667,6 +2077,26 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1667
2077
|
/>
|
|
1668
2078
|
) : column.toLowerCase() === "status" ? (
|
|
1669
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>
|
|
1670
2100
|
) : (
|
|
1671
2101
|
<span className={row?.[column] ? "" : "dm-cell-empty"}>
|
|
1672
2102
|
{formatCellValue(row?.[column], column) || "—"}
|
|
@@ -1711,9 +2141,36 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
|
|
|
1711
2141
|
rowIndex={selectedEntry?.originalIndex ?? null}
|
|
1712
2142
|
row={selectedRecord}
|
|
1713
2143
|
saving={saving}
|
|
1714
|
-
onClose={() => setSelectedRow(null)}
|
|
2144
|
+
onClose={() => { setSelectedRow(null); setInitialSidecar(null); }}
|
|
1715
2145
|
onSave={onSave}
|
|
2146
|
+
onFocusSandboxRow={onFocusSandboxRow}
|
|
2147
|
+
initialSidecar={initialSidecar}
|
|
2148
|
+
onClearInitialSidecar={() => setInitialSidecar(null)}
|
|
1716
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
|
+
)}
|
|
1717
2174
|
</div>
|
|
1718
2175
|
);
|
|
1719
2176
|
}
|
|
@@ -1991,6 +2448,7 @@ export default function DataModelShell() {
|
|
|
1991
2448
|
const [helperInitialPrompt, setHelperInitialPrompt] = useState("");
|
|
1992
2449
|
const [helperInitialThread, setHelperInitialThread] = useState(null);
|
|
1993
2450
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
2451
|
+
const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
|
|
1994
2452
|
const pendingPatchRef = useRef({});
|
|
1995
2453
|
const saveTimerRef = useRef(null);
|
|
1996
2454
|
|
|
@@ -2083,6 +2541,19 @@ export default function DataModelShell() {
|
|
|
2083
2541
|
|
|
2084
2542
|
const selectedTable = tables.find((t) => t.source === selectedSource) || tables[0] || null;
|
|
2085
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
|
+
|
|
2086
2557
|
useEffect(() => {
|
|
2087
2558
|
if (!selectedSource && tables[0]) setSelectedSource(tables[0].source);
|
|
2088
2559
|
}, [selectedSource, tables]);
|
|
@@ -2101,6 +2572,12 @@ export default function DataModelShell() {
|
|
|
2101
2572
|
}
|
|
2102
2573
|
}, [searchParams, selectedSource, tables]);
|
|
2103
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
|
+
|
|
2104
2581
|
// Flush any accumulated patch keys to the server. Called by the debounce
|
|
2105
2582
|
// timer and on visibilitychange/beforeunload so no local edit is lost.
|
|
2106
2583
|
const flushPendingPatch = useCallback(async () => {
|
|
@@ -2271,7 +2748,7 @@ export default function DataModelShell() {
|
|
|
2271
2748
|
run: () => setAddOpen(true)
|
|
2272
2749
|
},
|
|
2273
2750
|
{
|
|
2274
|
-
id: "nav.
|
|
2751
|
+
id: "nav.builder", group: "Navigation", label: "Go to Builder",
|
|
2275
2752
|
run: () => { window.location.href = "/"; }
|
|
2276
2753
|
},
|
|
2277
2754
|
{
|
|
@@ -2416,7 +2893,17 @@ export default function DataModelShell() {
|
|
|
2416
2893
|
selectedTable && (
|
|
2417
2894
|
<section className="dm-detail-v2 dm-detail-v3">
|
|
2418
2895
|
<SourceValidationBanner table={selectedTable} />
|
|
2419
|
-
<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
|
+
/>
|
|
2420
2907
|
</section>
|
|
2421
2908
|
)
|
|
2422
2909
|
)}
|