@growthub/cli 0.13.5 → 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/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/globals.css +104 -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 +18 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +17 -9
- 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-graph.js +2 -2
- 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-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 +2 -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
|
+
}
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css
CHANGED
|
@@ -396,6 +396,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
|
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
.workspace-builder {
|
|
399
|
+
--workspace-rail-width: 264px;
|
|
399
400
|
min-height: 100vh;
|
|
400
401
|
display: grid;
|
|
401
402
|
grid-template-columns: 264px minmax(0, 1fr) 320px;
|
|
@@ -427,6 +428,7 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
|
|
|
427
428
|
overflow: hidden;
|
|
428
429
|
}
|
|
429
430
|
body.workspace-rail-collapsed .workspace-builder {
|
|
431
|
+
--workspace-rail-width: 52px;
|
|
430
432
|
grid-template-columns: 52px minmax(0, 1fr) 320px;
|
|
431
433
|
}
|
|
432
434
|
body.workspace-rail-collapsed .workspace-builder.workspace-settings-page {
|
|
@@ -456,24 +458,19 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
456
458
|
.workspace-rail.is-collapsed .workspace-rail-topbar {
|
|
457
459
|
justify-content: center;
|
|
458
460
|
width: 100%;
|
|
461
|
+
display: contents;
|
|
459
462
|
}
|
|
460
463
|
.workspace-rail.is-collapsed .workspace-rail-topbar-actions {
|
|
461
|
-
display:
|
|
462
|
-
flex-direction: column;
|
|
463
|
-
justify-content: center;
|
|
464
|
-
gap: 8px;
|
|
465
|
-
}
|
|
466
|
-
.workspace-rail.is-collapsed .workspace-rail-topbar-actions .workspace-rail-icon-btn {
|
|
467
|
-
order: 2;
|
|
464
|
+
display: contents;
|
|
468
465
|
}
|
|
469
466
|
.workspace-rail.is-collapsed .workspace-rail-icon-btn[aria-pressed="true"] {
|
|
470
467
|
order: 1;
|
|
471
468
|
}
|
|
472
469
|
.workspace-rail.is-collapsed .workspace-rail-icon-btn[data-rail-search] {
|
|
473
|
-
order:
|
|
470
|
+
order: 3;
|
|
474
471
|
}
|
|
475
472
|
.workspace-rail.is-collapsed .workspace-rail-icon-btn[aria-label="Workspace settings"] {
|
|
476
|
-
order:
|
|
473
|
+
order: 4;
|
|
477
474
|
}
|
|
478
475
|
.workspace-rail.is-collapsed .workspace-rail-icon-btn[aria-pressed="true"] svg {
|
|
479
476
|
transform: rotate(180deg);
|
|
@@ -482,6 +479,7 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
482
479
|
display: flex;
|
|
483
480
|
justify-content: center;
|
|
484
481
|
width: 100%;
|
|
482
|
+
order: 2;
|
|
485
483
|
}
|
|
486
484
|
.workspace-rail.is-collapsed .workspace-rail-tabs {
|
|
487
485
|
flex-direction: column;
|
|
@@ -2128,6 +2126,63 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
2128
2126
|
transition: opacity .12s ease;
|
|
2129
2127
|
}
|
|
2130
2128
|
|
|
2129
|
+
.workspace-dashboard-surface {
|
|
2130
|
+
background: #f7f7f8;
|
|
2131
|
+
padding: 0 32px 24px;
|
|
2132
|
+
}
|
|
2133
|
+
.workspace-dashboard-surface .dm-workflow-toolbar {
|
|
2134
|
+
margin: 0 0 18px;
|
|
2135
|
+
padding: 0;
|
|
2136
|
+
background: #ffffff;
|
|
2137
|
+
border-bottom-color: #e8e8e8;
|
|
2138
|
+
}
|
|
2139
|
+
.workspace-dashboard-surface .workspace-canvas {
|
|
2140
|
+
width: 100%;
|
|
2141
|
+
margin: 0 auto;
|
|
2142
|
+
border: 1px solid #e5e7eb;
|
|
2143
|
+
border-radius: 8px;
|
|
2144
|
+
background: #f7f7f8;
|
|
2145
|
+
max-height: calc(100vh - 72px);
|
|
2146
|
+
}
|
|
2147
|
+
.workspace-dashboard-surface .workspace-tabs {
|
|
2148
|
+
gap: 6px;
|
|
2149
|
+
min-height: 44px;
|
|
2150
|
+
padding: 8px 12px 12px;
|
|
2151
|
+
background: #ffffff;
|
|
2152
|
+
border-bottom: 1px solid #ececec;
|
|
2153
|
+
border-radius: 8px 8px 0 0;
|
|
2154
|
+
}
|
|
2155
|
+
.workspace-dashboard-surface .workspace-tabs button {
|
|
2156
|
+
min-height: 30px;
|
|
2157
|
+
}
|
|
2158
|
+
.workspace-dashboard-surface .workspace-grid {
|
|
2159
|
+
gap: 12px;
|
|
2160
|
+
padding: 18px;
|
|
2161
|
+
}
|
|
2162
|
+
.workspace-dashboard-surface .workspace-grid-cell {
|
|
2163
|
+
background: #ffffff;
|
|
2164
|
+
border-color: #e8e8e8;
|
|
2165
|
+
}
|
|
2166
|
+
.workspace-dashboard-surface.is-dashboard-editing {
|
|
2167
|
+
padding-inline: 16px;
|
|
2168
|
+
}
|
|
2169
|
+
.workspace-dashboard-surface.is-dashboard-editing .workspace-canvas {
|
|
2170
|
+
max-height: calc(100vh - 64px);
|
|
2171
|
+
}
|
|
2172
|
+
.workspace-dashboard-surface.is-dashboard-editing .workspace-grid {
|
|
2173
|
+
gap: 10px;
|
|
2174
|
+
padding: 14px;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
@media (max-width: 900px) {
|
|
2178
|
+
.workspace-dashboard-surface {
|
|
2179
|
+
padding-inline: 20px;
|
|
2180
|
+
}
|
|
2181
|
+
.workspace-dashboard-surface .dm-workflow-toolbar {
|
|
2182
|
+
margin-inline: 0;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2131
2186
|
@media (max-width: 1080px) {
|
|
2132
2187
|
.workspace-builder {
|
|
2133
2188
|
grid-template-columns: 180px minmax(0, 1fr);
|
|
@@ -4890,32 +4945,49 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
4890
4945
|
background: #f7f7f8;
|
|
4891
4946
|
overflow: hidden;
|
|
4892
4947
|
}
|
|
4948
|
+
.workspace-surface.workspace-dashboard-surface {
|
|
4949
|
+
background: #f7f7f8;
|
|
4950
|
+
padding: 0 32px 24px;
|
|
4951
|
+
}
|
|
4952
|
+
.workspace-surface.workspace-dashboard-surface.is-dashboard-editing {
|
|
4953
|
+
padding-inline: 16px;
|
|
4954
|
+
}
|
|
4893
4955
|
.dm-workflow-toolbar {
|
|
4894
4956
|
height: 40px;
|
|
4895
4957
|
min-height: 40px;
|
|
4896
4958
|
align-items: center;
|
|
4897
4959
|
flex-wrap: nowrap;
|
|
4898
4960
|
gap: 12px;
|
|
4899
|
-
padding: 0
|
|
4900
|
-
margin: 0;
|
|
4961
|
+
padding: 0 32px;
|
|
4962
|
+
margin: 0 -32px 18px;
|
|
4901
4963
|
border-bottom: 1px solid #ececef;
|
|
4902
4964
|
background: #fbfbfc;
|
|
4903
4965
|
overflow: hidden;
|
|
4904
4966
|
}
|
|
4967
|
+
.workspace-surface.workspace-dashboard-surface .dm-workflow-toolbar {
|
|
4968
|
+
padding-inline: 32px;
|
|
4969
|
+
margin-inline: -32px;
|
|
4970
|
+
}
|
|
4971
|
+
.workspace-dashboard-surface.is-dashboard-editing .dm-workflow-toolbar {
|
|
4972
|
+
padding-inline: 16px;
|
|
4973
|
+
margin-inline: -16px;
|
|
4974
|
+
}
|
|
4905
4975
|
.dm-workflow-titlebar {
|
|
4906
4976
|
display: flex;
|
|
4907
4977
|
align-items: center;
|
|
4908
4978
|
gap: 6px;
|
|
4909
|
-
min-width:
|
|
4910
|
-
flex: 1;
|
|
4979
|
+
min-width: max-content;
|
|
4980
|
+
flex: 1 0 auto;
|
|
4911
4981
|
color: #8b8b91;
|
|
4912
4982
|
font-size: 13px;
|
|
4913
4983
|
font-weight: 500;
|
|
4984
|
+
white-space: nowrap;
|
|
4914
4985
|
}
|
|
4915
4986
|
.dm-workflow-titlebar h1 {
|
|
4916
4987
|
margin: 0;
|
|
4917
|
-
min-width:
|
|
4918
|
-
max-width:
|
|
4988
|
+
min-width: max-content;
|
|
4989
|
+
max-width: none;
|
|
4990
|
+
flex: 0 0 auto;
|
|
4919
4991
|
overflow: hidden;
|
|
4920
4992
|
text-overflow: ellipsis;
|
|
4921
4993
|
white-space: nowrap;
|
|
@@ -4924,6 +4996,20 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
4924
4996
|
font-weight: 600;
|
|
4925
4997
|
letter-spacing: 0;
|
|
4926
4998
|
}
|
|
4999
|
+
.dm-workflow-breadcrumb-link {
|
|
5000
|
+
appearance: none;
|
|
5001
|
+
border: 0;
|
|
5002
|
+
background: transparent;
|
|
5003
|
+
padding: 0;
|
|
5004
|
+
color: #a1a1aa;
|
|
5005
|
+
font: inherit;
|
|
5006
|
+
font-size: 13px;
|
|
5007
|
+
font-weight: 600;
|
|
5008
|
+
cursor: pointer;
|
|
5009
|
+
}
|
|
5010
|
+
.dm-workflow-breadcrumb-link:hover {
|
|
5011
|
+
color: #4b4b52;
|
|
5012
|
+
}
|
|
4927
5013
|
.dm-workflow-title-icon {
|
|
4928
5014
|
display: grid;
|
|
4929
5015
|
place-items: center;
|
|
@@ -4943,8 +5029,9 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
4943
5029
|
display: flex;
|
|
4944
5030
|
align-items: center;
|
|
4945
5031
|
gap: 6px;
|
|
4946
|
-
flex
|
|
4947
|
-
|
|
5032
|
+
flex: 0 1 auto;
|
|
5033
|
+
min-width: 0;
|
|
5034
|
+
max-width: 58vw;
|
|
4948
5035
|
overflow-x: auto;
|
|
4949
5036
|
scrollbar-width: none;
|
|
4950
5037
|
}
|