@nwire/studio 0.12.1 → 0.13.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/package.json +5 -3
- package/src/App.vue +62 -56
- package/src/components/BcCard.stories.ts +47 -0
- package/src/components/BcCard.vue +152 -0
- package/src/components/DurationBar.stories.ts +55 -0
- package/src/components/DurationBar.vue +72 -0
- package/src/components/ErrorCard.stories.ts +133 -0
- package/src/components/ErrorCard.vue +153 -0
- package/src/components/GraphCanvas.stories.ts +48 -0
- package/src/components/GraphCanvas.vue +88 -0
- package/src/components/KpiTile.stories.ts +32 -0
- package/src/components/KpiTile.vue +39 -0
- package/src/components/LiveTable.stories.ts +78 -0
- package/src/components/LiveTable.vue +186 -0
- package/src/components/MetadataInspector.stories.ts +53 -0
- package/src/components/MetadataInspector.vue +105 -0
- package/src/components/NodeCard.stories.ts +44 -0
- package/src/components/NodeCard.vue +150 -0
- package/src/components/RcaPanel.stories.ts +95 -0
- package/src/components/RcaPanel.vue +223 -0
- package/src/components/ServiceNode.vue +134 -0
- package/src/components/SourceDrawer.vue +6 -4
- package/src/components/SourcePill.vue +10 -3
- package/src/components/StatusBadge.stories.ts +33 -0
- package/src/components/StatusBadge.vue +54 -0
- package/src/components/Waterfall.stories.ts +85 -0
- package/src/components/Waterfall.vue +53 -0
- package/src/components/WaterfallRow.vue +74 -0
- package/src/components/__tests__/BcCard.test.ts +53 -0
- package/src/components/__tests__/DurationBar.test.ts +31 -0
- package/src/components/__tests__/ErrorCard.test.ts +71 -0
- package/src/components/__tests__/KpiTile.test.ts +23 -0
- package/src/components/__tests__/LiveTable.test.ts +100 -0
- package/src/components/__tests__/MetadataInspector.test.ts +38 -0
- package/src/components/__tests__/NodeCard.test.ts +116 -0
- package/src/components/__tests__/RcaPanel.test.ts +81 -0
- package/src/components/__tests__/StatusBadge.test.ts +23 -0
- package/src/components/__tests__/Waterfall.test.ts +54 -0
- package/src/components/index.ts +13 -0
- package/src/composables/__tests__/composables-context.test.ts +107 -0
- package/src/composables/__tests__/useTelemetry.test.ts +104 -0
- package/src/composables/__tests__/useTelemetryRuns.test.ts +282 -0
- package/src/composables/useDiscovery.ts +73 -0
- package/src/composables/useEndpoints.ts +94 -0
- package/src/composables/useLogTail.ts +51 -0
- package/src/composables/useManifest.ts +43 -0
- package/src/composables/useProcesses.ts +114 -0
- package/src/composables/useProject.ts +34 -0
- package/src/composables/useTelemetry.ts +270 -0
- package/src/lib/__tests__/bc-graph.test.ts +218 -0
- package/src/lib/__tests__/dispatch-form.test.ts +113 -0
- package/src/lib/__tests__/error-friendly.test.ts +198 -0
- package/src/lib/__tests__/home.test.ts +231 -0
- package/src/lib/__tests__/inspect.test.ts +160 -0
- package/src/lib/__tests__/kind-colors.test.ts +59 -0
- package/src/lib/__tests__/live-table.test.ts +194 -0
- package/src/lib/__tests__/manifest-health.test.ts +120 -0
- package/src/lib/__tests__/manifest.test.ts +87 -0
- package/src/lib/__tests__/metadata.test.ts +47 -0
- package/src/lib/__tests__/node-metrics.test.ts +144 -0
- package/src/lib/__tests__/operate.test.ts +97 -0
- package/src/lib/__tests__/pipeline-flow.test.ts +79 -0
- package/src/lib/__tests__/rca.test.ts +124 -0
- package/src/lib/__tests__/telemetry.test.ts +91 -0
- package/src/lib/__tests__/topology-graph.test.ts +331 -0
- package/src/lib/__tests__/topology-view.test.ts +154 -0
- package/src/lib/__tests__/waterfall.test.ts +165 -0
- package/src/lib/bc-graph.ts +298 -0
- package/src/lib/dispatch-form.ts +160 -0
- package/src/lib/error-friendly.ts +288 -0
- package/src/lib/home.ts +191 -0
- package/src/lib/inspect.ts +226 -0
- package/src/lib/kind-colors.ts +132 -0
- package/src/lib/live-table.ts +204 -0
- package/src/lib/manifest-health.ts +71 -0
- package/src/lib/manifest.ts +139 -0
- package/src/lib/metadata.ts +52 -0
- package/src/lib/node-metrics.ts +242 -0
- package/src/lib/operate.ts +114 -0
- package/src/lib/pipeline-flow.ts +120 -0
- package/src/lib/rca.ts +193 -0
- package/src/lib/telemetry.ts +155 -0
- package/src/lib/topology-graph.ts +551 -0
- package/src/lib/topology-view.ts +185 -0
- package/src/lib/waterfall.ts +148 -0
- package/src/main.ts +63 -29
- package/src/pages/Errors.vue +272 -0
- package/src/pages/Home.stories.ts +7 -8
- package/src/pages/Home.vue +255 -540
- package/src/pages/Hooks.stories.ts +44 -0
- package/src/pages/Hooks.vue +165 -164
- package/src/pages/Inspect.vue +240 -0
- package/src/pages/Map.vue +187 -0
- package/src/pages/Operate.vue +74 -0
- package/src/pages/Plugins.stories.ts +1 -1
- package/src/pages/Plugins.vue +174 -238
- package/src/pages/Projects.vue +62 -60
- package/src/pages/Streams.vue +344 -0
- package/src/pages/Topology.vue +318 -136
- package/src/pages/Trace.vue +174 -412
- package/src/pages/__tests__/Home.test.ts +109 -54
- package/src/pages/__tests__/Hooks.test.ts +5 -5
- package/src/pages/__tests__/Inspect.test.ts +111 -0
- package/src/pages/__tests__/Plugins.test.ts +85 -35
- package/src/pages/__tests__/Trace.test.ts +117 -0
- package/src/pages/operate/CommandsPanel.vue +186 -0
- package/src/pages/{Dispatch.vue → operate/DispatchPanel.vue} +100 -203
- package/src/pages/operate/EndpointPicker.vue +56 -0
- package/src/pages/operate/RunPanel.vue +316 -0
- package/src/server/__tests__/nwire-read.test.ts +80 -0
- package/src/server/nwire-read.ts +63 -0
- package/vite.config.ts +220 -2
- package/src/lib/__tests__/normalize-cache.test.ts +0 -105
- package/src/lib/cache.ts +0 -312
- package/src/lib/normalize-cache.ts +0 -92
- package/src/pages/Actions.vue +0 -171
- package/src/pages/Apps.vue +0 -177
- package/src/pages/Commands.vue +0 -262
- package/src/pages/Events.vue +0 -210
- package/src/pages/Live.vue +0 -249
- package/src/pages/Overview.vue +0 -161
- package/src/pages/Projections.vue +0 -148
- package/src/pages/Queries.vue +0 -148
- package/src/pages/Run.vue +0 -618
- package/src/pages/Sinks.vue +0 -124
- package/src/pages/TraceNode.vue +0 -164
- package/src/pages/Workflows.vue +0 -184
- package/src/pages/__tests__/Actions.test.ts +0 -98
- package/src/pages/__tests__/Projections.test.ts +0 -90
- package/src/pages/__tests__/Queries.test.ts +0 -86
package/src/pages/Topology.vue
CHANGED
|
@@ -1,164 +1,346 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Topology —
|
|
3
|
+
* Topology — the app's live service map, not a single rectangle.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The Wiring tab draws the real pipeline left→right: sources (routes / webhooks
|
|
6
|
+
* / crons) → handlers (actions / queries) → effects (events / external calls /
|
|
7
|
+
* workflows / actors / projections) → sinks. Each node is a rich `ServiceNode`
|
|
8
|
+
* card (kind icon, title, status dot, live metrics), accent-coloured by kind and
|
|
9
|
+
* positioned by the pure layered layout in `topology-graph`. Telemetry overlays
|
|
10
|
+
* per-node throughput / errors / p50 and animates the edges that carry traffic;
|
|
11
|
+
* before any run the metrics show neutral placeholders. Clicking a node opens
|
|
12
|
+
* the shared `NodeCard` detail. The Endpoints tab is the "what's exposed" view.
|
|
13
|
+
*
|
|
14
|
+
* Stability: node positions come from the manifest only (deterministic layout),
|
|
15
|
+
* so a telemetry refresh updates counts in place and never moves a node.
|
|
9
16
|
*/
|
|
10
17
|
import { computed, ref } from "vue";
|
|
11
|
-
import { VueFlow, type Node, type Edge, MarkerType } from "@vue-flow/core";
|
|
18
|
+
import { VueFlow, type Node, type Edge, MarkerType, Position } from "@vue-flow/core";
|
|
12
19
|
import { Background } from "@vue-flow/background";
|
|
13
20
|
import { Controls } from "@vue-flow/controls";
|
|
14
|
-
import {
|
|
21
|
+
import { MiniMap } from "@vue-flow/minimap";
|
|
22
|
+
import { Network } from "lucide-vue-next";
|
|
23
|
+
import { useManifest } from "@/composables/useManifest";
|
|
24
|
+
import { useProject } from "@/composables/useProject";
|
|
25
|
+
import { useTelemetry } from "@/composables/useTelemetry";
|
|
26
|
+
import {
|
|
27
|
+
buildWiringGraph,
|
|
28
|
+
layoutWiringGraph,
|
|
29
|
+
buildEndpoints,
|
|
30
|
+
type WiringLayer,
|
|
31
|
+
} from "@/lib/topology-graph";
|
|
32
|
+
import { nodeDetail, type BcNodeDetail } from "@/lib/bc-graph";
|
|
33
|
+
import { kindColor, KIND_LEGEND } from "@/lib/kind-colors";
|
|
34
|
+
import { rollupMetrics, edgeActivity } from "@/lib/node-metrics";
|
|
35
|
+
import { NodeCard, EmptyState, SourceDrawer, ServiceNode } from "@/components";
|
|
15
36
|
import "@vue-flow/core/dist/style.css";
|
|
16
37
|
import "@vue-flow/core/dist/theme-default.css";
|
|
17
38
|
import "@vue-flow/controls/dist/style.css";
|
|
39
|
+
import "@vue-flow/minimap/dist/style.css";
|
|
18
40
|
|
|
19
|
-
const {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
41
|
+
const { activeCwd } = useProject();
|
|
42
|
+
const { manifest, view, isLoading } = useManifest(activeCwd);
|
|
43
|
+
|
|
44
|
+
// Live behaviour feed — overlaid as per-node metrics + edge animation.
|
|
45
|
+
const { records } = useTelemetry(activeCwd);
|
|
46
|
+
const metrics = computed(() => rollupMetrics(records.value));
|
|
47
|
+
const hasRun = computed(() => metrics.value.total > 0);
|
|
48
|
+
|
|
49
|
+
const tab = ref<"wiring" | "endpoints">("wiring");
|
|
50
|
+
|
|
51
|
+
/** The full wiring graph, plus the apps present (for the per-app filter). */
|
|
52
|
+
const fullGraph = computed(() => buildWiringGraph(view.value));
|
|
53
|
+
const apps = computed(() => fullGraph.value.apps);
|
|
54
|
+
|
|
55
|
+
/** When more than one app, scope the canvas to one at a time so the wiring reads. */
|
|
56
|
+
const appFilter = ref<string | null>(null);
|
|
57
|
+
const effectiveApp = computed(() =>
|
|
58
|
+
appFilter.value && apps.value.includes(appFilter.value)
|
|
59
|
+
? appFilter.value
|
|
60
|
+
: apps.value.length === 1
|
|
61
|
+
? apps.value[0]!
|
|
62
|
+
: null,
|
|
63
|
+
);
|
|
64
|
+
const graph = computed(() =>
|
|
65
|
+
effectiveApp.value ? buildWiringGraph(view.value, effectiveApp.value) : fullGraph.value,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const LAYER_LABEL: Record<WiringLayer, string> = {
|
|
69
|
+
source: "Sources",
|
|
70
|
+
handler: "Handlers",
|
|
71
|
+
effect: "Effects",
|
|
72
|
+
sink: "Sinks",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Node positions — from the manifest ONLY. This array's identity changes only
|
|
77
|
+
* when the wiring graph changes (manifest / app filter), so a telemetry tick
|
|
78
|
+
* never re-seeds VueFlow's positions: metrics ride the `data` object instead.
|
|
79
|
+
*/
|
|
80
|
+
const nodes = computed<Node[]>(() => {
|
|
81
|
+
const laid = layoutWiringGraph(graph.value, {
|
|
82
|
+
nodeWidth: 196,
|
|
83
|
+
nodeHeight: 84,
|
|
84
|
+
laneGap: 130,
|
|
85
|
+
rowGap: 34,
|
|
86
|
+
});
|
|
87
|
+
return laid.map((n) => ({
|
|
88
|
+
id: n.id,
|
|
89
|
+
type: "service",
|
|
90
|
+
position: { x: n.x, y: n.y },
|
|
91
|
+
sourcePosition: Position.Right,
|
|
92
|
+
targetPosition: Position.Left,
|
|
93
|
+
data: { kind: n.kind, name: n.name, layer: n.layer, public: n.public },
|
|
94
|
+
style: { width: `${n.width}px` },
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const edges = computed<Edge[]>(() => {
|
|
99
|
+
const present = new Set(nodes.value.map((n) => n.id));
|
|
100
|
+
const live = edgeActivity(
|
|
101
|
+
metrics.value.byNode,
|
|
102
|
+
graph.value.edges.map((e) => ({ id: e.id, from: e.from, to: e.to })),
|
|
103
|
+
);
|
|
104
|
+
return graph.value.edges
|
|
105
|
+
.filter((e) => present.has(e.from) && present.has(e.to))
|
|
106
|
+
.map((e) => {
|
|
107
|
+
const act = live.get(e.id);
|
|
108
|
+
const color = e.back ? "#71717a" : "#a78bfa";
|
|
109
|
+
const volume = act?.volume ?? 0;
|
|
110
|
+
const label = volume > 0 ? `${e.type} · ${volume}` : e.type;
|
|
111
|
+
return {
|
|
112
|
+
id: e.id,
|
|
113
|
+
source: e.from,
|
|
114
|
+
target: e.to,
|
|
115
|
+
label,
|
|
85
116
|
type: "smoothstep",
|
|
86
|
-
animated:
|
|
87
|
-
style: {
|
|
88
|
-
|
|
117
|
+
animated: act?.active ?? false,
|
|
118
|
+
style: {
|
|
119
|
+
stroke: color,
|
|
120
|
+
strokeWidth: act?.active ? 2.5 : 1.5,
|
|
121
|
+
strokeDasharray: e.back ? "4 3" : undefined,
|
|
122
|
+
opacity: act?.active ? 1 : 0.7,
|
|
123
|
+
},
|
|
124
|
+
labelStyle: { fontSize: "9px", fill: act?.active ? "#e4e4e7" : "#a1a1aa" },
|
|
89
125
|
labelBgStyle: { fill: "#18181b" },
|
|
90
|
-
labelBgPadding: [
|
|
91
|
-
labelBgBorderRadius:
|
|
92
|
-
markerEnd: { type: MarkerType.ArrowClosed, color
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return { nodes, edges };
|
|
126
|
+
labelBgPadding: [3, 1] as [number, number],
|
|
127
|
+
labelBgBorderRadius: 3,
|
|
128
|
+
markerEnd: { type: MarkerType.ArrowClosed, color },
|
|
129
|
+
} satisfies Edge;
|
|
130
|
+
});
|
|
98
131
|
});
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return { nodes: elements.value.nodes, edges };
|
|
133
|
+
/** Lane headers drawn above the canvas (only lanes that hold nodes). */
|
|
134
|
+
const lanes = computed<WiringLayer[]>(() => {
|
|
135
|
+
const present = new Set(graph.value.nodes.map((n) => n.layer));
|
|
136
|
+
return (["source", "handler", "effect", "sink"] as WiringLayer[]).filter((l) => present.has(l));
|
|
105
137
|
});
|
|
138
|
+
|
|
139
|
+
// Detail panel — reuses the Map's NodeCard + nodeDetail resolver.
|
|
140
|
+
const selectedId = ref<string | null>(null);
|
|
141
|
+
const detail = computed<BcNodeDetail | null>(() =>
|
|
142
|
+
nodeDetail(manifest.value ?? null, selectedId.value),
|
|
143
|
+
);
|
|
144
|
+
function onNodeClick(id: string): void {
|
|
145
|
+
selectedId.value = selectedId.value === id ? null : id;
|
|
146
|
+
}
|
|
147
|
+
const sourcePreview = ref<{ file: string; line?: number; column?: number } | null>(null);
|
|
148
|
+
|
|
149
|
+
const endpoints = computed(() => buildEndpoints(view.value));
|
|
150
|
+
|
|
151
|
+
const isEmpty = computed(() => !isLoading.value && fullGraph.value.nodes.length === 0);
|
|
152
|
+
|
|
153
|
+
/** MiniMap node tint — accent by kind so the overview reads at a glance. */
|
|
154
|
+
function miniColor(node: { data?: { kind?: string } }): string {
|
|
155
|
+
return kindColor(node.data?.kind);
|
|
156
|
+
}
|
|
106
157
|
</script>
|
|
107
158
|
|
|
108
159
|
<template>
|
|
109
|
-
<div
|
|
110
|
-
<div class="border-b border-zinc-800 px-6 py-3
|
|
111
|
-
<div>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
160
|
+
<div class="h-full flex flex-col" data-testid="topology-page">
|
|
161
|
+
<div class="border-b border-zinc-800 px-6 py-3">
|
|
162
|
+
<div class="flex items-center justify-between">
|
|
163
|
+
<div>
|
|
164
|
+
<h1 class="text-lg font-semibold tracking-tight">Topology</h1>
|
|
165
|
+
<p class="text-xs text-zinc-500">
|
|
166
|
+
How the app is wired: source → handler → effect → sink
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="flex items-center gap-3">
|
|
170
|
+
<span
|
|
171
|
+
class="inline-flex items-center gap-1.5 text-[11px]"
|
|
172
|
+
:class="hasRun ? 'text-emerald-300' : 'text-zinc-500'"
|
|
173
|
+
data-testid="topology-live"
|
|
174
|
+
>
|
|
175
|
+
<span
|
|
176
|
+
class="h-2 w-2 rounded-full"
|
|
177
|
+
:class="hasRun ? 'bg-emerald-400 animate-pulse' : 'bg-zinc-600'"
|
|
178
|
+
/>
|
|
179
|
+
{{ hasRun ? "Live" : "Idle" }}
|
|
180
|
+
</span>
|
|
181
|
+
<select
|
|
182
|
+
v-if="apps.length > 1 && tab === 'wiring'"
|
|
183
|
+
v-model="appFilter"
|
|
184
|
+
class="bg-zinc-900 border border-zinc-800 rounded text-xs text-zinc-300 px-2 py-1"
|
|
185
|
+
data-testid="topology-app-filter"
|
|
186
|
+
>
|
|
187
|
+
<option v-for="a in apps" :key="a" :value="a">{{ a }}</option>
|
|
188
|
+
</select>
|
|
189
|
+
<div class="flex rounded border border-zinc-800 overflow-hidden text-xs">
|
|
190
|
+
<button
|
|
191
|
+
class="px-3 py-1"
|
|
192
|
+
:class="tab === 'wiring' ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400'"
|
|
193
|
+
data-testid="topology-tab-wiring"
|
|
194
|
+
@click="tab = 'wiring'"
|
|
195
|
+
>
|
|
196
|
+
Wiring
|
|
197
|
+
</button>
|
|
198
|
+
<button
|
|
199
|
+
class="px-3 py-1 border-l border-zinc-800"
|
|
200
|
+
:class="tab === 'endpoints' ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400'"
|
|
201
|
+
data-testid="topology-tab-endpoints"
|
|
202
|
+
@click="tab = 'endpoints'"
|
|
203
|
+
>
|
|
204
|
+
Endpoints
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div v-if="isLoading" class="flex-1 grid place-items-center text-sm text-zinc-500">
|
|
212
|
+
Loading manifest…
|
|
213
|
+
</div>
|
|
214
|
+
<EmptyState
|
|
215
|
+
v-else-if="isEmpty"
|
|
216
|
+
:icon="Network"
|
|
217
|
+
title="No wiring discovered yet"
|
|
218
|
+
hint="Run nwire cache to build the manifest, then this maps the app's source → handler → effect → sink pipeline."
|
|
219
|
+
data-testid="topology-empty"
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
<!-- WIRING TAB -->
|
|
223
|
+
<div v-else-if="tab === 'wiring'" class="flex-1 flex min-h-0">
|
|
224
|
+
<div class="flex-1 relative min-w-0">
|
|
225
|
+
<!-- lane headers -->
|
|
226
|
+
<div
|
|
227
|
+
class="absolute top-0 left-0 right-0 z-10 flex pointer-events-none px-6 pt-2"
|
|
228
|
+
data-testid="topology-lanes"
|
|
229
|
+
>
|
|
230
|
+
<div
|
|
231
|
+
v-for="l in lanes"
|
|
232
|
+
:key="l"
|
|
233
|
+
class="flex-1 text-center text-[10px] uppercase tracking-wider text-zinc-600 font-medium"
|
|
234
|
+
>
|
|
235
|
+
{{ LAYER_LABEL[l] }}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<VueFlow
|
|
240
|
+
:nodes="nodes"
|
|
241
|
+
:edges="edges"
|
|
242
|
+
:fit-view-on-init="true"
|
|
243
|
+
:min-zoom="0.2"
|
|
244
|
+
:max-zoom="1.5"
|
|
245
|
+
:default-edge-options="{ type: 'smoothstep' }"
|
|
246
|
+
@node-click="onNodeClick($event.node.id)"
|
|
247
|
+
>
|
|
248
|
+
<template #node-service="{ data, id }">
|
|
249
|
+
<ServiceNode
|
|
250
|
+
:id="id"
|
|
251
|
+
:kind="data.kind"
|
|
252
|
+
:name="data.name"
|
|
253
|
+
:public="data.public"
|
|
254
|
+
:selected="selectedId === id"
|
|
255
|
+
:metrics="metrics.byNode.get(id)"
|
|
256
|
+
/>
|
|
257
|
+
</template>
|
|
258
|
+
<Background :pattern-color="'#27272a'" :gap="22" :size="1.4" />
|
|
259
|
+
<MiniMap pannable zoomable :node-color="miniColor" class="!bg-zinc-950/80" />
|
|
260
|
+
<Controls />
|
|
261
|
+
</VueFlow>
|
|
262
|
+
|
|
263
|
+
<!-- kind legend -->
|
|
264
|
+
<div
|
|
265
|
+
class="absolute bottom-3 left-3 flex flex-wrap gap-x-3 gap-y-1 rounded-md border border-zinc-800 bg-zinc-950/90 px-3 py-2 backdrop-blur"
|
|
266
|
+
data-testid="topology-legend"
|
|
267
|
+
>
|
|
268
|
+
<span
|
|
269
|
+
v-for="k in KIND_LEGEND"
|
|
270
|
+
:key="k"
|
|
271
|
+
class="inline-flex items-center gap-1 text-[10px] text-zinc-400"
|
|
272
|
+
>
|
|
273
|
+
<span class="h-1.5 w-1.5 rounded-full" :style="{ background: kindColor(k) }" />
|
|
274
|
+
{{ k }}
|
|
118
275
|
</span>
|
|
119
|
-
</
|
|
276
|
+
</div>
|
|
120
277
|
</div>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
278
|
+
|
|
279
|
+
<aside
|
|
280
|
+
v-if="selectedId"
|
|
281
|
+
class="w-96 shrink-0 border-l border-zinc-800 bg-zinc-950"
|
|
282
|
+
data-testid="topology-detail"
|
|
283
|
+
>
|
|
284
|
+
<NodeCard
|
|
285
|
+
:detail="detail"
|
|
286
|
+
@open-source="sourcePreview = $event"
|
|
287
|
+
@select-event="selectedId = `event:${$event}`"
|
|
288
|
+
/>
|
|
289
|
+
</aside>
|
|
125
290
|
</div>
|
|
126
291
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
292
|
+
<!-- ENDPOINTS TAB -->
|
|
293
|
+
<div v-else class="flex-1 overflow-y-auto p-6" data-testid="topology-endpoints">
|
|
294
|
+
<div
|
|
295
|
+
v-if="endpoints.length === 0"
|
|
296
|
+
class="text-sm text-zinc-500 grid place-items-center h-full"
|
|
297
|
+
data-testid="endpoints-empty"
|
|
133
298
|
>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
299
|
+
No routes exposed — this app has no HTTP surface.
|
|
300
|
+
</div>
|
|
301
|
+
<table v-else class="w-full text-sm">
|
|
302
|
+
<thead>
|
|
303
|
+
<tr
|
|
304
|
+
class="text-left text-[10px] uppercase tracking-wider text-zinc-600 border-b border-zinc-800"
|
|
305
|
+
>
|
|
306
|
+
<th class="py-2 pr-4 font-medium">Method</th>
|
|
307
|
+
<th class="py-2 pr-4 font-medium">Path</th>
|
|
308
|
+
<th class="py-2 pr-4 font-medium">Handler</th>
|
|
309
|
+
<th class="py-2 font-medium">App</th>
|
|
310
|
+
</tr>
|
|
311
|
+
</thead>
|
|
312
|
+
<tbody>
|
|
313
|
+
<tr
|
|
314
|
+
v-for="ep in endpoints"
|
|
315
|
+
:key="ep.id"
|
|
316
|
+
class="border-b border-zinc-900 hover:bg-zinc-900/40"
|
|
317
|
+
data-testid="endpoint-row"
|
|
318
|
+
>
|
|
319
|
+
<td class="py-2 pr-4">
|
|
320
|
+
<span class="font-mono text-[11px] text-emerald-300">{{ ep.method }}</span>
|
|
321
|
+
</td>
|
|
322
|
+
<td class="py-2 pr-4 font-mono text-[12px] text-zinc-200">{{ ep.path }}</td>
|
|
323
|
+
<td class="py-2 pr-4">
|
|
324
|
+
<button
|
|
325
|
+
v-if="ep.handler"
|
|
326
|
+
type="button"
|
|
327
|
+
class="font-mono text-[11px] text-violet-300 hover:underline"
|
|
328
|
+
data-testid="endpoint-handler"
|
|
329
|
+
@click="
|
|
330
|
+
tab = 'wiring';
|
|
331
|
+
selectedId = ep.handler!.id;
|
|
332
|
+
"
|
|
147
333
|
>
|
|
148
|
-
{{
|
|
149
|
-
</
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
</template>
|
|
159
|
-
<Background pattern-color="#27272a" />
|
|
160
|
-
<Controls />
|
|
161
|
-
</VueFlow>
|
|
334
|
+
{{ ep.handler.name }}
|
|
335
|
+
</button>
|
|
336
|
+
<span v-else class="text-[11px] text-zinc-600">—</span>
|
|
337
|
+
</td>
|
|
338
|
+
<td class="py-2 text-[11px] text-zinc-500">{{ ep.app }}</td>
|
|
339
|
+
</tr>
|
|
340
|
+
</tbody>
|
|
341
|
+
</table>
|
|
162
342
|
</div>
|
|
343
|
+
|
|
344
|
+
<SourceDrawer :source="sourcePreview" @close="sourcePreview = null" />
|
|
163
345
|
</div>
|
|
164
346
|
</template>
|