@growthub/cli 0.13.2 → 0.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +24 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +14 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +74 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +72 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +123 -27
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +224 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +754 -92
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +224 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +32 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +530 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +8 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +10 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/RunSetupPanel.jsx +261 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +119 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +779 -138
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +91 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +28 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +366 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +34 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-eligibility.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-redaction.js +64 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +665 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-host-catalog.js +168 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +595 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +164 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +11 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +111 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sandbox Local Agent Auth Onboarding V1 — record sidecar panel.
|
|
5
|
+
*
|
|
6
|
+
* Rendered for any sandbox row using `adapter: local-agent-host` in local
|
|
7
|
+
* locality. The panel is host-agnostic: per-host capabilities (whether a
|
|
8
|
+
* documented login/logout subcommand exists, the label, the install hint)
|
|
9
|
+
* come from `lib/sandbox-agent-host-catalog.js` via `getAgentHostCapabilities`.
|
|
10
|
+
*
|
|
11
|
+
* Mental model — identical for every host:
|
|
12
|
+
*
|
|
13
|
+
* 1. Check status host CLI installed and authed?
|
|
14
|
+
* 2. Run login (only when the catalog declares loginCommand)
|
|
15
|
+
* 3. Log out (only when the catalog declares logoutCommand)
|
|
16
|
+
* 4. Run sandbox existing button (unchanged)
|
|
17
|
+
*
|
|
18
|
+
* Hosts without a documented login flow surface only step 1 and a notes
|
|
19
|
+
* line directing the operator to sign in via the host CLI itself. There is
|
|
20
|
+
* no second product surface, no terminal emulator — just a uniform
|
|
21
|
+
* readiness bridge.
|
|
22
|
+
*
|
|
23
|
+
* Status values stamped on the row are intentionally distinct between
|
|
24
|
+
* confirmed-authenticated ("active") and merely-installed ("reachable")
|
|
25
|
+
* so the pill cannot overclaim auth from a `--version` probe.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useCallback, useState } from "react";
|
|
29
|
+
import { Bot, Code2, LogIn, LogOut, RefreshCw } from "lucide-react";
|
|
30
|
+
import { isSandboxLocalAgentHost } from "@/lib/sandbox-agent-auth-eligibility";
|
|
31
|
+
import { getAgentHostCapabilities } from "@/lib/sandbox-agent-host-catalog";
|
|
32
|
+
|
|
33
|
+
const STATUS_LABEL = {
|
|
34
|
+
active: "Active",
|
|
35
|
+
reachable: "Reachable",
|
|
36
|
+
stale: "Stale",
|
|
37
|
+
missing: "Missing",
|
|
38
|
+
checking: "Checking",
|
|
39
|
+
unknown: "Not checked"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function statusKind(status) {
|
|
43
|
+
if (status === "active") return "ok";
|
|
44
|
+
if (status === "stale") return "warn";
|
|
45
|
+
if (status === "missing") return "bad";
|
|
46
|
+
// "reachable" stays neutral — CLI is installed, but auth is NOT confirmed.
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function AuthStatusPill({ status }) {
|
|
51
|
+
const value = STATUS_LABEL[status] || "Unknown";
|
|
52
|
+
const kind = statusKind(status);
|
|
53
|
+
return (
|
|
54
|
+
<span className={`dm-db-status ${kind}`} data-agent-auth-status={status || "unknown"}>
|
|
55
|
+
<span />
|
|
56
|
+
{value}
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function SandboxAgentAuthPanel({ objectId, rowName, draft, disabled, onPatchDraft }) {
|
|
62
|
+
const [busy, setBusy] = useState(null); // "status" | "login" | "logout" | null
|
|
63
|
+
const [output, setOutput] = useState(null);
|
|
64
|
+
const [message, setMessage] = useState("");
|
|
65
|
+
|
|
66
|
+
const capabilities = getAgentHostCapabilities(draft);
|
|
67
|
+
const providerMatchesHost = String(draft?.agentAuthProvider || "").trim() === String(draft?.agentHost || "").trim();
|
|
68
|
+
const currentStatus =
|
|
69
|
+
providerMatchesHost && typeof draft?.agentAuthStatus === "string" && draft.agentAuthStatus.trim()
|
|
70
|
+
? draft.agentAuthStatus.trim()
|
|
71
|
+
: "unknown";
|
|
72
|
+
const lastChecked = providerMatchesHost ? draft?.agentAuthLastChecked || "" : "";
|
|
73
|
+
const lastMessage = providerMatchesHost ? draft?.agentAuthLastMessage || "" : "";
|
|
74
|
+
const displayMessage = normalizeAuthMessage(message || lastMessage, capabilities?.label)
|
|
75
|
+
|| (currentStatus === "unknown" ? "Run Check or Login to verify this local agent host." : "");
|
|
76
|
+
|
|
77
|
+
const canAct = Boolean(objectId && rowName) && !disabled;
|
|
78
|
+
|
|
79
|
+
const callAction = useCallback(
|
|
80
|
+
async (action, endpoint) => {
|
|
81
|
+
if (!canAct) return;
|
|
82
|
+
setBusy(action);
|
|
83
|
+
setMessage("");
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(endpoint, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "content-type": "application/json" },
|
|
88
|
+
body: JSON.stringify({ objectId, name: rowName })
|
|
89
|
+
});
|
|
90
|
+
const payload = await res.json();
|
|
91
|
+
setOutput(payload);
|
|
92
|
+
setMessage(payload.message || (payload.ok ? "Done" : payload.error || "Failed"));
|
|
93
|
+
if (typeof onPatchDraft === "function" && payload.status) {
|
|
94
|
+
onPatchDraft({
|
|
95
|
+
agentAuthStatus: payload.status,
|
|
96
|
+
agentAuthProvider: payload.provider || draft?.agentHost || "unknown",
|
|
97
|
+
agentAuthLastChecked: payload.checkedAt || new Date().toISOString(),
|
|
98
|
+
agentAuthLastExitCode:
|
|
99
|
+
typeof payload.exitCode === "number" ? payload.exitCode : null,
|
|
100
|
+
agentAuthLastMessage: payload.message || "",
|
|
101
|
+
agentAuthLastLoginUrl: payload.loginUrl || ""
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
setMessage(err?.message || `${action} failed`);
|
|
106
|
+
} finally {
|
|
107
|
+
setBusy(null);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[canAct, objectId, rowName, onPatchDraft, draft?.agentHost]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const onCheckStatus = () =>
|
|
114
|
+
callAction("status", "/api/workspace/sandbox-agent-auth/status");
|
|
115
|
+
const onLogin = () =>
|
|
116
|
+
callAction("login", "/api/workspace/sandbox-agent-auth/login");
|
|
117
|
+
const onLogout = () =>
|
|
118
|
+
callAction("logout", "/api/workspace/sandbox-agent-auth/logout");
|
|
119
|
+
|
|
120
|
+
if (!capabilities) return null;
|
|
121
|
+
const HostIcon = capabilities.slug === "codex_local" ? Code2 : Bot;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="dm-record-testbar" data-panel="sandbox-agent-auth" data-agent-host={capabilities.slug}>
|
|
125
|
+
<div className="dm-agent-auth-summary">
|
|
126
|
+
<HostIcon size={15} aria-hidden />
|
|
127
|
+
<div>
|
|
128
|
+
<strong>{capabilities.label}</strong>
|
|
129
|
+
<span>Local CLI session</span>
|
|
130
|
+
</div>
|
|
131
|
+
<AuthStatusPill status={currentStatus} />
|
|
132
|
+
</div>
|
|
133
|
+
<div className="dm-agent-auth-actions">
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className="dm-btn-ghost"
|
|
137
|
+
disabled={busy !== null || !canAct}
|
|
138
|
+
onClick={onCheckStatus}
|
|
139
|
+
title={`Probe the local ${capabilities.label} CLI`}
|
|
140
|
+
>
|
|
141
|
+
<RefreshCw size={13} aria-hidden />
|
|
142
|
+
{busy === "status" ? "Checking…" : "Check"}
|
|
143
|
+
</button>
|
|
144
|
+
{capabilities.canLogin && (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
className="dm-btn-primary-sm"
|
|
148
|
+
disabled={busy !== null || !canAct}
|
|
149
|
+
onClick={onLogin}
|
|
150
|
+
title={`Run the documented login flow for ${capabilities.label}`}
|
|
151
|
+
>
|
|
152
|
+
<LogIn size={13} aria-hidden />
|
|
153
|
+
{busy === "login" ? "Running…" : "Login"}
|
|
154
|
+
</button>
|
|
155
|
+
)}
|
|
156
|
+
{capabilities.canLogout && (
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
className="dm-btn-ghost"
|
|
160
|
+
disabled={busy !== null || !canAct}
|
|
161
|
+
onClick={onLogout}
|
|
162
|
+
title={`Run the documented logout flow for ${capabilities.label}`}
|
|
163
|
+
>
|
|
164
|
+
<LogOut size={13} aria-hidden />
|
|
165
|
+
{busy === "logout" ? "…" : "Logout"}
|
|
166
|
+
</button>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
{displayMessage && <span className="dm-agent-auth-message">{displayMessage}</span>}
|
|
170
|
+
{currentStatus === "missing" && (
|
|
171
|
+
<span className="dm-agent-auth-message is-warning">{capabilities.installHint}</span>
|
|
172
|
+
)}
|
|
173
|
+
{!capabilities.canLogin && capabilities.notes && (
|
|
174
|
+
<span className="dm-agent-auth-message" data-host-notes>
|
|
175
|
+
{capabilities.notes}
|
|
176
|
+
</span>
|
|
177
|
+
)}
|
|
178
|
+
{output && (output.stdout || output.stderr || output.loginUrl) && (
|
|
179
|
+
<SandboxAgentAuthOutput output={output} />
|
|
180
|
+
)}
|
|
181
|
+
{!output && lastChecked && (
|
|
182
|
+
<span className="dm-agent-auth-message is-muted">Last checked {formatChecked(lastChecked)}</span>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeAuthMessage(value, label) {
|
|
189
|
+
const text = String(value || "").trim();
|
|
190
|
+
if (!text) return "";
|
|
191
|
+
const normalized = text.toLowerCase();
|
|
192
|
+
if (normalized.includes("authenticated")) return "Authenticated.";
|
|
193
|
+
if (label && normalized.includes(String(label).toLowerCase())) {
|
|
194
|
+
return text.replace(new RegExp(String(label).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "").replace(/\s+/g, " ").trim();
|
|
195
|
+
}
|
|
196
|
+
return text;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function formatChecked(iso) {
|
|
200
|
+
if (!iso) return "";
|
|
201
|
+
try {
|
|
202
|
+
return new Date(iso).toLocaleString();
|
|
203
|
+
} catch {
|
|
204
|
+
return iso;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function SandboxAgentAuthOutput({ output }) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
if (output.loginUrl) lines.push(`login URL: ${output.loginUrl}`);
|
|
211
|
+
if (output.stdout) lines.push(`stdout:\n${output.stdout}`);
|
|
212
|
+
if (output.stderr) lines.push(`stderr:\n${output.stderr}`);
|
|
213
|
+
return (
|
|
214
|
+
<details className="dm-agent-auth-output" data-output="sandbox-agent-auth">
|
|
215
|
+
<summary>Auth output</summary>
|
|
216
|
+
<pre>{lines.join("\n\n")}</pre>
|
|
217
|
+
</details>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Re-export the eligibility predicate so callers can keep importing it from
|
|
222
|
+
// this component file. New code should import from
|
|
223
|
+
// `@/lib/sandbox-agent-auth-eligibility` directly.
|
|
224
|
+
export { isSandboxLocalAgentHost };
|
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
import { Play } from "lucide-react";
|
|
4
4
|
import { StatusPill } from "./StatusPill.jsx";
|
|
5
5
|
|
|
6
|
-
export function SandboxRunPanel({
|
|
6
|
+
export function SandboxRunPanel({
|
|
7
|
+
status,
|
|
8
|
+
sandboxRunning,
|
|
9
|
+
sandboxMessage,
|
|
10
|
+
onRun,
|
|
11
|
+
disabled,
|
|
12
|
+
canRun,
|
|
13
|
+
agentAuthStatus,
|
|
14
|
+
agentAuthHint
|
|
15
|
+
}) {
|
|
16
|
+
const hint = agentAuthHint
|
|
17
|
+
|| (sandboxMessage && isAuthHinted(sandboxMessage) ? "Claude auth may be stale — open Claude Auth panel above." : null);
|
|
7
18
|
return (
|
|
8
19
|
<div className="dm-record-testbar">
|
|
9
20
|
<StatusPill value={status} />
|
|
@@ -16,6 +27,26 @@ export function SandboxRunPanel({ status, sandboxRunning, sandboxMessage, onRun,
|
|
|
16
27
|
{sandboxRunning ? "Running…" : (<><Play size={13} aria-hidden /> Run sandbox</>)}
|
|
17
28
|
</button>
|
|
18
29
|
{sandboxMessage && <span>{sandboxMessage}</span>}
|
|
30
|
+
{hint && (
|
|
31
|
+
<span
|
|
32
|
+
data-agent-auth-hint
|
|
33
|
+
data-agent-auth-status={agentAuthStatus || "unknown"}
|
|
34
|
+
style={{ color: "#b45309", fontSize: 12, fontWeight: 600 }}
|
|
35
|
+
>
|
|
36
|
+
{hint}
|
|
37
|
+
</span>
|
|
38
|
+
)}
|
|
19
39
|
</div>
|
|
20
40
|
);
|
|
21
41
|
}
|
|
42
|
+
|
|
43
|
+
function isAuthHinted(text) {
|
|
44
|
+
if (typeof text !== "string") return false;
|
|
45
|
+
const lower = text.toLowerCase();
|
|
46
|
+
return (
|
|
47
|
+
lower.includes("not logged in")
|
|
48
|
+
|| lower.includes("login required")
|
|
49
|
+
|| lower.includes("authenticate")
|
|
50
|
+
|| lower.includes("auth required")
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Growthub Workspace Metadata Graph V1 — read-only inspector panel.
|
|
5
|
+
*
|
|
6
|
+
* Surfaces a selected metadata node's dependencies + dependents + warnings.
|
|
7
|
+
*
|
|
8
|
+
* Mounting status (V1):
|
|
9
|
+
* This component SHIPS with the worker kit but is NOT yet mounted by the
|
|
10
|
+
* existing builder / workflow / data-model surfaces. Mounting is
|
|
11
|
+
* intentionally deferred to a follow-up PR so the V1 scope stays focused
|
|
12
|
+
* on the typed projection + GET route. Operators that want the inspector
|
|
13
|
+
* now can import it directly:
|
|
14
|
+
*
|
|
15
|
+
* import { WorkspaceGraphInspectorPanel } from "@/app/data-model/components/WorkspaceGraphInspectorPanel.jsx";
|
|
16
|
+
*
|
|
17
|
+
* Intended future entry points:
|
|
18
|
+
*
|
|
19
|
+
* - Widget sidecar: View dependencies
|
|
20
|
+
* - Workflow sidecar: View dependencies
|
|
21
|
+
* - Run console: View graph lineage
|
|
22
|
+
* - Data Model row sidecar: View dependents
|
|
23
|
+
* - Workspace Settings: Metadata Graph
|
|
24
|
+
*
|
|
25
|
+
* V1 invariants:
|
|
26
|
+
* - Read-only. No edits. No deletes.
|
|
27
|
+
* - No fetch of provider data — only `GET /api/workspace/metadata-graph`.
|
|
28
|
+
* - No secrets rendered.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { useEffect, useMemo, useState } from "react";
|
|
32
|
+
import { findDependencies, findDependents } from "@/lib/workspace-metadata-graph";
|
|
33
|
+
|
|
34
|
+
const RELATION_LABELS = {
|
|
35
|
+
containsWidget: "contains widget",
|
|
36
|
+
bindsToObject: "binds to object",
|
|
37
|
+
usesField: "uses field",
|
|
38
|
+
filteredByField: "filtered by field",
|
|
39
|
+
sortedByField: "sorted by field",
|
|
40
|
+
backedBySourceRecord: "backed by source record",
|
|
41
|
+
scopedToEntity: "scoped to entity",
|
|
42
|
+
containsNode: "contains node",
|
|
43
|
+
usesSandbox: "uses sandbox",
|
|
44
|
+
readsObject: "reads object",
|
|
45
|
+
writesObject: "writes object",
|
|
46
|
+
requiresRunInput: "requires run input",
|
|
47
|
+
callsIntegration: "calls integration",
|
|
48
|
+
usesAgentHost: "uses agent host",
|
|
49
|
+
executedWorkflow: "executed workflow",
|
|
50
|
+
executedSandbox: "executed sandbox",
|
|
51
|
+
usedAgentHost: "used agent host",
|
|
52
|
+
producedArtifact: "produced artifact",
|
|
53
|
+
belongsToIntegration: "belongs to integration",
|
|
54
|
+
boundToIntegration: "bound to integration"
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const NODE_TYPE_LABELS = {
|
|
58
|
+
dashboard: "Dashboard",
|
|
59
|
+
widget: "Widget",
|
|
60
|
+
dataModelObject: "Data Model object",
|
|
61
|
+
field: "Field",
|
|
62
|
+
view: "View",
|
|
63
|
+
workflow: "Workflow",
|
|
64
|
+
workflowNode: "Workflow node",
|
|
65
|
+
runInput: "Run input",
|
|
66
|
+
sandbox: "Sandbox",
|
|
67
|
+
agentHost: "Agent host",
|
|
68
|
+
integration: "Integration",
|
|
69
|
+
integrationEntity: "Integration entity",
|
|
70
|
+
sourceRecord: "Source record",
|
|
71
|
+
run: "Run",
|
|
72
|
+
outputArtifact: "Output artifact"
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function relationLabel(relation) {
|
|
76
|
+
return RELATION_LABELS[relation] || relation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function nodeTypeLabel(type) {
|
|
80
|
+
return NODE_TYPE_LABELS[type] || type;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function WorkspaceGraphInspectorPanel({ selectedNodeId, onSelectNode }) {
|
|
84
|
+
const [graph, setGraph] = useState(null);
|
|
85
|
+
const [loading, setLoading] = useState(false);
|
|
86
|
+
const [error, setError] = useState(null);
|
|
87
|
+
const [internalSelectedId, setInternalSelectedId] = useState(selectedNodeId || "");
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
let canceled = false;
|
|
91
|
+
setLoading(true);
|
|
92
|
+
setError(null);
|
|
93
|
+
fetch("/api/workspace/metadata-graph")
|
|
94
|
+
.then((response) => {
|
|
95
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
96
|
+
return response.json();
|
|
97
|
+
})
|
|
98
|
+
.then((envelope) => {
|
|
99
|
+
if (canceled) return;
|
|
100
|
+
setGraph(envelope?.graph || { nodes: [], edges: [] });
|
|
101
|
+
})
|
|
102
|
+
.catch((err) => {
|
|
103
|
+
if (canceled) return;
|
|
104
|
+
setError(err?.message || "Failed to load metadata graph");
|
|
105
|
+
})
|
|
106
|
+
.finally(() => {
|
|
107
|
+
if (!canceled) setLoading(false);
|
|
108
|
+
});
|
|
109
|
+
return () => { canceled = true; };
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (selectedNodeId && selectedNodeId !== internalSelectedId) {
|
|
114
|
+
setInternalSelectedId(selectedNodeId);
|
|
115
|
+
}
|
|
116
|
+
}, [selectedNodeId, internalSelectedId]);
|
|
117
|
+
|
|
118
|
+
const nodes = graph?.nodes || [];
|
|
119
|
+
const selected = useMemo(
|
|
120
|
+
() => nodes.find((node) => node.id === internalSelectedId) || null,
|
|
121
|
+
[nodes, internalSelectedId]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const dependencies = useMemo(
|
|
125
|
+
() => (graph && selected ? findDependencies(graph, selected.id) : []),
|
|
126
|
+
[graph, selected]
|
|
127
|
+
);
|
|
128
|
+
const dependents = useMemo(
|
|
129
|
+
() => (graph && selected ? findDependents(graph, selected.id) : []),
|
|
130
|
+
[graph, selected]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const handleSelect = (nodeId) => {
|
|
134
|
+
setInternalSelectedId(nodeId);
|
|
135
|
+
if (typeof onSelectNode === "function") onSelectNode(nodeId);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="workspace-graph-inspector" aria-label="Workspace metadata graph inspector">
|
|
140
|
+
<header className="workspace-graph-inspector-header">
|
|
141
|
+
<h3>Workspace metadata graph</h3>
|
|
142
|
+
<p className="workspace-graph-inspector-subtitle">
|
|
143
|
+
Read-only projection of dashboards, widgets, workflows, sandboxes, and runs.
|
|
144
|
+
</p>
|
|
145
|
+
</header>
|
|
146
|
+
|
|
147
|
+
{loading && <p className="workspace-graph-inspector-loading">Loading metadata graph…</p>}
|
|
148
|
+
{error && (
|
|
149
|
+
<p className="workspace-graph-inspector-error" role="alert">
|
|
150
|
+
Could not load metadata graph: {error}
|
|
151
|
+
</p>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
<div className="workspace-graph-inspector-body">
|
|
155
|
+
<aside className="workspace-graph-inspector-list">
|
|
156
|
+
<h4>Nodes ({nodes.length})</h4>
|
|
157
|
+
<ul>
|
|
158
|
+
{nodes.map((node) => (
|
|
159
|
+
<li key={node.id}>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className={`workspace-graph-inspector-node-button${node.id === internalSelectedId ? " is-selected" : ""}`}
|
|
163
|
+
onClick={() => handleSelect(node.id)}
|
|
164
|
+
>
|
|
165
|
+
<span className="workspace-graph-inspector-node-type">{nodeTypeLabel(node.type)}</span>
|
|
166
|
+
<span className="workspace-graph-inspector-node-label">{node.label || node.id}</span>
|
|
167
|
+
</button>
|
|
168
|
+
</li>
|
|
169
|
+
))}
|
|
170
|
+
</ul>
|
|
171
|
+
</aside>
|
|
172
|
+
|
|
173
|
+
<section className="workspace-graph-inspector-detail">
|
|
174
|
+
{!selected && <p className="workspace-graph-inspector-empty">Select a node to view dependencies.</p>}
|
|
175
|
+
{selected && (
|
|
176
|
+
<>
|
|
177
|
+
<h4>{nodeTypeLabel(selected.type)} · {selected.label}</h4>
|
|
178
|
+
<dl className="workspace-graph-inspector-summary">
|
|
179
|
+
{Object.entries(selected.summary || {}).map(([key, value]) => (
|
|
180
|
+
<div key={key}>
|
|
181
|
+
<dt>{key}</dt>
|
|
182
|
+
<dd>{Array.isArray(value) ? value.join(", ") || "—" : (value === null || value === "" ? "—" : String(value))}</dd>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
</dl>
|
|
186
|
+
|
|
187
|
+
<section className="workspace-graph-inspector-edges">
|
|
188
|
+
<h5>Depends on ({dependencies.length})</h5>
|
|
189
|
+
<ul>
|
|
190
|
+
{dependencies.length === 0 && <li className="workspace-graph-inspector-empty">No dependencies.</li>}
|
|
191
|
+
{dependencies.map(({ node, relation }) => (
|
|
192
|
+
<li key={`dep::${node.id}::${relation}`}>
|
|
193
|
+
<button type="button" onClick={() => handleSelect(node.id)}>
|
|
194
|
+
<span className="workspace-graph-inspector-relation">{relationLabel(relation)}</span>
|
|
195
|
+
<span className="workspace-graph-inspector-node-type">{nodeTypeLabel(node.type)}</span>
|
|
196
|
+
<span className="workspace-graph-inspector-node-label">{node.label || node.id}</span>
|
|
197
|
+
</button>
|
|
198
|
+
</li>
|
|
199
|
+
))}
|
|
200
|
+
</ul>
|
|
201
|
+
</section>
|
|
202
|
+
|
|
203
|
+
<section className="workspace-graph-inspector-edges">
|
|
204
|
+
<h5>Used by ({dependents.length})</h5>
|
|
205
|
+
<ul>
|
|
206
|
+
{dependents.length === 0 && <li className="workspace-graph-inspector-empty">Nothing depends on this node yet.</li>}
|
|
207
|
+
{dependents.map(({ node, relation }) => (
|
|
208
|
+
<li key={`from::${node.id}::${relation}`}>
|
|
209
|
+
<button type="button" onClick={() => handleSelect(node.id)}>
|
|
210
|
+
<span className="workspace-graph-inspector-relation">{relationLabel(relation)}</span>
|
|
211
|
+
<span className="workspace-graph-inspector-node-type">{nodeTypeLabel(node.type)}</span>
|
|
212
|
+
<span className="workspace-graph-inspector-node-label">{node.label || node.id}</span>
|
|
213
|
+
</button>
|
|
214
|
+
</li>
|
|
215
|
+
))}
|
|
216
|
+
</ul>
|
|
217
|
+
</section>
|
|
218
|
+
</>
|
|
219
|
+
)}
|
|
220
|
+
</section>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default WorkspaceGraphInspectorPanel;
|