@nwire/studio 0.12.1 → 0.13.1
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/Commands.vue
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Commands panel — fires nwire CLI commands via the supervisor and
|
|
4
|
-
* tails their stdout/stderr. Shares the same `/__nwire/run/*` surface
|
|
5
|
-
* the Run page uses for topologies; the only difference is the start
|
|
6
|
-
* payload (`{ command, args }` vs `{ topology, port }`).
|
|
7
|
-
*/
|
|
8
|
-
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
9
|
-
import { useRoute } from "vue-router";
|
|
10
|
-
import {
|
|
11
|
-
Play,
|
|
12
|
-
Square,
|
|
13
|
-
RefreshCw,
|
|
14
|
-
Trash2,
|
|
15
|
-
Terminal,
|
|
16
|
-
CircleDot,
|
|
17
|
-
Loader2,
|
|
18
|
-
AlertTriangle,
|
|
19
|
-
CheckCircle2,
|
|
20
|
-
} from "lucide-vue-next";
|
|
21
|
-
|
|
22
|
-
interface CommandEntry {
|
|
23
|
-
name: string;
|
|
24
|
-
description: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface ManagedProcess {
|
|
28
|
-
id: string;
|
|
29
|
-
topology: string;
|
|
30
|
-
startedAt: string;
|
|
31
|
-
status: "idle" | "starting" | "running" | "stopping" | "exited" | "crashed";
|
|
32
|
-
pid?: number;
|
|
33
|
-
exitCode?: number | null;
|
|
34
|
-
signal?: string | null;
|
|
35
|
-
errorMessage?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface LogLine {
|
|
39
|
-
seq: number;
|
|
40
|
-
ts: string;
|
|
41
|
-
stream: "stdout" | "stderr";
|
|
42
|
-
line: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const route = useRoute();
|
|
46
|
-
const commands = ref<CommandEntry[]>([]);
|
|
47
|
-
const processes = ref<ManagedProcess[]>([]);
|
|
48
|
-
const selectedId = ref<string | null>(null);
|
|
49
|
-
/** Preselected command name from `?name=…` — highlights the row. */
|
|
50
|
-
const selectedCommand = ref<string | null>(null);
|
|
51
|
-
const argsInput = ref("");
|
|
52
|
-
const startBusy = ref(false);
|
|
53
|
-
const startError = ref<string | null>(null);
|
|
54
|
-
const logs = ref<LogLine[]>([]);
|
|
55
|
-
let es: EventSource | null = null;
|
|
56
|
-
let processesPoll: ReturnType<typeof setInterval> | null = null;
|
|
57
|
-
|
|
58
|
-
function applyQueryPreselect(): void {
|
|
59
|
-
const name = route.query.name;
|
|
60
|
-
selectedCommand.value = typeof name === "string" && name.length > 0 ? name : null;
|
|
61
|
-
}
|
|
62
|
-
watch(() => route.query.name, applyQueryPreselect);
|
|
63
|
-
|
|
64
|
-
async function loadCommands() {
|
|
65
|
-
const res = await fetch("/__nwire/run/commands");
|
|
66
|
-
const data = (await res.json()) as { commands: CommandEntry[] };
|
|
67
|
-
commands.value = data.commands;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function loadProcesses() {
|
|
71
|
-
const res = await fetch("/__nwire/run/processes");
|
|
72
|
-
const data = (await res.json()) as { processes: ManagedProcess[] };
|
|
73
|
-
// Filter to processes our panel started (topology label begins with "nwire ").
|
|
74
|
-
processes.value = data.processes.filter((p) => p.topology.startsWith("nwire "));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function exec(name: string) {
|
|
78
|
-
startBusy.value = true;
|
|
79
|
-
startError.value = null;
|
|
80
|
-
try {
|
|
81
|
-
const argv = argsInput.value.trim().split(/\s+/).filter(Boolean);
|
|
82
|
-
const res = await fetch("/__nwire/run/exec", {
|
|
83
|
-
method: "POST",
|
|
84
|
-
headers: { "content-type": "application/json" },
|
|
85
|
-
body: JSON.stringify({ command: name, args: argv }),
|
|
86
|
-
});
|
|
87
|
-
if (!res.ok) {
|
|
88
|
-
const data = (await res.json()) as { error?: string };
|
|
89
|
-
startError.value = data.error ?? `HTTP ${res.status}`;
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
await loadProcesses();
|
|
93
|
-
} finally {
|
|
94
|
-
startBusy.value = false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function stop(id: string) {
|
|
99
|
-
await fetch(`/__nwire/run/stop/${encodeURIComponent(id)}`, { method: "POST" });
|
|
100
|
-
await loadProcesses();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function forget(id: string) {
|
|
104
|
-
await fetch(`/__nwire/run/forget/${encodeURIComponent(id)}`, { method: "POST" });
|
|
105
|
-
if (selectedId.value === id) selectedId.value = null;
|
|
106
|
-
await loadProcesses();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function statusIcon(s: ManagedProcess["status"]) {
|
|
110
|
-
switch (s) {
|
|
111
|
-
case "running":
|
|
112
|
-
return { icon: CircleDot, color: "text-emerald-400" };
|
|
113
|
-
case "starting":
|
|
114
|
-
return { icon: Loader2, color: "text-amber-400 animate-spin" };
|
|
115
|
-
case "stopping":
|
|
116
|
-
return { icon: Loader2, color: "text-amber-400 animate-spin" };
|
|
117
|
-
case "exited":
|
|
118
|
-
return { icon: CheckCircle2, color: "text-zinc-500" };
|
|
119
|
-
case "crashed":
|
|
120
|
-
return { icon: AlertTriangle, color: "text-red-400" };
|
|
121
|
-
case "idle":
|
|
122
|
-
return { icon: Terminal, color: "text-zinc-500" };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
watch(selectedId, (id) => {
|
|
127
|
-
if (es) {
|
|
128
|
-
es.close();
|
|
129
|
-
es = null;
|
|
130
|
-
}
|
|
131
|
-
logs.value = [];
|
|
132
|
-
if (!id) return;
|
|
133
|
-
es = new EventSource(`/__nwire/run/logs/${encodeURIComponent(id)}/stream`);
|
|
134
|
-
es.onmessage = (ev) => {
|
|
135
|
-
try {
|
|
136
|
-
const line = JSON.parse(ev.data) as LogLine;
|
|
137
|
-
logs.value = [...logs.value.slice(-1999), line];
|
|
138
|
-
} catch {
|
|
139
|
-
// ignore non-JSON keepalive comments
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
onMounted(async () => {
|
|
145
|
-
applyQueryPreselect();
|
|
146
|
-
await Promise.all([loadCommands(), loadProcesses()]);
|
|
147
|
-
processesPoll = setInterval(loadProcesses, 1500);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
onUnmounted(() => {
|
|
151
|
-
if (es) es.close();
|
|
152
|
-
if (processesPoll) clearInterval(processesPoll);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const selectedProc = computed(() => processes.value.find((p) => p.id === selectedId.value) ?? null);
|
|
156
|
-
</script>
|
|
157
|
-
|
|
158
|
-
<template>
|
|
159
|
-
<div class="h-full flex">
|
|
160
|
-
<!-- Left: command picker -->
|
|
161
|
-
<div class="w-1/3 border-r border-zinc-800 flex flex-col">
|
|
162
|
-
<div class="border-b border-zinc-800 px-4 py-3">
|
|
163
|
-
<h1 class="text-lg font-semibold tracking-tight">Commands</h1>
|
|
164
|
-
<div class="mt-2">
|
|
165
|
-
<input
|
|
166
|
-
v-model="argsInput"
|
|
167
|
-
placeholder="extra args (optional, e.g. `units` for `nwire test units`)"
|
|
168
|
-
class="w-full bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-xs placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600"
|
|
169
|
-
/>
|
|
170
|
-
</div>
|
|
171
|
-
<div v-if="startError" class="text-xs text-red-400 mt-2">{{ startError }}</div>
|
|
172
|
-
</div>
|
|
173
|
-
<div class="flex-1 overflow-auto">
|
|
174
|
-
<button
|
|
175
|
-
v-for="c in commands"
|
|
176
|
-
:key="c.name"
|
|
177
|
-
class="w-full text-left px-4 py-2.5 border-b border-zinc-900 hover:bg-zinc-900/50 transition-colors"
|
|
178
|
-
:class="{ 'bg-zinc-900': selectedCommand === c.name }"
|
|
179
|
-
:data-testid="`command-row-${c.name}`"
|
|
180
|
-
:disabled="startBusy"
|
|
181
|
-
@click="exec(c.name)"
|
|
182
|
-
>
|
|
183
|
-
<div class="flex items-center gap-2">
|
|
184
|
-
<Play class="w-3 h-3 text-emerald-400 shrink-0" />
|
|
185
|
-
<span class="font-mono text-sm">nwire {{ c.name }}</span>
|
|
186
|
-
</div>
|
|
187
|
-
<div class="text-xs text-zinc-500 mt-1 ml-5">{{ c.description }}</div>
|
|
188
|
-
</button>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<!-- Middle: running processes -->
|
|
193
|
-
<div class="w-1/3 border-r border-zinc-800 flex flex-col">
|
|
194
|
-
<div class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between">
|
|
195
|
-
<h2 class="text-sm font-semibold tracking-tight">Active</h2>
|
|
196
|
-
<button class="text-xs text-zinc-500 hover:text-zinc-300" @click="loadProcesses">
|
|
197
|
-
<RefreshCw class="w-3 h-3 inline" />
|
|
198
|
-
</button>
|
|
199
|
-
</div>
|
|
200
|
-
<div class="flex-1 overflow-auto">
|
|
201
|
-
<div v-if="processes.length === 0" class="p-4 text-xs text-zinc-500">
|
|
202
|
-
Click a command on the left to launch it.
|
|
203
|
-
</div>
|
|
204
|
-
<button
|
|
205
|
-
v-for="p in processes"
|
|
206
|
-
:key="p.id"
|
|
207
|
-
class="w-full text-left px-4 py-2 border-b border-zinc-900 hover:bg-zinc-900/50"
|
|
208
|
-
:class="{ 'bg-zinc-900': selectedId === p.id }"
|
|
209
|
-
@click="selectedId = p.id"
|
|
210
|
-
>
|
|
211
|
-
<div class="flex items-center gap-2">
|
|
212
|
-
<component
|
|
213
|
-
:is="statusIcon(p.status).icon"
|
|
214
|
-
class="w-3 h-3 shrink-0"
|
|
215
|
-
:class="statusIcon(p.status).color"
|
|
216
|
-
/>
|
|
217
|
-
<span class="font-mono text-sm truncate">{{ p.topology }}</span>
|
|
218
|
-
</div>
|
|
219
|
-
<div class="text-[10px] text-zinc-500 mt-1 ml-5">
|
|
220
|
-
{{ p.status }} · pid {{ p.pid ?? "—" }} · started {{ p.startedAt.slice(11, 19) }}
|
|
221
|
-
</div>
|
|
222
|
-
</button>
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<!-- Right: live stdout -->
|
|
227
|
-
<div class="flex-1 flex flex-col">
|
|
228
|
-
<div class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between">
|
|
229
|
-
<h2 class="text-sm font-semibold tracking-tight font-mono">
|
|
230
|
-
{{ selectedProc ? selectedProc.topology : "Output" }}
|
|
231
|
-
</h2>
|
|
232
|
-
<div v-if="selectedProc" class="flex items-center gap-2">
|
|
233
|
-
<button
|
|
234
|
-
v-if="selectedProc.status === 'running' || selectedProc.status === 'starting'"
|
|
235
|
-
class="text-xs text-zinc-400 hover:text-red-400"
|
|
236
|
-
@click="stop(selectedProc.id)"
|
|
237
|
-
>
|
|
238
|
-
<Square class="w-3 h-3 inline" /> stop
|
|
239
|
-
</button>
|
|
240
|
-
<button
|
|
241
|
-
class="text-xs text-zinc-400 hover:text-zinc-300"
|
|
242
|
-
@click="forget(selectedProc.id)"
|
|
243
|
-
>
|
|
244
|
-
<Trash2 class="w-3 h-3 inline" /> forget
|
|
245
|
-
</button>
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
<div class="flex-1 overflow-auto bg-zinc-950 font-mono text-xs p-4">
|
|
249
|
-
<div v-if="logs.length === 0" class="text-zinc-600">
|
|
250
|
-
Select a process to view its stdout.
|
|
251
|
-
</div>
|
|
252
|
-
<div
|
|
253
|
-
v-for="l in logs"
|
|
254
|
-
:key="l.seq"
|
|
255
|
-
:class="l.stream === 'stderr' ? 'text-red-400' : 'text-zinc-300'"
|
|
256
|
-
>
|
|
257
|
-
{{ l.line }}
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
</template>
|
package/src/pages/Events.vue
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, ref, watch } from "vue";
|
|
3
|
-
import { useRoute, useRouter } from "vue-router";
|
|
4
|
-
import { useCache } from "@/lib/cache";
|
|
5
|
-
import { Radio, Search, Lock, Globe, ArrowRight } from "lucide-vue-next";
|
|
6
|
-
import { SchemaTree, SourcePill, SourceDrawer } from "@/components";
|
|
7
|
-
|
|
8
|
-
const route = useRoute();
|
|
9
|
-
const router = useRouter();
|
|
10
|
-
const { cache } = useCache();
|
|
11
|
-
const filter = ref("");
|
|
12
|
-
const selected = ref<string | null>(null);
|
|
13
|
-
const sourcePreview = ref<{ file: string; line: number; column?: number } | null>(null);
|
|
14
|
-
|
|
15
|
-
function applyQueryPreselect(): void {
|
|
16
|
-
const name = route.query.name;
|
|
17
|
-
if (typeof name === "string" && name.length > 0) {
|
|
18
|
-
selected.value = name;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
onMounted(applyQueryPreselect);
|
|
23
|
-
watch(() => route.query.name, applyQueryPreselect);
|
|
24
|
-
|
|
25
|
-
const filtered = computed(() => {
|
|
26
|
-
if (!cache.value) return [];
|
|
27
|
-
const q = filter.value.toLowerCase();
|
|
28
|
-
return cache.value.events.filter(
|
|
29
|
-
(e) =>
|
|
30
|
-
!q ||
|
|
31
|
-
e.name.toLowerCase().includes(q) ||
|
|
32
|
-
(e.description ?? "").toLowerCase().includes(q) ||
|
|
33
|
-
e.app.toLowerCase().includes(q),
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const detail = computed(() => filtered.value.find((e) => e.name === selected.value) ?? null);
|
|
38
|
-
|
|
39
|
-
/** Producers — graph edges where `to` is this event and via === "emits". */
|
|
40
|
-
const producers = computed(() => {
|
|
41
|
-
if (!cache.value || !detail.value) return [];
|
|
42
|
-
return cache.value.graph.events
|
|
43
|
-
.filter((edge) => edge.to === detail.value!.name && edge.via === "emits")
|
|
44
|
-
.map((edge) => edge.from);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
/** Consumers — graph edges where `from` is this event. */
|
|
48
|
-
const consumers = computed(() => {
|
|
49
|
-
if (!cache.value || !detail.value) return [];
|
|
50
|
-
return cache.value.graph.events
|
|
51
|
-
.filter((edge) => edge.from === detail.value!.name)
|
|
52
|
-
.map((edge) => ({ name: edge.to, via: edge.via }));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
function openProducer(name: string): void {
|
|
56
|
-
void router.push({ path: "/actions", query: { name } });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function openConsumer(c: { name: string; via: string }): void {
|
|
60
|
-
if (c.via === "folds") {
|
|
61
|
-
void router.push({ path: "/projections", query: { name: c.name } });
|
|
62
|
-
} else if (c.via === "subscribes") {
|
|
63
|
-
void router.push({ path: "/workflows", query: { name: c.name } });
|
|
64
|
-
} else if (c.via === "dispatches") {
|
|
65
|
-
void router.push({ path: "/actions", query: { name: c.name } });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Payload schema — the scanner emits it on actions (`inputSchema`) but
|
|
71
|
-
* not yet on events. Render whatever the manifest happens to carry
|
|
72
|
-
* under `.schema` if anything; otherwise the SchemaTree slot just
|
|
73
|
-
* doesn't render.
|
|
74
|
-
*/
|
|
75
|
-
const detailSchema = computed<unknown>(() => {
|
|
76
|
-
const d = detail.value as { schema?: unknown } | null;
|
|
77
|
-
return d?.schema;
|
|
78
|
-
});
|
|
79
|
-
</script>
|
|
80
|
-
|
|
81
|
-
<template>
|
|
82
|
-
<div v-if="cache" class="h-full flex">
|
|
83
|
-
<div class="w-2/5 border-r border-zinc-800 flex flex-col">
|
|
84
|
-
<div class="border-b border-zinc-800 px-4 py-3">
|
|
85
|
-
<h1 class="text-lg font-semibold tracking-tight">Events</h1>
|
|
86
|
-
<div class="relative mt-2">
|
|
87
|
-
<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-500" />
|
|
88
|
-
<input
|
|
89
|
-
v-model="filter"
|
|
90
|
-
placeholder="filter…"
|
|
91
|
-
class="w-full bg-zinc-900 border border-zinc-800 rounded pl-7 pr-2 py-1.5 text-sm placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600"
|
|
92
|
-
/>
|
|
93
|
-
</div>
|
|
94
|
-
<div class="text-[10px] text-zinc-500 mt-1">
|
|
95
|
-
{{ filtered.length }} / {{ cache.events.length }}
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
<div class="flex-1 overflow-auto">
|
|
99
|
-
<button
|
|
100
|
-
v-for="e in filtered"
|
|
101
|
-
:key="`${e.app}::${e.name}`"
|
|
102
|
-
class="w-full text-left px-4 py-2.5 border-b border-zinc-900 hover:bg-zinc-900/50 transition-colors"
|
|
103
|
-
:class="{ 'bg-zinc-900': selected === e.name }"
|
|
104
|
-
@click="selected = e.name"
|
|
105
|
-
>
|
|
106
|
-
<div class="flex items-center justify-between">
|
|
107
|
-
<div class="flex items-center gap-2 min-w-0">
|
|
108
|
-
<Radio class="w-3 h-3 text-purple-400 shrink-0" />
|
|
109
|
-
<span class="font-mono text-sm truncate">{{ e.name }}</span>
|
|
110
|
-
</div>
|
|
111
|
-
<component
|
|
112
|
-
:is="e.public ? Globe : Lock"
|
|
113
|
-
class="w-3 h-3 shrink-0"
|
|
114
|
-
:class="e.public ? 'text-emerald-400' : 'text-zinc-500'"
|
|
115
|
-
:title="e.public ? 'public — reaches outbound sinks' : 'private — stays in-process'"
|
|
116
|
-
/>
|
|
117
|
-
</div>
|
|
118
|
-
<div v-if="e.description" class="text-xs text-zinc-500 mt-1 line-clamp-2 ml-5">
|
|
119
|
-
{{ e.description }}
|
|
120
|
-
</div>
|
|
121
|
-
</button>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<div class="flex-1 overflow-auto">
|
|
126
|
-
<div v-if="!detail" class="p-6 text-zinc-500 text-sm">Select an event to view its flow.</div>
|
|
127
|
-
<div v-else class="p-6 space-y-5">
|
|
128
|
-
<div>
|
|
129
|
-
<div class="text-[10px] uppercase tracking-wide text-zinc-500 flex items-center gap-2">
|
|
130
|
-
<span>{{ detail.app }}</span>
|
|
131
|
-
<span
|
|
132
|
-
class="px-1.5 py-0.5 rounded text-[10px] uppercase"
|
|
133
|
-
:class="
|
|
134
|
-
detail.public
|
|
135
|
-
? 'bg-emerald-950/50 border border-emerald-900 text-emerald-300'
|
|
136
|
-
: 'bg-zinc-950/50 border border-zinc-800 text-zinc-400'
|
|
137
|
-
"
|
|
138
|
-
>
|
|
139
|
-
{{ detail.public ? "public" : "private" }}
|
|
140
|
-
</span>
|
|
141
|
-
<span v-if="detail.version" class="text-zinc-600">v{{ detail.version }}</span>
|
|
142
|
-
</div>
|
|
143
|
-
<h2 class="font-mono text-xl mt-1">{{ detail.name }}</h2>
|
|
144
|
-
<p v-if="detail.description" class="text-sm text-zinc-400 mt-2 max-w-2xl">
|
|
145
|
-
{{ detail.description }}
|
|
146
|
-
</p>
|
|
147
|
-
</div>
|
|
148
|
-
|
|
149
|
-
<div v-if="producers.length > 0 || consumers.length > 0">
|
|
150
|
-
<h3 class="text-xs uppercase tracking-wide text-zinc-500 mb-2">Flow</h3>
|
|
151
|
-
<div class="text-sm space-y-2">
|
|
152
|
-
<div v-if="producers.length > 0" class="flex items-start gap-2">
|
|
153
|
-
<span class="text-zinc-500 mt-0.5">Produced by:</span>
|
|
154
|
-
<div class="flex flex-wrap gap-1.5">
|
|
155
|
-
<button
|
|
156
|
-
v-for="p in producers"
|
|
157
|
-
:key="p"
|
|
158
|
-
type="button"
|
|
159
|
-
class="flex items-center gap-1 text-xs font-mono bg-zinc-900 border border-zinc-800 rounded px-1.5 py-0.5 hover:bg-zinc-800"
|
|
160
|
-
@click="openProducer(p)"
|
|
161
|
-
>
|
|
162
|
-
<ArrowRight class="w-2.5 h-2.5 text-amber-400" />
|
|
163
|
-
{{ p }}
|
|
164
|
-
</button>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
<div v-if="consumers.length > 0" class="flex items-start gap-2">
|
|
168
|
-
<span class="text-zinc-500 mt-0.5">Consumed by:</span>
|
|
169
|
-
<div class="flex flex-wrap gap-1.5">
|
|
170
|
-
<button
|
|
171
|
-
v-for="(c, i) in consumers"
|
|
172
|
-
:key="i"
|
|
173
|
-
type="button"
|
|
174
|
-
class="flex items-center gap-1 text-xs font-mono bg-zinc-900 border border-zinc-800 rounded px-1.5 py-0.5 hover:bg-zinc-800"
|
|
175
|
-
@click="openConsumer(c)"
|
|
176
|
-
>
|
|
177
|
-
<ArrowRight class="w-2.5 h-2.5 text-zinc-500" />
|
|
178
|
-
{{ c.name }}
|
|
179
|
-
<span class="text-[9px] uppercase text-zinc-500">{{ c.via }}</span>
|
|
180
|
-
</button>
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
<div
|
|
184
|
-
v-if="detail.audience && detail.audience.length > 0"
|
|
185
|
-
class="flex items-start gap-2"
|
|
186
|
-
>
|
|
187
|
-
<span class="text-zinc-500 mt-0.5">Audience:</span>
|
|
188
|
-
<div class="flex flex-wrap gap-1.5">
|
|
189
|
-
<span
|
|
190
|
-
v-for="a in detail.audience"
|
|
191
|
-
:key="a"
|
|
192
|
-
class="text-xs font-mono bg-zinc-900 border border-zinc-800 rounded px-1.5 py-0.5"
|
|
193
|
-
>
|
|
194
|
-
{{ a }}
|
|
195
|
-
</span>
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<div v-if="detail.source" class="flex items-center gap-2">
|
|
202
|
-
<SourcePill :source="detail.source" @click="sourcePreview = detail.source!" />
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
<SchemaTree v-if="detailSchema" :schema="detailSchema" label="Payload schema" />
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
<SourceDrawer :source="sourcePreview" @close="sourcePreview = null" />
|
|
209
|
-
</div>
|
|
210
|
-
</template>
|