@growthub/cli 0.14.4 → 0.14.5
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/resolvers/[integrationId]/route.js +157 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolvers/route.js +86 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +30 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +400 -188
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +3 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +24 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +7 -82
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/resolver-constructor.js +217 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-registry.js +99 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/unified-resolver-registry.js +545 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +30 -2
- package/package.json +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +0 -141
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +0 -64
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +0 -376
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/resolvers/[integrationId]
|
|
3
|
+
*
|
|
4
|
+
* CMS SDK v1.5.1 — the governed, addressable endpoint every registered resolver
|
|
5
|
+
* is exposed at. A resolver is the workspace's provider-agnostic "API → governed
|
|
6
|
+
* rows" abstraction; this makes it a real, hittable Next.js route inside the
|
|
7
|
+
* monorepo, so other apps and external callers consume governed records by
|
|
8
|
+
* integrationId. One dynamic route serves every resolver (a projection of the
|
|
9
|
+
* unified registry) — runtime-agnostic and drift-free by construction.
|
|
10
|
+
*
|
|
11
|
+
* Identity: the path segment is matched canonically. The governed record keeps
|
|
12
|
+
* its human integrationId; the resolver registers under either the raw id
|
|
13
|
+
* (config-driven/Nango) or the slug (generated), so lookup tries the raw value
|
|
14
|
+
* then the slug — `/api/resolvers/my-crm` and `/api/resolvers/asana` both resolve.
|
|
15
|
+
*
|
|
16
|
+
* Same-level governance as every mutation/execution route (Governed Application
|
|
17
|
+
* Control Plane V1): `x-growthub-app-scope` is runtime-enforced, scope rejections
|
|
18
|
+
* emit a canonical outcome receipt, and under a scope the 404 body does NOT leak
|
|
19
|
+
* the list of other registered integrations. Secret-safe: tokens never leave the
|
|
20
|
+
* server, and error messages are redacted.
|
|
21
|
+
*
|
|
22
|
+
* Response — success: { ok, integrationId, resolverId, recordRef, connectorKind, recordCount, records }
|
|
23
|
+
* Response — no resolver: 404 { ok:false, reason:"no-resolver", registeredResolvers? }
|
|
24
|
+
* Response — scope violation: 422 AppScopeViolation
|
|
25
|
+
* Response — resolver error: 502 { ok:false, reason:"fetch-error", error }
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { NextResponse } from "next/server";
|
|
29
|
+
import { requireAppScope, checkScopedRegistryAccess } from "@/lib/workspace-app-registry";
|
|
30
|
+
import { readWorkspaceConfig } from "@/lib/workspace-config";
|
|
31
|
+
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
32
|
+
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
33
|
+
import { listGovernedWorkspaceIntegrations } from "@/lib/adapters/integrations";
|
|
34
|
+
import { loadAllResolvers } from "@/lib/adapters/integrations/resolver-loader";
|
|
35
|
+
import { getSourceResolver, listRegisteredResolvers } from "@/lib/adapters/integrations/source-resolver-registry";
|
|
36
|
+
import { slugifyIntegrationId } from "@/lib/unified-resolver-registry";
|
|
37
|
+
|
|
38
|
+
const MAX_LIMIT = 200;
|
|
39
|
+
const DEFAULT_LIMIT = 50;
|
|
40
|
+
|
|
41
|
+
/** Strip anything secret-shaped from an error string before it leaves the server. */
|
|
42
|
+
function redact(message) {
|
|
43
|
+
return String(message || "")
|
|
44
|
+
.replace(/(authorization|bearer|api[_-]?key|token|secret|password)\s*[:=]\s*\S+/gi, "$1 [redacted]")
|
|
45
|
+
.replace(/\bBearer\s+[A-Za-z0-9._-]+/gi, "Bearer [redacted]")
|
|
46
|
+
.slice(0, 300);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Find the governed api-registry row backing an integrationId (raw or slug). */
|
|
50
|
+
function findRecordRef(workspaceConfig, integrationId) {
|
|
51
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
52
|
+
const wantSlug = slugifyIntegrationId(integrationId, "");
|
|
53
|
+
for (const object of objects) {
|
|
54
|
+
if (object?.objectType !== "api-registry") continue;
|
|
55
|
+
for (const row of Array.isArray(object.rows) ? object.rows : []) {
|
|
56
|
+
const id = String(row?.integrationId || "").trim();
|
|
57
|
+
if (!id) continue;
|
|
58
|
+
if (id === integrationId || slugifyIntegrationId(id, "") === wantSlug) {
|
|
59
|
+
return { objectId: String(object.id || ""), rowName: String(row.Name || id), integrationId: id };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function GET(request, context) {
|
|
67
|
+
const params = await context?.params;
|
|
68
|
+
const rawId = String(params?.integrationId || "").trim();
|
|
69
|
+
if (!rawId) {
|
|
70
|
+
return NextResponse.json({ ok: false, reason: "bad-request", error: "integrationId is required" }, { status: 400 });
|
|
71
|
+
}
|
|
72
|
+
const slugId = slugifyIntegrationId(rawId, "");
|
|
73
|
+
|
|
74
|
+
const workspaceConfig = await readWorkspaceConfig().catch(() => ({}));
|
|
75
|
+
|
|
76
|
+
// App-scope gate — data-plane isolation. Checked against both forms so a scoped
|
|
77
|
+
// agent cannot dodge scope by varying slug/raw. The 404 path below is also
|
|
78
|
+
// scope-aware so it never leaks the registered-integration list under a scope.
|
|
79
|
+
const scope = requireAppScope(request, workspaceConfig);
|
|
80
|
+
if (scope.scoped) {
|
|
81
|
+
const violation =
|
|
82
|
+
scope.violation ||
|
|
83
|
+
checkScopedRegistryAccess(scope.context, rawId) && checkScopedRegistryAccess(scope.context, slugId);
|
|
84
|
+
if (violation) {
|
|
85
|
+
await appendOutcomeReceipt({
|
|
86
|
+
kind: "agent-outcome",
|
|
87
|
+
lane: "untrusted-direct",
|
|
88
|
+
outcomeStatus: "blocked",
|
|
89
|
+
appId: violation.appScope || scope.appId,
|
|
90
|
+
summary: `resolver endpoint rejected (422 app scope): ${violation.violationType}`,
|
|
91
|
+
nextActions: violation.repairPlan,
|
|
92
|
+
});
|
|
93
|
+
return NextResponse.json(violation, { status: 422 });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await loadAllResolvers();
|
|
98
|
+
const resolver = getSourceResolver(rawId) || (slugId ? getSourceResolver(slugId) : null);
|
|
99
|
+
if (!resolver) {
|
|
100
|
+
const body = { ok: false, reason: "no-resolver", integrationId: rawId, resolverId: slugId };
|
|
101
|
+
// Only an UNSCOPED caller gets the discovery aid — under a scope this would
|
|
102
|
+
// leak other apps' integration ids.
|
|
103
|
+
if (!scope.scoped) {
|
|
104
|
+
body.registeredResolvers = listRegisteredResolvers();
|
|
105
|
+
if (slugId && slugId !== rawId && body.registeredResolvers.includes(slugId)) {
|
|
106
|
+
body.hint = `No resolver for "${rawId}". Did you mean the canonical id "${slugId}"?`;
|
|
107
|
+
} else {
|
|
108
|
+
body.hint = "Register this API in the Data Model API Registry cockpit and construct its resolver, then re-call this endpoint.";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return NextResponse.json(body, { status: 404 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { searchParams } = new URL(request.url);
|
|
115
|
+
const limitRaw = Number(searchParams.get("limit"));
|
|
116
|
+
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(limitRaw, MAX_LIMIT) : DEFAULT_LIMIT;
|
|
117
|
+
|
|
118
|
+
const adapterConfig = readAdapterConfig();
|
|
119
|
+
let connection = null;
|
|
120
|
+
try {
|
|
121
|
+
const integrations = await listGovernedWorkspaceIntegrations();
|
|
122
|
+
connection = integrations.find((i) => i.provider === rawId || i.id === rawId || i.provider === slugId) || null;
|
|
123
|
+
} catch {
|
|
124
|
+
// Non-fatal — resolver falls back to env-only auth.
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let records;
|
|
128
|
+
try {
|
|
129
|
+
records = await resolver.fetchRecords(adapterConfig, connection, {});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return NextResponse.json(
|
|
132
|
+
{ ok: false, reason: "fetch-error", integrationId: rawId, error: redact(err?.message) || "resolver.fetchRecords threw" },
|
|
133
|
+
{ status: 502 },
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(records)) {
|
|
138
|
+
return NextResponse.json(
|
|
139
|
+
{ ok: false, reason: "bad-resolver-response", integrationId: rawId, error: "resolver.fetchRecords must return an array" },
|
|
140
|
+
{ status: 502 },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return NextResponse.json({
|
|
145
|
+
ok: true,
|
|
146
|
+
integrationId: rawId,
|
|
147
|
+
resolverId: slugId,
|
|
148
|
+
// Downstream apps/agents know exactly which governed row they consumed.
|
|
149
|
+
recordRef: findRecordRef(workspaceConfig, rawId),
|
|
150
|
+
source: "resolver-endpoint",
|
|
151
|
+
connectorKind: typeof resolver.connectorKind === "string" ? resolver.connectorKind : null,
|
|
152
|
+
recordCount: records.length,
|
|
153
|
+
records: records.slice(0, limit),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { GET };
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { NextResponse } from "next/server";
|
|
12
|
-
import { readWorkspaceConfig } from "@/lib/workspace-config";
|
|
12
|
+
import { readWorkspaceConfig, describePersistenceMode } from "@/lib/workspace-config";
|
|
13
13
|
import { computeConfiguredEnvRefs, listPersistenceAdapterReadiness } from "@/lib/env-status";
|
|
14
14
|
|
|
15
15
|
async function GET() {
|
|
@@ -21,10 +21,14 @@ async function GET() {
|
|
|
21
21
|
}
|
|
22
22
|
const configuredEnvRefs = computeConfiguredEnvRefs(workspaceConfig, process.env);
|
|
23
23
|
const persistenceAdapters = listPersistenceAdapterReadiness(process.env);
|
|
24
|
+
// Persistence mode + canSave so the scheduler provisioning cockpit can honestly
|
|
25
|
+
// gate the "set it up for me" action (server-file writes need a writable runtime).
|
|
26
|
+
const persistence = describePersistenceMode();
|
|
24
27
|
return NextResponse.json({
|
|
25
28
|
kind: "growthub-env-status-v1",
|
|
26
29
|
configuredEnvRefs,
|
|
27
30
|
persistenceAdapters,
|
|
31
|
+
persistence: { mode: persistence.mode, canSave: persistence.canSave === true },
|
|
28
32
|
});
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* Used by the generic resolver management panel and ResolverControlPanel in the
|
|
7
7
|
* widget inspector. No provider names appear in the response shape.
|
|
8
8
|
*
|
|
9
|
+
* CMS SDK v1.5.1 — additionally returns `registry`: the Unified API Resolver
|
|
10
|
+
* Registry index that correlates every governed `api-registry` record to its
|
|
11
|
+
* resolver (provenance, file, registered/tested state, response shape, score,
|
|
12
|
+
* next action, governed endpoint). The legacy fields are preserved verbatim.
|
|
13
|
+
* When the runtime is writable, the externalized index + endpoint manifest
|
|
14
|
+
* artifacts are written through so agents can read one file out of band.
|
|
15
|
+
*
|
|
9
16
|
* Response:
|
|
10
17
|
* {
|
|
11
18
|
* files: string[],
|
|
@@ -16,25 +23,100 @@
|
|
|
16
23
|
* hasListEntities: boolean,
|
|
17
24
|
* configSchema: SchemaField[] | null
|
|
18
25
|
* }[],
|
|
19
|
-
* canUpload: boolean
|
|
26
|
+
* canUpload: boolean,
|
|
27
|
+
* registry: ResolverRegistryIndex // @growthub/api-contract/resolver-registry
|
|
20
28
|
* }
|
|
21
29
|
*/
|
|
22
30
|
|
|
23
31
|
import { NextResponse } from "next/server";
|
|
24
32
|
import { loadAllResolvers, listResolverFiles } from "@/lib/adapters/integrations/resolver-loader";
|
|
25
33
|
import { describeRegisteredResolvers } from "@/lib/adapters/integrations/source-resolver-registry";
|
|
26
|
-
import { describePersistenceMode } from "@/lib/workspace-config";
|
|
34
|
+
import { describePersistenceMode, readWorkspaceConfig, readWorkspaceSourceRecords } from "@/lib/workspace-config";
|
|
35
|
+
import { appendOutcomeReceipt } from "@/lib/workspace-outcome-receipts";
|
|
36
|
+
import { computeConfiguredEnvRefs } from "@/lib/env-status";
|
|
37
|
+
import { deriveResolverRegistry } from "@/lib/unified-resolver-registry";
|
|
38
|
+
import { readResolverFileMeta, persistResolverRegistryArtifacts } from "@/lib/server-resolver-registry";
|
|
27
39
|
|
|
28
40
|
async function GET() {
|
|
29
41
|
await loadAllResolvers();
|
|
30
42
|
const files = await listResolverFiles();
|
|
31
43
|
const resolvers = describeRegisteredResolvers();
|
|
32
44
|
const persistence = describePersistenceMode();
|
|
45
|
+
const registeredIds = resolvers.map((r) => r.integrationId);
|
|
46
|
+
|
|
47
|
+
// Unified registry derivation — correlate every governed record to its
|
|
48
|
+
// resolver. All IO happens here; the deriver stays pure. Derivation failure is
|
|
49
|
+
// NEVER silently hidden: the response carries an explicit `registryStatus` so
|
|
50
|
+
// an agent/client can distinguish "no entries" from "registry failed", and a
|
|
51
|
+
// writable runtime's artifact-write outcome is reported, not swallowed.
|
|
52
|
+
let registry = null;
|
|
53
|
+
let registryStatus = "ok";
|
|
54
|
+
let registryError = null;
|
|
55
|
+
let artifactWritten = false;
|
|
56
|
+
let artifactReason = null;
|
|
57
|
+
try {
|
|
58
|
+
const [workspaceConfig, sourceRecords, fileMeta] = await Promise.all([
|
|
59
|
+
readWorkspaceConfig().catch(() => ({})),
|
|
60
|
+
readWorkspaceSourceRecords().then((r) => r || {}).catch(() => ({})),
|
|
61
|
+
readResolverFileMeta().catch(() => ({})),
|
|
62
|
+
]);
|
|
63
|
+
let configuredEnvRefs = [];
|
|
64
|
+
try {
|
|
65
|
+
configuredEnvRefs = computeConfiguredEnvRefs(workspaceConfig) || [];
|
|
66
|
+
} catch {
|
|
67
|
+
configuredEnvRefs = [];
|
|
68
|
+
}
|
|
69
|
+
registry = deriveResolverRegistry({
|
|
70
|
+
workspaceConfig,
|
|
71
|
+
files,
|
|
72
|
+
registeredIds,
|
|
73
|
+
fileMeta,
|
|
74
|
+
sourceRecords,
|
|
75
|
+
runtime: { configuredEnvRefs },
|
|
76
|
+
});
|
|
77
|
+
// Write-through the externalized artifacts when writable. On a writable
|
|
78
|
+
// runtime a failed write is a real problem (stale projections) — surface it
|
|
79
|
+
// and emit a governance receipt; on read-only it is expected (live-only).
|
|
80
|
+
if (persistence.canSave) {
|
|
81
|
+
const result = await persistResolverRegistryArtifacts(registry).catch((err) => ({
|
|
82
|
+
written: false,
|
|
83
|
+
reason: err?.message || "artifact write threw",
|
|
84
|
+
}));
|
|
85
|
+
artifactWritten = Boolean(result?.written);
|
|
86
|
+
artifactReason = result?.reason || null;
|
|
87
|
+
if (!artifactWritten) {
|
|
88
|
+
await appendOutcomeReceipt({
|
|
89
|
+
kind: "agent-outcome",
|
|
90
|
+
lane: "server-authoritative",
|
|
91
|
+
outcomeStatus: "failed",
|
|
92
|
+
summary: `resolver registry artifact write failed: ${artifactReason || "unknown"}`,
|
|
93
|
+
}).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
artifactReason = persistence.reason || "read-only runtime — registry is live-only";
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
registry = null;
|
|
100
|
+
registryStatus = "degraded";
|
|
101
|
+
registryError = { reason: "derivation-failed", message: String(err?.message || err).slice(0, 300) };
|
|
102
|
+
await appendOutcomeReceipt({
|
|
103
|
+
kind: "agent-outcome",
|
|
104
|
+
lane: "server-authoritative",
|
|
105
|
+
outcomeStatus: "failed",
|
|
106
|
+
summary: `resolver registry derivation failed: ${registryError.message}`,
|
|
107
|
+
}).catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
|
|
33
110
|
return NextResponse.json({
|
|
34
111
|
files,
|
|
35
|
-
registeredIds
|
|
112
|
+
registeredIds,
|
|
36
113
|
resolvers,
|
|
37
|
-
canUpload: persistence.canSave
|
|
114
|
+
canUpload: persistence.canSave,
|
|
115
|
+
registry,
|
|
116
|
+
registryStatus,
|
|
117
|
+
registryError,
|
|
118
|
+
artifactWritten,
|
|
119
|
+
artifactReason,
|
|
38
120
|
});
|
|
39
121
|
}
|
|
40
122
|
|
|
@@ -56,14 +56,17 @@ export function ApiRegistryCreationCockpit({
|
|
|
56
56
|
onCollapsedChange,
|
|
57
57
|
}) {
|
|
58
58
|
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
59
|
+
const hasVisibleAction = Array.isArray(state?.steps) && state.steps.some((step) => step?.action);
|
|
60
|
+
const shouldHide = Boolean(hideWhenComplete && state?.complete && !hasVisibleAction);
|
|
59
61
|
useEffect(() => {
|
|
60
|
-
setCollapsed(defaultCollapsed || Boolean(hideWhenComplete && state?.complete));
|
|
61
|
-
}, [defaultCollapsed, hideWhenComplete, state?.complete, state?.integrationId]);
|
|
62
|
+
setCollapsed(defaultCollapsed || Boolean(hideWhenComplete && state?.complete && !hasVisibleAction));
|
|
63
|
+
}, [defaultCollapsed, hideWhenComplete, state?.complete, state?.integrationId, hasVisibleAction]);
|
|
62
64
|
if (!state || !Array.isArray(state.steps)) return null;
|
|
63
|
-
if (
|
|
65
|
+
if (shouldHide) return null;
|
|
64
66
|
const candidates = profile?.candidates || {};
|
|
65
67
|
const candidateEntries = Object.entries(candidates).filter(([, v]) => v);
|
|
66
68
|
const previewRow = dataSourcePreview?.row || null;
|
|
69
|
+
const workflowAction = state?.workflowAction || null;
|
|
67
70
|
const toggleCollapsed = () => {
|
|
68
71
|
const next = !collapsed;
|
|
69
72
|
setCollapsed(next);
|
|
@@ -77,6 +80,28 @@ export function ApiRegistryCreationCockpit({
|
|
|
77
80
|
onAction?.(action);
|
|
78
81
|
};
|
|
79
82
|
|
|
83
|
+
if (hideWhenComplete && state.complete && workflowAction) {
|
|
84
|
+
return (
|
|
85
|
+
<section className="dm-api-action-card dm-api-action-card-workflow" aria-label="Workflow canvas">
|
|
86
|
+
<div className="dm-api-action-card-body">
|
|
87
|
+
<p className="dm-api-action-card-eyebrow">Workflow canvas</p>
|
|
88
|
+
<h3>Use this API in a workflow</h3>
|
|
89
|
+
<p>{workflowAction.description}</p>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="dm-api-action-card-actions">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="dm-btn-outline dm-api-action-card-cta"
|
|
95
|
+
disabled={disabled || Boolean(busyAction)}
|
|
96
|
+
onClick={() => runAction(workflowAction)}
|
|
97
|
+
>
|
|
98
|
+
{busyAction === `workflow:${workflowAction.id}` ? "Opening…" : workflowAction.label}
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
80
105
|
return (
|
|
81
106
|
<section className={`dm-api-action-card dm-cockpit${collapsed ? " is-collapsed" : ""}`} aria-label="API creation journey">
|
|
82
107
|
<button
|
|
@@ -180,11 +205,11 @@ export function ApiRegistryCreationCockpit({
|
|
|
180
205
|
</div>
|
|
181
206
|
) : null}
|
|
182
207
|
|
|
183
|
-
{!collapsed && Array.isArray(receipts) && receipts.
|
|
208
|
+
{!collapsed && Array.isArray(receipts) && receipts.some((r) => r.ok) ? (
|
|
184
209
|
<div className="dm-cockpit-receipts">
|
|
185
210
|
<p className="dm-api-action-card-eyebrow">Receipts</p>
|
|
186
211
|
<ul>
|
|
187
|
-
{receipts.slice(0, 6).map((r, i) => (
|
|
212
|
+
{receipts.filter((r) => r.ok).slice(0, 6).map((r, i) => (
|
|
188
213
|
<li key={`${r.at}-${i}`} className="dm-cockpit-receipt">
|
|
189
214
|
<StatusChip mod={r.ok ? "ok" : "bad"} className="dm-cockpit-receipt-chip">{r.kind}</StatusChip>
|
|
190
215
|
<span className="dm-cockpit-receipt-text">{r.detail}</span>
|
|
@@ -4,7 +4,7 @@ import { CheckCircle2, X } from "lucide-react";
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Read-only summary of a successfully tested API Registry row.
|
|
7
|
-
* Shown at the top of the
|
|
7
|
+
* Shown at the top of the workflow draft flow (not a blocking modal).
|
|
8
8
|
*/
|
|
9
9
|
export function ApiRegistryReviewModal({ registryRow, onClose = null }) {
|
|
10
10
|
if (!registryRow) return null;
|
|
@@ -22,7 +22,7 @@ export function ApiRegistryReviewModal({ registryRow, onClose = null }) {
|
|
|
22
22
|
<p className="dm-api-review-banner-eyebrow">Connected API</p>
|
|
23
23
|
<h3>{registryRow.Name || integrationId}</h3>
|
|
24
24
|
<p>
|
|
25
|
-
This endpoint returned a valid response. You can now
|
|
25
|
+
This endpoint returned a valid response. You can now use it in a workflow canvas.
|
|
26
26
|
</p>
|
|
27
27
|
<code className="dm-api-review-banner-route">
|
|
28
28
|
{method} {baseUrl && endpoint ? `${baseUrl.replace(/\/+$/, "")}/${endpoint.replace(/^\/+/, "")}` : endpoint || baseUrl}
|