@growthub/cli 0.13.5 → 0.13.7
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 +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +172 -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 +531 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +274 -18
- 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 +62 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +554 -48
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +24 -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-activation.js +534 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -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 +5 -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
|
@@ -305,7 +305,7 @@ function findSandboxRowsForRegistry(workspaceConfig, integrationId) {
|
|
|
305
305
|
if (!sandboxObject) return [];
|
|
306
306
|
const rows = Array.isArray(sandboxObject.rows) ? sandboxObject.rows : [];
|
|
307
307
|
return rows.filter((row) => {
|
|
308
|
-
const graph = parseOrchestrationGraph(row?.orchestrationGraph);
|
|
308
|
+
const graph = parseOrchestrationGraph(row?.orchestrationConfig || row?.orchestrationGraph);
|
|
309
309
|
if (!graph?.nodes) return String(row?.schedulerRegistryId || "").trim() === id;
|
|
310
310
|
return graph.nodes.some(
|
|
311
311
|
(node) => node?.type === "api-registry-call"
|
|
@@ -359,7 +359,7 @@ function buildSandboxRowFromApiRegistry(workspaceConfig, registryRow, options =
|
|
|
359
359
|
lastRunId: "",
|
|
360
360
|
lastSourceId: "",
|
|
361
361
|
lastResponse: "",
|
|
362
|
-
|
|
362
|
+
orchestrationConfig: serializeOrchestrationGraph(graph),
|
|
363
363
|
description: String(options.description || registryRow?.description || "").trim()
|
|
364
364
|
};
|
|
365
365
|
}
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Growthub Workspace Customer Activation Layer V1 — derivation helpers.
|
|
3
|
+
*
|
|
4
|
+
* Reads the existing governed artifacts (workspaceConfig + sidecar
|
|
5
|
+
* source records + workspace metadata graph store) and projects a typed,
|
|
6
|
+
* read-only activation checklist:
|
|
7
|
+
*
|
|
8
|
+
* - what template the workspace was scaffolded from (provenance)
|
|
9
|
+
* - which setup steps are still pending
|
|
10
|
+
* - what the next obvious action is
|
|
11
|
+
*
|
|
12
|
+
* Authority order:
|
|
13
|
+
*
|
|
14
|
+
* 1. growthub.config.json (governed workspace artifact)
|
|
15
|
+
* 2. growthub.source-records.json (sidecar source/run state)
|
|
16
|
+
* 3. derived metadata store (workspace-metadata-store.js)
|
|
17
|
+
*
|
|
18
|
+
* Invariants:
|
|
19
|
+
*
|
|
20
|
+
* - Pure derivation. No React. No fetch. No mutation of inputs.
|
|
21
|
+
* - Never includes secrets, OAuth tokens, provider credentials, or
|
|
22
|
+
* access/refresh tokens in the output. Connection IDs are surfaced
|
|
23
|
+
* as booleans only (`connectionConfigured: true/false`).
|
|
24
|
+
* - Activation state is NOT persisted — every checklist field is
|
|
25
|
+
* recomputable from inputs on every page load. Don't add hidden
|
|
26
|
+
* storage. Don't add localStorage. Don't add a separate sidecar.
|
|
27
|
+
* - Backwards-compatible: a workspace with no `provenance` block still
|
|
28
|
+
* produces a valid (generic) activation state.
|
|
29
|
+
*
|
|
30
|
+
* The output shape:
|
|
31
|
+
*
|
|
32
|
+
* {
|
|
33
|
+
* kind: "growthub-workspace-activation-state-v1"
|
|
34
|
+
* version: 1
|
|
35
|
+
* template: "blank" | "project-management" | <slug>
|
|
36
|
+
* templateName:string
|
|
37
|
+
* headline: string // user-facing "what is this workspace for"
|
|
38
|
+
* subheadline: string // user-facing "what's next"
|
|
39
|
+
* complete: boolean
|
|
40
|
+
* completedCount: number
|
|
41
|
+
* totalCount: number
|
|
42
|
+
* nextStepId: string | null
|
|
43
|
+
* steps: [
|
|
44
|
+
* {
|
|
45
|
+
* id: string // stable; safe to use as key
|
|
46
|
+
* label: string // short title
|
|
47
|
+
* description:string // one-line user-facing copy
|
|
48
|
+
* status: "complete"|"pending"|"blocked"|"optional"
|
|
49
|
+
* href: string // deep link into existing surface
|
|
50
|
+
* hint?: string // why it's blocked (no secrets)
|
|
51
|
+
* cta?: string // button label override
|
|
52
|
+
* }
|
|
53
|
+
* ]
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const ACTIVATION_KIND = "growthub-workspace-activation-state-v1";
|
|
58
|
+
const ACTIVATION_VERSION = 1;
|
|
59
|
+
|
|
60
|
+
const TEMPLATE_PROJECT_MANAGEMENT = "project-management";
|
|
61
|
+
|
|
62
|
+
function isPlainObject(value) {
|
|
63
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeString(value) {
|
|
67
|
+
if (value == null) return "";
|
|
68
|
+
return typeof value === "string" ? value : String(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findDataModelObject(workspaceConfig, predicate) {
|
|
72
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
73
|
+
? workspaceConfig.dataModel.objects
|
|
74
|
+
: [];
|
|
75
|
+
for (const object of objects) {
|
|
76
|
+
if (!isPlainObject(object)) continue;
|
|
77
|
+
if (predicate(object)) return object;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function listObjectRows(object) {
|
|
83
|
+
return Array.isArray(object?.rows) ? object.rows : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeBoolean(value) {
|
|
87
|
+
return Boolean(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deriveProvenance(workspaceConfig) {
|
|
91
|
+
const provenance = isPlainObject(workspaceConfig?.provenance) ? workspaceConfig.provenance : null;
|
|
92
|
+
const template = safeString(provenance?.template).trim().toLowerCase();
|
|
93
|
+
const templateKind = safeString(provenance?.templateKind).trim();
|
|
94
|
+
const privacy = safeString(provenance?.privacy).trim();
|
|
95
|
+
const note = safeString(provenance?.note).trim();
|
|
96
|
+
const templateName = safeString(workspaceConfig?.name || provenance?.label || "").trim();
|
|
97
|
+
return {
|
|
98
|
+
hasProvenance: Boolean(provenance),
|
|
99
|
+
template: template || "blank",
|
|
100
|
+
templateKind,
|
|
101
|
+
privacy,
|
|
102
|
+
note,
|
|
103
|
+
templateName,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Detect whether a string-or-list-of-strings field on an api-registry row
|
|
109
|
+
* contains at least one configured connection ID. We surface this as a
|
|
110
|
+
* boolean only — the connection identifier itself is never echoed into the
|
|
111
|
+
* activation state (it can be PII-adjacent).
|
|
112
|
+
*/
|
|
113
|
+
function hasConnectionId(row) {
|
|
114
|
+
if (!isPlainObject(row)) return false;
|
|
115
|
+
const raw = row.connectionIds ?? row.connectionId;
|
|
116
|
+
if (Array.isArray(raw)) {
|
|
117
|
+
return raw.some((entry) => safeString(entry).trim().length > 0);
|
|
118
|
+
}
|
|
119
|
+
return safeString(raw).trim().length > 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect whether the workspace source-records sidecar has any rows for
|
|
124
|
+
* the given source id. Reads `recordCount` first (cheap) and falls back to
|
|
125
|
+
* `records.length`. Never inspects the underlying records.
|
|
126
|
+
*/
|
|
127
|
+
function hasSourceRecords(workspaceSourceRecords, sourceId) {
|
|
128
|
+
if (!isPlainObject(workspaceSourceRecords)) return false;
|
|
129
|
+
const key = safeString(sourceId).trim();
|
|
130
|
+
if (!key) return false;
|
|
131
|
+
const sidecar = workspaceSourceRecords[key];
|
|
132
|
+
if (!isPlainObject(sidecar)) return false;
|
|
133
|
+
if (Number.isFinite(sidecar.recordCount) && sidecar.recordCount > 0) return true;
|
|
134
|
+
if (Array.isArray(sidecar.records) && sidecar.records.length > 0) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pull the latest workflow + sandbox row status from the typed metadata
|
|
140
|
+
* store when it's available, falling back to inspecting workspaceConfig
|
|
141
|
+
* directly. The store path is preferred because it dedupes runs and tags
|
|
142
|
+
* pipeline health, but neither path is required.
|
|
143
|
+
*/
|
|
144
|
+
function findWorkflowRow(workspaceConfig, predicate) {
|
|
145
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
146
|
+
? workspaceConfig.dataModel.objects
|
|
147
|
+
: [];
|
|
148
|
+
for (const object of objects) {
|
|
149
|
+
if (!isPlainObject(object) || object.objectType !== "sandbox-environment") continue;
|
|
150
|
+
const rows = listObjectRows(object);
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
if (!isPlainObject(row)) continue;
|
|
153
|
+
if (predicate(row, object)) return { object, row };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseSafe(value) {
|
|
160
|
+
if (isPlainObject(value)) return value;
|
|
161
|
+
if (typeof value !== "string") return null;
|
|
162
|
+
const trimmed = value.trim();
|
|
163
|
+
if (!trimmed) return null;
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(trimmed);
|
|
166
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function deriveLatestRunStatus(workflowRow) {
|
|
173
|
+
if (!isPlainObject(workflowRow)) return { status: "never", ok: false };
|
|
174
|
+
const status = safeString(workflowRow.status).trim().toLowerCase();
|
|
175
|
+
const lastResponse = parseSafe(workflowRow.lastResponse);
|
|
176
|
+
if (!lastResponse) {
|
|
177
|
+
if (status === "tested" || status === "ok" || status === "success") {
|
|
178
|
+
return { status: "ok", ok: true };
|
|
179
|
+
}
|
|
180
|
+
if (status === "failed" || status === "error") {
|
|
181
|
+
return { status: "failed", ok: false };
|
|
182
|
+
}
|
|
183
|
+
return { status: "never", ok: false };
|
|
184
|
+
}
|
|
185
|
+
const exitCode = Number.isFinite(lastResponse.exitCode) ? Number(lastResponse.exitCode) : null;
|
|
186
|
+
const ok = exitCode === 0 && !safeString(lastResponse.error).trim();
|
|
187
|
+
return { status: ok ? "ok" : "failed", ok };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findDashboardByIdOrName(workspaceConfig, predicate) {
|
|
191
|
+
const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
|
|
192
|
+
for (const dashboard of dashboards) {
|
|
193
|
+
if (!isPlainObject(dashboard)) continue;
|
|
194
|
+
if (predicate(dashboard)) return dashboard;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Project Management Workspace Template — adapter
|
|
201
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Derive the Project Management activation checklist.
|
|
205
|
+
*
|
|
206
|
+
* The seed config (`templates/seeded-configs/project-management.config.json`)
|
|
207
|
+
* ships a Nango-backed api-registry row, a Project Task Source, a sandbox
|
|
208
|
+
* workflow row, and a dashboard. This helper checks each one and builds
|
|
209
|
+
* the deep-link checklist.
|
|
210
|
+
*/
|
|
211
|
+
function deriveProjectManagementActivationState({ workspaceConfig, workspaceSourceRecords, metadataGraph }) {
|
|
212
|
+
const provenance = deriveProvenance(workspaceConfig);
|
|
213
|
+
const registry = findDataModelObject(workspaceConfig, (o) => o.objectType === "api-registry");
|
|
214
|
+
const registryRow = registry
|
|
215
|
+
? listObjectRows(registry).find((row) => safeString(row?.connectorKind).trim().toLowerCase() === "nango")
|
|
216
|
+
|| listObjectRows(registry)[0]
|
|
217
|
+
: null;
|
|
218
|
+
const providerConfigKey = safeString(registryRow?.providerConfigKey).trim()
|
|
219
|
+
|| safeString(registryRow?.integrationId).trim();
|
|
220
|
+
const connectionConfigured = hasConnectionId(registryRow);
|
|
221
|
+
|
|
222
|
+
const sourceObject = findDataModelObject(workspaceConfig, (o) => o.id === "project-task-source"
|
|
223
|
+
|| o.objectType === "data-source");
|
|
224
|
+
const sourceRow = listObjectRows(sourceObject)[0] || null;
|
|
225
|
+
const sourceId = safeString(sourceRow?.sourceId).trim();
|
|
226
|
+
const sourceHasRecords = hasSourceRecords(workspaceSourceRecords, sourceId);
|
|
227
|
+
|
|
228
|
+
const workflowMatch = findWorkflowRow(workspaceConfig, (row) => {
|
|
229
|
+
const name = safeString(row?.Name || row?.name).trim();
|
|
230
|
+
return name === "project-active-tasks-workflow"
|
|
231
|
+
|| name.toLowerCase().includes("project-active-tasks");
|
|
232
|
+
});
|
|
233
|
+
const workflowRow = workflowMatch?.row || null;
|
|
234
|
+
const workflowObjectId = workflowMatch?.object?.id || "sandbox-environments";
|
|
235
|
+
const workflowRowName = safeString(workflowRow?.Name || workflowRow?.name).trim();
|
|
236
|
+
const workflowRun = deriveLatestRunStatus(workflowRow);
|
|
237
|
+
|
|
238
|
+
const dashboard = findDashboardByIdOrName(workspaceConfig,
|
|
239
|
+
(d) => d.id === "project-management-template"
|
|
240
|
+
|| safeString(d.name).trim().toLowerCase() === "project management");
|
|
241
|
+
|
|
242
|
+
// NANGO_SECRET_KEY presence is impossible to detect in pure derivation
|
|
243
|
+
// (it's an env var the browser can't read). The metadata graph exposes a
|
|
244
|
+
// safe boolean via authority/runtime status if the server has surfaced it;
|
|
245
|
+
// otherwise the step always asks the user to confirm.
|
|
246
|
+
const integrationsRuntime = isPlainObject(metadataGraph?.runtime) ? metadataGraph.runtime : null;
|
|
247
|
+
const nangoConfiguredHint = integrationsRuntime?.nangoConfigured;
|
|
248
|
+
// If the runtime explicitly says configured, mark the env step as
|
|
249
|
+
// complete. If it explicitly says not-configured OR we have no signal at
|
|
250
|
+
// all, we don't claim it's complete — the user still needs to confirm.
|
|
251
|
+
const nangoEnvComplete = nangoConfiguredHint === true;
|
|
252
|
+
|
|
253
|
+
const steps = [];
|
|
254
|
+
|
|
255
|
+
steps.push({
|
|
256
|
+
id: "provider-env",
|
|
257
|
+
label: "Configure provider auth",
|
|
258
|
+
description: "Set NANGO_SECRET_KEY in your runtime environment so the workspace can talk to Nango.",
|
|
259
|
+
status: nangoEnvComplete ? "complete" : "pending",
|
|
260
|
+
href: "/settings/integrations",
|
|
261
|
+
hint: nangoEnvComplete
|
|
262
|
+
? ""
|
|
263
|
+
: "Set NANGO_SECRET_KEY in .env.local (or your hosted runtime), then restart the workspace server.",
|
|
264
|
+
cta: nangoEnvComplete ? "Review integrations" : "Open integration settings",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const nangoStepHref = registry && registryRow
|
|
268
|
+
? `/data-model?object=${encodeURIComponent(registry.id)}&row=${encodeURIComponent(safeString(registryRow.integrationId).trim() || "asana-active-tasks")}`
|
|
269
|
+
: "/data-model";
|
|
270
|
+
|
|
271
|
+
steps.push({
|
|
272
|
+
id: "nango-connection",
|
|
273
|
+
label: "Connect provider through Nango",
|
|
274
|
+
description: connectionConfigured
|
|
275
|
+
? `Connection configured for providerConfigKey "${providerConfigKey || "asana"}".`
|
|
276
|
+
: "Open the API Registry row and run Create Connect Session, then paste the connectionId Nango returns.",
|
|
277
|
+
status: connectionConfigured ? "complete" : (nangoEnvComplete ? "pending" : "blocked"),
|
|
278
|
+
href: nangoStepHref,
|
|
279
|
+
hint: connectionConfigured
|
|
280
|
+
? ""
|
|
281
|
+
: nangoEnvComplete
|
|
282
|
+
? "Use the Nango panel in the API Registry row — your workspace never sees provider tokens."
|
|
283
|
+
: "Finish the previous step first — Nango needs NANGO_SECRET_KEY to mint a Connect Session.",
|
|
284
|
+
cta: connectionConfigured ? "Manage connection" : "Open Nango panel",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const workflowHref = workflowRowName
|
|
288
|
+
? `/workflows?object=${encodeURIComponent(workflowObjectId)}&row=${encodeURIComponent(workflowRowName)}&field=orchestrationConfig`
|
|
289
|
+
: "/workflows";
|
|
290
|
+
|
|
291
|
+
const workflowComplete = workflowRun.ok && sourceHasRecords;
|
|
292
|
+
|
|
293
|
+
steps.push({
|
|
294
|
+
id: "workflow-run",
|
|
295
|
+
label: "Run the Active Tasks workflow",
|
|
296
|
+
description: workflowComplete
|
|
297
|
+
? "Workflow has executed successfully and refreshed the Project Task Source."
|
|
298
|
+
: workflowRun.status === "failed"
|
|
299
|
+
? "The last run failed — open the run trace to see what went wrong."
|
|
300
|
+
: "Open the seeded workflow and click Test to pull active tasks from your project.",
|
|
301
|
+
status: workflowComplete
|
|
302
|
+
? "complete"
|
|
303
|
+
: connectionConfigured
|
|
304
|
+
? (workflowRun.status === "failed" ? "blocked" : "pending")
|
|
305
|
+
: "blocked",
|
|
306
|
+
href: workflowHref,
|
|
307
|
+
hint: workflowComplete
|
|
308
|
+
? ""
|
|
309
|
+
: connectionConfigured
|
|
310
|
+
? workflowRun.status === "failed"
|
|
311
|
+
? "Open the workflow's See Runs trace and check the failing node's response."
|
|
312
|
+
: "Fill projectGid + workspaceGid in the input node, then click Test."
|
|
313
|
+
: "Finish the Nango connection step first.",
|
|
314
|
+
cta: workflowComplete ? "Open workflow" : workflowRun.status === "failed" ? "Open run trace" : "Open workflow",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const dashboardHref = dashboard
|
|
318
|
+
? `/?dashboard=${encodeURIComponent(dashboard.id)}`
|
|
319
|
+
: "/";
|
|
320
|
+
|
|
321
|
+
steps.push({
|
|
322
|
+
id: "dashboard-view",
|
|
323
|
+
label: "View the Active Tasks dashboard",
|
|
324
|
+
description: workflowComplete
|
|
325
|
+
? "Open the dashboard to see the latest active project tasks."
|
|
326
|
+
: "After the workflow runs successfully, hydrated tasks will appear in this dashboard.",
|
|
327
|
+
status: workflowComplete ? "complete" : "pending",
|
|
328
|
+
href: dashboardHref,
|
|
329
|
+
cta: "Open dashboard",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
steps.push({
|
|
333
|
+
id: "customize",
|
|
334
|
+
label: "Customize this workspace",
|
|
335
|
+
description: "Duplicate the dashboard, add objects, schedule the workflow, or wire another provider through Nango.",
|
|
336
|
+
status: "optional",
|
|
337
|
+
href: "/data-model",
|
|
338
|
+
cta: "Customize",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const completedCount = steps.filter((step) => step.status === "complete").length;
|
|
342
|
+
const requiredCount = steps.filter((step) => step.status !== "optional").length;
|
|
343
|
+
const complete = completedCount >= requiredCount;
|
|
344
|
+
const nextStep = steps.find((step) => step.status === "pending" || step.status === "blocked");
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
kind: ACTIVATION_KIND,
|
|
348
|
+
version: ACTIVATION_VERSION,
|
|
349
|
+
template: TEMPLATE_PROJECT_MANAGEMENT,
|
|
350
|
+
templateName: provenance.templateName || "Project Management Workspace",
|
|
351
|
+
headline: complete
|
|
352
|
+
? "Your Project Management workspace is live."
|
|
353
|
+
: `You're ${requiredCount - completedCount} step${requiredCount - completedCount === 1 ? "" : "s"} from your first task dashboard.`,
|
|
354
|
+
subheadline: complete
|
|
355
|
+
? "Inspect runs, customize objects, or wire another provider."
|
|
356
|
+
: nextStep
|
|
357
|
+
? `Next: ${nextStep.label}.`
|
|
358
|
+
: "Open the dashboard to see your latest tasks.",
|
|
359
|
+
complete,
|
|
360
|
+
completedCount,
|
|
361
|
+
totalCount: requiredCount,
|
|
362
|
+
nextStepId: nextStep ? nextStep.id : null,
|
|
363
|
+
steps,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
368
|
+
// Blank governed workspace — adapter
|
|
369
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Generic activation checklist for blank governed workspaces. Drives the
|
|
373
|
+
* user toward the first object → first dashboard → first widget → first
|
|
374
|
+
* workflow → first run, using only routes that already exist.
|
|
375
|
+
*/
|
|
376
|
+
function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceRecords, metadataGraph }) {
|
|
377
|
+
const provenance = deriveProvenance(workspaceConfig);
|
|
378
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
379
|
+
? workspaceConfig.dataModel.objects
|
|
380
|
+
: [];
|
|
381
|
+
|
|
382
|
+
// Hidden helper objects don't count as "user-created".
|
|
383
|
+
const HIDDEN_OBJECT_IDS = new Set([
|
|
384
|
+
"workspace-helper-sandbox",
|
|
385
|
+
"nav-folders",
|
|
386
|
+
"helper-threads",
|
|
387
|
+
"sandbox-environments",
|
|
388
|
+
"workflow-api-registry",
|
|
389
|
+
"workspace-ui-cache",
|
|
390
|
+
]);
|
|
391
|
+
const userObjects = objects.filter((o) => isPlainObject(o)
|
|
392
|
+
&& safeString(o.id).trim()
|
|
393
|
+
&& !HIDDEN_OBJECT_IDS.has(o.id));
|
|
394
|
+
const objectCreated = userObjects.length > 0;
|
|
395
|
+
|
|
396
|
+
const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
|
|
397
|
+
const dashboardCreated = dashboards.length > 0;
|
|
398
|
+
const widgetCount = dashboards.reduce((acc, dashboard) => {
|
|
399
|
+
const tabs = Array.isArray(dashboard?.tabs) ? dashboard.tabs : [];
|
|
400
|
+
for (const tab of tabs) {
|
|
401
|
+
const widgets = Array.isArray(tab?.widgets) ? tab.widgets : [];
|
|
402
|
+
acc += widgets.length;
|
|
403
|
+
}
|
|
404
|
+
return acc;
|
|
405
|
+
}, 0);
|
|
406
|
+
const widgetAdded = widgetCount > 0;
|
|
407
|
+
|
|
408
|
+
const workflowMatch = findWorkflowRow(workspaceConfig, () => true);
|
|
409
|
+
const workflowCreated = Boolean(workflowMatch?.row);
|
|
410
|
+
const workflowRun = deriveLatestRunStatus(workflowMatch?.row);
|
|
411
|
+
|
|
412
|
+
const steps = [
|
|
413
|
+
{
|
|
414
|
+
id: "create-object",
|
|
415
|
+
label: "Create your first object",
|
|
416
|
+
description: objectCreated
|
|
417
|
+
? `${userObjects.length} object${userObjects.length === 1 ? "" : "s"} in your Data Model.`
|
|
418
|
+
: "Start by adding a custom object or connecting a data source in Management.",
|
|
419
|
+
status: objectCreated ? "complete" : "pending",
|
|
420
|
+
href: "/data-model",
|
|
421
|
+
cta: objectCreated ? "Open Data Model" : "Create object",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "create-dashboard",
|
|
425
|
+
label: "Create a dashboard",
|
|
426
|
+
description: dashboardCreated
|
|
427
|
+
? `${dashboards.length} dashboard${dashboards.length === 1 ? "" : "s"} ready.`
|
|
428
|
+
: "Add a dashboard from the Builder to visualize your data.",
|
|
429
|
+
status: dashboardCreated ? "complete" : (objectCreated ? "pending" : "blocked"),
|
|
430
|
+
href: "/",
|
|
431
|
+
hint: dashboardCreated || objectCreated ? "" : "Add an object first so dashboards have data to bind to.",
|
|
432
|
+
cta: dashboardCreated ? "Open Builder" : "New dashboard",
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: "add-widget",
|
|
436
|
+
label: "Add a widget",
|
|
437
|
+
description: widgetAdded
|
|
438
|
+
? `${widgetCount} widget${widgetCount === 1 ? "" : "s"} placed.`
|
|
439
|
+
: "Bind a chart or view widget to one of your objects.",
|
|
440
|
+
status: widgetAdded ? "complete" : (dashboardCreated ? "pending" : "blocked"),
|
|
441
|
+
href: "/",
|
|
442
|
+
hint: widgetAdded || dashboardCreated ? "" : "Add a dashboard first.",
|
|
443
|
+
cta: widgetAdded ? "Open Builder" : "Add widget",
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: "create-workflow",
|
|
447
|
+
label: "Create a workflow",
|
|
448
|
+
description: workflowCreated
|
|
449
|
+
? "Sandbox workflow scaffolded."
|
|
450
|
+
: "Open Workflows to assemble your first automation.",
|
|
451
|
+
status: workflowCreated ? "complete" : "pending",
|
|
452
|
+
href: "/workflows",
|
|
453
|
+
cta: workflowCreated ? "Open Workflows" : "New workflow",
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: "run-workflow",
|
|
457
|
+
label: "Run your workflow",
|
|
458
|
+
description: workflowRun.ok
|
|
459
|
+
? "Workflow has run successfully at least once."
|
|
460
|
+
: workflowRun.status === "failed"
|
|
461
|
+
? "Last run failed — open the trace and fix the failing node."
|
|
462
|
+
: "Click Test inside the workflow to do a first run.",
|
|
463
|
+
status: workflowRun.ok ? "complete" : (workflowCreated ? "pending" : "blocked"),
|
|
464
|
+
href: "/workflows",
|
|
465
|
+
hint: workflowRun.ok || workflowCreated ? "" : "Create a workflow first.",
|
|
466
|
+
cta: workflowRun.ok ? "View runs" : "Open workflow",
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
const completedCount = steps.filter((step) => step.status === "complete").length;
|
|
471
|
+
const requiredCount = steps.length;
|
|
472
|
+
const complete = completedCount >= requiredCount;
|
|
473
|
+
const nextStep = steps.find((step) => step.status === "pending" || step.status === "blocked");
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
kind: ACTIVATION_KIND,
|
|
477
|
+
version: ACTIVATION_VERSION,
|
|
478
|
+
template: "blank",
|
|
479
|
+
templateName: provenance.templateName || "Governed Workspace",
|
|
480
|
+
headline: complete
|
|
481
|
+
? "Your workspace is set up."
|
|
482
|
+
: "Get started with your governed workspace.",
|
|
483
|
+
subheadline: complete
|
|
484
|
+
? "Add more objects, widgets, or workflows as your needs grow."
|
|
485
|
+
: nextStep
|
|
486
|
+
? `Next: ${nextStep.label}.`
|
|
487
|
+
: "Pick the next step that fits what you want to build.",
|
|
488
|
+
complete,
|
|
489
|
+
completedCount,
|
|
490
|
+
totalCount: requiredCount,
|
|
491
|
+
nextStepId: nextStep ? nextStep.id : null,
|
|
492
|
+
steps,
|
|
493
|
+
// Silence unused-arg warnings — sidecar inputs are accepted by every
|
|
494
|
+
// adapter so callers can pass the same envelope through.
|
|
495
|
+
_sourceRecords: workspaceSourceRecords ? true : false,
|
|
496
|
+
_metadata: metadataGraph ? true : false,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
501
|
+
// Public entry — template router
|
|
502
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Resolve the activation state for the current workspace.
|
|
506
|
+
*
|
|
507
|
+
* Templates are routed by `workspaceConfig.provenance.template`. Unknown
|
|
508
|
+
* templates and workspaces without provenance fall back to the blank
|
|
509
|
+
* adapter so existing workspaces never lose their UX.
|
|
510
|
+
*/
|
|
511
|
+
function deriveWorkspaceActivationState(input = {}) {
|
|
512
|
+
const safeInput = {
|
|
513
|
+
workspaceConfig: isPlainObject(input.workspaceConfig) ? input.workspaceConfig : {},
|
|
514
|
+
workspaceSourceRecords: isPlainObject(input.workspaceSourceRecords) ? input.workspaceSourceRecords : {},
|
|
515
|
+
metadataGraph: isPlainObject(input.metadataGraph) ? input.metadataGraph : null,
|
|
516
|
+
};
|
|
517
|
+
const provenance = deriveProvenance(safeInput.workspaceConfig);
|
|
518
|
+
if (provenance.template === TEMPLATE_PROJECT_MANAGEMENT) {
|
|
519
|
+
return deriveProjectManagementActivationState(safeInput);
|
|
520
|
+
}
|
|
521
|
+
return deriveBlankWorkspaceActivationState(safeInput);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export {
|
|
525
|
+
ACTIVATION_KIND,
|
|
526
|
+
ACTIVATION_VERSION,
|
|
527
|
+
TEMPLATE_PROJECT_MANAGEMENT,
|
|
528
|
+
deriveWorkspaceActivationState,
|
|
529
|
+
deriveProjectManagementActivationState,
|
|
530
|
+
deriveBlankWorkspaceActivationState,
|
|
531
|
+
deriveProvenance,
|
|
532
|
+
hasConnectionId,
|
|
533
|
+
hasSourceRecords,
|
|
534
|
+
};
|
|
@@ -417,6 +417,7 @@ function deriveManualObjectTable(object, options = {}) {
|
|
|
417
417
|
source,
|
|
418
418
|
objectType: object.objectType || "custom",
|
|
419
419
|
icon: object.icon || null,
|
|
420
|
+
locked: object.locked !== undefined ? Boolean(object.locked) : Boolean(object.objectType && object.objectType !== "custom"),
|
|
420
421
|
pickerHidden: Boolean(object.pickerHidden),
|
|
421
422
|
columns: hydratedColumns,
|
|
422
423
|
rows: hydratedRows,
|
|
@@ -447,6 +448,7 @@ function deriveManualObjectTable(object, options = {}) {
|
|
|
447
448
|
const HIDDEN_HELPER_OBJECT_IDS = new Set([
|
|
448
449
|
"workspace-helper-sandbox",
|
|
449
450
|
"nav-folders",
|
|
451
|
+
"workspace-ui-cache",
|
|
450
452
|
]);
|
|
451
453
|
|
|
452
454
|
/**
|
|
@@ -888,7 +890,7 @@ const OBJECT_TYPE_PRESETS = {
|
|
|
888
890
|
"lastRunId",
|
|
889
891
|
"lastSourceId",
|
|
890
892
|
"lastResponse",
|
|
891
|
-
"
|
|
893
|
+
"orchestrationConfig",
|
|
892
894
|
"description",
|
|
893
895
|
"resolverTemplateId",
|
|
894
896
|
"connectorKind",
|
|
@@ -941,6 +941,83 @@ function deriveWorkspaceWorkflowActionMetadataItems(workflowNodeItems) {
|
|
|
941
941
|
return { items, warnings: [] };
|
|
942
942
|
}
|
|
943
943
|
|
|
944
|
+
/**
|
|
945
|
+
* Workspace PROVENANCE metadata.
|
|
946
|
+
*
|
|
947
|
+
* Surfaces the `provenance` block of a seeded workspace template (e.g.
|
|
948
|
+
* the Project Management seed) as safe booleans + strings so the customer
|
|
949
|
+
* activation layer can derive setup state without re-parsing the config.
|
|
950
|
+
* The block intentionally only echoes non-secret descriptors:
|
|
951
|
+
*
|
|
952
|
+
* - template slug (e.g. "project-management")
|
|
953
|
+
* - template kind (e.g. "workspace-template")
|
|
954
|
+
* - privacy descriptor (e.g. "sanitized-no-secrets-no-provider-data")
|
|
955
|
+
* - booleans: has provider api-registry row, has any configured
|
|
956
|
+
* connectionId, has any persisted source records, has at least one
|
|
957
|
+
* seeded workflow row, has at least one seeded dashboard.
|
|
958
|
+
*
|
|
959
|
+
* Returns a single-item list so the metadata store can stay shape-stable
|
|
960
|
+
* even when no provenance block exists.
|
|
961
|
+
*/
|
|
962
|
+
function deriveWorkspaceProvenanceMetadataItems(workspaceConfig, workspaceSourceRecords) {
|
|
963
|
+
const safeConfig = isPlainObject(workspaceConfig) ? workspaceConfig : {};
|
|
964
|
+
const provenance = isPlainObject(safeConfig.provenance) ? safeConfig.provenance : null;
|
|
965
|
+
const objects = Array.isArray(safeConfig.dataModel?.objects) ? safeConfig.dataModel.objects : [];
|
|
966
|
+
let apiRegistryRows = 0;
|
|
967
|
+
let nangoRows = 0;
|
|
968
|
+
let connectionsConfigured = 0;
|
|
969
|
+
let sandboxRows = 0;
|
|
970
|
+
for (const object of objects) {
|
|
971
|
+
if (!isPlainObject(object)) continue;
|
|
972
|
+
const rows = Array.isArray(object.rows) ? object.rows : [];
|
|
973
|
+
if (object.objectType === "api-registry") {
|
|
974
|
+
apiRegistryRows += rows.length;
|
|
975
|
+
for (const row of rows) {
|
|
976
|
+
if (!isPlainObject(row)) continue;
|
|
977
|
+
if (safeString(row.connectorKind).trim().toLowerCase() === "nango") nangoRows += 1;
|
|
978
|
+
const raw = row.connectionIds ?? row.connectionId;
|
|
979
|
+
if (Array.isArray(raw)) {
|
|
980
|
+
if (raw.some((entry) => safeString(entry).trim())) connectionsConfigured += 1;
|
|
981
|
+
} else if (safeString(raw).trim()) {
|
|
982
|
+
connectionsConfigured += 1;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (object.objectType === "sandbox-environment") {
|
|
987
|
+
sandboxRows += rows.length;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
let sourceRecordKeys = 0;
|
|
991
|
+
if (isPlainObject(workspaceSourceRecords)) {
|
|
992
|
+
for (const value of Object.values(workspaceSourceRecords)) {
|
|
993
|
+
if (!isPlainObject(value)) continue;
|
|
994
|
+
const count = Number.isFinite(value.recordCount)
|
|
995
|
+
? Number(value.recordCount)
|
|
996
|
+
: Array.isArray(value.records) ? value.records.length : 0;
|
|
997
|
+
if (count > 0) sourceRecordKeys += 1;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
items: [{
|
|
1002
|
+
kind: "workspaceProvenance",
|
|
1003
|
+
id: safeString(provenance?.template).trim() || "blank",
|
|
1004
|
+
metadataId: stableId("provenance", safeString(provenance?.template).trim() || "blank"),
|
|
1005
|
+
template: safeString(provenance?.template).trim() || "blank",
|
|
1006
|
+
templateKind: safeString(provenance?.templateKind).trim(),
|
|
1007
|
+
privacy: safeString(provenance?.privacy).trim(),
|
|
1008
|
+
mirrors: safeString(provenance?.mirrors).trim(),
|
|
1009
|
+
hasProvenance: Boolean(provenance),
|
|
1010
|
+
apiRegistryRows,
|
|
1011
|
+
nangoRows,
|
|
1012
|
+
connectionsConfigured,
|
|
1013
|
+
sandboxRows,
|
|
1014
|
+
hydratedSourceRecordKeys: sourceRecordKeys,
|
|
1015
|
+
hasSeededDashboard: Array.isArray(safeConfig.dashboards) && safeConfig.dashboards.length > 0
|
|
1016
|
+
}],
|
|
1017
|
+
warnings: []
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
944
1021
|
/**
|
|
945
1022
|
* Worker kit metadata.
|
|
946
1023
|
*
|
|
@@ -1073,6 +1150,9 @@ function buildWorkspaceMetadataStore({
|
|
|
1073
1150
|
const workerKits = deriveWorkspaceWorkerKitMetadataItems(safeConfig);
|
|
1074
1151
|
warnings.push(...workerKits.warnings);
|
|
1075
1152
|
|
|
1153
|
+
const provenance = deriveWorkspaceProvenanceMetadataItems(safeConfig, safeSourceRecords);
|
|
1154
|
+
warnings.push(...provenance.warnings);
|
|
1155
|
+
|
|
1076
1156
|
const pipelineHealth = deriveWorkspacePipelineHealthMetadataItems(sandboxes.items, runs.items);
|
|
1077
1157
|
warnings.push(...pipelineHealth.warnings);
|
|
1078
1158
|
|
|
@@ -1098,6 +1178,7 @@ function buildWorkspaceMetadataStore({
|
|
|
1098
1178
|
runs: runs.items,
|
|
1099
1179
|
outputArtifacts: runs.outputArtifacts,
|
|
1100
1180
|
workerKits: workerKits.items,
|
|
1181
|
+
provenance: provenance.items,
|
|
1101
1182
|
pipelineHealth: pipelineHealth.items,
|
|
1102
1183
|
warnings
|
|
1103
1184
|
};
|
|
@@ -1181,6 +1262,7 @@ export {
|
|
|
1181
1262
|
deriveWorkspaceRunRecordMetadataItems,
|
|
1182
1263
|
deriveWorkspaceRunMetadataItems,
|
|
1183
1264
|
deriveWorkspaceWorkerKitMetadataItems,
|
|
1265
|
+
deriveWorkspaceProvenanceMetadataItems,
|
|
1184
1266
|
deriveWorkspacePipelineHealthMetadataItems,
|
|
1185
1267
|
isSecretKey
|
|
1186
1268
|
};
|