@growthub/cli 0.13.4 → 0.13.6
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/QUICKSTART.md +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +496 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +120 -17
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +31 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +16 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +218 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
- package/dist/index.js +127 -44
- package/package.json +1 -1
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { CheckCircle, ExternalLink, Loader2, RefreshCw, ShieldCheck, XCircle } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* NangoConnectionPanel — interactive sidecar for an api-registry row whose
|
|
8
|
+
* `connectorKind === "nango"`. Aligns with Nango's documented OAuth
|
|
9
|
+
* lifecycle:
|
|
10
|
+
*
|
|
11
|
+
* 1. Create Connect Session — needs `providerConfigKey` only. Nango
|
|
12
|
+
* mints a session token + connect_link. The user opens the link and
|
|
13
|
+
* completes OAuth on the provider.
|
|
14
|
+
* 2. Nango generates the `connectionId` server-side and delivers it via
|
|
15
|
+
* the auth webhook (which an operator can persist into the row's
|
|
16
|
+
* `connectionIds` later — webhook persistence is a follow-up).
|
|
17
|
+
* 3. The user pastes the `connectionId` shown in Nango Cloud / received
|
|
18
|
+
* via webhook into the panel; "Check Connection" then verifies it.
|
|
19
|
+
* 4. Reconnect uses the existing `connectionId` as input and produces a
|
|
20
|
+
* fresh authorization session for the same connection.
|
|
21
|
+
*
|
|
22
|
+
* The panel reads `providerConfigKey`, `integrationId`, and `connectionIds`
|
|
23
|
+
* off the row (with sensible fallbacks) and never persists secrets. It
|
|
24
|
+
* calls server routes which themselves go through `@nangohq/node`. The
|
|
25
|
+
* browser never sees the Nango secret key or any provider OAuth credential.
|
|
26
|
+
*
|
|
27
|
+
* Props:
|
|
28
|
+
* row — the api-registry row being edited
|
|
29
|
+
* disabled — disable all controls (typically when the parent is saving)
|
|
30
|
+
* onUpdateRow(patch) — optional: parent applies patch (e.g. status:
|
|
31
|
+
* "connected", lastTested timestamp) when verification succeeds
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const POLL_INTERVAL_MS = 3000;
|
|
35
|
+
const POLL_DURATION_MS = 60000;
|
|
36
|
+
|
|
37
|
+
function deriveProviderConfigKey(row) {
|
|
38
|
+
if (typeof row?.providerConfigKey === "string" && row.providerConfigKey.trim()) {
|
|
39
|
+
return row.providerConfigKey.trim();
|
|
40
|
+
}
|
|
41
|
+
if (typeof row?.integrationId === "string" && row.integrationId.trim()) {
|
|
42
|
+
return row.integrationId.trim();
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function deriveDefaultConnectionId(row) {
|
|
48
|
+
if (Array.isArray(row?.connectionIds) && row.connectionIds.length) {
|
|
49
|
+
return String(row.connectionIds[0]).trim();
|
|
50
|
+
}
|
|
51
|
+
if (typeof row?.connectionIds === "string" && row.connectionIds.trim()) {
|
|
52
|
+
return row.connectionIds.split(",").map((c) => c.trim()).filter(Boolean)[0] || "";
|
|
53
|
+
}
|
|
54
|
+
if (typeof row?.connectionId === "string" && row.connectionId.trim()) {
|
|
55
|
+
return row.connectionId.trim();
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function deriveInitialStatus(row) {
|
|
61
|
+
const status = typeof row?.status === "string" ? row.status.trim().toLowerCase() : "";
|
|
62
|
+
if (status === "connected") {
|
|
63
|
+
return {
|
|
64
|
+
kind: "connected",
|
|
65
|
+
message: row?.lastTested
|
|
66
|
+
? `Connected · last verified ${formatRelativeTime(row.lastTested)}`
|
|
67
|
+
: "Connected"
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (status === "failed" || status === "error") {
|
|
71
|
+
return { kind: "error", message: "Last verification failed. Re-check the connection." };
|
|
72
|
+
}
|
|
73
|
+
return { kind: "unknown", message: "" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatRelativeTime(isoString) {
|
|
77
|
+
const ts = new Date(isoString);
|
|
78
|
+
if (Number.isNaN(ts.getTime())) return "recently";
|
|
79
|
+
const diffMs = Date.now() - ts.getTime();
|
|
80
|
+
if (diffMs < 60_000) return "just now";
|
|
81
|
+
if (diffMs < 3_600_000) return `${Math.floor(diffMs / 60_000)}m ago`;
|
|
82
|
+
if (diffMs < 86_400_000) return `${Math.floor(diffMs / 3_600_000)}h ago`;
|
|
83
|
+
return `${Math.floor(diffMs / 86_400_000)}d ago`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateProviderConfigKey(value) {
|
|
87
|
+
if (!value) return "providerConfigKey is required (the Nango integration key).";
|
|
88
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(value)) {
|
|
89
|
+
return "Use letters, digits, _, ., or - (max 64 chars, starts alphanumeric).";
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateConnectionIdField(value, { required }) {
|
|
95
|
+
if (!value) return required ? "connectionId is required to verify or reconnect." : null;
|
|
96
|
+
if (value.length > 256) return "connectionId is too long (max 256 chars).";
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function StatusBadge({ status, label }) {
|
|
101
|
+
const map = {
|
|
102
|
+
connected: { className: "dm-api-action-card-status connected", Icon: CheckCircle },
|
|
103
|
+
"not-connected": { className: "dm-api-action-card-status warn", Icon: XCircle },
|
|
104
|
+
error: { className: "dm-api-action-card-status error", Icon: XCircle },
|
|
105
|
+
pending: { className: "dm-api-action-card-status pending", Icon: Loader2 },
|
|
106
|
+
unknown: { className: "dm-api-action-card-status", Icon: ShieldCheck }
|
|
107
|
+
};
|
|
108
|
+
const { className, Icon } = map[status] || map.unknown;
|
|
109
|
+
return (
|
|
110
|
+
<span className={className} role="status" aria-live="polite">
|
|
111
|
+
<Icon size={14} aria-hidden="true" />
|
|
112
|
+
<span>{label || status}</span>
|
|
113
|
+
</span>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function NangoConnectionPanel({ row, disabled, onUpdateRow }) {
|
|
118
|
+
const initialProviderConfigKey = useMemo(() => deriveProviderConfigKey(row), [row]);
|
|
119
|
+
const initialConnectionId = useMemo(() => deriveDefaultConnectionId(row), [row]);
|
|
120
|
+
const initialStatus = useMemo(() => deriveInitialStatus(row), [row]);
|
|
121
|
+
const lastTested = typeof row?.lastTested === "string" ? row.lastTested : "";
|
|
122
|
+
const persistedStatus = typeof row?.status === "string" ? row.status.trim().toLowerCase() : "";
|
|
123
|
+
|
|
124
|
+
const [providerConfigKey, setProviderConfigKey] = useState(initialProviderConfigKey);
|
|
125
|
+
const [connectionId, setConnectionId] = useState(initialConnectionId);
|
|
126
|
+
const [fieldErrors, setFieldErrors] = useState({});
|
|
127
|
+
const [creatingSession, setCreatingSession] = useState(false);
|
|
128
|
+
const [checkingConnection, setCheckingConnection] = useState(false);
|
|
129
|
+
const [polling, setPolling] = useState(false);
|
|
130
|
+
const [statusKind, setStatusKind] = useState(initialStatus.kind);
|
|
131
|
+
const [statusMessage, setStatusMessage] = useState(initialStatus.message);
|
|
132
|
+
const [connectLink, setConnectLink] = useState(null);
|
|
133
|
+
const [sessionMode, setSessionMode] = useState(null);
|
|
134
|
+
const [lastSummary, setLastSummary] = useState(null);
|
|
135
|
+
const [errorRecovery, setErrorRecovery] = useState(null);
|
|
136
|
+
|
|
137
|
+
const pollTimerRef = useRef(null);
|
|
138
|
+
const pollDeadlineRef = useRef(0);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setProviderConfigKey(initialProviderConfigKey);
|
|
142
|
+
setConnectionId(initialConnectionId);
|
|
143
|
+
setFieldErrors({});
|
|
144
|
+
setConnectLink(null);
|
|
145
|
+
setSessionMode(null);
|
|
146
|
+
setLastSummary(null);
|
|
147
|
+
setErrorRecovery(null);
|
|
148
|
+
setStatusKind(initialStatus.kind);
|
|
149
|
+
setStatusMessage(initialStatus.message);
|
|
150
|
+
setPolling(false);
|
|
151
|
+
if (pollTimerRef.current) {
|
|
152
|
+
clearTimeout(pollTimerRef.current);
|
|
153
|
+
pollTimerRef.current = null;
|
|
154
|
+
}
|
|
155
|
+
}, [row?.id, row?.integrationId, initialProviderConfigKey, initialConnectionId, initialStatus.kind, initialStatus.message]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => () => {
|
|
158
|
+
if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
|
|
159
|
+
}, []);
|
|
160
|
+
|
|
161
|
+
const handleProviderConfigKeyBlur = useCallback((value) => {
|
|
162
|
+
setFieldErrors((prev) => ({ ...prev, providerConfigKey: validateProviderConfigKey(value.trim()) }));
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const handleConnectionIdBlur = useCallback((value) => {
|
|
166
|
+
setFieldErrors((prev) => ({ ...prev, connectionId: validateConnectionIdField(value.trim(), { required: false }) }));
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const runStatusCheck = useCallback(async (silent = false) => {
|
|
170
|
+
const connectionIdValue = connectionId.trim();
|
|
171
|
+
const connectionIdError = validateConnectionIdField(connectionIdValue, { required: true });
|
|
172
|
+
if (connectionIdError) {
|
|
173
|
+
setFieldErrors((prev) => ({ ...prev, connectionId: connectionIdError }));
|
|
174
|
+
return { ok: false, missingConnectionId: true };
|
|
175
|
+
}
|
|
176
|
+
if (!silent) setCheckingConnection(true);
|
|
177
|
+
setErrorRecovery(null);
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch("/api/workspace/integrations/nango/connection-status", {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "content-type": "application/json" },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
providerConfigKey: providerConfigKey.trim(),
|
|
184
|
+
connectionId: connectionIdValue
|
|
185
|
+
})
|
|
186
|
+
});
|
|
187
|
+
const payload = await response.json();
|
|
188
|
+
if (!response.ok || !payload?.ok) {
|
|
189
|
+
const message = payload?.error || `HTTP ${response.status}`;
|
|
190
|
+
setStatusKind("error");
|
|
191
|
+
setStatusMessage(message);
|
|
192
|
+
setErrorRecovery({
|
|
193
|
+
message,
|
|
194
|
+
code: payload?.code || null,
|
|
195
|
+
hint: payload?.code === "NANGO_NOT_CONFIGURED"
|
|
196
|
+
? "Set NANGO_SECRET_KEY in this runtime to enable Nango."
|
|
197
|
+
: payload?.code === "NANGO_SDK_UNAVAILABLE"
|
|
198
|
+
? "Install @nangohq/node in apps/workspace."
|
|
199
|
+
: null
|
|
200
|
+
});
|
|
201
|
+
return { ok: false };
|
|
202
|
+
}
|
|
203
|
+
setLastSummary(payload);
|
|
204
|
+
if (payload.status === "connected") {
|
|
205
|
+
setStatusKind("connected");
|
|
206
|
+
setStatusMessage(`Connected · environment: ${payload.environment || "dev"}`);
|
|
207
|
+
if (typeof onUpdateRow === "function") {
|
|
208
|
+
onUpdateRow({
|
|
209
|
+
status: "connected",
|
|
210
|
+
lastTested: new Date().toISOString(),
|
|
211
|
+
lastResponse: JSON.stringify({ connection: payload.connection || {} })
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return { ok: true, connected: true };
|
|
215
|
+
}
|
|
216
|
+
setStatusKind("not-connected");
|
|
217
|
+
setStatusMessage(payload.reason || "Not connected yet.");
|
|
218
|
+
return { ok: true, connected: false };
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const message = error?.message || "network error";
|
|
221
|
+
setStatusKind("error");
|
|
222
|
+
setStatusMessage(message);
|
|
223
|
+
setErrorRecovery({ message, code: null, hint: null });
|
|
224
|
+
return { ok: false };
|
|
225
|
+
} finally {
|
|
226
|
+
if (!silent) setCheckingConnection(false);
|
|
227
|
+
}
|
|
228
|
+
}, [providerConfigKey, connectionId, onUpdateRow]);
|
|
229
|
+
|
|
230
|
+
const startPolling = useCallback(() => {
|
|
231
|
+
if (!connectionId.trim()) {
|
|
232
|
+
// Without a connectionId we can't poll — the user must paste it once
|
|
233
|
+
// Nango delivers it (via Connect UI or the auth webhook).
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
|
|
237
|
+
pollDeadlineRef.current = Date.now() + POLL_DURATION_MS;
|
|
238
|
+
setPolling(true);
|
|
239
|
+
const tick = async () => {
|
|
240
|
+
if (Date.now() > pollDeadlineRef.current) {
|
|
241
|
+
setPolling(false);
|
|
242
|
+
if (statusKind !== "connected") {
|
|
243
|
+
setStatusKind("not-connected");
|
|
244
|
+
setStatusMessage("Still waiting on Nango Connect — click Check Connection when you've finished OAuth.");
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const result = await runStatusCheck(true);
|
|
249
|
+
if (result?.connected) {
|
|
250
|
+
setPolling(false);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
pollTimerRef.current = setTimeout(tick, POLL_INTERVAL_MS);
|
|
254
|
+
};
|
|
255
|
+
pollTimerRef.current = setTimeout(tick, POLL_INTERVAL_MS);
|
|
256
|
+
}, [runStatusCheck, statusKind, connectionId]);
|
|
257
|
+
|
|
258
|
+
const createSession = useCallback(async ({ reconnect }) => {
|
|
259
|
+
const providerError = validateProviderConfigKey(providerConfigKey.trim());
|
|
260
|
+
const connectionIdError = reconnect
|
|
261
|
+
? validateConnectionIdField(connectionId.trim(), { required: true })
|
|
262
|
+
: null;
|
|
263
|
+
setFieldErrors((prev) => ({ ...prev, providerConfigKey: providerError, connectionId: connectionIdError }));
|
|
264
|
+
if (providerError || connectionIdError) return;
|
|
265
|
+
|
|
266
|
+
setCreatingSession(true);
|
|
267
|
+
setConnectLink(null);
|
|
268
|
+
setSessionMode(null);
|
|
269
|
+
setErrorRecovery(null);
|
|
270
|
+
setStatusKind("pending");
|
|
271
|
+
setStatusMessage(reconnect ? "Creating Nango Reconnect session…" : "Creating Nango Connect session…");
|
|
272
|
+
|
|
273
|
+
// Build correlation tags so the auth webhook can identify which row
|
|
274
|
+
// asked for OAuth. No secrets — only stable identifiers.
|
|
275
|
+
const tags = {};
|
|
276
|
+
if (typeof row?.id === "string" && row.id.trim()) tags.row_id = row.id.trim();
|
|
277
|
+
if (typeof row?.integrationId === "string" && row.integrationId.trim()) tags.integration_id = row.integrationId.trim();
|
|
278
|
+
if (typeof row?.objectId === "string" && row.objectId.trim()) tags.object_id = row.objectId.trim();
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const response = await fetch("/api/workspace/integrations/nango/connect-session", {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "content-type": "application/json" },
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
providerConfigKey: providerConfigKey.trim(),
|
|
286
|
+
...(reconnect ? { connectionId: connectionId.trim(), reconnect: true } : {}),
|
|
287
|
+
tags
|
|
288
|
+
})
|
|
289
|
+
});
|
|
290
|
+
const payload = await response.json();
|
|
291
|
+
if (!response.ok || !payload?.ok) {
|
|
292
|
+
const message = payload?.error || `HTTP ${response.status}`;
|
|
293
|
+
setStatusKind("error");
|
|
294
|
+
setStatusMessage(message);
|
|
295
|
+
setErrorRecovery({
|
|
296
|
+
message,
|
|
297
|
+
code: payload?.code || null,
|
|
298
|
+
hint: payload?.code === "NANGO_NOT_CONFIGURED"
|
|
299
|
+
? "Set NANGO_SECRET_KEY in this runtime to enable Nango."
|
|
300
|
+
: payload?.code === "NANGO_SDK_UNAVAILABLE"
|
|
301
|
+
? "Install @nangohq/node in apps/workspace."
|
|
302
|
+
: null
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!payload.connectLink) {
|
|
307
|
+
setStatusKind("error");
|
|
308
|
+
setStatusMessage("Nango returned a session without a connect_link. Check your Nango Cloud setup.");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
setConnectLink(payload.connectLink);
|
|
312
|
+
setSessionMode(payload.mode || (reconnect ? "reconnect" : "connect"));
|
|
313
|
+
setStatusKind("pending");
|
|
314
|
+
setStatusMessage(
|
|
315
|
+
reconnect
|
|
316
|
+
? "Reconnect link ready — complete OAuth, then we'll re-verify automatically."
|
|
317
|
+
: "Open the Connect link to finish OAuth. Nango will return a connectionId — paste it below or wait for the auth webhook to persist it."
|
|
318
|
+
);
|
|
319
|
+
try {
|
|
320
|
+
const win = window.open(payload.connectLink, "_blank", "noopener,noreferrer");
|
|
321
|
+
if (win) win.focus();
|
|
322
|
+
} catch {
|
|
323
|
+
// window.open may be blocked silently — the visible link below still works.
|
|
324
|
+
}
|
|
325
|
+
// Auto-poll only when we already have a connectionId (reconnect flow,
|
|
326
|
+
// or the user pre-filled it for a known existing connection).
|
|
327
|
+
if (connectionId.trim()) {
|
|
328
|
+
startPolling();
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const message = error?.message || "network error";
|
|
332
|
+
setStatusKind("error");
|
|
333
|
+
setStatusMessage(message);
|
|
334
|
+
setErrorRecovery({ message, code: null, hint: null });
|
|
335
|
+
} finally {
|
|
336
|
+
setCreatingSession(false);
|
|
337
|
+
}
|
|
338
|
+
}, [providerConfigKey, connectionId, row, startPolling]);
|
|
339
|
+
|
|
340
|
+
const handleCreateSession = useCallback(() => createSession({ reconnect: false }), [createSession]);
|
|
341
|
+
const handleReconnect = useCallback(() => createSession({ reconnect: true }), [createSession]);
|
|
342
|
+
|
|
343
|
+
const handleDisconnect = useCallback(() => {
|
|
344
|
+
setConnectLink(null);
|
|
345
|
+
setSessionMode(null);
|
|
346
|
+
setLastSummary(null);
|
|
347
|
+
setStatusKind("unknown");
|
|
348
|
+
setStatusMessage("");
|
|
349
|
+
setPolling(false);
|
|
350
|
+
if (pollTimerRef.current) {
|
|
351
|
+
clearTimeout(pollTimerRef.current);
|
|
352
|
+
pollTimerRef.current = null;
|
|
353
|
+
}
|
|
354
|
+
if (typeof onUpdateRow === "function") {
|
|
355
|
+
onUpdateRow({ status: "configured", lastResponse: "" });
|
|
356
|
+
}
|
|
357
|
+
}, [onUpdateRow]);
|
|
358
|
+
|
|
359
|
+
const isBusy = creatingSession || checkingConnection || polling;
|
|
360
|
+
const hasProviderError = Boolean(fieldErrors.providerConfigKey);
|
|
361
|
+
const hasConnectionIdError = Boolean(fieldErrors.connectionId);
|
|
362
|
+
const hasConnectionId = Boolean(connectionId.trim());
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<section className="dm-api-action-card" aria-label="Nango connection">
|
|
366
|
+
<div className="dm-api-action-card-body">
|
|
367
|
+
<p className="dm-api-action-card-eyebrow">Nango</p>
|
|
368
|
+
<h3>Connect this API through Nango</h3>
|
|
369
|
+
<p>
|
|
370
|
+
Nango handles OAuth and API authentication for hundreds of providers. Your provider credentials stay on Nango — this workspace only sees safe connection metadata, never tokens.
|
|
371
|
+
</p>
|
|
372
|
+
|
|
373
|
+
<ol className="dm-nango-steps">
|
|
374
|
+
<li>Enter the <strong>providerConfigKey</strong> (the Nango integration key).</li>
|
|
375
|
+
<li>Click <strong>Create Connect Session</strong> and complete OAuth in the new tab.</li>
|
|
376
|
+
<li>Paste the <strong>connectionId</strong> Nango shows you (or wait for the auth webhook to persist it), then click <strong>Check Connection</strong>.</li>
|
|
377
|
+
</ol>
|
|
378
|
+
|
|
379
|
+
<div className="dm-nango-fields">
|
|
380
|
+
<label className="dm-field">
|
|
381
|
+
<span>providerConfigKey</span>
|
|
382
|
+
<input
|
|
383
|
+
type="text"
|
|
384
|
+
value={providerConfigKey}
|
|
385
|
+
onChange={(event) => setProviderConfigKey(event.target.value)}
|
|
386
|
+
onBlur={(event) => handleProviderConfigKeyBlur(event.target.value)}
|
|
387
|
+
placeholder="e.g. hubspot-prod"
|
|
388
|
+
disabled={disabled || isBusy}
|
|
389
|
+
aria-invalid={hasProviderError}
|
|
390
|
+
aria-describedby={hasProviderError ? "nango-pck-error" : undefined}
|
|
391
|
+
/>
|
|
392
|
+
{hasProviderError
|
|
393
|
+
? <small id="nango-pck-error" className="dm-field-error">{fieldErrors.providerConfigKey}</small>
|
|
394
|
+
: <small className="dm-field-hint">Defaults to <code>integrationId</code> when blank.</small>}
|
|
395
|
+
</label>
|
|
396
|
+
|
|
397
|
+
<label className="dm-field">
|
|
398
|
+
<span>connectionId <small>(required to verify or reconnect)</small></span>
|
|
399
|
+
<input
|
|
400
|
+
type="text"
|
|
401
|
+
value={connectionId}
|
|
402
|
+
onChange={(event) => setConnectionId(event.target.value)}
|
|
403
|
+
onBlur={(event) => handleConnectionIdBlur(event.target.value)}
|
|
404
|
+
placeholder="Nango returns this after OAuth"
|
|
405
|
+
disabled={disabled || isBusy}
|
|
406
|
+
aria-invalid={hasConnectionIdError}
|
|
407
|
+
aria-describedby={hasConnectionIdError ? "nango-cid-error" : undefined}
|
|
408
|
+
/>
|
|
409
|
+
{hasConnectionIdError
|
|
410
|
+
? <small id="nango-cid-error" className="dm-field-error">{fieldErrors.connectionId}</small>
|
|
411
|
+
: <small className="dm-field-hint">Nango generates this during OAuth and delivers it via auth webhook.</small>}
|
|
412
|
+
</label>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<StatusBadge status={statusKind} label={statusMessage || statusKind} />
|
|
416
|
+
|
|
417
|
+
{persistedStatus === "connected" && lastTested ? (
|
|
418
|
+
<p className="dm-nango-last-tested" aria-label="Last connection verification">
|
|
419
|
+
Last verified <time dateTime={lastTested}>{formatRelativeTime(lastTested)}</time>
|
|
420
|
+
</p>
|
|
421
|
+
) : null}
|
|
422
|
+
|
|
423
|
+
{connectLink ? (
|
|
424
|
+
<p className="dm-nango-connect-link">
|
|
425
|
+
<a href={connectLink} target="_blank" rel="noopener noreferrer">
|
|
426
|
+
<ExternalLink size={14} aria-hidden="true" />
|
|
427
|
+
{sessionMode === "reconnect" ? " Reopen Nango Reconnect link" : " Reopen Nango Connect link"}
|
|
428
|
+
</a>
|
|
429
|
+
</p>
|
|
430
|
+
) : null}
|
|
431
|
+
|
|
432
|
+
{errorRecovery ? (
|
|
433
|
+
<div className="dm-nango-error-recovery" role="alert">
|
|
434
|
+
<p>{errorRecovery.message}</p>
|
|
435
|
+
{errorRecovery.hint ? <p className="dm-nango-error-hint">{errorRecovery.hint}</p> : null}
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
className="dm-btn-outline"
|
|
439
|
+
onClick={() => runStatusCheck(false)}
|
|
440
|
+
disabled={disabled || isBusy || hasProviderError}
|
|
441
|
+
>
|
|
442
|
+
<RefreshCw size={14} aria-hidden="true" /> Try Again
|
|
443
|
+
</button>
|
|
444
|
+
</div>
|
|
445
|
+
) : null}
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div className="dm-api-action-card-actions">
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
className="dm-btn-primary-sm dm-api-action-card-cta"
|
|
452
|
+
onClick={handleCreateSession}
|
|
453
|
+
disabled={disabled || isBusy || hasProviderError}
|
|
454
|
+
>
|
|
455
|
+
{creatingSession && sessionMode !== "reconnect"
|
|
456
|
+
? <><Loader2 className="dm-spinner" size={14} aria-hidden="true" /> Creating session…</>
|
|
457
|
+
: "Create Connect Session"}
|
|
458
|
+
</button>
|
|
459
|
+
{hasConnectionId ? (
|
|
460
|
+
<button
|
|
461
|
+
type="button"
|
|
462
|
+
className="dm-btn-outline dm-api-action-card-cta"
|
|
463
|
+
onClick={handleReconnect}
|
|
464
|
+
disabled={disabled || isBusy || hasProviderError || hasConnectionIdError}
|
|
465
|
+
>
|
|
466
|
+
{creatingSession && sessionMode === "reconnect"
|
|
467
|
+
? <><Loader2 className="dm-spinner" size={14} aria-hidden="true" /> Reconnecting…</>
|
|
468
|
+
: "Reconnect"}
|
|
469
|
+
</button>
|
|
470
|
+
) : null}
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
className="dm-btn-outline dm-api-action-card-cta"
|
|
474
|
+
onClick={() => runStatusCheck(false)}
|
|
475
|
+
disabled={disabled || isBusy || hasProviderError}
|
|
476
|
+
>
|
|
477
|
+
{checkingConnection
|
|
478
|
+
? <><Loader2 className="dm-spinner" size={14} aria-hidden="true" /> Checking…</>
|
|
479
|
+
: polling
|
|
480
|
+
? <><Loader2 className="dm-spinner" size={14} aria-hidden="true" /> Auto-polling…</>
|
|
481
|
+
: "Check Connection"}
|
|
482
|
+
</button>
|
|
483
|
+
{(statusKind === "connected" || persistedStatus === "connected") ? (
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
className="dm-btn-outline dm-api-action-card-cta"
|
|
487
|
+
onClick={handleDisconnect}
|
|
488
|
+
disabled={disabled || isBusy}
|
|
489
|
+
>
|
|
490
|
+
Reset
|
|
491
|
+
</button>
|
|
492
|
+
) : null}
|
|
493
|
+
</div>
|
|
494
|
+
</section>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
export function OrchestrationGraphEmptyCanvas({
|
|
6
6
|
onStartFromRegistry,
|
|
7
7
|
onStartBlank,
|
|
8
|
+
onStartAgentSwarm,
|
|
8
9
|
onPasteGraph,
|
|
9
10
|
disabled
|
|
10
11
|
}) {
|
|
@@ -23,6 +24,11 @@ export function OrchestrationGraphEmptyCanvas({
|
|
|
23
24
|
<button type="button" className="dm-btn-outline" disabled={disabled} onClick={onStartBlank}>
|
|
24
25
|
Start blank
|
|
25
26
|
</button>
|
|
27
|
+
{onStartAgentSwarm && (
|
|
28
|
+
<button type="button" className="dm-btn-outline" disabled={disabled} onClick={onStartAgentSwarm}>
|
|
29
|
+
Add Agent Swarm
|
|
30
|
+
</button>
|
|
31
|
+
)}
|
|
26
32
|
</div>
|
|
27
33
|
<details
|
|
28
34
|
className="dm-orchestration-canvas__paste"
|
|
@@ -897,7 +897,94 @@ export function OrchestrationNodeConfigPanel({
|
|
|
897
897
|
</div>
|
|
898
898
|
)}
|
|
899
899
|
|
|
900
|
-
{activeTab === "configuration" && type === "ai-agent" && (
|
|
900
|
+
{activeTab === "configuration" && type === "ai-agent" && (config.role || config.taskPrompt || config.required != null) && (
|
|
901
|
+
<div className="dm-orchestration-config__pane">
|
|
902
|
+
<label className="dm-orchestration-config__field">
|
|
903
|
+
<span>Role</span>
|
|
904
|
+
<input
|
|
905
|
+
value={config.role || node.label || ""}
|
|
906
|
+
disabled={disabled}
|
|
907
|
+
onChange={(e) => patchConfig({ role: e.target.value, __nodePatch: { label: e.target.value } })}
|
|
908
|
+
/>
|
|
909
|
+
</label>
|
|
910
|
+
<label className="dm-orchestration-config__field">
|
|
911
|
+
<span>Description</span>
|
|
912
|
+
<input
|
|
913
|
+
placeholder="One-sentence charter"
|
|
914
|
+
value={config.description || ""}
|
|
915
|
+
disabled={disabled}
|
|
916
|
+
onChange={(e) => patchConfig({ description: e.target.value })}
|
|
917
|
+
/>
|
|
918
|
+
</label>
|
|
919
|
+
<label className="dm-orchestration-config__field">
|
|
920
|
+
<span>Task</span>
|
|
921
|
+
<textarea
|
|
922
|
+
rows={4}
|
|
923
|
+
value={config.taskPrompt || ""}
|
|
924
|
+
disabled={disabled}
|
|
925
|
+
onChange={(e) => patchConfig({ taskPrompt: e.target.value })}
|
|
926
|
+
/>
|
|
927
|
+
</label>
|
|
928
|
+
<label className="dm-orchestration-config__field">
|
|
929
|
+
<span>Tools</span>
|
|
930
|
+
<input
|
|
931
|
+
placeholder="read, summarize"
|
|
932
|
+
value={Array.isArray(config.tools) ? config.tools.join(", ") : ""}
|
|
933
|
+
disabled={disabled}
|
|
934
|
+
onChange={(e) => patchConfig({
|
|
935
|
+
tools: e.target.value.split(",").map((t) => t.trim()).filter(Boolean)
|
|
936
|
+
})}
|
|
937
|
+
/>
|
|
938
|
+
</label>
|
|
939
|
+
<label className="dm-orchestration-config__field">
|
|
940
|
+
<span>Max tokens</span>
|
|
941
|
+
<input
|
|
942
|
+
type="number"
|
|
943
|
+
min="0"
|
|
944
|
+
placeholder="0 = inherit"
|
|
945
|
+
value={config.maxTokens || 0}
|
|
946
|
+
disabled={disabled}
|
|
947
|
+
onChange={(e) => patchConfig({ maxTokens: Math.max(0, Number(e.target.value) || 0) })}
|
|
948
|
+
/>
|
|
949
|
+
</label>
|
|
950
|
+
<label className="dm-orchestration-config__field">
|
|
951
|
+
<span>Agent host</span>
|
|
952
|
+
<select
|
|
953
|
+
value={config.agentHost || ""}
|
|
954
|
+
disabled={disabled}
|
|
955
|
+
onChange={(e) => patchConfig({ agentHost: e.target.value })}
|
|
956
|
+
>
|
|
957
|
+
<option value="">Inherit</option>
|
|
958
|
+
{Object.entries(HOST_AUTH_CATALOG || {}).map(([slug, host]) => (
|
|
959
|
+
<option key={slug} value={slug}>{host?.label || slug}</option>
|
|
960
|
+
))}
|
|
961
|
+
</select>
|
|
962
|
+
</label>
|
|
963
|
+
<label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
|
|
964
|
+
<input
|
|
965
|
+
type="checkbox"
|
|
966
|
+
checked={config.required !== false}
|
|
967
|
+
disabled={disabled}
|
|
968
|
+
onChange={(e) => patchConfig({ required: e.target.checked })}
|
|
969
|
+
/>
|
|
970
|
+
<span>Required</span>
|
|
971
|
+
</label>
|
|
972
|
+
<label
|
|
973
|
+
className="dm-orchestration-config__field dm-orchestration-config__field-inline"
|
|
974
|
+
title="Network is granted only when both this and the row's networkAllow are on."
|
|
975
|
+
>
|
|
976
|
+
<input
|
|
977
|
+
type="checkbox"
|
|
978
|
+
checked={config.networkAccess === true}
|
|
979
|
+
disabled={disabled}
|
|
980
|
+
onChange={(e) => patchConfig({ networkAccess: e.target.checked })}
|
|
981
|
+
/>
|
|
982
|
+
<span>Network</span>
|
|
983
|
+
</label>
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
|
|
987
|
+
{activeTab === "configuration" && type === "ai-agent" && !(config.role || config.taskPrompt || config.required != null) && (
|
|
901
988
|
<div className="dm-orchestration-config__pane">
|
|
902
989
|
<label className="dm-orchestration-config__field">
|
|
903
990
|
<span>Model</span>
|
|
@@ -215,6 +215,43 @@ async function copyToClipboard(text) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
function formatRewardScore(value) {
|
|
219
|
+
const n = Number(value);
|
|
220
|
+
if (!Number.isFinite(n)) return "—";
|
|
221
|
+
return n.toFixed(2);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function SwarmSection({ swarm }) {
|
|
225
|
+
if (!swarm || typeof swarm !== "object") return null;
|
|
226
|
+
const tasks = Array.isArray(swarm.tasks) ? swarm.tasks : [];
|
|
227
|
+
if (tasks.length === 0 && !swarm.orchestrator?.plan && !swarm.synthesis?.answer) return null;
|
|
228
|
+
const completed = tasks.filter((t) => t?.status === "completed").length;
|
|
229
|
+
const score = swarm.reward ? formatRewardScore(swarm.reward.score) : "—";
|
|
230
|
+
const kind = swarm.reward?.kind || "structural-v1";
|
|
231
|
+
const synthesis = swarm.synthesis || null;
|
|
232
|
+
return (
|
|
233
|
+
<section className="dm-run-console__section">
|
|
234
|
+
<h3>Swarm</h3>
|
|
235
|
+
<p className="dm-swarm-summary__line">
|
|
236
|
+
<span><strong>{completed}/{tasks.length}</strong></span>
|
|
237
|
+
<span>score <strong>{score}</strong></span>
|
|
238
|
+
<span className="dm-swarm-summary__kind" title={swarm.reward?.note || ""}>{kind}</span>
|
|
239
|
+
</p>
|
|
240
|
+
{synthesis?.answer ? (
|
|
241
|
+
<details className="dm-swarm-phase" open>
|
|
242
|
+
<summary>
|
|
243
|
+
synthesizer
|
|
244
|
+
{synthesis.parsedOutcomeScore != null
|
|
245
|
+
? ` · ${Number(synthesis.parsedOutcomeScore).toFixed(2)}`
|
|
246
|
+
: ""}
|
|
247
|
+
</summary>
|
|
248
|
+
<pre>{synthesis.answer}</pre>
|
|
249
|
+
</details>
|
|
250
|
+
) : null}
|
|
251
|
+
</section>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
218
255
|
function InputsSection({ payload }) {
|
|
219
256
|
const runInputs = payload?.runInputs;
|
|
220
257
|
const summary = payload?.inputSummary;
|
|
@@ -477,6 +514,9 @@ export function OrchestrationRunTracePanel({
|
|
|
477
514
|
const payload = activeConsoleRecord?.payload || {};
|
|
478
515
|
const output = activeConsoleRecord?.output || {};
|
|
479
516
|
const context = activeConsoleRecord?.context || {};
|
|
517
|
+
const swarmPayload = activeConsoleRecord?.swarm
|
|
518
|
+
|| (activeRawRecord && activeRawRecord.swarm)
|
|
519
|
+
|| null;
|
|
480
520
|
|
|
481
521
|
return (
|
|
482
522
|
<section className="dm-run-console" aria-label="Live runs console">
|
|
@@ -710,6 +750,7 @@ export function OrchestrationRunTracePanel({
|
|
|
710
750
|
<CodeBlock label="Command" body={payload.command} />
|
|
711
751
|
<CodeBlock label="Instructions" body={payload.instructions} />
|
|
712
752
|
</section>
|
|
753
|
+
<SwarmSection swarm={swarmPayload} />
|
|
713
754
|
<InputsSection payload={payload} />
|
|
714
755
|
</div>
|
|
715
756
|
)}
|