@growthub/cli 0.9.8 → 0.9.10
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/README.md +23 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +8 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +9 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/settings/integrations/route.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +4 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1264 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +111 -77
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +1691 -138
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +8 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +9 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +10 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +62 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +220 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +10 -64
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +3 -0
- package/package.json +1 -1
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "workspace-builder-default",
|
|
3
|
-
"name": "Workspace
|
|
4
|
-
"description": "Default no-code composition for the official Growthub Custom Workspace Starter. The interface starts as a
|
|
3
|
+
"name": "Growthub Workspace",
|
|
4
|
+
"description": "Default no-code composition for the official Growthub Custom Workspace Starter. The interface starts as a governed dashboard workspace and can be customized by editing this file or by future governed UI controls.",
|
|
5
|
+
"branding": {
|
|
6
|
+
"name": "Growthub Workspace",
|
|
7
|
+
"logoUrl": "",
|
|
8
|
+
"accent": "#3f68ff"
|
|
9
|
+
},
|
|
5
10
|
"capabilities": [
|
|
6
11
|
"dashboards",
|
|
7
12
|
"canvas",
|
|
@@ -47,7 +52,7 @@
|
|
|
47
52
|
},
|
|
48
53
|
"provenance": {
|
|
49
54
|
"createdBy": "cli",
|
|
50
|
-
"mirrors": "growthub-
|
|
55
|
+
"mirrors": "growthub-custom-workspace-starter-v1",
|
|
51
56
|
"note": "Shipped with growthub-custom-workspace-starter-v1; safe to edit inside a governed fork and validate through the starter export smoke path."
|
|
52
57
|
}
|
|
53
58
|
}
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
function readAdapterConfig() {
|
|
2
2
|
return {
|
|
3
3
|
deployTarget: "vercel",
|
|
4
|
-
dataAdapter: readEnum("AGENCY_PORTAL_DATA_ADAPTER", ["postgres", "qstash-kv", "provider-managed"], "provider-managed"),
|
|
5
|
-
authAdapter: readEnum("AGENCY_PORTAL_AUTH_ADAPTER", ["oidc", "clerk", "authjs", "provider-managed"], "provider-managed"),
|
|
6
|
-
paymentAdapter: readEnum("AGENCY_PORTAL_PAYMENT_ADAPTER", ["none", "stripe", "polar"], "none"),
|
|
7
|
-
integrationAdapter: readEnum("AGENCY_PORTAL_INTEGRATION_ADAPTER", ["growthub-bridge", "byo-api-key", "static"], "static"),
|
|
8
|
-
reportingAdapter: process.env.AGENCY_PORTAL_REPORTING_ADAPTER || void 0,
|
|
4
|
+
dataAdapter: readEnum(["GROWTHUB_WORKSPACE_DATA_ADAPTER", "AGENCY_PORTAL_DATA_ADAPTER"], ["postgres", "qstash-kv", "provider-managed"], "provider-managed"),
|
|
5
|
+
authAdapter: readEnum(["GROWTHUB_WORKSPACE_AUTH_ADAPTER", "AGENCY_PORTAL_AUTH_ADAPTER"], ["oidc", "clerk", "authjs", "provider-managed"], "provider-managed"),
|
|
6
|
+
paymentAdapter: readEnum(["GROWTHUB_WORKSPACE_PAYMENT_ADAPTER", "AGENCY_PORTAL_PAYMENT_ADAPTER"], ["none", "stripe", "polar"], "none"),
|
|
7
|
+
integrationAdapter: readEnum(["GROWTHUB_WORKSPACE_INTEGRATION_ADAPTER", "AGENCY_PORTAL_INTEGRATION_ADAPTER"], ["growthub-bridge", "byo-api-key", "static"], "static"),
|
|
8
|
+
reportingAdapter: process.env.GROWTHUB_WORKSPACE_REPORTING_ADAPTER || process.env.AGENCY_PORTAL_REPORTING_ADAPTER || void 0,
|
|
9
9
|
growthubBridge: {
|
|
10
10
|
baseUrl: process.env.GROWTHUB_BRIDGE_BASE_URL || void 0,
|
|
11
11
|
integrationsPath: process.env.GROWTHUB_BRIDGE_INTEGRATIONS_PATH || "/api/mcp/accounts",
|
|
@@ -17,8 +17,10 @@ function readAdapterConfig() {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
function readEnum(
|
|
21
|
-
const
|
|
20
|
+
function readEnum(keys, allowed, fallback) {
|
|
21
|
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
22
|
+
const key = keyList.find((candidate) => process.env[candidate]);
|
|
23
|
+
const value = key ? process.env[key] : undefined;
|
|
22
24
|
if (!value) return fallback;
|
|
23
25
|
if (allowed.includes(value)) return value;
|
|
24
26
|
throw new Error(`${key} must be one of: ${allowed.join(", ")}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
governedWorkspaceIntegrationCatalog
|
|
4
4
|
} from "@/lib/domain/integrations";
|
|
5
5
|
import {
|
|
6
6
|
normalizeGrowthubBridgePayload
|
|
@@ -19,7 +19,7 @@ function describeIntegrationAdapter() {
|
|
|
19
19
|
return {
|
|
20
20
|
id: "byo-api-key",
|
|
21
21
|
label: "Bring your own API key",
|
|
22
|
-
requiredEnv: ["
|
|
22
|
+
requiredEnv: ["GROWTHUB_WORKSPACE_BYO_CONNECTIONS_JSON"],
|
|
23
23
|
authority: "workspace-env"
|
|
24
24
|
};
|
|
25
25
|
}
|
|
@@ -30,16 +30,16 @@ function describeIntegrationAdapter() {
|
|
|
30
30
|
authority: "local-catalog"
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
async function
|
|
33
|
+
async function listGovernedWorkspaceIntegrations() {
|
|
34
34
|
const config = readAdapterConfig();
|
|
35
35
|
if (config.integrationAdapter !== "growthub-bridge") {
|
|
36
36
|
if (config.integrationAdapter === "byo-api-key") {
|
|
37
37
|
return mergeBringYourOwnRows(readBringYourOwnRows());
|
|
38
38
|
}
|
|
39
|
-
return
|
|
39
|
+
return governedWorkspaceIntegrationCatalog;
|
|
40
40
|
}
|
|
41
41
|
if (!config.growthubBridge.baseUrl || !process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN) {
|
|
42
|
-
return
|
|
42
|
+
return governedWorkspaceIntegrationCatalog;
|
|
43
43
|
}
|
|
44
44
|
const url = new URL(config.growthubBridge.integrationsPath, config.growthubBridge.baseUrl);
|
|
45
45
|
const headers = {
|
|
@@ -54,7 +54,7 @@ async function listAgencyPortalIntegrations() {
|
|
|
54
54
|
next: { revalidate: 30 }
|
|
55
55
|
});
|
|
56
56
|
if (!response.ok) {
|
|
57
|
-
return
|
|
57
|
+
return governedWorkspaceIntegrationCatalog;
|
|
58
58
|
}
|
|
59
59
|
const payload = await response.json();
|
|
60
60
|
const merged = mergeBridgeRows(normalizeGrowthubBridgePayload(payload));
|
|
@@ -80,7 +80,7 @@ function applyApiKeyOverlays(integrations, config) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
function readBringYourOwnRows() {
|
|
83
|
-
const raw = process.env.AGENCY_PORTAL_BYO_CONNECTIONS_JSON;
|
|
83
|
+
const raw = process.env.GROWTHUB_WORKSPACE_BYO_CONNECTIONS_JSON || process.env.AGENCY_PORTAL_BYO_CONNECTIONS_JSON;
|
|
84
84
|
const rows = [];
|
|
85
85
|
if (process.env.WINDSOR_API_KEY) {
|
|
86
86
|
rows.push({
|
|
@@ -129,7 +129,7 @@ function mergeBringYourOwnRows(rows) {
|
|
|
129
129
|
}
|
|
130
130
|
function mergeBridgeRows(rows) {
|
|
131
131
|
const seenProviders = /* @__PURE__ */ new Set();
|
|
132
|
-
const merged =
|
|
132
|
+
const merged = governedWorkspaceIntegrationCatalog.map((catalogItem) => {
|
|
133
133
|
const row = rows.find((item) => {
|
|
134
134
|
const provider = item.provider || item.id;
|
|
135
135
|
return provider === catalogItem.provider || item.id === catalogItem.id;
|
|
@@ -160,7 +160,7 @@ function mergeBridgeRows(rows) {
|
|
|
160
160
|
const provider = row.provider || row.id;
|
|
161
161
|
if (!provider) return false;
|
|
162
162
|
if (seenProviders.has(provider)) return false;
|
|
163
|
-
return !
|
|
163
|
+
return !governedWorkspaceIntegrationCatalog.some((item) => item.provider === provider || item.id === row.id);
|
|
164
164
|
});
|
|
165
165
|
return [...merged, ...discoveredRows.map(toDiscoveredIntegration)];
|
|
166
166
|
}
|
|
@@ -194,5 +194,5 @@ function toDiscoveredIntegration(row) {
|
|
|
194
194
|
}
|
|
195
195
|
export {
|
|
196
196
|
describeIntegrationAdapter,
|
|
197
|
-
|
|
197
|
+
listGovernedWorkspaceIntegrations
|
|
198
198
|
};
|
|
@@ -172,7 +172,7 @@ const workspaceIntegrations = [
|
|
|
172
172
|
setupMode: "hosted-authority"
|
|
173
173
|
}
|
|
174
174
|
];
|
|
175
|
-
const
|
|
175
|
+
const governedWorkspaceIntegrationCatalog = [...dataSources, ...workspaceIntegrations];
|
|
176
176
|
function groupIntegrationsByLane(integrations) {
|
|
177
177
|
return {
|
|
178
178
|
dataSources: integrations.filter((item) => item.lane === "data-source"),
|
|
@@ -180,6 +180,6 @@ function groupIntegrationsByLane(integrations) {
|
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
182
|
export {
|
|
183
|
-
|
|
183
|
+
governedWorkspaceIntegrationCatalog,
|
|
184
184
|
groupIntegrationsByLane
|
|
185
185
|
};
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace persistence — thin adapter boundary.
|
|
3
|
+
*
|
|
4
|
+
* Modes (Workspace Builder Runtime V1 contract — `docs/WORKSPACE_BUILDER_RUNTIME_V1.md`):
|
|
5
|
+
*
|
|
6
|
+
* - `filesystem` Local Next.js dev or any runtime that opts in via
|
|
7
|
+
* `WORKSPACE_CONFIG_ALLOW_FS_WRITE=true`. Save writes
|
|
8
|
+
* `growthub.config.json` on disk.
|
|
9
|
+
* - `read-only` Vercel / Netlify-style runtimes where the bundle is
|
|
10
|
+
* immutable. `PATCH /api/workspace` returns 409 with the
|
|
11
|
+
* same `guidance` string the no-code Save UI surfaces.
|
|
12
|
+
* - `database` (future) Reserved adapter slot. Not implemented in V1 — the
|
|
13
|
+
* return shape is stable so a hosted adapter can be wired
|
|
14
|
+
* without changing UI or API contracts.
|
|
15
|
+
*
|
|
16
|
+
* `describePersistenceMode()` is the single source of truth the GET payload,
|
|
17
|
+
* the no-code Settings/Readiness panel, and the PATCH 409 path all read.
|
|
18
|
+
*/
|
|
19
|
+
|
|
1
20
|
import { promises as fs } from "node:fs";
|
|
2
21
|
import path from "node:path";
|
|
3
22
|
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
@@ -8,6 +27,15 @@ import {
|
|
|
8
27
|
validateWorkspaceConfig
|
|
9
28
|
} from "@/lib/workspace-schema";
|
|
10
29
|
|
|
30
|
+
const PERSISTENCE_ADAPTERS = Object.freeze({
|
|
31
|
+
FILESYSTEM: "filesystem",
|
|
32
|
+
READ_ONLY: "read-only",
|
|
33
|
+
DATABASE: "database"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const READ_ONLY_GUIDANCE =
|
|
37
|
+
"Edit growthub.config.json locally, or set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true on a writable runtime.";
|
|
38
|
+
|
|
11
39
|
function resolveWorkspaceConfigPath() {
|
|
12
40
|
return path.resolve(/*turbopackIgnore: true*/ process.cwd(), "growthub.config.json");
|
|
13
41
|
}
|
|
@@ -18,23 +46,47 @@ async function readWorkspaceConfig() {
|
|
|
18
46
|
return JSON.parse(raw);
|
|
19
47
|
}
|
|
20
48
|
|
|
49
|
+
/**
|
|
50
|
+
* `canSave` is a *logical* statement about adapter mode, not a *filesystem*
|
|
51
|
+
* guarantee. A `filesystem`-mode workspace whose `growthub.config.json` is
|
|
52
|
+
* actually read-only on disk (permission denied, RO mount) will still report
|
|
53
|
+
* `canSave: true`; the no-code Save UI surfaces the underlying fs error
|
|
54
|
+
* (workspace-builder.jsx#save → setConfigMessage) and PATCH returns 500 with
|
|
55
|
+
* the original error message. Read-only-mode 409 is the *contractual* not-save
|
|
56
|
+
* path and gets verbatim `guidance` instead.
|
|
57
|
+
*/
|
|
21
58
|
function describePersistenceMode() {
|
|
22
|
-
const target = process.env.AGENCY_PORTAL_DEPLOY_TARGET || "vercel";
|
|
59
|
+
const target = process.env.GROWTHUB_WORKSPACE_DEPLOY_TARGET || process.env.AGENCY_PORTAL_DEPLOY_TARGET || "vercel";
|
|
23
60
|
const isReadOnlyDeploy = target === "vercel" || target === "netlify";
|
|
24
61
|
const allowFsWrite = process.env.WORKSPACE_CONFIG_ALLOW_FS_WRITE === "true";
|
|
62
|
+
const baseFilesystem = (reason) => ({
|
|
63
|
+
mode: PERSISTENCE_ADAPTERS.FILESYSTEM,
|
|
64
|
+
adapter: PERSISTENCE_ADAPTERS.FILESYSTEM,
|
|
65
|
+
canSave: true,
|
|
66
|
+
saveLabel: "Save writes growthub.config.json on disk.",
|
|
67
|
+
reason,
|
|
68
|
+
nextAction: null,
|
|
69
|
+
guidance: null
|
|
70
|
+
});
|
|
25
71
|
if (allowFsWrite) {
|
|
26
|
-
return
|
|
72
|
+
return baseFilesystem("WORKSPACE_CONFIG_ALLOW_FS_WRITE=true");
|
|
27
73
|
}
|
|
28
74
|
if (process.env.NODE_ENV === "development") {
|
|
29
|
-
return
|
|
75
|
+
return baseFilesystem("Local Next.js development");
|
|
30
76
|
}
|
|
31
77
|
if (isReadOnlyDeploy) {
|
|
78
|
+
const reason = `Deploy target ${target} treats the bundle as read-only. Set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true on a writable runtime, or wire a hosted persistence adapter.`;
|
|
32
79
|
return {
|
|
33
|
-
mode:
|
|
34
|
-
|
|
80
|
+
mode: PERSISTENCE_ADAPTERS.READ_ONLY,
|
|
81
|
+
adapter: PERSISTENCE_ADAPTERS.READ_ONLY,
|
|
82
|
+
canSave: false,
|
|
83
|
+
saveLabel: "Save is disabled in this runtime.",
|
|
84
|
+
reason,
|
|
85
|
+
nextAction: "Set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true or connect a persistence adapter.",
|
|
86
|
+
guidance: READ_ONLY_GUIDANCE
|
|
35
87
|
};
|
|
36
88
|
}
|
|
37
|
-
return
|
|
89
|
+
return baseFilesystem("Local development");
|
|
38
90
|
}
|
|
39
91
|
|
|
40
92
|
function applyPatch(currentConfig, patch) {
|
|
@@ -76,10 +128,11 @@ function applyPatch(currentConfig, patch) {
|
|
|
76
128
|
async function writeWorkspaceConfig(patch) {
|
|
77
129
|
const persistence = describePersistenceMode();
|
|
78
130
|
const adapter = readAdapterConfig();
|
|
79
|
-
if (persistence.mode !==
|
|
131
|
+
if (persistence.mode !== PERSISTENCE_ADAPTERS.FILESYSTEM || !persistence.canSave) {
|
|
80
132
|
const error = new Error(persistence.reason);
|
|
81
133
|
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
82
134
|
error.adapter = adapter.integrationAdapter;
|
|
135
|
+
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
83
136
|
throw error;
|
|
84
137
|
}
|
|
85
138
|
const current = await readWorkspaceConfig();
|
|
@@ -104,6 +157,8 @@ export {
|
|
|
104
157
|
GRID_COLUMNS,
|
|
105
158
|
GRID_ROWS,
|
|
106
159
|
KNOWN_WIDGET_KINDS,
|
|
160
|
+
PERSISTENCE_ADAPTERS,
|
|
161
|
+
READ_ONLY_GUIDANCE,
|
|
107
162
|
describePersistenceMode,
|
|
108
163
|
readWorkspaceConfig,
|
|
109
164
|
resolveWorkspaceConfigPath,
|
|
@@ -1,8 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Config Contract V1 — local source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Authoritative reference: `docs/WORKSPACE_CONFIG_CONTRACT_V1.md`.
|
|
5
|
+
* Companion runtime doc: `docs/WORKSPACE_BUILDER_RUNTIME_V1.md`.
|
|
6
|
+
*
|
|
7
|
+
* This file owns the workspace config validator. The persisted file lives at
|
|
8
|
+
* `<workspace>/growthub.config.json`. The PATCH allowlist on `/api/workspace`
|
|
9
|
+
* is permanently restricted to:
|
|
10
|
+
*
|
|
11
|
+
* - `dashboards` dashboard rows (id, name, status, tabs, activeTabId)
|
|
12
|
+
* - `widgetTypes` palette of allowed widget kinds (label/icon)
|
|
13
|
+
* - `canvas` active canvas: layout, single-tab `widgets[]`, or
|
|
14
|
+
* multi-tab `tabs[]` + `activeTabId`, plus `bindings`
|
|
15
|
+
*
|
|
16
|
+
* Other top-level fields (`id`, `name`, `description`, `capabilities`,
|
|
17
|
+
* `branding`, `pipelines`, `integrations`, `provenance`) are preserved
|
|
18
|
+
* round-trip but cannot be mutated through PATCH. The validator rejects
|
|
19
|
+
* unknown fields inside the three allowlisted sections.
|
|
20
|
+
*
|
|
21
|
+
* Canonical canvas shape (mutually exclusive — never both at once):
|
|
22
|
+
*
|
|
23
|
+
* single-tab → `canvas.widgets[]` (DO NOT also serialize tabs)
|
|
24
|
+
* multi-tab → `canvas.tabs[]` + `activeTabId` (DO NOT also serialize widgets)
|
|
25
|
+
*
|
|
26
|
+
* Import/export envelope: `{ version: 1, kind: "growthub-workspace-template",
|
|
27
|
+
* exportedAt, source, name, description, payload }`. Raw `{dashboards,
|
|
28
|
+
* widgetTypes, canvas}` payloads are also accepted for back-compat.
|
|
29
|
+
*
|
|
30
|
+
* Widget grid is a strict 12-column × 16-row fixed lattice with
|
|
31
|
+
* non-overlapping integer rectangles. Widget IDs are minted at clone time —
|
|
32
|
+
* template widgets intentionally omit `id`.
|
|
33
|
+
*
|
|
34
|
+
* Validation errors are surfaced as readable strings on the thrown error
|
|
35
|
+
* (`error.details: string[]`) so agents and the no-code Save UI can round-trip
|
|
36
|
+
* them without parsing a stack trace.
|
|
37
|
+
*/
|
|
38
|
+
|
|
1
39
|
const GRID_COLUMNS = 12;
|
|
2
40
|
const GRID_ROWS = 16;
|
|
3
41
|
const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
|
|
4
42
|
const KNOWN_FIELDS = ["dashboards", "widgetTypes", "canvas"];
|
|
5
|
-
const KNOWN_DATA_BINDING_MODES = ["manual", "json", "csv"];
|
|
43
|
+
const KNOWN_DATA_BINDING_MODES = ["manual", "json", "csv", "integration"];
|
|
44
|
+
const KNOWN_CHART_TYPES = ["bar-vertical", "bar-horizontal", "line", "pie", "sum", "gauge"];
|
|
45
|
+
const KNOWN_FILTER_OPERATORS = ["eq", "ne", "contains", "gt", "lt", "isEmpty", "isNotEmpty"];
|
|
46
|
+
const KNOWN_FILTER_CONJUNCTIONS = ["and", "or"];
|
|
47
|
+
const KNOWN_SORT_DIRECTIONS = ["asc", "desc"];
|
|
48
|
+
const KNOWN_AGGREGATIONS = ["sum", "avg", "count", "min", "max"];
|
|
6
49
|
const WORKSPACE_TEMPLATE_KIND = "growthub-workspace-template";
|
|
7
50
|
const WORKSPACE_TEMPLATE_VERSION = 1;
|
|
8
51
|
const WORKSPACE_TEMPLATE_SOURCE = "growthub-custom-workspace-starter-v1";
|
|
@@ -23,7 +66,12 @@ const WIDGET_SCHEMA_CONTRACTS = {
|
|
|
23
66
|
config: "kind-specific config object"
|
|
24
67
|
},
|
|
25
68
|
ChartWidgetConfig: {
|
|
26
|
-
values: "number[]",
|
|
69
|
+
values: "number[] (legacy preserved)",
|
|
70
|
+
chartType: `${KNOWN_CHART_TYPES.join(" | ")} optional, defaults to bar-vertical`,
|
|
71
|
+
xAxis: "ChartAxisConfig optional",
|
|
72
|
+
yAxis: "ChartAxisConfig optional",
|
|
73
|
+
style: "ChartStyleConfig optional",
|
|
74
|
+
filter: "FilterConfig optional",
|
|
27
75
|
binding: "StaticDataBinding optional"
|
|
28
76
|
},
|
|
29
77
|
ViewWidgetConfig: {
|
|
@@ -31,8 +79,42 @@ const WIDGET_SCHEMA_CONTRACTS = {
|
|
|
31
79
|
layout: "Table",
|
|
32
80
|
columns: "string[]",
|
|
33
81
|
rows: "record[]",
|
|
82
|
+
fieldSettings: "FieldSettingsConfig optional (hidden[], order[])",
|
|
83
|
+
sort: "SortClause[] optional ({ fieldId, direction })",
|
|
84
|
+
filter: "FilterConfig optional ({ op, clauses[] })",
|
|
34
85
|
binding: "StaticDataBinding optional"
|
|
35
86
|
},
|
|
87
|
+
ChartAxisConfig: {
|
|
88
|
+
field: "string optional",
|
|
89
|
+
sort: "string optional (asc | desc | position)",
|
|
90
|
+
aggregation: `${KNOWN_AGGREGATIONS.join(" | ")} optional`,
|
|
91
|
+
groupBy: "string optional",
|
|
92
|
+
omitZero: "boolean optional",
|
|
93
|
+
min: "string | number optional",
|
|
94
|
+
max: "string | number optional"
|
|
95
|
+
},
|
|
96
|
+
ChartStyleConfig: {
|
|
97
|
+
colors: "string optional (auto | manual swatch label)",
|
|
98
|
+
axisName: "string optional",
|
|
99
|
+
dataLabels: "boolean optional"
|
|
100
|
+
},
|
|
101
|
+
FieldSettingsConfig: {
|
|
102
|
+
hidden: "string[] of column names hidden from preview",
|
|
103
|
+
order: "string[] of column names defining custom order"
|
|
104
|
+
},
|
|
105
|
+
SortClause: {
|
|
106
|
+
fieldId: "non-empty string (column name)",
|
|
107
|
+
direction: KNOWN_SORT_DIRECTIONS.join(" | ")
|
|
108
|
+
},
|
|
109
|
+
FilterConfig: {
|
|
110
|
+
op: KNOWN_FILTER_CONJUNCTIONS.join(" | "),
|
|
111
|
+
clauses: "FilterClause[]"
|
|
112
|
+
},
|
|
113
|
+
FilterClause: {
|
|
114
|
+
fieldId: "non-empty string (column name)",
|
|
115
|
+
operator: KNOWN_FILTER_OPERATORS.join(" | "),
|
|
116
|
+
value: "string | number | boolean optional"
|
|
117
|
+
},
|
|
36
118
|
IframeWidgetConfig: {
|
|
37
119
|
url: "string"
|
|
38
120
|
},
|
|
@@ -295,6 +377,127 @@ function validateStaticDataBinding(binding, path, errors) {
|
|
|
295
377
|
if (binding.csv !== undefined && typeof binding.csv !== "string") {
|
|
296
378
|
errors.push(`${path}.csv must be a string`);
|
|
297
379
|
}
|
|
380
|
+
if (binding.integrationId !== undefined && typeof binding.integrationId !== "string") {
|
|
381
|
+
errors.push(`${path}.integrationId must be a string`);
|
|
382
|
+
}
|
|
383
|
+
if (binding.lane !== undefined && typeof binding.lane !== "string") {
|
|
384
|
+
errors.push(`${path}.lane must be a string`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function validateFieldSettings(fieldSettings, path, errors) {
|
|
389
|
+
if (fieldSettings === undefined) return;
|
|
390
|
+
if (!isPlainObject(fieldSettings)) {
|
|
391
|
+
errors.push(`${path} must be a plain object`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (fieldSettings.hidden !== undefined) validateStringArray(fieldSettings.hidden, `${path}.hidden`, errors);
|
|
395
|
+
if (fieldSettings.order !== undefined) validateStringArray(fieldSettings.order, `${path}.order`, errors);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function validateSortClauses(sort, path, errors) {
|
|
399
|
+
if (sort === undefined) return;
|
|
400
|
+
if (!Array.isArray(sort)) {
|
|
401
|
+
errors.push(`${path} must be an array`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
sort.forEach((clause, index) => {
|
|
405
|
+
const prefix = `${path}[${index}]`;
|
|
406
|
+
if (!isPlainObject(clause)) {
|
|
407
|
+
errors.push(`${prefix} must be a plain object`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (typeof clause.fieldId !== "string" || !clause.fieldId) {
|
|
411
|
+
errors.push(`${prefix}.fieldId must be a non-empty string`);
|
|
412
|
+
}
|
|
413
|
+
if (clause.direction !== undefined && !KNOWN_SORT_DIRECTIONS.includes(clause.direction)) {
|
|
414
|
+
errors.push(`${prefix}.direction must be one of ${KNOWN_SORT_DIRECTIONS.join(", ")}`);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function validateFilterClauses(filter, path, errors) {
|
|
420
|
+
if (filter === undefined) return;
|
|
421
|
+
if (!isPlainObject(filter)) {
|
|
422
|
+
errors.push(`${path} must be a plain object`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (filter.op !== undefined && !KNOWN_FILTER_CONJUNCTIONS.includes(filter.op)) {
|
|
426
|
+
errors.push(`${path}.op must be one of ${KNOWN_FILTER_CONJUNCTIONS.join(", ")}`);
|
|
427
|
+
}
|
|
428
|
+
if (filter.clauses !== undefined) {
|
|
429
|
+
if (!Array.isArray(filter.clauses)) {
|
|
430
|
+
errors.push(`${path}.clauses must be an array`);
|
|
431
|
+
} else {
|
|
432
|
+
filter.clauses.forEach((clause, index) => {
|
|
433
|
+
const prefix = `${path}.clauses[${index}]`;
|
|
434
|
+
if (!isPlainObject(clause)) {
|
|
435
|
+
errors.push(`${prefix} must be a plain object`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (typeof clause.fieldId !== "string" || !clause.fieldId) {
|
|
439
|
+
errors.push(`${prefix}.fieldId must be a non-empty string`);
|
|
440
|
+
}
|
|
441
|
+
if (clause.operator !== undefined && !KNOWN_FILTER_OPERATORS.includes(clause.operator)) {
|
|
442
|
+
errors.push(`${prefix}.operator must be one of ${KNOWN_FILTER_OPERATORS.join(", ")}`);
|
|
443
|
+
}
|
|
444
|
+
if (
|
|
445
|
+
clause.value !== undefined &&
|
|
446
|
+
typeof clause.value !== "string" &&
|
|
447
|
+
typeof clause.value !== "number" &&
|
|
448
|
+
typeof clause.value !== "boolean"
|
|
449
|
+
) {
|
|
450
|
+
errors.push(`${prefix}.value must be a string, number, or boolean`);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function validateChartAxis(axis, path, errors) {
|
|
458
|
+
if (axis === undefined) return;
|
|
459
|
+
if (!isPlainObject(axis)) {
|
|
460
|
+
errors.push(`${path} must be a plain object`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (axis.field !== undefined && typeof axis.field !== "string") {
|
|
464
|
+
errors.push(`${path}.field must be a string`);
|
|
465
|
+
}
|
|
466
|
+
if (axis.sort !== undefined && typeof axis.sort !== "string") {
|
|
467
|
+
errors.push(`${path}.sort must be a string`);
|
|
468
|
+
}
|
|
469
|
+
if (axis.aggregation !== undefined && !KNOWN_AGGREGATIONS.includes(axis.aggregation)) {
|
|
470
|
+
errors.push(`${path}.aggregation must be one of ${KNOWN_AGGREGATIONS.join(", ")}`);
|
|
471
|
+
}
|
|
472
|
+
if (axis.groupBy !== undefined && typeof axis.groupBy !== "string") {
|
|
473
|
+
errors.push(`${path}.groupBy must be a string`);
|
|
474
|
+
}
|
|
475
|
+
if (axis.omitZero !== undefined && typeof axis.omitZero !== "boolean") {
|
|
476
|
+
errors.push(`${path}.omitZero must be a boolean`);
|
|
477
|
+
}
|
|
478
|
+
if (axis.min !== undefined && typeof axis.min !== "string" && typeof axis.min !== "number") {
|
|
479
|
+
errors.push(`${path}.min must be a string or number`);
|
|
480
|
+
}
|
|
481
|
+
if (axis.max !== undefined && typeof axis.max !== "string" && typeof axis.max !== "number") {
|
|
482
|
+
errors.push(`${path}.max must be a string or number`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function validateChartStyle(style, path, errors) {
|
|
487
|
+
if (style === undefined) return;
|
|
488
|
+
if (!isPlainObject(style)) {
|
|
489
|
+
errors.push(`${path} must be a plain object`);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (style.colors !== undefined && typeof style.colors !== "string") {
|
|
493
|
+
errors.push(`${path}.colors must be a string`);
|
|
494
|
+
}
|
|
495
|
+
if (style.axisName !== undefined && typeof style.axisName !== "string") {
|
|
496
|
+
errors.push(`${path}.axisName must be a string`);
|
|
497
|
+
}
|
|
498
|
+
if (style.dataLabels !== undefined && typeof style.dataLabels !== "boolean") {
|
|
499
|
+
errors.push(`${path}.dataLabels must be a boolean`);
|
|
500
|
+
}
|
|
298
501
|
}
|
|
299
502
|
|
|
300
503
|
function validateWidgetConfig(kind, config, path, errors) {
|
|
@@ -315,6 +518,13 @@ function validateWidgetConfig(kind, config, path, errors) {
|
|
|
315
518
|
});
|
|
316
519
|
}
|
|
317
520
|
}
|
|
521
|
+
if (config.chartType !== undefined && !KNOWN_CHART_TYPES.includes(config.chartType)) {
|
|
522
|
+
errors.push(`${path}.chartType must be one of ${KNOWN_CHART_TYPES.join(", ")}`);
|
|
523
|
+
}
|
|
524
|
+
validateChartAxis(config.xAxis, `${path}.xAxis`, errors);
|
|
525
|
+
validateChartAxis(config.yAxis, `${path}.yAxis`, errors);
|
|
526
|
+
validateChartStyle(config.style, `${path}.style`, errors);
|
|
527
|
+
validateFilterClauses(config.filter, `${path}.filter`, errors);
|
|
318
528
|
validateStaticDataBinding(config.binding, `${path}.binding`, errors);
|
|
319
529
|
}
|
|
320
530
|
if (kind === "view") {
|
|
@@ -322,6 +532,9 @@ function validateWidgetConfig(kind, config, path, errors) {
|
|
|
322
532
|
if (config.layout !== undefined && config.layout !== "Table") errors.push(`${path}.layout must be Table`);
|
|
323
533
|
if (config.columns !== undefined) validateStringArray(config.columns, `${path}.columns`, errors);
|
|
324
534
|
if (config.rows !== undefined && !Array.isArray(config.rows)) errors.push(`${path}.rows must be an array`);
|
|
535
|
+
validateFieldSettings(config.fieldSettings, `${path}.fieldSettings`, errors);
|
|
536
|
+
validateSortClauses(config.sort, `${path}.sort`, errors);
|
|
537
|
+
validateFilterClauses(config.filter, `${path}.filter`, errors);
|
|
325
538
|
validateStaticDataBinding(config.binding, `${path}.binding`, errors);
|
|
326
539
|
}
|
|
327
540
|
if (kind === "iframe" && config.url !== undefined && typeof config.url !== "string") {
|
|
@@ -749,8 +962,13 @@ export {
|
|
|
749
962
|
DASHBOARD_TEMPLATES,
|
|
750
963
|
GRID_COLUMNS,
|
|
751
964
|
GRID_ROWS,
|
|
965
|
+
KNOWN_AGGREGATIONS,
|
|
966
|
+
KNOWN_CHART_TYPES,
|
|
752
967
|
KNOWN_DATA_BINDING_MODES,
|
|
753
968
|
KNOWN_FIELDS,
|
|
969
|
+
KNOWN_FILTER_CONJUNCTIONS,
|
|
970
|
+
KNOWN_FILTER_OPERATORS,
|
|
971
|
+
KNOWN_SORT_DIRECTIONS,
|
|
754
972
|
KNOWN_WIDGET_KINDS,
|
|
755
973
|
SAMPLE_DATA_BINDINGS,
|
|
756
974
|
SAMPLE_VIEW_ROWS,
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json
CHANGED
|
@@ -8,15 +8,10 @@
|
|
|
8
8
|
"name": "growthub-workspace-app",
|
|
9
9
|
"version": "1.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
+
"lucide-react": "^0.468.0",
|
|
11
12
|
"next": "16.2.4",
|
|
12
13
|
"react": "19.2.4",
|
|
13
14
|
"react-dom": "19.2.4"
|
|
14
|
-
},
|
|
15
|
-
"devDependencies": {
|
|
16
|
-
"@types/node": "^20",
|
|
17
|
-
"@types/react": "^19",
|
|
18
|
-
"@types/react-dom": "^19",
|
|
19
|
-
"typescript": "^5"
|
|
20
15
|
}
|
|
21
16
|
},
|
|
22
17
|
"node_modules/@emnapi/runtime": {
|
|
@@ -638,36 +633,6 @@
|
|
|
638
633
|
"tslib": "^2.8.0"
|
|
639
634
|
}
|
|
640
635
|
},
|
|
641
|
-
"node_modules/@types/node": {
|
|
642
|
-
"version": "20.19.39",
|
|
643
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
|
644
|
-
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
|
645
|
-
"dev": true,
|
|
646
|
-
"license": "MIT",
|
|
647
|
-
"dependencies": {
|
|
648
|
-
"undici-types": "~6.21.0"
|
|
649
|
-
}
|
|
650
|
-
},
|
|
651
|
-
"node_modules/@types/react": {
|
|
652
|
-
"version": "19.2.14",
|
|
653
|
-
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
|
654
|
-
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
|
655
|
-
"dev": true,
|
|
656
|
-
"license": "MIT",
|
|
657
|
-
"dependencies": {
|
|
658
|
-
"csstype": "^3.2.2"
|
|
659
|
-
}
|
|
660
|
-
},
|
|
661
|
-
"node_modules/@types/react-dom": {
|
|
662
|
-
"version": "19.2.3",
|
|
663
|
-
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
|
664
|
-
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
|
665
|
-
"dev": true,
|
|
666
|
-
"license": "MIT",
|
|
667
|
-
"peerDependencies": {
|
|
668
|
-
"@types/react": "^19.2.0"
|
|
669
|
-
}
|
|
670
|
-
},
|
|
671
636
|
"node_modules/baseline-browser-mapping": {
|
|
672
637
|
"version": "2.10.21",
|
|
673
638
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
|
|
@@ -706,13 +671,6 @@
|
|
|
706
671
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
|
707
672
|
"license": "MIT"
|
|
708
673
|
},
|
|
709
|
-
"node_modules/csstype": {
|
|
710
|
-
"version": "3.2.3",
|
|
711
|
-
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
|
712
|
-
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
|
713
|
-
"dev": true,
|
|
714
|
-
"license": "MIT"
|
|
715
|
-
},
|
|
716
674
|
"node_modules/detect-libc": {
|
|
717
675
|
"version": "2.1.2",
|
|
718
676
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
@@ -723,6 +681,15 @@
|
|
|
723
681
|
"node": ">=8"
|
|
724
682
|
}
|
|
725
683
|
},
|
|
684
|
+
"node_modules/lucide-react": {
|
|
685
|
+
"version": "0.468.0",
|
|
686
|
+
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
|
|
687
|
+
"integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
|
|
688
|
+
"license": "ISC",
|
|
689
|
+
"peerDependencies": {
|
|
690
|
+
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
|
691
|
+
}
|
|
692
|
+
},
|
|
726
693
|
"node_modules/nanoid": {
|
|
727
694
|
"version": "3.3.11",
|
|
728
695
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
|
@@ -950,27 +917,6 @@
|
|
|
950
917
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
951
918
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
|
952
919
|
"license": "0BSD"
|
|
953
|
-
},
|
|
954
|
-
"node_modules/typescript": {
|
|
955
|
-
"version": "5.9.3",
|
|
956
|
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
957
|
-
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
958
|
-
"dev": true,
|
|
959
|
-
"license": "Apache-2.0",
|
|
960
|
-
"bin": {
|
|
961
|
-
"tsc": "bin/tsc",
|
|
962
|
-
"tsserver": "bin/tsserver"
|
|
963
|
-
},
|
|
964
|
-
"engines": {
|
|
965
|
-
"node": ">=14.17"
|
|
966
|
-
}
|
|
967
|
-
},
|
|
968
|
-
"node_modules/undici-types": {
|
|
969
|
-
"version": "6.21.0",
|
|
970
|
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
|
971
|
-
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
|
972
|
-
"dev": true,
|
|
973
|
-
"license": "MIT"
|
|
974
920
|
}
|
|
975
921
|
}
|
|
976
922
|
}
|
|
@@ -73,9 +73,12 @@
|
|
|
73
73
|
"apps/workspace/app/layout.jsx",
|
|
74
74
|
"apps/workspace/app/page.jsx",
|
|
75
75
|
"apps/workspace/app/globals.css",
|
|
76
|
+
"apps/workspace/app/workspace-builder.jsx",
|
|
76
77
|
"apps/workspace/app/settings/integrations/page.jsx",
|
|
77
78
|
"apps/workspace/app/api/workspace/route.js",
|
|
78
79
|
"apps/workspace/app/api/settings/integrations/route.js",
|
|
80
|
+
"apps/workspace/lib/workspace-schema.js",
|
|
81
|
+
"apps/workspace/lib/workspace-config.js",
|
|
79
82
|
"apps/workspace/lib/domain/portal.js",
|
|
80
83
|
"apps/workspace/lib/domain/integrations.js",
|
|
81
84
|
"apps/workspace/lib/adapters/env.js",
|