@growthub/cli 0.13.8 → 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/codex-sites/route.js +13 -0
- 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 +501 -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 +215 -13
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/codex-sites-data-model-card.jsx +81 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +31 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/settings-accordion-section.jsx +50 -0
- 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/app/workspace-builder.jsx +137 -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/codex-sites-local-state.js +139 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-workspace-adapter.js +156 -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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { listLocalCodexSites } from "@/lib/codex-sites-local-state";
|
|
3
|
+
|
|
4
|
+
async function GET() {
|
|
5
|
+
const sites = await listLocalCodexSites();
|
|
6
|
+
return NextResponse.json({
|
|
7
|
+
ok: true,
|
|
8
|
+
source: "codex-local-session-state",
|
|
9
|
+
sites
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { GET };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/env-status
|
|
3
|
+
*
|
|
4
|
+
* The honest, secret-safe auth-readiness signal the creation cockpit needs.
|
|
5
|
+
* Returns the referenced auth/env ref SLUGS that currently resolve to a value
|
|
6
|
+
* in the server runtime (process.env) — so the api-registry drawer can mark
|
|
7
|
+
* "auth configured" from real runtime truth instead of guessing. Never returns,
|
|
8
|
+
* logs, or hashes a value.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NextResponse } from "next/server";
|
|
12
|
+
import { readWorkspaceConfig } from "@/lib/workspace-config";
|
|
13
|
+
import { computeConfiguredEnvRefs, listPersistenceAdapterReadiness } from "@/lib/env-status";
|
|
14
|
+
|
|
15
|
+
async function GET() {
|
|
16
|
+
let workspaceConfig = {};
|
|
17
|
+
try {
|
|
18
|
+
workspaceConfig = await readWorkspaceConfig();
|
|
19
|
+
} catch {
|
|
20
|
+
workspaceConfig = {};
|
|
21
|
+
}
|
|
22
|
+
const configuredEnvRefs = computeConfiguredEnvRefs(workspaceConfig, process.env);
|
|
23
|
+
const persistenceAdapters = listPersistenceAdapterReadiness(process.env);
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
kind: "growthub-env-status-v1",
|
|
26
|
+
configuredEnvRefs,
|
|
27
|
+
persistenceAdapters,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { GET };
|
|
@@ -41,9 +41,98 @@ import {
|
|
|
41
41
|
buildApplyReceipt,
|
|
42
42
|
upsertHelperThreadRow,
|
|
43
43
|
} from "@/lib/workspace-helper-apply";
|
|
44
|
+
import { RESOLVER_PROPOSAL_TYPE, buildResolverProposal, validateResolverProposal } from "@/lib/workspace-resolver-proposal";
|
|
45
|
+
import { writeResolverProposalFile } from "@/lib/server-resolver-write";
|
|
44
46
|
|
|
45
47
|
const HELPER_APPLY_SOURCE_KEY = "helper:apply:receipts";
|
|
46
48
|
|
|
49
|
+
function findRegistryRow(config, integrationId) {
|
|
50
|
+
const id = String(integrationId || "").trim();
|
|
51
|
+
if (!id) return null;
|
|
52
|
+
const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
|
|
53
|
+
for (const object of objects) {
|
|
54
|
+
if (object?.objectType !== "api-registry") continue;
|
|
55
|
+
const row = (Array.isArray(object.rows) ? object.rows : []).find((candidate) =>
|
|
56
|
+
String(candidate?.integrationId || "").trim() === id
|
|
57
|
+
);
|
|
58
|
+
if (row) return row;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findDataSourceForRegistry(config, integrationId) {
|
|
64
|
+
const id = String(integrationId || "").trim();
|
|
65
|
+
if (!id) return null;
|
|
66
|
+
const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
|
|
67
|
+
for (const object of objects) {
|
|
68
|
+
if (object?.objectType !== "data-source") continue;
|
|
69
|
+
const row = (Array.isArray(object.rows) ? object.rows : []).find((candidate) =>
|
|
70
|
+
String(candidate?.registryId || "").trim() === id
|
|
71
|
+
);
|
|
72
|
+
if (row) return { object, row };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inferRootPathFromLastResponse(lastResponse) {
|
|
78
|
+
if (!lastResponse) return "";
|
|
79
|
+
let parsed = lastResponse;
|
|
80
|
+
if (typeof lastResponse === "string") {
|
|
81
|
+
try { parsed = JSON.parse(lastResponse); } catch { return ""; }
|
|
82
|
+
}
|
|
83
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
|
|
84
|
+
for (const key of ["records", "items", "data", "results", "capabilities"]) {
|
|
85
|
+
if (Array.isArray(parsed[key])) return key;
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeResolverProposal(proposal, config) {
|
|
91
|
+
if (proposal?.type !== RESOLVER_PROPOSAL_TYPE || String(proposal?.code || "").trim()) return proposal;
|
|
92
|
+
const integrationId = proposal?.payload?.integrationId;
|
|
93
|
+
const registryRow = findRegistryRow(config, integrationId);
|
|
94
|
+
if (!registryRow) return proposal;
|
|
95
|
+
const generated = buildResolverProposal({
|
|
96
|
+
integrationId: registryRow.integrationId,
|
|
97
|
+
baseUrl: registryRow.baseUrl,
|
|
98
|
+
endpoint: registryRow.endpoint,
|
|
99
|
+
method: registryRow.method,
|
|
100
|
+
authRef: registryRow.authRef,
|
|
101
|
+
rootPath: proposal?.payload?.rootPath || inferRootPathFromLastResponse(registryRow.lastResponse),
|
|
102
|
+
entityType: proposal?.payload?.entityType || registryRow.entityTypes || "records",
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
...generated,
|
|
106
|
+
rationale: proposal.rationale || generated.rationale,
|
|
107
|
+
confidence: proposal.confidence || generated.confidence,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeDataModelObjectProposal(proposal, config, fallbackIntegrationId = "") {
|
|
112
|
+
if (proposal?.type !== "dataModel.object.update" || proposal?.payload?.id) return proposal;
|
|
113
|
+
const integrationId = proposal?.payload?.registryId || proposal?.payload?.integrationId || fallbackIntegrationId;
|
|
114
|
+
const target = findDataSourceForRegistry(config, integrationId);
|
|
115
|
+
if (!target?.object) return proposal;
|
|
116
|
+
return {
|
|
117
|
+
...proposal,
|
|
118
|
+
payload: {
|
|
119
|
+
...proposal.payload,
|
|
120
|
+
id: target.object.id,
|
|
121
|
+
sourceId: target.object.sourceId || target.row?.sourceId || target.object.binding?.sourceId || "",
|
|
122
|
+
binding: {
|
|
123
|
+
...(target.object.binding || {}),
|
|
124
|
+
sourceStorage: target.object.binding?.sourceStorage || target.row?.sourceStorage || "workspace-source-records",
|
|
125
|
+
sourceId: target.object.binding?.sourceId || target.row?.sourceId || target.object.sourceId || "",
|
|
126
|
+
registryId: target.object.binding?.registryId || integrationId,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeApplyProposal(proposal, config, context = {}) {
|
|
133
|
+
return normalizeDataModelObjectProposal(normalizeResolverProposal(proposal, config), config, context.integrationId);
|
|
134
|
+
}
|
|
135
|
+
|
|
47
136
|
async function POST(request) {
|
|
48
137
|
let body;
|
|
49
138
|
try {
|
|
@@ -78,7 +167,41 @@ async function POST(request) {
|
|
|
78
167
|
const skipped = [];
|
|
79
168
|
let workingConfig = currentConfig;
|
|
80
169
|
|
|
81
|
-
|
|
170
|
+
// Resolver-file lane (AWaC: server file, NOT a config PATCH field). Handled
|
|
171
|
+
// separately so it never touches writeWorkspaceConfig and never widens the
|
|
172
|
+
// PATCH allowlist. Gated by filesystem/read-only; emits a receipt either way.
|
|
173
|
+
const fallbackIntegrationId = body.proposals
|
|
174
|
+
.map((proposal) => proposal?.payload?.integrationId || proposal?.payload?.registryId)
|
|
175
|
+
.find((value) => String(value || "").trim());
|
|
176
|
+
const normalizedProposals = body.proposals.map((proposal) =>
|
|
177
|
+
normalizeApplyProposal(proposal, currentConfig, { integrationId: fallbackIntegrationId })
|
|
178
|
+
);
|
|
179
|
+
const resolverProposals = normalizedProposals.filter((p) => p?.type === RESOLVER_PROPOSAL_TYPE);
|
|
180
|
+
const configProposals = normalizedProposals.filter((p) => p?.type !== RESOLVER_PROPOSAL_TYPE);
|
|
181
|
+
|
|
182
|
+
for (const proposal of resolverProposals) {
|
|
183
|
+
const validation = validateResolverProposal(proposal);
|
|
184
|
+
if (!validation.ok) {
|
|
185
|
+
skipped.push({ proposal, reason: validation.error || "invalid resolver proposal" });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const result = await writeResolverProposalFile(proposal);
|
|
190
|
+
applied.push({
|
|
191
|
+
...buildApplyReceipt(proposal, appliedAt, reviewedBy, sessionId),
|
|
192
|
+
resolverPath: result.path,
|
|
193
|
+
resolverFilename: result.filename,
|
|
194
|
+
});
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err?.code === "WORKSPACE_PERSISTENCE_READ_ONLY") {
|
|
197
|
+
skipped.push({ proposal, reason: `read-only runtime — ${err.guidance || "resolver not written"}` });
|
|
198
|
+
} else {
|
|
199
|
+
skipped.push({ proposal, reason: err?.message || "resolver write failed" });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const proposal of configProposals) {
|
|
82
205
|
if (
|
|
83
206
|
!proposal ||
|
|
84
207
|
typeof proposal.type !== "string" ||
|
|
@@ -113,7 +236,9 @@ async function POST(request) {
|
|
|
113
236
|
// Patch — collect every affected field from accepted proposals AND
|
|
114
237
|
// append the thread row update (so the user-visible Helper Threads object
|
|
115
238
|
// refreshes in the same atomic write as the proposed mutations).
|
|
116
|
-
|
|
239
|
+
// resolver.create writes a server file (affectedField "server-file"), so it
|
|
240
|
+
// must NOT contribute a field to the config PATCH — exclude it here.
|
|
241
|
+
const mutatingApplied = applied.filter((r) => r.type !== "explain.object" && r.affectedField !== "server-file");
|
|
117
242
|
|
|
118
243
|
// Upsert the thread row so audit history reflects this apply turn even
|
|
119
244
|
// when nothing mutated (all skipped / explain-only) and even when the
|
|
@@ -129,7 +254,7 @@ async function POST(request) {
|
|
|
129
254
|
try {
|
|
130
255
|
const existingRows = (workingConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads")?.rows || [];
|
|
131
256
|
const existingRow = existingRows.find((r) => r?.id === threadId) || {};
|
|
132
|
-
const firstProposal =
|
|
257
|
+
const firstProposal = normalizedProposals?.[0];
|
|
133
258
|
const seedTitle = existingRow.title
|
|
134
259
|
|| (firstProposal?.rationale ? String(firstProposal.rationale).slice(0, 72) : "Helper thread");
|
|
135
260
|
|
|
@@ -192,7 +317,7 @@ async function POST(request) {
|
|
|
192
317
|
intent: safeIntent,
|
|
193
318
|
prompt: existingRow.prompt || "",
|
|
194
319
|
summary: existingRow.summary || "",
|
|
195
|
-
proposals: existingRow.proposals ||
|
|
320
|
+
proposals: existingRow.proposals || normalizedProposals || [],
|
|
196
321
|
warnings: existingRow.warnings || [],
|
|
197
322
|
receipts: existingRow.receipts || null,
|
|
198
323
|
model: existingRow.model || "external-apply",
|
|
@@ -207,7 +332,7 @@ async function POST(request) {
|
|
|
207
332
|
affectedField: a.affectedField,
|
|
208
333
|
rationale: a.rationale,
|
|
209
334
|
confidence: a.confidence,
|
|
210
|
-
payload:
|
|
335
|
+
payload: normalizedProposals?.[idx]?.payload ?? null,
|
|
211
336
|
})),
|
|
212
337
|
lastSkipped: skipped.map((s) => ({
|
|
213
338
|
type: s.proposal?.type,
|
|
@@ -51,6 +51,7 @@ export function WorkspaceActivationPanel({
|
|
|
51
51
|
workspaceSourceRecords,
|
|
52
52
|
metadataGraph,
|
|
53
53
|
onOpenHelper,
|
|
54
|
+
onStepAction,
|
|
54
55
|
compact = false,
|
|
55
56
|
showLenses = false,
|
|
56
57
|
}) {
|
|
@@ -136,7 +137,22 @@ export function WorkspaceActivationPanel({
|
|
|
136
137
|
<span>{step.hint}</span>
|
|
137
138
|
</p>
|
|
138
139
|
) : null}
|
|
139
|
-
{step.
|
|
140
|
+
{step.action && onStepAction ? (
|
|
141
|
+
<a
|
|
142
|
+
href={`#${step.action}`}
|
|
143
|
+
className={
|
|
144
|
+
"workspace-activation-step-cta"
|
|
145
|
+
+ (isNext ? " is-primary" : "")
|
|
146
|
+
}
|
|
147
|
+
onClick={(event) => {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
onStepAction(step);
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
<span>{step.cta || (step.status === "complete" ? "Review" : "Open")}</span>
|
|
153
|
+
<ArrowRight size={12} aria-hidden="true" />
|
|
154
|
+
</a>
|
|
155
|
+
) : step.href ? (
|
|
140
156
|
<Link
|
|
141
157
|
href={step.href}
|
|
142
158
|
className={
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import { X } from "lucide-react";
|
|
5
6
|
|
|
6
7
|
const HELPER_SANDBOX_OBJECT_ID = "workspace-helper-sandbox";
|
|
@@ -185,7 +186,9 @@ function WorkspaceHelperSetupModal({ workspaceConfig, open, onClose, onSaved })
|
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
return
|
|
189
|
+
if (typeof document === "undefined") return null;
|
|
190
|
+
|
|
191
|
+
return createPortal((
|
|
189
192
|
<div className="workspace-helper-setup-modal-backdrop" role="presentation">
|
|
190
193
|
<div className="workspace-helper-setup-modal" role="dialog" aria-modal="true" aria-label="Set up workspace helper">
|
|
191
194
|
<button type="button" className="workspace-helper-setup-modal-close" onClick={onClose} aria-label="Close setup">
|
|
@@ -342,7 +345,7 @@ function WorkspaceHelperSetupModal({ workspaceConfig, open, onClose, onSaved })
|
|
|
342
345
|
</div>
|
|
343
346
|
</div>
|
|
344
347
|
</div>
|
|
345
|
-
);
|
|
348
|
+
), document.body);
|
|
346
349
|
}
|
|
347
350
|
|
|
348
351
|
export {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ApiRegistryCreationCockpit — the governed creation interface for one API,
|
|
7
|
+
* rendered inside the existing api-registry record drawer (DataModelShell).
|
|
8
|
+
*
|
|
9
|
+
* Renders the ordered journey from `deriveApiRegistryCreationState`
|
|
10
|
+
* (lib/api-registry-creation-flow.js) plus, once the API is tested, the response
|
|
11
|
+
* Shape analysis (`profileApiResponse` / `recommendResolver`) and a receipts log
|
|
12
|
+
* of real actions. Every actionable step's button calls back into the drawer's
|
|
13
|
+
* existing governed handlers via `onAction(action)` — status, the highlighted
|
|
14
|
+
* "next" step, the activation score, and which buttons are live all come from
|
|
15
|
+
* derivation/real responses, never guessed.
|
|
16
|
+
*
|
|
17
|
+
* Visual language is the workspace's own: the `.dm-db-status` chip and `dm-btn-*`
|
|
18
|
+
* buttons. No invented colors, no new primitive.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const STEP_STATUS = {
|
|
22
|
+
complete: { mod: "ok", label: "Done" },
|
|
23
|
+
active: { mod: "warn", label: "Next" },
|
|
24
|
+
pending: { mod: "", label: "Pending" },
|
|
25
|
+
blocked: { mod: "", label: "Blocked" },
|
|
26
|
+
optional: { mod: "", label: "Optional" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const RESOLVER_LEVEL = {
|
|
30
|
+
optional: { mod: "", label: "Resolver optional" },
|
|
31
|
+
recommended: { mod: "warn", label: "Resolver recommended" },
|
|
32
|
+
required: { mod: "bad", label: "Resolver required" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function StatusChip({ mod, children, className = "" }) {
|
|
36
|
+
return (
|
|
37
|
+
<span className={`dm-db-status${mod ? ` ${mod}` : ""}${className ? ` ${className}` : ""}`}>
|
|
38
|
+
<span />
|
|
39
|
+
{children}
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ApiRegistryCreationCockpit({
|
|
45
|
+
state,
|
|
46
|
+
onAction,
|
|
47
|
+
busyAction = "",
|
|
48
|
+
disabled = false,
|
|
49
|
+
profile = null,
|
|
50
|
+
resolverRec = null,
|
|
51
|
+
receipts = [],
|
|
52
|
+
dataSourcePreview = null,
|
|
53
|
+
eyebrow = "Governed creation",
|
|
54
|
+
defaultCollapsed = false,
|
|
55
|
+
hideWhenComplete = false,
|
|
56
|
+
onCollapsedChange,
|
|
57
|
+
}) {
|
|
58
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setCollapsed(defaultCollapsed || Boolean(hideWhenComplete && state?.complete));
|
|
61
|
+
}, [defaultCollapsed, hideWhenComplete, state?.complete, state?.integrationId]);
|
|
62
|
+
if (!state || !Array.isArray(state.steps)) return null;
|
|
63
|
+
if (hideWhenComplete && state.complete) return null;
|
|
64
|
+
const candidates = profile?.candidates || {};
|
|
65
|
+
const candidateEntries = Object.entries(candidates).filter(([, v]) => v);
|
|
66
|
+
const previewRow = dataSourcePreview?.row || null;
|
|
67
|
+
const toggleCollapsed = () => {
|
|
68
|
+
const next = !collapsed;
|
|
69
|
+
setCollapsed(next);
|
|
70
|
+
onCollapsedChange?.(next);
|
|
71
|
+
};
|
|
72
|
+
const runAction = (action) => {
|
|
73
|
+
if (action?.id === "edit") {
|
|
74
|
+
setCollapsed(true);
|
|
75
|
+
onCollapsedChange?.(true);
|
|
76
|
+
}
|
|
77
|
+
onAction?.(action);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<section className={`dm-api-action-card dm-cockpit${collapsed ? " is-collapsed" : ""}`} aria-label="API creation journey">
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
className="dm-cockpit-head"
|
|
85
|
+
aria-expanded={!collapsed}
|
|
86
|
+
onClick={toggleCollapsed}
|
|
87
|
+
>
|
|
88
|
+
<div className="dm-api-action-card-body">
|
|
89
|
+
<p className="dm-api-action-card-eyebrow">{eyebrow} · {state.score}% activated</p>
|
|
90
|
+
<h3>{state.headline}</h3>
|
|
91
|
+
</div>
|
|
92
|
+
<span className="dm-cockpit-count">{state.completedCount}/{state.totalCount}</span>
|
|
93
|
+
</button>
|
|
94
|
+
|
|
95
|
+
{!collapsed && <ol className="dm-cockpit-steps">
|
|
96
|
+
{state.steps.map((step) => {
|
|
97
|
+
const meta = STEP_STATUS[step.status] || STEP_STATUS.pending;
|
|
98
|
+
const isNext = step.id === state.nextStepId;
|
|
99
|
+
const action = step.action;
|
|
100
|
+
const isBusy = action && busyAction === `${step.id}:${action.id}`;
|
|
101
|
+
return (
|
|
102
|
+
<li
|
|
103
|
+
key={step.id}
|
|
104
|
+
className={`dm-cockpit-step${isNext ? " dm-cockpit-step-next" : ""}${step.status === "blocked" ? " dm-cockpit-step-muted" : ""}`}
|
|
105
|
+
>
|
|
106
|
+
<StatusChip mod={meta.mod} className="dm-cockpit-step-chip">{meta.label}</StatusChip>
|
|
107
|
+
<div className="dm-cockpit-step-body">
|
|
108
|
+
<p className="dm-cockpit-step-label">{step.label}</p>
|
|
109
|
+
<p className="dm-cockpit-step-desc">{step.description}</p>
|
|
110
|
+
{step.hint ? <p className="dm-cockpit-step-hint">{step.hint}</p> : null}
|
|
111
|
+
</div>
|
|
112
|
+
{action ? (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
className={isNext ? "dm-btn-primary-sm" : "dm-btn-outline"}
|
|
116
|
+
disabled={disabled || Boolean(busyAction)}
|
|
117
|
+
onClick={() => runAction(action)}
|
|
118
|
+
>
|
|
119
|
+
{isBusy ? "Working…" : action.label}
|
|
120
|
+
</button>
|
|
121
|
+
) : null}
|
|
122
|
+
</li>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</ol>}
|
|
126
|
+
|
|
127
|
+
{!collapsed && profile && profile.parsed ? (
|
|
128
|
+
<div className="dm-cockpit-shape">
|
|
129
|
+
<div className="dm-cockpit-shape-head">
|
|
130
|
+
<p className="dm-api-action-card-eyebrow">Response shape</p>
|
|
131
|
+
{resolverRec ? (
|
|
132
|
+
<StatusChip mod={(RESOLVER_LEVEL[resolverRec.level] || RESOLVER_LEVEL.optional).mod}>
|
|
133
|
+
{(RESOLVER_LEVEL[resolverRec.level] || RESOLVER_LEVEL.optional).label}
|
|
134
|
+
</StatusChip>
|
|
135
|
+
) : null}
|
|
136
|
+
</div>
|
|
137
|
+
<p className="dm-cockpit-step-desc">
|
|
138
|
+
{profile.usable
|
|
139
|
+
? `${profile.recordCount} record${profile.recordCount === 1 ? "" : "s"}${profile.arrayPath ? ` at "${profile.arrayPath}"` : " (top-level)"} · entity "${profile.suggestedEntityType}".`
|
|
140
|
+
: "No record array detected in the response."}
|
|
141
|
+
</p>
|
|
142
|
+
{resolverRec ? <p className="dm-cockpit-step-hint">{resolverRec.reason}</p> : null}
|
|
143
|
+
{candidateEntries.length ? (
|
|
144
|
+
<div className="dm-cockpit-fields">
|
|
145
|
+
{candidateEntries.map(([role, name]) => (
|
|
146
|
+
<span key={role} className="dm-cockpit-field"><b>{role}</b>{name}</span>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
) : null}
|
|
150
|
+
{profile.hasPagination ? (
|
|
151
|
+
<p className="dm-cockpit-step-hint">Pagination keys present — a resolver is needed to fetch every page.</p>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
) : null}
|
|
155
|
+
|
|
156
|
+
{!collapsed && previewRow ? (
|
|
157
|
+
<div className="dm-cockpit-shape">
|
|
158
|
+
<p className="dm-api-action-card-eyebrow">Data Source preview</p>
|
|
159
|
+
<p className="dm-cockpit-step-desc">
|
|
160
|
+
Create will add a live-backed Data Source object that references this API by <code>registryId</code> and writes records to the source-records sidecar. Nothing fetches until you Refresh.
|
|
161
|
+
</p>
|
|
162
|
+
<div className="dm-cockpit-fields">
|
|
163
|
+
<span className="dm-cockpit-field"><b>name</b>{previewRow.Name}</span>
|
|
164
|
+
<span className="dm-cockpit-field"><b>sourceId</b>{previewRow.sourceId}</span>
|
|
165
|
+
<span className="dm-cockpit-field"><b>storage</b>{previewRow.sourceStorage}</span>
|
|
166
|
+
<span className="dm-cockpit-field"><b>entity</b>{previewRow.entityType}</span>
|
|
167
|
+
<span className="dm-cockpit-field"><b>registryId</b>{previewRow.registryId}</span>
|
|
168
|
+
{previewRow.authRef ? <span className="dm-cockpit-field"><b>authRef</b>{previewRow.authRef}</span> : null}
|
|
169
|
+
</div>
|
|
170
|
+
{Array.isArray(dataSourcePreview.fields) && dataSourcePreview.fields.length ? (
|
|
171
|
+
<>
|
|
172
|
+
<p className="dm-cockpit-step-hint">Detected fields it will carry:</p>
|
|
173
|
+
<div className="dm-cockpit-fields">
|
|
174
|
+
{dataSourcePreview.fields.slice(0, 10).map((f) => (
|
|
175
|
+
<span key={f.name} className="dm-cockpit-field"><b>{f.role || f.type}</b>{f.name}</span>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
</>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
182
|
+
|
|
183
|
+
{!collapsed && Array.isArray(receipts) && receipts.length ? (
|
|
184
|
+
<div className="dm-cockpit-receipts">
|
|
185
|
+
<p className="dm-api-action-card-eyebrow">Receipts</p>
|
|
186
|
+
<ul>
|
|
187
|
+
{receipts.slice(0, 6).map((r, i) => (
|
|
188
|
+
<li key={`${r.at}-${i}`} className="dm-cockpit-receipt">
|
|
189
|
+
<StatusChip mod={r.ok ? "ok" : "bad"} className="dm-cockpit-receipt-chip">{r.kind}</StatusChip>
|
|
190
|
+
<span className="dm-cockpit-receipt-text">{r.detail}</span>
|
|
191
|
+
</li>
|
|
192
|
+
))}
|
|
193
|
+
</ul>
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
</section>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default ApiRegistryCreationCockpit;
|