@hoox-sh/hoox-tui 0.2.0
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 +247 -0
- package/bin/hoox-tui.js +35 -0
- package/dist/main.js +17594 -0
- package/package.json +72 -0
- package/src/app.tsx +696 -0
- package/src/cli-bridge-test-double.ts +212 -0
- package/src/components/layout/layout.test.tsx +205 -0
- package/src/components/layout/sidebar.tsx +69 -0
- package/src/components/layout/statusbar.test.tsx +423 -0
- package/src/components/layout/statusbar.tsx +348 -0
- package/src/components/shared/animated-border.tsx +58 -0
- package/src/components/shared/chrome.test.tsx +29 -0
- package/src/components/shared/command-palette.tsx +361 -0
- package/src/components/shared/cool-brackets.tsx +75 -0
- package/src/components/shared/crash-screen.tsx +167 -0
- package/src/components/shared/error-boundary.tsx +99 -0
- package/src/components/shared/keybinding-hint.tsx +37 -0
- package/src/components/shared/panel.tsx +61 -0
- package/src/components/shared/quit-modal.tsx +65 -0
- package/src/components/shared/shared.test.tsx +538 -0
- package/src/components/shared/spinner.tsx +85 -0
- package/src/components/shared/status-dot.tsx +54 -0
- package/src/components/shared/view-header.tsx +53 -0
- package/src/components/ui/connection-toasts.test.ts +50 -0
- package/src/components/ui/connection-toasts.ts +215 -0
- package/src/components/ui/dialog.tsx +281 -0
- package/src/components/ui/select.tsx +197 -0
- package/src/components/ui/toast.tsx +185 -0
- package/src/components/views/ai-chat.test.tsx +81 -0
- package/src/components/views/ai-chat.tsx +576 -0
- package/src/components/views/config-editor/action-bar.tsx +102 -0
- package/src/components/views/config-editor/code-editor.tsx +597 -0
- package/src/components/views/config-editor/file-tree.tsx +113 -0
- package/src/components/views/config-editor/types.ts +37 -0
- package/src/components/views/config-editor.test.tsx +465 -0
- package/src/components/views/config-editor.tsx +709 -0
- package/src/components/views/dashboard/alerts-panel.tsx +154 -0
- package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
- package/src/components/views/dashboard/model-health-section.tsx +276 -0
- package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
- package/src/components/views/dashboard.test.tsx +347 -0
- package/src/components/views/dashboard.tsx +647 -0
- package/src/components/views/db-query.test.tsx +115 -0
- package/src/components/views/db-query.tsx +849 -0
- package/src/components/views/edge-topology.test.tsx +198 -0
- package/src/components/views/edge-topology.tsx +572 -0
- package/src/components/views/kv-viewer.test.tsx +241 -0
- package/src/components/views/kv-viewer.tsx +717 -0
- package/src/components/views/logs-viewer.test.tsx +273 -0
- package/src/components/views/logs-viewer.tsx +616 -0
- package/src/components/views/queue-depth.test.tsx +221 -0
- package/src/components/views/queue-depth.tsx +376 -0
- package/src/components/views/secrets-viewer.test.tsx +65 -0
- package/src/components/views/secrets-viewer.tsx +548 -0
- package/src/components/views/service-manager.test.tsx +358 -0
- package/src/components/views/service-manager.tsx +946 -0
- package/src/components/views/settings.test.tsx +648 -0
- package/src/components/views/settings.tsx +1497 -0
- package/src/components/views/setup-wizard.test.tsx +297 -0
- package/src/components/views/setup-wizard.tsx +1321 -0
- package/src/components/views/trade-monitor.test.tsx +592 -0
- package/src/components/views/trade-monitor.tsx +745 -0
- package/src/components/views/worker-detail.test.tsx +289 -0
- package/src/components/views/worker-detail.tsx +676 -0
- package/src/components/views/worker-settings.test.tsx +43 -0
- package/src/components/views/worker-settings.tsx +563 -0
- package/src/components/views/workers-overview.test.tsx +260 -0
- package/src/components/views/workers-overview.tsx +631 -0
- package/src/hooks/hooks.test.ts +72 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/renderer-ref.ts +22 -0
- package/src/hooks/use-keyboard.ts +130 -0
- package/src/hooks/use-polling.ts +73 -0
- package/src/hooks/use-service-data.ts +18 -0
- package/src/main.tsx +117 -0
- package/src/network-test-double.ts +133 -0
- package/src/opentui-augment.d.ts +45 -0
- package/src/opentui-jsx.d.ts +70 -0
- package/src/services/cli-bridge/index.ts +1662 -0
- package/src/services/cli-bridge/standalone.ts +315 -0
- package/src/services/cli-bridge/types.ts +307 -0
- package/src/services/cli-bridge.test.ts +406 -0
- package/src/services/clipboard.test.ts +57 -0
- package/src/services/clipboard.ts +180 -0
- package/src/services/dashboard-settings-loader.ts +31 -0
- package/src/services/dev-log.test.ts +168 -0
- package/src/services/dev-log.ts +194 -0
- package/src/services/hoox-path-service.test.ts +183 -0
- package/src/services/hoox-path-service.ts +185 -0
- package/src/services/tui-connection.test.ts +212 -0
- package/src/services/tui-connection.ts +236 -0
- package/src/services/tui-storage.test.ts +92 -0
- package/src/services/tui-storage.ts +86 -0
- package/src/stores/config-store.test.ts +360 -0
- package/src/stores/service-store.test.ts +1026 -0
- package/src/stores/session-restore.test.ts +134 -0
- package/src/stores/ui-store.test.ts +276 -0
- package/src/test-setup.ts +265 -0
- package/src/test-utils.ts +179 -0
- package/src/types.ts +153 -0
- package/src/utils/colors.test.ts +326 -0
- package/src/utils/formatters.test.ts +236 -0
- package/src/view-registry.test.ts +132 -0
- package/src/view-registry.tsx +368 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @jsxImportSource @opentui/react */
|
|
7
|
+
import { useState, useEffect, useMemo } from "react";
|
|
8
|
+
import {
|
|
9
|
+
Colors,
|
|
10
|
+
getHooxRepoPath,
|
|
11
|
+
resolveHooxRuntimeRoot,
|
|
12
|
+
useUIStore,
|
|
13
|
+
} from "@hoox-sh/hoox-shared";
|
|
14
|
+
import { useKeyboard } from "@opentui/react";
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { ErrorBoundary } from "../shared/error-boundary";
|
|
18
|
+
import { ViewHeader } from "../shared/view-header";
|
|
19
|
+
import { EmptyState } from "../shared/spinner";
|
|
20
|
+
|
|
21
|
+
/** Cap default flow list so huge graphs stay responsive. */
|
|
22
|
+
export const MAX_FLOWS_PREVIEW = 15;
|
|
23
|
+
|
|
24
|
+
// Define types for the graph metadata
|
|
25
|
+
interface GraphMetadata {
|
|
26
|
+
workers: Record<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
description: string;
|
|
30
|
+
category: string;
|
|
31
|
+
tags: string[];
|
|
32
|
+
isPublic: boolean;
|
|
33
|
+
smartPlacement: boolean;
|
|
34
|
+
}
|
|
35
|
+
>;
|
|
36
|
+
infrastructure: Record<
|
|
37
|
+
string,
|
|
38
|
+
{
|
|
39
|
+
label: string;
|
|
40
|
+
category: string;
|
|
41
|
+
description: string;
|
|
42
|
+
tags: string[];
|
|
43
|
+
bindingName: string;
|
|
44
|
+
}
|
|
45
|
+
>;
|
|
46
|
+
dataFlows: Array<{
|
|
47
|
+
source: string;
|
|
48
|
+
target: string;
|
|
49
|
+
description: string;
|
|
50
|
+
flowType: string;
|
|
51
|
+
}>;
|
|
52
|
+
communities: Array<{
|
|
53
|
+
id: string;
|
|
54
|
+
label: string;
|
|
55
|
+
description: string;
|
|
56
|
+
nodeIds: string[];
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve graph-metadata.json regardless of launch CWD.
|
|
62
|
+
* Candidates: runtime root (HOOX_REPO / cwd / ~/.hoox/repo), walk-up from
|
|
63
|
+
* cwd, and relative to this source file (packages/tui/… → monorepo root).
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* Resolve path to graph-metadata.json.
|
|
67
|
+
*
|
|
68
|
+
* Override with `HOOX_GRAPH_METADATA_PATH` (absolute or relative) for tests
|
|
69
|
+
* and non-standard layouts — avoids process-wide `fs` mocks in the suite.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveGraphMetadataPath(): string | null {
|
|
72
|
+
const override = process.env.HOOX_GRAPH_METADATA_PATH?.trim();
|
|
73
|
+
if (override) {
|
|
74
|
+
const resolved = path.isAbsolute(override)
|
|
75
|
+
? override
|
|
76
|
+
: path.resolve(process.cwd(), override);
|
|
77
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fileName = "graph-metadata.json";
|
|
81
|
+
const candidates: string[] = [
|
|
82
|
+
path.resolve(process.cwd(), fileName),
|
|
83
|
+
// This file lives at packages/tui/src/components/views/
|
|
84
|
+
path.resolve(import.meta.dir, "../../../../../", fileName),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const runtime = resolveHooxRuntimeRoot();
|
|
88
|
+
if (runtime.root) {
|
|
89
|
+
candidates.push(path.join(runtime.root, fileName));
|
|
90
|
+
}
|
|
91
|
+
candidates.push(path.join(getHooxRepoPath(), fileName));
|
|
92
|
+
|
|
93
|
+
// Walk up from CWD looking for the monorepo marker
|
|
94
|
+
let dir = process.cwd();
|
|
95
|
+
for (let i = 0; i < 8; i++) {
|
|
96
|
+
candidates.push(path.join(dir, fileName));
|
|
97
|
+
const parent = path.dirname(dir);
|
|
98
|
+
if (parent === dir) break;
|
|
99
|
+
dir = parent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const candidate of candidates) {
|
|
103
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function EdgeTopology() {
|
|
109
|
+
return (
|
|
110
|
+
<ErrorBoundary viewName="Edge Topology">
|
|
111
|
+
<EdgeTopologyInner />
|
|
112
|
+
</ErrorBoundary>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function EdgeTopologyInner() {
|
|
117
|
+
const activeView = useUIStore((s) => s.activeView);
|
|
118
|
+
const isActive = activeView === "edge-topology";
|
|
119
|
+
|
|
120
|
+
const [metadata, setMetadata] = useState<GraphMetadata | null>(null);
|
|
121
|
+
const [error, setError] = useState<string | null>(null);
|
|
122
|
+
const [selectedNode, setSelectedNode] = useState<string | null>(null);
|
|
123
|
+
const [activeCommunity, setActiveCommunity] = useState<string | null>(null);
|
|
124
|
+
const [focusedIndex, setFocusedIndex] = useState<number>(0);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
let cancelled = false;
|
|
128
|
+
try {
|
|
129
|
+
const graphPath = resolveGraphMetadataPath();
|
|
130
|
+
if (!graphPath) {
|
|
131
|
+
if (!cancelled) {
|
|
132
|
+
setError(
|
|
133
|
+
"graph-metadata.json not found. Run `bun run graph` from the monorepo root, set HOOX_REPO, or `hoox doctor --fix-runtime`."
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const data = fs.readFileSync(graphPath, "utf-8");
|
|
139
|
+
if (cancelled) return;
|
|
140
|
+
setMetadata(JSON.parse(data) as GraphMetadata);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (!cancelled) {
|
|
143
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return () => {
|
|
147
|
+
cancelled = true;
|
|
148
|
+
};
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
const workers = metadata ? Object.entries(metadata.workers) : [];
|
|
152
|
+
const infra = metadata ? Object.entries(metadata.infrastructure) : [];
|
|
153
|
+
const communities = metadata ? metadata.communities : [];
|
|
154
|
+
|
|
155
|
+
// Combine all selectable items for keyboard navigation
|
|
156
|
+
const selectableItems = useMemo(() => {
|
|
157
|
+
if (!metadata)
|
|
158
|
+
return [] as Array<{
|
|
159
|
+
type: string;
|
|
160
|
+
id: string | null;
|
|
161
|
+
label?: string;
|
|
162
|
+
}>;
|
|
163
|
+
|
|
164
|
+
// Filter nodes if a community is selected
|
|
165
|
+
const visibleWorkers = activeCommunity
|
|
166
|
+
? workers.filter(([id]) =>
|
|
167
|
+
communities
|
|
168
|
+
.find((c) => c.id === activeCommunity)
|
|
169
|
+
?.nodeIds.includes(`workspace:${id}`)
|
|
170
|
+
)
|
|
171
|
+
: workers;
|
|
172
|
+
|
|
173
|
+
const visibleInfra = activeCommunity
|
|
174
|
+
? infra.filter(([id]) =>
|
|
175
|
+
communities
|
|
176
|
+
.find((c) => c.id === activeCommunity)
|
|
177
|
+
?.nodeIds.includes(`infra:${id}`)
|
|
178
|
+
)
|
|
179
|
+
: infra;
|
|
180
|
+
|
|
181
|
+
return [
|
|
182
|
+
{ type: "community", id: null, label: "All" },
|
|
183
|
+
...communities.map((c) => ({
|
|
184
|
+
type: "community",
|
|
185
|
+
id: c.id,
|
|
186
|
+
label: c.label,
|
|
187
|
+
})),
|
|
188
|
+
...visibleWorkers.map(([id]) => ({ type: "worker", id })),
|
|
189
|
+
...visibleInfra.map(([id]) => ({ type: "infra", id })),
|
|
190
|
+
];
|
|
191
|
+
}, [metadata, activeCommunity, workers, infra, communities]);
|
|
192
|
+
|
|
193
|
+
// Clamp focus when the selectable set shrinks (community filter)
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (selectableItems.length === 0) {
|
|
196
|
+
setFocusedIndex(0);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setFocusedIndex((prev) =>
|
|
200
|
+
Math.min(prev, Math.max(0, selectableItems.length - 1))
|
|
201
|
+
);
|
|
202
|
+
}, [selectableItems.length]);
|
|
203
|
+
|
|
204
|
+
useKeyboard((key) => {
|
|
205
|
+
if (!isActive) return;
|
|
206
|
+
if (key.name === "escape") {
|
|
207
|
+
setSelectedNode(null);
|
|
208
|
+
setActiveCommunity(null);
|
|
209
|
+
setFocusedIndex(0);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const n = selectableItems.length;
|
|
214
|
+
if (n === 0) return;
|
|
215
|
+
|
|
216
|
+
if (key.name === "right" || key.name === "tab") {
|
|
217
|
+
setFocusedIndex((prev) => (prev + 1) % n);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (key.name === "left" || (key.name === "tab" && key.shift)) {
|
|
222
|
+
setFocusedIndex((prev) => (prev - 1 + n) % n);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (key.name === "return" || key.name === "space") {
|
|
227
|
+
const item = selectableItems[focusedIndex];
|
|
228
|
+
if (!item) return;
|
|
229
|
+
|
|
230
|
+
if (item.type === "community") {
|
|
231
|
+
setActiveCommunity(item.id);
|
|
232
|
+
setSelectedNode(null);
|
|
233
|
+
setFocusedIndex(0); // Reset focus when community changes
|
|
234
|
+
} else if (item.id) {
|
|
235
|
+
setSelectedNode(item.id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (error) {
|
|
241
|
+
return (
|
|
242
|
+
<box flexDirection="column" padding={1} flexGrow={1}>
|
|
243
|
+
<ViewHeader title="EDGE TOPOLOGY" showDivider={false} />
|
|
244
|
+
<EmptyState
|
|
245
|
+
message="Error loading topology data:"
|
|
246
|
+
suggestion={error}
|
|
247
|
+
icon="⚠"
|
|
248
|
+
/>
|
|
249
|
+
</box>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!metadata) {
|
|
254
|
+
return (
|
|
255
|
+
<box
|
|
256
|
+
flexDirection="column"
|
|
257
|
+
padding={1}
|
|
258
|
+
justifyContent="center"
|
|
259
|
+
alignItems="center"
|
|
260
|
+
height="100%"
|
|
261
|
+
>
|
|
262
|
+
<text fg={Colors.muted}>Loading Edge Topology...</text>
|
|
263
|
+
</box>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Determine what to show in the right column
|
|
268
|
+
// (metadata is non-null here: the guard at the top of this branch returns early)
|
|
269
|
+
let rightColumnContent;
|
|
270
|
+
if (selectedNode) {
|
|
271
|
+
const worker = metadata.workers[selectedNode];
|
|
272
|
+
const infraNode = metadata.infrastructure[selectedNode];
|
|
273
|
+
|
|
274
|
+
if (worker) {
|
|
275
|
+
const inboundFlows = metadata.dataFlows.filter(
|
|
276
|
+
(f) => f.target === selectedNode
|
|
277
|
+
);
|
|
278
|
+
const outboundFlows = metadata.dataFlows.filter(
|
|
279
|
+
(f) => f.source === selectedNode
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
rightColumnContent = (
|
|
283
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
284
|
+
<text bold fg={Colors.success} marginBottom={1}>
|
|
285
|
+
{selectedNode.replace("workers/", "")}
|
|
286
|
+
</text>
|
|
287
|
+
<text fg={Colors.foreground} marginBottom={1}>
|
|
288
|
+
{worker.description}
|
|
289
|
+
</text>
|
|
290
|
+
|
|
291
|
+
<text bold fg={Colors.muted} marginTop={1}>
|
|
292
|
+
TAGS
|
|
293
|
+
</text>
|
|
294
|
+
<box flexDirection="row" flexWrap="wrap" marginBottom={1}>
|
|
295
|
+
{(worker.tags ?? []).map((t) => (
|
|
296
|
+
<text key={t} fg={Colors.info} marginRight={1}>
|
|
297
|
+
#{t}
|
|
298
|
+
</text>
|
|
299
|
+
))}
|
|
300
|
+
</box>
|
|
301
|
+
|
|
302
|
+
<text bold fg={Colors.muted} marginTop={1}>
|
|
303
|
+
{`INBOUND FLOWS (${inboundFlows.length})`}
|
|
304
|
+
</text>
|
|
305
|
+
{inboundFlows.slice(0, MAX_FLOWS_PREVIEW).map((f, i) => (
|
|
306
|
+
<text key={i} fg={Colors.foreground} dim>
|
|
307
|
+
← {f.source.replace("workers/", "")}: {f.flowType}
|
|
308
|
+
</text>
|
|
309
|
+
))}
|
|
310
|
+
{inboundFlows.length > MAX_FLOWS_PREVIEW && (
|
|
311
|
+
<text dim fg={Colors.muted}>
|
|
312
|
+
… +{inboundFlows.length - MAX_FLOWS_PREVIEW} more
|
|
313
|
+
</text>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
<text bold fg={Colors.muted} marginTop={1}>
|
|
317
|
+
{`OUTBOUND FLOWS (${outboundFlows.length})`}
|
|
318
|
+
</text>
|
|
319
|
+
{outboundFlows.slice(0, MAX_FLOWS_PREVIEW).map((f, i) => (
|
|
320
|
+
<text key={i} fg={Colors.foreground} dim>
|
|
321
|
+
→ {f.target.replace("workers/", "")}: {f.flowType}
|
|
322
|
+
</text>
|
|
323
|
+
))}
|
|
324
|
+
{outboundFlows.length > MAX_FLOWS_PREVIEW && (
|
|
325
|
+
<text dim fg={Colors.muted}>
|
|
326
|
+
… +{outboundFlows.length - MAX_FLOWS_PREVIEW} more
|
|
327
|
+
</text>
|
|
328
|
+
)}
|
|
329
|
+
</box>
|
|
330
|
+
);
|
|
331
|
+
} else if (infraNode) {
|
|
332
|
+
rightColumnContent = (
|
|
333
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
334
|
+
<text bold fg={Colors.info} marginBottom={1}>
|
|
335
|
+
{infraNode.label}
|
|
336
|
+
</text>
|
|
337
|
+
<text fg={Colors.foreground} marginBottom={1}>
|
|
338
|
+
{infraNode.description}
|
|
339
|
+
</text>
|
|
340
|
+
|
|
341
|
+
<text bold fg={Colors.muted} marginTop={1}>
|
|
342
|
+
BINDING
|
|
343
|
+
</text>
|
|
344
|
+
<text fg={Colors.accent} marginBottom={1}>
|
|
345
|
+
{infraNode.bindingName}
|
|
346
|
+
</text>
|
|
347
|
+
|
|
348
|
+
<text bold fg={Colors.muted} marginTop={1}>
|
|
349
|
+
TAGS
|
|
350
|
+
</text>
|
|
351
|
+
<box flexDirection="row" flexWrap="wrap" marginBottom={1}>
|
|
352
|
+
{(infraNode.tags ?? []).map((t) => (
|
|
353
|
+
<text key={t} fg={Colors.info} marginRight={1}>
|
|
354
|
+
#{t}
|
|
355
|
+
</text>
|
|
356
|
+
))}
|
|
357
|
+
</box>
|
|
358
|
+
</box>
|
|
359
|
+
);
|
|
360
|
+
} else {
|
|
361
|
+
rightColumnContent = (
|
|
362
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
363
|
+
<text fg={Colors.warning}>
|
|
364
|
+
Node not found in metadata: {selectedNode}
|
|
365
|
+
</text>
|
|
366
|
+
</box>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
// Default flows view
|
|
371
|
+
rightColumnContent = (
|
|
372
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
373
|
+
{metadata.dataFlows.length === 0 ? (
|
|
374
|
+
<EmptyState message="No data flows in graph metadata." icon="↔" />
|
|
375
|
+
) : (
|
|
376
|
+
<>
|
|
377
|
+
{metadata.dataFlows.slice(0, MAX_FLOWS_PREVIEW).map((flow, idx) => (
|
|
378
|
+
<box key={idx} flexDirection="column" marginBottom={1}>
|
|
379
|
+
<box flexDirection="row">
|
|
380
|
+
<text fg={Colors.success}>
|
|
381
|
+
{flow.source.replace("workers/", "")}
|
|
382
|
+
</text>
|
|
383
|
+
<text fg={Colors.muted} marginX={1}>
|
|
384
|
+
→
|
|
385
|
+
</text>
|
|
386
|
+
<text fg={Colors.info}>
|
|
387
|
+
{flow.target.replace("workers/", "")}
|
|
388
|
+
</text>
|
|
389
|
+
</box>
|
|
390
|
+
<text dim fg={Colors.muted}>
|
|
391
|
+
{flow.flowType}
|
|
392
|
+
</text>
|
|
393
|
+
</box>
|
|
394
|
+
))}
|
|
395
|
+
{metadata.dataFlows.length > MAX_FLOWS_PREVIEW && (
|
|
396
|
+
<text dim fg={Colors.muted}>
|
|
397
|
+
... and {metadata.dataFlows.length - MAX_FLOWS_PREVIEW} more
|
|
398
|
+
flows
|
|
399
|
+
</text>
|
|
400
|
+
)}
|
|
401
|
+
</>
|
|
402
|
+
)}
|
|
403
|
+
</box>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<box flexDirection="column" width="100%" height="100%">
|
|
409
|
+
<ViewHeader
|
|
410
|
+
title="EDGE TOPOLOGY"
|
|
411
|
+
meta={
|
|
412
|
+
<text fg={Colors.muted} dim>
|
|
413
|
+
{`${workers.length} Workers • ${infra.length} Infrastructure Nodes • ${metadata.dataFlows.length} Data Flows`}
|
|
414
|
+
</text>
|
|
415
|
+
}
|
|
416
|
+
/>
|
|
417
|
+
|
|
418
|
+
{/* Main Content */}
|
|
419
|
+
<box flexDirection="row" flexGrow={1} paddingTop={1}>
|
|
420
|
+
{/* Left Column: Communities & Nodes */}
|
|
421
|
+
<box
|
|
422
|
+
flexDirection="column"
|
|
423
|
+
width="60%"
|
|
424
|
+
paddingRight={2}
|
|
425
|
+
borderRight
|
|
426
|
+
borderStyle="single"
|
|
427
|
+
borderColor={Colors.border}
|
|
428
|
+
>
|
|
429
|
+
{/* Communities Filter */}
|
|
430
|
+
<box flexDirection="row" marginBottom={1} flexWrap="wrap">
|
|
431
|
+
<text fg={Colors.muted} marginRight={1}>
|
|
432
|
+
View:
|
|
433
|
+
</text>
|
|
434
|
+
<text
|
|
435
|
+
fg={activeCommunity === null ? Colors.accent : Colors.text}
|
|
436
|
+
bg={
|
|
437
|
+
selectableItems[focusedIndex]?.id === null &&
|
|
438
|
+
selectableItems[focusedIndex]?.type === "community"
|
|
439
|
+
? Colors.muted
|
|
440
|
+
: undefined
|
|
441
|
+
}
|
|
442
|
+
bold={activeCommunity === null}
|
|
443
|
+
marginRight={2}
|
|
444
|
+
>
|
|
445
|
+
[All]
|
|
446
|
+
</text>
|
|
447
|
+
{communities.map((c) => (
|
|
448
|
+
<text
|
|
449
|
+
key={c.id}
|
|
450
|
+
fg={activeCommunity === c.id ? Colors.accent : Colors.text}
|
|
451
|
+
bg={
|
|
452
|
+
selectableItems[focusedIndex]?.id === c.id &&
|
|
453
|
+
selectableItems[focusedIndex]?.type === "community"
|
|
454
|
+
? Colors.muted
|
|
455
|
+
: undefined
|
|
456
|
+
}
|
|
457
|
+
bold={activeCommunity === c.id}
|
|
458
|
+
marginRight={2}
|
|
459
|
+
>
|
|
460
|
+
[{c.label}]
|
|
461
|
+
</text>
|
|
462
|
+
))}
|
|
463
|
+
</box>
|
|
464
|
+
|
|
465
|
+
{/* Workers Grid */}
|
|
466
|
+
<text bold fg={Colors.success} marginBottom={1}>
|
|
467
|
+
WORKERS
|
|
468
|
+
</text>
|
|
469
|
+
<box flexDirection="row" flexWrap="wrap" marginBottom={2}>
|
|
470
|
+
{(activeCommunity
|
|
471
|
+
? workers.filter(([id]) =>
|
|
472
|
+
communities
|
|
473
|
+
.find((c) => c.id === activeCommunity)
|
|
474
|
+
?.nodeIds.includes(`workspace:${id}`)
|
|
475
|
+
)
|
|
476
|
+
: workers
|
|
477
|
+
).map(([id, w]) => {
|
|
478
|
+
const isFocused =
|
|
479
|
+
selectableItems[focusedIndex]?.id === id &&
|
|
480
|
+
selectableItems[focusedIndex]?.type === "worker";
|
|
481
|
+
return (
|
|
482
|
+
<box
|
|
483
|
+
key={id}
|
|
484
|
+
width="30%"
|
|
485
|
+
marginRight={1}
|
|
486
|
+
marginBottom={1}
|
|
487
|
+
padding={1}
|
|
488
|
+
borderStyle="rounded"
|
|
489
|
+
borderColor={
|
|
490
|
+
selectedNode === id
|
|
491
|
+
? Colors.accent
|
|
492
|
+
: isFocused
|
|
493
|
+
? Colors.foreground
|
|
494
|
+
: Colors.border
|
|
495
|
+
}
|
|
496
|
+
flexDirection="column"
|
|
497
|
+
>
|
|
498
|
+
<text bold fg={w.isPublic ? Colors.warning : Colors.text}>
|
|
499
|
+
{id.replace("workers/", "")}
|
|
500
|
+
</text>
|
|
501
|
+
<text dim fg={Colors.muted}>
|
|
502
|
+
{w.category}
|
|
503
|
+
</text>
|
|
504
|
+
{w.smartPlacement && (
|
|
505
|
+
<text fg={Colors.success} dim>
|
|
506
|
+
⚡ Smart Placement
|
|
507
|
+
</text>
|
|
508
|
+
)}
|
|
509
|
+
</box>
|
|
510
|
+
);
|
|
511
|
+
})}
|
|
512
|
+
</box>
|
|
513
|
+
|
|
514
|
+
{/* Infrastructure Grid */}
|
|
515
|
+
<text bold fg={Colors.info} marginBottom={1}>
|
|
516
|
+
INFRASTRUCTURE
|
|
517
|
+
</text>
|
|
518
|
+
<box flexDirection="row" flexWrap="wrap">
|
|
519
|
+
{(activeCommunity
|
|
520
|
+
? infra.filter(([id]) =>
|
|
521
|
+
communities
|
|
522
|
+
.find((c) => c.id === activeCommunity)
|
|
523
|
+
?.nodeIds.includes(`infra:${id}`)
|
|
524
|
+
)
|
|
525
|
+
: infra
|
|
526
|
+
).map(([id, i]) => {
|
|
527
|
+
const isFocused =
|
|
528
|
+
selectableItems[focusedIndex]?.id === id &&
|
|
529
|
+
selectableItems[focusedIndex]?.type === "infra";
|
|
530
|
+
return (
|
|
531
|
+
<box
|
|
532
|
+
key={id}
|
|
533
|
+
width="30%"
|
|
534
|
+
marginRight={1}
|
|
535
|
+
marginBottom={1}
|
|
536
|
+
padding={1}
|
|
537
|
+
borderStyle="rounded"
|
|
538
|
+
borderColor={
|
|
539
|
+
selectedNode === id
|
|
540
|
+
? Colors.accent
|
|
541
|
+
: isFocused
|
|
542
|
+
? Colors.foreground
|
|
543
|
+
: Colors.border
|
|
544
|
+
}
|
|
545
|
+
flexDirection="column"
|
|
546
|
+
>
|
|
547
|
+
<text bold fg={Colors.info}>
|
|
548
|
+
{i.label}
|
|
549
|
+
</text>
|
|
550
|
+
<text dim fg={Colors.muted}>
|
|
551
|
+
{i.category}
|
|
552
|
+
</text>
|
|
553
|
+
<text dim fg={Colors.muted}>
|
|
554
|
+
{i.bindingName}
|
|
555
|
+
</text>
|
|
556
|
+
</box>
|
|
557
|
+
);
|
|
558
|
+
})}
|
|
559
|
+
</box>
|
|
560
|
+
</box>
|
|
561
|
+
|
|
562
|
+
{/* Right Column: Details & Flows */}
|
|
563
|
+
<box flexDirection="column" width="40%" paddingLeft={2}>
|
|
564
|
+
<text bold fg={Colors.accent} marginBottom={1}>
|
|
565
|
+
{selectedNode ? "NODE DETAILS" : "SYSTEM FLOWS"}
|
|
566
|
+
</text>
|
|
567
|
+
{rightColumnContent}
|
|
568
|
+
</box>
|
|
569
|
+
</box>
|
|
570
|
+
</box>
|
|
571
|
+
);
|
|
572
|
+
}
|