@growthub/cli 0.13.9 → 0.14.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/env-status/route.js +31 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +227 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +6 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +414 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +339 -77
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +229 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +224 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +30 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
Trash2,
|
|
43
43
|
Type,
|
|
44
44
|
Unlock,
|
|
45
|
+
Upload,
|
|
45
46
|
Users,
|
|
46
47
|
X,
|
|
47
48
|
Zap,
|
|
@@ -80,9 +81,16 @@ import { SourceTestPanel } from "./SourceTestPanel.jsx";
|
|
|
80
81
|
import { SandboxToolDraftPanel } from "./SandboxToolDraftPanel.jsx";
|
|
81
82
|
import { SandboxToolConfirmModal } from "./SandboxToolConfirmModal.jsx";
|
|
82
83
|
import { OrchestrationRunTracePanel } from "./OrchestrationRunTracePanel.jsx";
|
|
84
|
+
import { ApiRegistryCreationCockpit } from "./ApiRegistryCreationCockpit.jsx";
|
|
85
|
+
import { deriveApiRegistryCreationState } from "@/lib/api-registry-creation-flow";
|
|
86
|
+
import { deriveSandboxServerlessState } from "@/lib/sandbox-serverless-flow";
|
|
87
|
+
import { profileApiResponse, recommendResolver } from "@/lib/api-response-profile";
|
|
88
|
+
import { classifyCreationError } from "@/lib/creation-error-recovery";
|
|
83
89
|
import {
|
|
84
90
|
buildSandboxRowFromApiRegistry,
|
|
85
91
|
findSandboxRowsForRegistry,
|
|
92
|
+
buildDataSourceRowFromApiRegistry,
|
|
93
|
+
findDataSourceRowsForRegistry,
|
|
86
94
|
getOrchestrationGraphUiState,
|
|
87
95
|
redactSecretsFromText
|
|
88
96
|
} from "@/lib/orchestration-graph";
|
|
@@ -517,8 +525,11 @@ function StaticSelect({ value, options, disabled, onChange, placeholder = "Selec
|
|
|
517
525
|
);
|
|
518
526
|
}
|
|
519
527
|
|
|
520
|
-
function DrawerSection({ title, children, defaultOpen = false }) {
|
|
528
|
+
function DrawerSection({ title, children, defaultOpen = false, forceOpen = false }) {
|
|
521
529
|
const [open, setOpen] = useState(defaultOpen);
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
if (forceOpen) setOpen(true);
|
|
532
|
+
}, [forceOpen]);
|
|
522
533
|
return (
|
|
523
534
|
<section className={`dm-drawer-section${open ? " open" : ""}`}>
|
|
524
535
|
<button type="button" className="dm-drawer-section-toggle" onClick={() => setOpen((current) => !current)}>
|
|
@@ -652,13 +663,30 @@ function SandboxRecordFields({
|
|
|
652
663
|
onOpenGraphSidecar,
|
|
653
664
|
onOpenTraceSidecar
|
|
654
665
|
}) {
|
|
666
|
+
const router = useRouter();
|
|
655
667
|
const [sandboxAdapters, setSandboxAdapters] = useState([]);
|
|
668
|
+
const [serverlessSignals, setServerlessSignals] = useState({ configuredEnvRefs: [], persistenceAdapters: [] });
|
|
656
669
|
useEffect(() => {
|
|
657
670
|
fetch("/api/workspace/sandbox-adapters", { cache: "no-store" })
|
|
658
671
|
.then((res) => res.json())
|
|
659
672
|
.then((payload) => setSandboxAdapters(Array.isArray(payload.adapters) ? payload.adapters : []))
|
|
660
673
|
.catch(() => setSandboxAdapters([]));
|
|
661
674
|
}, []);
|
|
675
|
+
// Real runtime truth for the serverless/persistence cockpit (env-status).
|
|
676
|
+
useEffect(() => {
|
|
677
|
+
let cancelled = false;
|
|
678
|
+
fetch("/api/workspace/env-status", { cache: "no-store" })
|
|
679
|
+
.then((res) => (res.ok ? res.json() : {}))
|
|
680
|
+
.then((payload) => {
|
|
681
|
+
if (cancelled) return;
|
|
682
|
+
setServerlessSignals({
|
|
683
|
+
configuredEnvRefs: Array.isArray(payload.configuredEnvRefs) ? payload.configuredEnvRefs : [],
|
|
684
|
+
persistenceAdapters: Array.isArray(payload.persistenceAdapters) ? payload.persistenceAdapters : [],
|
|
685
|
+
});
|
|
686
|
+
})
|
|
687
|
+
.catch(() => {});
|
|
688
|
+
return () => { cancelled = true; };
|
|
689
|
+
}, [rowIndex, table.objectId]);
|
|
662
690
|
|
|
663
691
|
const locality = String(draft.runLocality || "local").trim().toLowerCase() === "serverless" ? "serverless" : "local";
|
|
664
692
|
const savedEnvRefs = useMemo(() => listSavedEnvRefs(workspaceConfig || {}), [workspaceConfig]);
|
|
@@ -697,8 +725,30 @@ function SandboxRecordFields({
|
|
|
697
725
|
|
|
698
726
|
const netOn = ["true", "1", "on", "yes"].includes(String(draft.networkAllow || "").trim().toLowerCase());
|
|
699
727
|
|
|
728
|
+
// Same cockpit interface + mental model as the API Registry lane, driven by
|
|
729
|
+
// the serverless/scheduling/persistence derivation. Steps are status-only
|
|
730
|
+
// here (inlineEditing) — the editable fields below are the editor.
|
|
731
|
+
const serverlessState = deriveSandboxServerlessState({
|
|
732
|
+
sandboxRow: draft,
|
|
733
|
+
workspaceConfig,
|
|
734
|
+
configuredEnvRefs: serverlessSignals.configuredEnvRefs,
|
|
735
|
+
persistenceAdapters: serverlessSignals.persistenceAdapters,
|
|
736
|
+
inlineEditing: true,
|
|
737
|
+
});
|
|
738
|
+
function handleServerlessAction(action) {
|
|
739
|
+
if (!action) return;
|
|
740
|
+
if (action.id === "toggle-locality") setRunLocality(serverlessState.isServerless ? "local" : "serverless");
|
|
741
|
+
else if (action.id === "open-settings") router.push(action.href || "/settings");
|
|
742
|
+
}
|
|
743
|
+
|
|
700
744
|
return (
|
|
701
745
|
<div className="dm-sandbox-config">
|
|
746
|
+
<ApiRegistryCreationCockpit
|
|
747
|
+
state={serverlessState}
|
|
748
|
+
onAction={handleServerlessAction}
|
|
749
|
+
disabled={!table.mutable || saving}
|
|
750
|
+
eyebrow={serverlessState.isServerless ? "Serverless workflow" : "Workflow runtime"}
|
|
751
|
+
/>
|
|
702
752
|
<DrawerSection title="Identity & Mode">
|
|
703
753
|
<label className="dm-record-field">
|
|
704
754
|
<span>Name</span>
|
|
@@ -1141,9 +1191,20 @@ function DataModelRecordDrawer({
|
|
|
1141
1191
|
const [createdSandboxMeta, setCreatedSandboxMeta] = useState(null);
|
|
1142
1192
|
const [createdSandboxTesting, setCreatedSandboxTesting] = useState(false);
|
|
1143
1193
|
const [createdSandboxTestMessage, setCreatedSandboxTestMessage] = useState("");
|
|
1194
|
+
const [creatingDataSource, setCreatingDataSource] = useState(false);
|
|
1195
|
+
const [createdDataSourceMeta, setCreatedDataSourceMeta] = useState(null);
|
|
1196
|
+
const [dataSourceMessage, setDataSourceMessage] = useState("");
|
|
1197
|
+
const [cockpitBusy, setCockpitBusy] = useState("");
|
|
1198
|
+
const [cockpitCollapsed, setCockpitCollapsed] = useState(false);
|
|
1199
|
+
// Real runtime truth for the creation cockpit: which auth refs resolve in the
|
|
1200
|
+
// server runtime, and the live source-records sidecar. Fetched (never guessed)
|
|
1201
|
+
// so auth/refresh readiness reflect actual state, and refreshed after actions.
|
|
1202
|
+
const [creationSignals, setCreationSignals] = useState({ configuredEnvRefs: [], sourceRecords: {} });
|
|
1203
|
+
const [creationReceipts, setCreationReceipts] = useState([]);
|
|
1144
1204
|
const [sidecarMode, setSidecarMode] = useState(null);
|
|
1145
1205
|
const [traceField, setTraceField] = useState(null);
|
|
1146
1206
|
const [traceRunId, setTraceRunId] = useState("");
|
|
1207
|
+
const drawerScrollRef = useRef(null);
|
|
1147
1208
|
const drawerKeyRef = useRef("");
|
|
1148
1209
|
const router = useRouter();
|
|
1149
1210
|
|
|
@@ -1165,6 +1226,14 @@ function DataModelRecordDrawer({
|
|
|
1165
1226
|
setSandboxToolDraft({});
|
|
1166
1227
|
setCreatedSandboxMeta(null);
|
|
1167
1228
|
setCreatedSandboxTestMessage("");
|
|
1229
|
+
setCreatingDataSource(false);
|
|
1230
|
+
setCreatedDataSourceMeta(null);
|
|
1231
|
+
setDataSourceMessage("");
|
|
1232
|
+
setCreationReceipts([]);
|
|
1233
|
+
setCockpitCollapsed(false);
|
|
1234
|
+
requestAnimationFrame(() => {
|
|
1235
|
+
if (drawerScrollRef.current) drawerScrollRef.current.scrollTop = 0;
|
|
1236
|
+
});
|
|
1168
1237
|
}
|
|
1169
1238
|
if (initialSidecar?.mode === "trace") {
|
|
1170
1239
|
setSidecarMode("trace");
|
|
@@ -1177,6 +1246,34 @@ function DataModelRecordDrawer({
|
|
|
1177
1246
|
}
|
|
1178
1247
|
}, [row, rowIndex, initialSidecar, table.id, table.objectId, table.source, table.columns, table.fieldSettings?.hidden]);
|
|
1179
1248
|
|
|
1249
|
+
// Load real cockpit truth (configured auth refs + source-records sidecar) when
|
|
1250
|
+
// an API Registry row is open. The creation cockpit derives auth/refresh
|
|
1251
|
+
// readiness from these — never guessed, never faked.
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
if (table.objectType !== "api-registry" || rowIndex === null || rowIndex === undefined) return;
|
|
1254
|
+
let cancelled = false;
|
|
1255
|
+
(async () => {
|
|
1256
|
+
try {
|
|
1257
|
+
const [statusRes, wsRes] = await Promise.all([
|
|
1258
|
+
fetch("/api/workspace/env-status", { cache: "no-store" }),
|
|
1259
|
+
fetch("/api/workspace", { cache: "no-store" }),
|
|
1260
|
+
]);
|
|
1261
|
+
const status = statusRes.ok ? await statusRes.json() : {};
|
|
1262
|
+
const ws = wsRes.ok ? await wsRes.json() : {};
|
|
1263
|
+
if (cancelled) return;
|
|
1264
|
+
setCreationSignals({
|
|
1265
|
+
configuredEnvRefs: Array.isArray(status.configuredEnvRefs) ? status.configuredEnvRefs : [],
|
|
1266
|
+
sourceRecords: ws.workspaceSourceRecords && typeof ws.workspaceSourceRecords === "object"
|
|
1267
|
+
? ws.workspaceSourceRecords
|
|
1268
|
+
: {},
|
|
1269
|
+
});
|
|
1270
|
+
} catch {
|
|
1271
|
+
/* leave signals as-is — cockpit degrades to pending, never fakes */
|
|
1272
|
+
}
|
|
1273
|
+
})();
|
|
1274
|
+
return () => { cancelled = true; };
|
|
1275
|
+
}, [table.objectType, table.objectId, rowIndex]);
|
|
1276
|
+
|
|
1180
1277
|
if (rowIndex === null || rowIndex === undefined || !row) return null;
|
|
1181
1278
|
|
|
1182
1279
|
const isApiRegistry = table.objectType === "api-registry";
|
|
@@ -1258,6 +1355,14 @@ function DataModelRecordDrawer({
|
|
|
1258
1355
|
});
|
|
1259
1356
|
setDraft((current) => ({ ...current, status, lastTested: new Date().toISOString(), lastResponse: responseText }));
|
|
1260
1357
|
setTestMessage(payload.ok ? "Connected" : payload.error || "Connection failed");
|
|
1358
|
+
if (isApiRegistry) {
|
|
1359
|
+
if (payload.ok) {
|
|
1360
|
+
pushReceipt({ kind: "api-test", ok: true, detail: `Tested — HTTP ${payload.status ?? 200}${payload.usedServerSecret ? " · used server secret" : ""}.` });
|
|
1361
|
+
} else {
|
|
1362
|
+
const recovery = classifyCreationError({ phase: "test", httpStatus: payload.status, detail: redactSecretsFromText(payload.error || `HTTP ${payload.status ?? ""} failed`) });
|
|
1363
|
+
pushReceipt({ kind: "api-test", ok: false, detail: `${recovery.safeDetail}. ${recovery.requiredAction}` });
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1261
1366
|
} catch (err) {
|
|
1262
1367
|
const responseText = JSON.stringify({ error: err.message || "Connection failed" }, null, 2);
|
|
1263
1368
|
onSave((config) => {
|
|
@@ -1341,6 +1446,221 @@ function DataModelRecordDrawer({
|
|
|
1341
1446
|
}
|
|
1342
1447
|
}
|
|
1343
1448
|
|
|
1449
|
+
function createDataSourceFromRegistry() {
|
|
1450
|
+
const integrationId = String(draft?.integrationId || "").trim();
|
|
1451
|
+
if (!integrationId) {
|
|
1452
|
+
setDataSourceMessage("This API Registry row needs an integrationId before a Data Source can reference it.");
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (findDataSourceRowsForRegistry(workspaceConfig, integrationId).length > 0) {
|
|
1456
|
+
setDataSourceMessage("A Data Source already references this API. Open it instead of creating a duplicate.");
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
setCreatingDataSource(true);
|
|
1460
|
+
setDataSourceMessage("");
|
|
1461
|
+
try {
|
|
1462
|
+
let createdMeta = null;
|
|
1463
|
+
onSave((config) => {
|
|
1464
|
+
let next = config;
|
|
1465
|
+
if (findDataSourceRowsForRegistry(next, integrationId).length > 0) return next;
|
|
1466
|
+
// Dedicated data-source object per API (one object = one live integration,
|
|
1467
|
+
// matching the refresh-sources contract which binds integrationId per object).
|
|
1468
|
+
const dsName = `${String(draft?.Name || integrationId).trim()} Source`;
|
|
1469
|
+
const beforeIds = new Set((next.dataModel?.objects || []).map((o) => o.id));
|
|
1470
|
+
next = createTypedBusinessObject(next, { name: dsName, objectType: "data-source" });
|
|
1471
|
+
const newObj = (next.dataModel?.objects || []).find((o) => o.objectType === "data-source" && !beforeIds.has(o.id));
|
|
1472
|
+
if (!newObj) return next;
|
|
1473
|
+
const sourceTable = listWorkspaceDataModelTables(next).find((t) => t.objectId === newObj.id);
|
|
1474
|
+
const profile = profileApiResponse(draft?.lastResponse);
|
|
1475
|
+
const newRow = buildDataSourceRowFromApiRegistry(next, draft, {
|
|
1476
|
+
entityType: profile?.parsed ? profile.suggestedEntityType : undefined,
|
|
1477
|
+
});
|
|
1478
|
+
if (sourceTable) next = appendRowsToTable(next, sourceTable, [newRow]);
|
|
1479
|
+
// Make the object live-backed so refresh-sources hydrates the sidecar
|
|
1480
|
+
// (keyed by object id). Without this binding, refresh skips it as
|
|
1481
|
+
// "not-live-backed" and the journey would never close.
|
|
1482
|
+
next = {
|
|
1483
|
+
...next,
|
|
1484
|
+
dataModel: {
|
|
1485
|
+
...next.dataModel,
|
|
1486
|
+
objects: (next.dataModel.objects || []).map((o) =>
|
|
1487
|
+
o.id === newObj.id
|
|
1488
|
+
? {
|
|
1489
|
+
...o,
|
|
1490
|
+
sourceId: newRow.sourceId,
|
|
1491
|
+
binding: {
|
|
1492
|
+
mode: "integration",
|
|
1493
|
+
lane: "data-source",
|
|
1494
|
+
sourceStorage: "workspace-source-records",
|
|
1495
|
+
integrationId,
|
|
1496
|
+
sourceId: newRow.sourceId,
|
|
1497
|
+
source: newRow.Name,
|
|
1498
|
+
},
|
|
1499
|
+
}
|
|
1500
|
+
: o
|
|
1501
|
+
),
|
|
1502
|
+
},
|
|
1503
|
+
};
|
|
1504
|
+
createdMeta = { objectId: newObj.id, name: newRow.Name, sourceId: newRow.sourceId };
|
|
1505
|
+
return next;
|
|
1506
|
+
});
|
|
1507
|
+
if (createdMeta) {
|
|
1508
|
+
setCreatedDataSourceMeta(createdMeta);
|
|
1509
|
+
setDataSourceMessage("Data Source created and live-backed. Use Refresh to pull records into the workspace — nothing auto-fetches.");
|
|
1510
|
+
pushReceipt({ kind: "data-source-created", ok: true, detail: `Created "${createdMeta.name}" (sourceId ${createdMeta.sourceId}), live-backed via registryId ${integrationId}.` });
|
|
1511
|
+
reloadCreationSignals();
|
|
1512
|
+
} else {
|
|
1513
|
+
setDataSourceMessage("A Data Source already references this API.");
|
|
1514
|
+
}
|
|
1515
|
+
} finally {
|
|
1516
|
+
setCreatingDataSource(false);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function openDataSourceRow(objectIdOverride) {
|
|
1521
|
+
const objectId = String(objectIdOverride || createdDataSourceMeta?.objectId || "").trim();
|
|
1522
|
+
if (!objectId) return;
|
|
1523
|
+
onClose();
|
|
1524
|
+
router.push(`/data-model?object=${encodeURIComponent(objectId)}`);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// Append a creation receipt (test / create / refresh outcomes). Secret-safe —
|
|
1528
|
+
// detail strings are caller-redacted; receipts hold no values.
|
|
1529
|
+
function pushReceipt(entry) {
|
|
1530
|
+
setCreationReceipts((cur) => [{ at: new Date().toISOString(), ...entry }, ...cur].slice(0, 12));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Pull real cockpit truth: configured auth refs (server-resolved, slugs only)
|
|
1534
|
+
// + the live source-records sidecar. Safe to call repeatedly; never throws.
|
|
1535
|
+
async function reloadCreationSignals() {
|
|
1536
|
+
try {
|
|
1537
|
+
const [statusRes, wsRes] = await Promise.all([
|
|
1538
|
+
fetch("/api/workspace/env-status", { cache: "no-store" }),
|
|
1539
|
+
fetch("/api/workspace", { cache: "no-store" }),
|
|
1540
|
+
]);
|
|
1541
|
+
const status = statusRes.ok ? await statusRes.json() : {};
|
|
1542
|
+
const ws = wsRes.ok ? await wsRes.json() : {};
|
|
1543
|
+
setCreationSignals({
|
|
1544
|
+
configuredEnvRefs: Array.isArray(status.configuredEnvRefs) ? status.configuredEnvRefs : [],
|
|
1545
|
+
sourceRecords: ws.workspaceSourceRecords && typeof ws.workspaceSourceRecords === "object"
|
|
1546
|
+
? ws.workspaceSourceRecords
|
|
1547
|
+
: {},
|
|
1548
|
+
});
|
|
1549
|
+
} catch {
|
|
1550
|
+
/* signals stay at their last value — the cockpit degrades to pending, never fakes */
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Refresh the linked Data Source through the sidecar dispatcher
|
|
1555
|
+
// (refresh-sources, plural) keyed by the data-source OBJECT id, so the records
|
|
1556
|
+
// land in the source-records sidecar the cockpit reads — then reload signals
|
|
1557
|
+
// so the refresh step flips to complete from real state.
|
|
1558
|
+
async function refreshLinkedSource({ objectId }) {
|
|
1559
|
+
const sourceObjectId = String(objectId || "").trim();
|
|
1560
|
+
if (!sourceObjectId) {
|
|
1561
|
+
setDataSourceMessage("No linked Data Source object to refresh yet.");
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
setDataSourceMessage("");
|
|
1565
|
+
try {
|
|
1566
|
+
const res = await fetch("/api/workspace/refresh-sources", {
|
|
1567
|
+
method: "POST",
|
|
1568
|
+
headers: { "content-type": "application/json" },
|
|
1569
|
+
body: JSON.stringify({ sourceIds: [sourceObjectId] }),
|
|
1570
|
+
});
|
|
1571
|
+
const payload = await res.json();
|
|
1572
|
+
const result = Array.isArray(payload.refreshed) ? payload.refreshed.find((r) => r.sourceId === sourceObjectId) : null;
|
|
1573
|
+
if (res.ok && result) {
|
|
1574
|
+
const msg = `Refreshed — ${result.recordCount ?? 0} record(s) pulled into the sidecar.`;
|
|
1575
|
+
setDataSourceMessage("");
|
|
1576
|
+
pushReceipt({ kind: "source-refresh", ok: true, detail: msg });
|
|
1577
|
+
} else if (res.ok && Array.isArray(payload.skipped) && payload.skipped.includes(sourceObjectId)) {
|
|
1578
|
+
const detail = (payload.skippedDetail || []).find((d) => d.sourceId === sourceObjectId);
|
|
1579
|
+
const recovery = classifyCreationError({ phase: "refresh", reason: detail?.reason });
|
|
1580
|
+
setDataSourceMessage(`Refresh skipped: ${recovery.safeDetail}. ${recovery.requiredAction}`);
|
|
1581
|
+
pushReceipt({ kind: "source-refresh", ok: false, detail: `Skipped (${recovery.errorKind}). ${recovery.requiredAction}` });
|
|
1582
|
+
} else {
|
|
1583
|
+
const recovery = classifyCreationError({ phase: "refresh", httpStatus: res.status, detail: redactSecretsFromText(payload.error || "Refresh failed") });
|
|
1584
|
+
setDataSourceMessage(`${recovery.safeDetail} — ${recovery.requiredAction}`);
|
|
1585
|
+
pushReceipt({ kind: "source-refresh", ok: false, detail: `${recovery.safeDetail}. ${recovery.requiredAction}` });
|
|
1586
|
+
}
|
|
1587
|
+
await reloadCreationSignals();
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
setDataSourceMessage(redactSecretsFromText(err.message || "Refresh failed"));
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// The creation cockpit emits a single action descriptor per step; map it to
|
|
1594
|
+
// the drawer's existing governed handlers. No new mutation paths.
|
|
1595
|
+
async function handleCockpitAction(action) {
|
|
1596
|
+
if (!action || !action.id) return;
|
|
1597
|
+
const tag = `${action.stepId}:${action.id}`;
|
|
1598
|
+
setCockpitBusy(tag);
|
|
1599
|
+
try {
|
|
1600
|
+
switch (action.id) {
|
|
1601
|
+
case "edit":
|
|
1602
|
+
setEditMode(true);
|
|
1603
|
+
setCockpitCollapsed(true);
|
|
1604
|
+
break;
|
|
1605
|
+
case "open-settings":
|
|
1606
|
+
onClose();
|
|
1607
|
+
router.push(action.href || "/settings");
|
|
1608
|
+
break;
|
|
1609
|
+
case "open-resolver":
|
|
1610
|
+
// Hand off to the governed helper widget — the resolver proposal lane.
|
|
1611
|
+
// Carries the integrationId so the helper can scope a resolver proposal.
|
|
1612
|
+
onClose();
|
|
1613
|
+
router.push(`/data-model?helper=open&resolverFor=${encodeURIComponent(String(draft?.integrationId || "").trim())}`);
|
|
1614
|
+
break;
|
|
1615
|
+
case "test":
|
|
1616
|
+
await testApiRecord();
|
|
1617
|
+
await reloadCreationSignals();
|
|
1618
|
+
break;
|
|
1619
|
+
case "create-data-source":
|
|
1620
|
+
createDataSourceFromRegistry();
|
|
1621
|
+
break;
|
|
1622
|
+
case "open-data-source":
|
|
1623
|
+
openDataSourceRow(action.objectId);
|
|
1624
|
+
break;
|
|
1625
|
+
case "create-sandbox-tool":
|
|
1626
|
+
setSandboxToolFlow("draft");
|
|
1627
|
+
break;
|
|
1628
|
+
case "refresh-source":
|
|
1629
|
+
await refreshLinkedSource({ objectId: action.objectId });
|
|
1630
|
+
break;
|
|
1631
|
+
default:
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
} finally {
|
|
1635
|
+
setCockpitBusy("");
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const creationState = isApiRegistry
|
|
1640
|
+
? deriveApiRegistryCreationState({
|
|
1641
|
+
workspaceConfig,
|
|
1642
|
+
registryRow: draft,
|
|
1643
|
+
sourceRecords: creationSignals.sourceRecords,
|
|
1644
|
+
runtime: { configuredEnvRefs: creationSignals.configuredEnvRefs },
|
|
1645
|
+
})
|
|
1646
|
+
: null;
|
|
1647
|
+
// Shape analysis from the tested response — drives the resolver recommendation
|
|
1648
|
+
// and the field candidates the operator sees before creating a Data Source.
|
|
1649
|
+
const creationProfile = isApiRegistry && creationState?.tested
|
|
1650
|
+
? profileApiResponse(draft?.lastResponse)
|
|
1651
|
+
: null;
|
|
1652
|
+
const creationResolverRec = creationProfile ? recommendResolver(creationProfile) : null;
|
|
1653
|
+
// Preview the exact Data Source that "Create Data Source" will produce, before
|
|
1654
|
+
// any mutation — shown once tested and while no source is linked yet.
|
|
1655
|
+
const creationDataSourcePreview = isApiRegistry && creationState?.tested && !creationState.sourceExists
|
|
1656
|
+
? {
|
|
1657
|
+
row: buildDataSourceRowFromApiRegistry(workspaceConfig, draft, {
|
|
1658
|
+
entityType: creationProfile?.suggestedEntityType,
|
|
1659
|
+
}),
|
|
1660
|
+
fields: creationProfile?.fields || [],
|
|
1661
|
+
}
|
|
1662
|
+
: null;
|
|
1663
|
+
|
|
1344
1664
|
async function runSandboxToolByName({ objectId, name }) {
|
|
1345
1665
|
const rowName = String(name || "").trim();
|
|
1346
1666
|
const objectIdValue = String(objectId || "").trim();
|
|
@@ -1524,7 +1844,9 @@ function DataModelRecordDrawer({
|
|
|
1524
1844
|
<header className="dm-record-drawer-head">
|
|
1525
1845
|
<div>
|
|
1526
1846
|
<p>Record</p>
|
|
1527
|
-
<h2
|
|
1847
|
+
<h2 title={draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}>
|
|
1848
|
+
{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}
|
|
1849
|
+
</h2>
|
|
1528
1850
|
</div>
|
|
1529
1851
|
<div className="dm-record-drawer-actions">
|
|
1530
1852
|
{isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
|
|
@@ -1535,7 +1857,7 @@ function DataModelRecordDrawer({
|
|
|
1535
1857
|
onClick={runSandbox}
|
|
1536
1858
|
>
|
|
1537
1859
|
<Play size={13} aria-hidden />
|
|
1538
|
-
{sandboxRunning ? "Running…" : "
|
|
1860
|
+
{sandboxRunning ? "Running…" : "Execute"}
|
|
1539
1861
|
</button>
|
|
1540
1862
|
)}
|
|
1541
1863
|
{!isSandbox && sandboxToolFlow !== "draft" && (
|
|
@@ -1557,6 +1879,7 @@ function DataModelRecordDrawer({
|
|
|
1557
1879
|
onTest={testApiRecord}
|
|
1558
1880
|
/>
|
|
1559
1881
|
)}
|
|
1882
|
+
<div className="dm-record-scroll" ref={drawerScrollRef}>
|
|
1560
1883
|
{isApiRegistry && sandboxToolFlow === "created" && createdSandboxMeta && (
|
|
1561
1884
|
<section className="dm-api-action-card dm-api-action-card-success" aria-label="Sandbox tool created">
|
|
1562
1885
|
<div className="dm-api-action-card-body">
|
|
@@ -1585,6 +1908,24 @@ function DataModelRecordDrawer({
|
|
|
1585
1908
|
</div>
|
|
1586
1909
|
</section>
|
|
1587
1910
|
)}
|
|
1911
|
+
{isApiRegistry && sandboxToolFlow !== "draft" && sandboxToolFlow !== "confirm" && creationState && (
|
|
1912
|
+
<>
|
|
1913
|
+
<ApiRegistryCreationCockpit
|
|
1914
|
+
state={creationState}
|
|
1915
|
+
onAction={handleCockpitAction}
|
|
1916
|
+
busyAction={cockpitBusy}
|
|
1917
|
+
disabled={saving || creatingDataSource || testing}
|
|
1918
|
+
profile={creationProfile}
|
|
1919
|
+
resolverRec={creationResolverRec}
|
|
1920
|
+
receipts={creationReceipts}
|
|
1921
|
+
dataSourcePreview={creationDataSourcePreview}
|
|
1922
|
+
defaultCollapsed={cockpitCollapsed}
|
|
1923
|
+
hideWhenComplete
|
|
1924
|
+
onCollapsedChange={setCockpitCollapsed}
|
|
1925
|
+
/>
|
|
1926
|
+
{dataSourceMessage ? <p className="dm-sandbox-tool-test-msg">{dataSourceMessage}</p> : null}
|
|
1927
|
+
</>
|
|
1928
|
+
)}
|
|
1588
1929
|
{isApiRegistry && sandboxToolFlow === "draft" && (
|
|
1589
1930
|
<SandboxToolDraftPanel
|
|
1590
1931
|
registryRow={draft}
|
|
@@ -1667,7 +2008,11 @@ function DataModelRecordDrawer({
|
|
|
1667
2008
|
rowIndex={rowIndex}
|
|
1668
2009
|
/>
|
|
1669
2010
|
) : groupRecordColumns(table.columns || []).map((section) => (
|
|
1670
|
-
<DrawerSection
|
|
2011
|
+
<DrawerSection
|
|
2012
|
+
key={section.title}
|
|
2013
|
+
title={section.title}
|
|
2014
|
+
forceOpen={isApiRegistry && editMode}
|
|
2015
|
+
>
|
|
1671
2016
|
{section.columns.map((column) => (
|
|
1672
2017
|
<RecordFieldEditor
|
|
1673
2018
|
key={column}
|
|
@@ -1685,7 +2030,7 @@ function DataModelRecordDrawer({
|
|
|
1685
2030
|
</DrawerSection>
|
|
1686
2031
|
))}
|
|
1687
2032
|
{!isSandbox && editMode && (
|
|
1688
|
-
<DrawerSection title="Fields"
|
|
2033
|
+
<DrawerSection title="Fields">
|
|
1689
2034
|
<div className="dm-drawer-field-editor">
|
|
1690
2035
|
{pendingColumns.map((column, index) => (
|
|
1691
2036
|
<div key={`${column}-${index}`} className="dm-drawer-field-row">
|
|
@@ -1714,6 +2059,7 @@ function DataModelRecordDrawer({
|
|
|
1714
2059
|
</DrawerSection>
|
|
1715
2060
|
)}
|
|
1716
2061
|
</div>
|
|
2062
|
+
</div>
|
|
1717
2063
|
{!isSandbox && editMode && (
|
|
1718
2064
|
<footer className="dm-record-drawer-foot">
|
|
1719
2065
|
<button type="button" className="dm-btn-outline" onClick={cancelEdits}>Cancel</button>
|
|
@@ -1763,7 +2109,9 @@ function DataModelTableSurface({
|
|
|
1763
2109
|
onSave,
|
|
1764
2110
|
onOpenThread,
|
|
1765
2111
|
focusSandboxRowName,
|
|
2112
|
+
focusRecordValue,
|
|
1766
2113
|
onFocusSandboxRowConsumed,
|
|
2114
|
+
onFocusRecordConsumed,
|
|
1767
2115
|
onFocusSandboxRow,
|
|
1768
2116
|
selectedRecordIndex,
|
|
1769
2117
|
onSelectedRecordIndexChange,
|
|
@@ -1861,6 +2209,24 @@ function DataModelTableSurface({
|
|
|
1861
2209
|
onFocusSandboxRowConsumed?.();
|
|
1862
2210
|
}, [focusSandboxRowName, table.id, table.objectType, table.rows, rowEntries, pageSize, onFocusSandboxRowConsumed]);
|
|
1863
2211
|
|
|
2212
|
+
useEffect(() => {
|
|
2213
|
+
if (!focusRecordValue) return;
|
|
2214
|
+
const wanted = String(focusRecordValue).trim();
|
|
2215
|
+
if (!wanted) return;
|
|
2216
|
+
const originalIndex = (table.rows || []).findIndex((r) => (
|
|
2217
|
+
String(r?.integrationId || "").trim() === wanted
|
|
2218
|
+
|| String(r?.Name || r?.name || r?.slug || r?.id || "").trim() === wanted
|
|
2219
|
+
));
|
|
2220
|
+
if (originalIndex < 0) return;
|
|
2221
|
+
const visibleIndex = rowEntries.findIndex((entry) => entry.originalIndex === originalIndex);
|
|
2222
|
+
if (visibleIndex < 0) return;
|
|
2223
|
+
const pageForRow = Math.floor(visibleIndex / pageSize);
|
|
2224
|
+
setPageIndex(pageForRow);
|
|
2225
|
+
setSelectedRow(visibleIndex);
|
|
2226
|
+
selectOriginalIndex(originalIndex);
|
|
2227
|
+
onFocusRecordConsumed?.();
|
|
2228
|
+
}, [focusRecordValue, table.id, table.rows, rowEntries, pageSize, onFocusRecordConsumed]);
|
|
2229
|
+
|
|
1864
2230
|
useEffect(() => {
|
|
1865
2231
|
setPageIndex((current) => Math.min(current, pageCount - 1));
|
|
1866
2232
|
}, [pageCount]);
|
|
@@ -2094,9 +2460,9 @@ function DataModelTableSurface({
|
|
|
2094
2460
|
const a = document.createElement("a");
|
|
2095
2461
|
a.href = url; a.download = `${table.source.replace(/\s+/g, "-").toLowerCase()}.csv`;
|
|
2096
2462
|
a.click(); URL.revokeObjectURL(url);
|
|
2097
|
-
}}
|
|
2463
|
+
}}><Download size={13} />Export CSV</button>
|
|
2098
2464
|
)}
|
|
2099
|
-
{table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}
|
|
2465
|
+
{table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}><Upload size={13} />Import CSV</button>}
|
|
2100
2466
|
{table.mutable && (
|
|
2101
2467
|
<button type="button" className="dm-btn-primary-sm" disabled={saving} onClick={() => onSave((config) => addTableRow(config, table))}>
|
|
2102
2468
|
<Plus size={13} />Add record
|
|
@@ -2648,9 +3014,11 @@ export default function DataModelShell() {
|
|
|
2648
3014
|
const [helperInitialThread, setHelperInitialThread] = useState(null);
|
|
2649
3015
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
2650
3016
|
const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
|
|
3017
|
+
const [focusRecordValue, setFocusRecordValue] = useState(null);
|
|
2651
3018
|
const [selectedRecordByTable, setSelectedRecordByTable] = useState({});
|
|
2652
3019
|
const pendingPatchRef = useRef({});
|
|
2653
3020
|
const saveTimerRef = useRef(null);
|
|
3021
|
+
const consumedHelperRouteRef = useRef("");
|
|
2654
3022
|
|
|
2655
3023
|
// Cross-page rail entrypoints. Settings / integrations pages render
|
|
2656
3024
|
// <WorkspaceRail> without an in-process helper handler — clicking the
|
|
@@ -2663,7 +3031,11 @@ export default function DataModelShell() {
|
|
|
2663
3031
|
if (!workspaceConfig) return;
|
|
2664
3032
|
const helperParam = searchParams?.get("helper");
|
|
2665
3033
|
const threadParam = searchParams?.get("thread");
|
|
2666
|
-
|
|
3034
|
+
const resolverForParam = searchParams?.get("resolverFor");
|
|
3035
|
+
if (!helperParam && !threadParam && !resolverForParam) return;
|
|
3036
|
+
const routeKey = `${helperParam || ""}:${threadParam || ""}:${resolverForParam || ""}`;
|
|
3037
|
+
if (routeKey === consumedHelperRouteRef.current) return;
|
|
3038
|
+
consumedHelperRouteRef.current = routeKey;
|
|
2667
3039
|
if (threadParam) {
|
|
2668
3040
|
const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
|
|
2669
3041
|
const row = (ht?.rows || []).find((r) => r?.id === threadParam);
|
|
@@ -2671,6 +3043,12 @@ export default function DataModelShell() {
|
|
|
2671
3043
|
setHelperInitialThread(row);
|
|
2672
3044
|
setHelperOpen(true);
|
|
2673
3045
|
}
|
|
3046
|
+
} else if (resolverForParam) {
|
|
3047
|
+
setHelperIntent("register_api");
|
|
3048
|
+
setHelperInitialThread(null);
|
|
3049
|
+
setHelperInitialPrompt(`Create the response resolver for API Registry integration "${resolverForParam}". Use the tested lastResponse from that registry row, extract the records from the response into governed Data Source rows, and keep the resolver scoped to this integrationId.`);
|
|
3050
|
+
setHelperOpen(true);
|
|
3051
|
+
return;
|
|
2674
3052
|
} else if (helperParam === "open") {
|
|
2675
3053
|
setHelperInitialThread(null);
|
|
2676
3054
|
setHelperOpen(true);
|
|
@@ -2678,6 +3056,7 @@ export default function DataModelShell() {
|
|
|
2678
3056
|
const next = new URLSearchParams(searchParams.toString());
|
|
2679
3057
|
next.delete("helper");
|
|
2680
3058
|
next.delete("thread");
|
|
3059
|
+
next.delete("resolverFor");
|
|
2681
3060
|
const query = next.toString();
|
|
2682
3061
|
router.replace(query ? `/data-model?${query}` : "/data-model", { scroll: false });
|
|
2683
3062
|
}, [workspaceConfig, searchParams, router]);
|
|
@@ -2778,7 +3157,20 @@ export default function DataModelShell() {
|
|
|
2778
3157
|
useEffect(() => {
|
|
2779
3158
|
const rowParam = searchParams?.get("row");
|
|
2780
3159
|
if (!rowParam || !tables.length) return;
|
|
2781
|
-
|
|
3160
|
+
const objectParam = searchParams?.get("object");
|
|
3161
|
+
const target = objectParam
|
|
3162
|
+
? tables.find((table) => (
|
|
3163
|
+
table.objectId === objectParam
|
|
3164
|
+
|| table.id === objectParam
|
|
3165
|
+
|| table.source === objectParam
|
|
3166
|
+
|| table.label === objectParam
|
|
3167
|
+
))
|
|
3168
|
+
: null;
|
|
3169
|
+
if (target?.objectType === "sandbox-environment" || (!target && rowParam)) {
|
|
3170
|
+
focusSandboxEnvironmentRow({ rowName: rowParam, deferOpen: true });
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
requestAnimationFrame(() => setFocusRecordValue(rowParam));
|
|
2782
3174
|
}, [focusSandboxEnvironmentRow, searchParams, tables]);
|
|
2783
3175
|
|
|
2784
3176
|
// Flush any accumulated patch keys to the server. Called by the debounce
|
|
@@ -3155,6 +3547,17 @@ export default function DataModelShell() {
|
|
|
3155
3547
|
router.push(`/?dashboard=${encodeURIComponent(target.dashboardId)}`);
|
|
3156
3548
|
}
|
|
3157
3549
|
}}
|
|
3550
|
+
onOpenSwarmWorkflow={(target) => {
|
|
3551
|
+
const objectId = String(target?.objectId || "").trim();
|
|
3552
|
+
const rowName = String(target?.name || "").trim();
|
|
3553
|
+
if (!objectId || !rowName) return;
|
|
3554
|
+
const params = new URLSearchParams({
|
|
3555
|
+
object: objectId,
|
|
3556
|
+
row: rowName,
|
|
3557
|
+
field: "orchestrationGraph"
|
|
3558
|
+
});
|
|
3559
|
+
router.push(`/workflows?${params.toString()}`);
|
|
3560
|
+
}}
|
|
3158
3561
|
onApplied={(updatedConfig) => {
|
|
3159
3562
|
// Anchor the user on the most recently created/updated Data Model
|
|
3160
3563
|
// object so a helper-driven object.create lands on the surface
|
|
@@ -3205,7 +3608,9 @@ export default function DataModelShell() {
|
|
|
3205
3608
|
onSave={save}
|
|
3206
3609
|
onOpenThread={openHelperThreadFromRow}
|
|
3207
3610
|
focusSandboxRowName={focusSandboxRowName}
|
|
3611
|
+
focusRecordValue={focusRecordValue}
|
|
3208
3612
|
onFocusSandboxRowConsumed={() => setFocusSandboxRowName(null)}
|
|
3613
|
+
onFocusRecordConsumed={() => setFocusRecordValue(null)}
|
|
3209
3614
|
onFocusSandboxRow={focusSandboxEnvironmentRow}
|
|
3210
3615
|
selectedRecordIndex={selectedTableKey ? selectedRecordByTable[selectedTableKey] ?? null : null}
|
|
3211
3616
|
onSelectedRecordIndexChange={(index) => {
|