@nwire/studio 0.12.0 → 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.
Files changed (130) hide show
  1. package/package.json +5 -3
  2. package/src/App.vue +62 -56
  3. package/src/components/BcCard.stories.ts +47 -0
  4. package/src/components/BcCard.vue +152 -0
  5. package/src/components/DurationBar.stories.ts +55 -0
  6. package/src/components/DurationBar.vue +72 -0
  7. package/src/components/ErrorCard.stories.ts +133 -0
  8. package/src/components/ErrorCard.vue +153 -0
  9. package/src/components/GraphCanvas.stories.ts +48 -0
  10. package/src/components/GraphCanvas.vue +88 -0
  11. package/src/components/KpiTile.stories.ts +32 -0
  12. package/src/components/KpiTile.vue +39 -0
  13. package/src/components/LiveTable.stories.ts +78 -0
  14. package/src/components/LiveTable.vue +186 -0
  15. package/src/components/MetadataInspector.stories.ts +53 -0
  16. package/src/components/MetadataInspector.vue +105 -0
  17. package/src/components/NodeCard.stories.ts +44 -0
  18. package/src/components/NodeCard.vue +150 -0
  19. package/src/components/RcaPanel.stories.ts +95 -0
  20. package/src/components/RcaPanel.vue +223 -0
  21. package/src/components/ServiceNode.vue +134 -0
  22. package/src/components/SourceDrawer.vue +6 -4
  23. package/src/components/SourcePill.vue +10 -3
  24. package/src/components/StatusBadge.stories.ts +33 -0
  25. package/src/components/StatusBadge.vue +54 -0
  26. package/src/components/Waterfall.stories.ts +85 -0
  27. package/src/components/Waterfall.vue +53 -0
  28. package/src/components/WaterfallRow.vue +74 -0
  29. package/src/components/__tests__/BcCard.test.ts +53 -0
  30. package/src/components/__tests__/DurationBar.test.ts +31 -0
  31. package/src/components/__tests__/ErrorCard.test.ts +71 -0
  32. package/src/components/__tests__/KpiTile.test.ts +23 -0
  33. package/src/components/__tests__/LiveTable.test.ts +100 -0
  34. package/src/components/__tests__/MetadataInspector.test.ts +38 -0
  35. package/src/components/__tests__/NodeCard.test.ts +116 -0
  36. package/src/components/__tests__/RcaPanel.test.ts +81 -0
  37. package/src/components/__tests__/StatusBadge.test.ts +23 -0
  38. package/src/components/__tests__/Waterfall.test.ts +54 -0
  39. package/src/components/index.ts +13 -0
  40. package/src/composables/__tests__/composables-context.test.ts +107 -0
  41. package/src/composables/__tests__/useTelemetry.test.ts +104 -0
  42. package/src/composables/__tests__/useTelemetryRuns.test.ts +282 -0
  43. package/src/composables/useDiscovery.ts +73 -0
  44. package/src/composables/useEndpoints.ts +94 -0
  45. package/src/composables/useLogTail.ts +51 -0
  46. package/src/composables/useManifest.ts +43 -0
  47. package/src/composables/useProcesses.ts +114 -0
  48. package/src/composables/useProject.ts +34 -0
  49. package/src/composables/useTelemetry.ts +270 -0
  50. package/src/lib/__tests__/bc-graph.test.ts +218 -0
  51. package/src/lib/__tests__/dispatch-form.test.ts +113 -0
  52. package/src/lib/__tests__/error-friendly.test.ts +198 -0
  53. package/src/lib/__tests__/home.test.ts +231 -0
  54. package/src/lib/__tests__/inspect.test.ts +160 -0
  55. package/src/lib/__tests__/kind-colors.test.ts +59 -0
  56. package/src/lib/__tests__/live-table.test.ts +194 -0
  57. package/src/lib/__tests__/manifest-health.test.ts +120 -0
  58. package/src/lib/__tests__/manifest.test.ts +87 -0
  59. package/src/lib/__tests__/metadata.test.ts +47 -0
  60. package/src/lib/__tests__/node-metrics.test.ts +144 -0
  61. package/src/lib/__tests__/operate.test.ts +97 -0
  62. package/src/lib/__tests__/pipeline-flow.test.ts +79 -0
  63. package/src/lib/__tests__/rca.test.ts +124 -0
  64. package/src/lib/__tests__/telemetry.test.ts +91 -0
  65. package/src/lib/__tests__/topology-graph.test.ts +331 -0
  66. package/src/lib/__tests__/topology-view.test.ts +154 -0
  67. package/src/lib/__tests__/waterfall.test.ts +165 -0
  68. package/src/lib/bc-graph.ts +298 -0
  69. package/src/lib/dispatch-form.ts +160 -0
  70. package/src/lib/error-friendly.ts +288 -0
  71. package/src/lib/home.ts +191 -0
  72. package/src/lib/inspect.ts +226 -0
  73. package/src/lib/kind-colors.ts +132 -0
  74. package/src/lib/live-table.ts +204 -0
  75. package/src/lib/manifest-health.ts +71 -0
  76. package/src/lib/manifest.ts +139 -0
  77. package/src/lib/metadata.ts +52 -0
  78. package/src/lib/node-metrics.ts +242 -0
  79. package/src/lib/operate.ts +114 -0
  80. package/src/lib/pipeline-flow.ts +120 -0
  81. package/src/lib/rca.ts +193 -0
  82. package/src/lib/telemetry.ts +155 -0
  83. package/src/lib/topology-graph.ts +551 -0
  84. package/src/lib/topology-view.ts +185 -0
  85. package/src/lib/waterfall.ts +148 -0
  86. package/src/main.ts +63 -29
  87. package/src/pages/Errors.vue +272 -0
  88. package/src/pages/Home.stories.ts +7 -8
  89. package/src/pages/Home.vue +255 -540
  90. package/src/pages/Hooks.stories.ts +44 -0
  91. package/src/pages/Hooks.vue +165 -164
  92. package/src/pages/Inspect.vue +240 -0
  93. package/src/pages/Map.vue +187 -0
  94. package/src/pages/Operate.vue +74 -0
  95. package/src/pages/Plugins.stories.ts +1 -1
  96. package/src/pages/Plugins.vue +174 -238
  97. package/src/pages/Projects.vue +62 -60
  98. package/src/pages/Streams.vue +344 -0
  99. package/src/pages/Topology.vue +318 -136
  100. package/src/pages/Trace.vue +174 -412
  101. package/src/pages/__tests__/Home.test.ts +109 -54
  102. package/src/pages/__tests__/Hooks.test.ts +5 -5
  103. package/src/pages/__tests__/Inspect.test.ts +111 -0
  104. package/src/pages/__tests__/Plugins.test.ts +85 -35
  105. package/src/pages/__tests__/Trace.test.ts +117 -0
  106. package/src/pages/operate/CommandsPanel.vue +186 -0
  107. package/src/pages/{Dispatch.vue → operate/DispatchPanel.vue} +100 -203
  108. package/src/pages/operate/EndpointPicker.vue +56 -0
  109. package/src/pages/operate/RunPanel.vue +316 -0
  110. package/src/server/__tests__/nwire-read.test.ts +80 -0
  111. package/src/server/nwire-read.ts +63 -0
  112. package/vite.config.ts +220 -2
  113. package/src/lib/__tests__/normalize-cache.test.ts +0 -105
  114. package/src/lib/cache.ts +0 -312
  115. package/src/lib/normalize-cache.ts +0 -92
  116. package/src/pages/Actions.vue +0 -171
  117. package/src/pages/Apps.vue +0 -177
  118. package/src/pages/Commands.vue +0 -262
  119. package/src/pages/Events.vue +0 -210
  120. package/src/pages/Live.vue +0 -249
  121. package/src/pages/Overview.vue +0 -161
  122. package/src/pages/Projections.vue +0 -148
  123. package/src/pages/Queries.vue +0 -148
  124. package/src/pages/Run.vue +0 -618
  125. package/src/pages/Sinks.vue +0 -124
  126. package/src/pages/TraceNode.vue +0 -164
  127. package/src/pages/Workflows.vue +0 -184
  128. package/src/pages/__tests__/Actions.test.ts +0 -98
  129. package/src/pages/__tests__/Projections.test.ts +0 -90
  130. package/src/pages/__tests__/Queries.test.ts +0 -86
@@ -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>
@@ -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>