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