@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/Live.vue
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { ref, computed, onMounted, onUnmounted } from "vue";
|
|
3
|
-
import {
|
|
4
|
-
Radio,
|
|
5
|
-
PlayCircle,
|
|
6
|
-
PauseCircle,
|
|
7
|
-
Trash2,
|
|
8
|
-
Filter,
|
|
9
|
-
Network,
|
|
10
|
-
Globe,
|
|
11
|
-
Search,
|
|
12
|
-
} from "lucide-vue-next";
|
|
13
|
-
|
|
14
|
-
interface BufferedEvent {
|
|
15
|
-
seq: number;
|
|
16
|
-
eventName: string;
|
|
17
|
-
payload: unknown;
|
|
18
|
-
envelope: {
|
|
19
|
-
messageId: string;
|
|
20
|
-
correlationId: string;
|
|
21
|
-
causationId: string;
|
|
22
|
-
tenant?: string;
|
|
23
|
-
userId?: string;
|
|
24
|
-
timestamp: string;
|
|
25
|
-
version: number;
|
|
26
|
-
};
|
|
27
|
-
source: "in-process" | "external";
|
|
28
|
-
appName: string;
|
|
29
|
-
capturedAt: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const events = ref<BufferedEvent[]>([]);
|
|
33
|
-
const paused = ref(false);
|
|
34
|
-
const filter = ref("");
|
|
35
|
-
const selectedCorrelation = ref<string | null>(null);
|
|
36
|
-
const status = ref<"connecting" | "open" | "closed" | "error">("connecting");
|
|
37
|
-
let es: EventSource | null = null;
|
|
38
|
-
|
|
39
|
-
onMounted(() => {
|
|
40
|
-
connect();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
onUnmounted(() => {
|
|
44
|
-
es?.close();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
function connect() {
|
|
48
|
-
status.value = "connecting";
|
|
49
|
-
es?.close();
|
|
50
|
-
es = new EventSource("/_nwire/events/stream");
|
|
51
|
-
es.onopen = () => {
|
|
52
|
-
status.value = "open";
|
|
53
|
-
};
|
|
54
|
-
es.onerror = () => {
|
|
55
|
-
status.value = "error";
|
|
56
|
-
};
|
|
57
|
-
es.onmessage = (msg) => {
|
|
58
|
-
if (paused.value) return;
|
|
59
|
-
try {
|
|
60
|
-
const evt = JSON.parse(msg.data) as BufferedEvent;
|
|
61
|
-
events.value.unshift(evt);
|
|
62
|
-
if (events.value.length > 1000) events.value.pop();
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.error("bad event", err);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function reconnect() {
|
|
70
|
-
connect();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function clear() {
|
|
74
|
-
events.value = [];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const filtered = computed(() => {
|
|
78
|
-
const q = filter.value.toLowerCase();
|
|
79
|
-
return events.value.filter((e) => {
|
|
80
|
-
if (selectedCorrelation.value && e.envelope.correlationId !== selectedCorrelation.value) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
if (!q) return true;
|
|
84
|
-
return (
|
|
85
|
-
e.eventName.toLowerCase().includes(q) ||
|
|
86
|
-
e.envelope.messageId.toLowerCase().includes(q) ||
|
|
87
|
-
e.envelope.correlationId.toLowerCase().includes(q) ||
|
|
88
|
-
(e.envelope.tenant?.toLowerCase().includes(q) ?? false) ||
|
|
89
|
-
(e.envelope.userId?.toLowerCase().includes(q) ?? false)
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const selectedTrace = computed(() => {
|
|
95
|
-
if (!selectedCorrelation.value) return [];
|
|
96
|
-
return events.value
|
|
97
|
-
.filter((e) => e.envelope.correlationId === selectedCorrelation.value)
|
|
98
|
-
.sort((a, b) => a.seq - b.seq);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
function shortId(id: string): string {
|
|
102
|
-
return id.split("-")[0] ?? id.slice(0, 8);
|
|
103
|
-
}
|
|
104
|
-
</script>
|
|
105
|
-
|
|
106
|
-
<template>
|
|
107
|
-
<div class="h-full flex">
|
|
108
|
-
<!-- Stream column -->
|
|
109
|
-
<div class="flex-1 flex flex-col border-r border-zinc-800 min-w-0">
|
|
110
|
-
<div class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between gap-3">
|
|
111
|
-
<div class="flex items-center gap-3 min-w-0">
|
|
112
|
-
<Radio
|
|
113
|
-
class="w-5 h-5 shrink-0"
|
|
114
|
-
:class="{
|
|
115
|
-
'text-emerald-400 animate-pulse': status === 'open' && !paused,
|
|
116
|
-
'text-zinc-400': paused,
|
|
117
|
-
'text-amber-400': status === 'connecting',
|
|
118
|
-
'text-rose-400': status === 'error' || status === 'closed',
|
|
119
|
-
}"
|
|
120
|
-
/>
|
|
121
|
-
<h1 class="font-semibold text-lg truncate">Live</h1>
|
|
122
|
-
<span
|
|
123
|
-
class="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-zinc-900 text-zinc-400"
|
|
124
|
-
>
|
|
125
|
-
{{ status }}
|
|
126
|
-
</span>
|
|
127
|
-
<span class="text-xs text-zinc-500 tabular-nums"
|
|
128
|
-
>{{ filtered.length }} / {{ events.length }}</span
|
|
129
|
-
>
|
|
130
|
-
</div>
|
|
131
|
-
<div class="flex items-center gap-1 shrink-0">
|
|
132
|
-
<button
|
|
133
|
-
class="p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-100"
|
|
134
|
-
:title="paused ? 'Resume' : 'Pause'"
|
|
135
|
-
@click="paused = !paused"
|
|
136
|
-
>
|
|
137
|
-
<PlayCircle v-if="paused" class="w-4 h-4 text-emerald-400" />
|
|
138
|
-
<PauseCircle v-else class="w-4 h-4" />
|
|
139
|
-
</button>
|
|
140
|
-
<button
|
|
141
|
-
class="p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-zinc-100"
|
|
142
|
-
title="Clear"
|
|
143
|
-
@click="clear"
|
|
144
|
-
>
|
|
145
|
-
<Trash2 class="w-4 h-4" />
|
|
146
|
-
</button>
|
|
147
|
-
<button
|
|
148
|
-
v-if="status === 'error' || status === 'closed'"
|
|
149
|
-
class="px-2 py-1 rounded text-xs bg-zinc-800 hover:bg-zinc-700"
|
|
150
|
-
@click="reconnect"
|
|
151
|
-
>
|
|
152
|
-
Reconnect
|
|
153
|
-
</button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<div class="px-4 py-2 border-b border-zinc-800 flex items-center gap-2">
|
|
158
|
-
<div class="relative flex-1">
|
|
159
|
-
<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-500" />
|
|
160
|
-
<input
|
|
161
|
-
v-model="filter"
|
|
162
|
-
placeholder="filter event name / id / tenant / user…"
|
|
163
|
-
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"
|
|
164
|
-
/>
|
|
165
|
-
</div>
|
|
166
|
-
<button
|
|
167
|
-
v-if="selectedCorrelation"
|
|
168
|
-
class="text-xs px-2 py-1 rounded bg-purple-950/50 border border-purple-900 text-purple-300 hover:bg-purple-900/50"
|
|
169
|
-
@click="selectedCorrelation = null"
|
|
170
|
-
>
|
|
171
|
-
<Filter class="w-3 h-3 inline mr-1" />
|
|
172
|
-
clear trace filter
|
|
173
|
-
</button>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
<div class="flex-1 overflow-auto">
|
|
177
|
-
<div v-if="filtered.length === 0" class="p-6 text-sm text-zinc-500">
|
|
178
|
-
{{ events.length === 0 ? "Waiting for events…" : "No events match the filter." }}
|
|
179
|
-
</div>
|
|
180
|
-
<button
|
|
181
|
-
v-for="evt in filtered"
|
|
182
|
-
:key="evt.seq"
|
|
183
|
-
class="w-full text-left px-4 py-2 border-b border-zinc-900 hover:bg-zinc-900/50 transition-colors"
|
|
184
|
-
:class="{
|
|
185
|
-
'bg-zinc-900/70':
|
|
186
|
-
selectedCorrelation && evt.envelope.correlationId === selectedCorrelation,
|
|
187
|
-
}"
|
|
188
|
-
@click="selectedCorrelation = evt.envelope.correlationId"
|
|
189
|
-
>
|
|
190
|
-
<div class="flex items-center justify-between gap-3">
|
|
191
|
-
<div class="flex items-center gap-2 min-w-0">
|
|
192
|
-
<component
|
|
193
|
-
:is="evt.source === 'external' ? Network : Globe"
|
|
194
|
-
class="w-3 h-3 shrink-0"
|
|
195
|
-
:class="evt.source === 'external' ? 'text-purple-400' : 'text-emerald-400'"
|
|
196
|
-
/>
|
|
197
|
-
<span class="font-mono text-sm truncate">{{ evt.eventName }}</span>
|
|
198
|
-
<span class="text-[10px] text-zinc-500 shrink-0">{{ evt.appName }}</span>
|
|
199
|
-
</div>
|
|
200
|
-
<span class="text-[10px] text-zinc-500 tabular-nums shrink-0">
|
|
201
|
-
{{ new Date(evt.capturedAt).toLocaleTimeString() }}
|
|
202
|
-
</span>
|
|
203
|
-
</div>
|
|
204
|
-
<div class="text-[10px] text-zinc-500 font-mono mt-0.5 truncate">
|
|
205
|
-
msg {{ shortId(evt.envelope.messageId) }} · corr
|
|
206
|
-
{{ shortId(evt.envelope.correlationId) }}
|
|
207
|
-
<span v-if="evt.envelope.tenant">· tenant {{ evt.envelope.tenant }}</span>
|
|
208
|
-
<span v-if="evt.envelope.userId">· user {{ evt.envelope.userId }}</span>
|
|
209
|
-
</div>
|
|
210
|
-
</button>
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<!-- Trace column -->
|
|
215
|
-
<div class="w-1/2 flex flex-col">
|
|
216
|
-
<div class="border-b border-zinc-800 px-4 py-3">
|
|
217
|
-
<h2 class="text-sm font-medium tracking-tight">
|
|
218
|
-
{{ selectedCorrelation ? "Trace" : "Details" }}
|
|
219
|
-
</h2>
|
|
220
|
-
<p v-if="selectedCorrelation" class="text-[10px] text-zinc-500 font-mono mt-0.5">
|
|
221
|
-
corr {{ selectedCorrelation }}
|
|
222
|
-
</p>
|
|
223
|
-
<p v-else class="text-xs text-zinc-500 mt-0.5">Click an event to see its causation tree.</p>
|
|
224
|
-
</div>
|
|
225
|
-
<div class="flex-1 overflow-auto p-4 space-y-2">
|
|
226
|
-
<div v-for="(evt, i) in selectedTrace" :key="evt.seq" class="relative pl-4">
|
|
227
|
-
<div
|
|
228
|
-
class="absolute left-1 top-0 bottom-0 w-px bg-zinc-800"
|
|
229
|
-
v-if="i < selectedTrace.length - 1"
|
|
230
|
-
></div>
|
|
231
|
-
<div class="absolute left-0 top-2.5 w-2 h-2 rounded-full bg-emerald-400"></div>
|
|
232
|
-
<div class="rounded border border-zinc-800 bg-zinc-900/40 p-3">
|
|
233
|
-
<div class="flex items-center justify-between mb-1">
|
|
234
|
-
<span class="font-mono text-sm">{{ evt.eventName }}</span>
|
|
235
|
-
<span class="text-[10px] text-zinc-500">{{ evt.appName }}</span>
|
|
236
|
-
</div>
|
|
237
|
-
<div class="text-[10px] text-zinc-500 font-mono mb-2">
|
|
238
|
-
msg {{ shortId(evt.envelope.messageId) }} ← caused by
|
|
239
|
-
{{ shortId(evt.envelope.causationId) }}
|
|
240
|
-
</div>
|
|
241
|
-
<pre class="text-[11px] bg-zinc-950 border border-zinc-800 rounded p-2 overflow-auto">{{
|
|
242
|
-
JSON.stringify(evt.payload, null, 2)
|
|
243
|
-
}}</pre>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
</template>
|
package/src/pages/Overview.vue
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed } from "vue";
|
|
3
|
-
import { RouterLink } from "vue-router";
|
|
4
|
-
import { useCache } from "@/lib/cache";
|
|
5
|
-
import {
|
|
6
|
-
Boxes,
|
|
7
|
-
Zap,
|
|
8
|
-
Radio,
|
|
9
|
-
Layers,
|
|
10
|
-
Database,
|
|
11
|
-
GitBranch,
|
|
12
|
-
Network,
|
|
13
|
-
LayoutDashboard,
|
|
14
|
-
Waves,
|
|
15
|
-
Send,
|
|
16
|
-
Play,
|
|
17
|
-
Workflow,
|
|
18
|
-
} from "lucide-vue-next";
|
|
19
|
-
import { PageHeader, KindBadge } from "@/components";
|
|
20
|
-
|
|
21
|
-
const { cache } = useCache();
|
|
22
|
-
|
|
23
|
-
// "Daily" shortcuts — the high-frequency entry points.
|
|
24
|
-
const quickActions = [
|
|
25
|
-
{
|
|
26
|
-
to: "/live",
|
|
27
|
-
label: "Trace",
|
|
28
|
-
desc: "Watch actions / events / workflows fire in real time",
|
|
29
|
-
icon: Waves,
|
|
30
|
-
color: "text-cyan-400",
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
to: "/dispatch",
|
|
34
|
-
label: "Try",
|
|
35
|
-
desc: "Dispatch any action with a form generated from its schema",
|
|
36
|
-
icon: Send,
|
|
37
|
-
color: "text-amber-400",
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
to: "/run",
|
|
41
|
-
label: "Processes",
|
|
42
|
-
desc: "Start, stop, and watch logs for your wires",
|
|
43
|
-
icon: Play,
|
|
44
|
-
color: "text-emerald-400",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
to: "/eventstorm",
|
|
48
|
-
label: "Flow",
|
|
49
|
-
desc: "See the causal graph: action → event → workflow → action",
|
|
50
|
-
icon: Workflow,
|
|
51
|
-
color: "text-violet-400",
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const hasData = computed(() => !!cache.value && cache.value.apps.length > 0);
|
|
56
|
-
|
|
57
|
-
const stats = computed(() => {
|
|
58
|
-
if (!cache.value) return [];
|
|
59
|
-
return [
|
|
60
|
-
{ label: "Apps", value: cache.value.apps.length, icon: Network, color: "text-blue-400" },
|
|
61
|
-
{ label: "Plugins", value: cache.value.plugins.length, icon: Boxes, color: "text-emerald-400" },
|
|
62
|
-
{ label: "Actions", value: cache.value.actions.length, icon: Zap, color: "text-amber-400" },
|
|
63
|
-
{ label: "Events", value: cache.value.events.length, icon: Radio, color: "text-purple-400" },
|
|
64
|
-
{ label: "Actors", value: cache.value.actors.length, icon: Layers, color: "text-pink-400" },
|
|
65
|
-
{
|
|
66
|
-
label: "Projections",
|
|
67
|
-
value: cache.value.projections.length,
|
|
68
|
-
icon: Database,
|
|
69
|
-
color: "text-cyan-400",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
label: "Workflows",
|
|
73
|
-
value: cache.value.workflows.length,
|
|
74
|
-
icon: GitBranch,
|
|
75
|
-
color: "text-violet-400",
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
label: "Resolvers",
|
|
79
|
-
value: cache.value.resolvers.length,
|
|
80
|
-
icon: Network,
|
|
81
|
-
color: "text-rose-400",
|
|
82
|
-
},
|
|
83
|
-
];
|
|
84
|
-
});
|
|
85
|
-
</script>
|
|
86
|
-
|
|
87
|
-
<template>
|
|
88
|
-
<div v-if="cache" class="p-6 space-y-6" data-testid="overview-page">
|
|
89
|
-
<PageHeader
|
|
90
|
-
title="Overview"
|
|
91
|
-
:icon="LayoutDashboard"
|
|
92
|
-
icon-color="text-emerald-400"
|
|
93
|
-
:subtitle="
|
|
94
|
-
hasData
|
|
95
|
-
? 'Snapshot from .nwire/manifest.json'
|
|
96
|
-
: 'No apps discovered yet — try the shortcuts below'
|
|
97
|
-
"
|
|
98
|
-
/>
|
|
99
|
-
|
|
100
|
-
<!-- Quick actions — what you actually do here daily. -->
|
|
101
|
-
<div>
|
|
102
|
-
<h2 class="text-sm font-medium text-zinc-300 uppercase tracking-wide mb-3">Start here</h2>
|
|
103
|
-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
104
|
-
<RouterLink
|
|
105
|
-
v-for="a in quickActions"
|
|
106
|
-
:key="a.to"
|
|
107
|
-
:to="a.to"
|
|
108
|
-
class="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3 hover:bg-zinc-900 hover:border-zinc-700 transition-colors block"
|
|
109
|
-
>
|
|
110
|
-
<div class="flex items-center gap-2 mb-1">
|
|
111
|
-
<component :is="a.icon" class="w-4 h-4" :class="a.color" />
|
|
112
|
-
<span class="font-medium text-zinc-100">{{ a.label }}</span>
|
|
113
|
-
</div>
|
|
114
|
-
<div class="text-xs text-zinc-400 leading-relaxed">{{ a.desc }}</div>
|
|
115
|
-
</RouterLink>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div v-if="hasData" class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3">
|
|
120
|
-
<div
|
|
121
|
-
v-for="s in stats"
|
|
122
|
-
:key="s.label"
|
|
123
|
-
class="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3"
|
|
124
|
-
data-testid="overview-stat"
|
|
125
|
-
>
|
|
126
|
-
<div class="flex items-center justify-between mb-1">
|
|
127
|
-
<span class="text-xs text-zinc-500 uppercase tracking-wide">{{ s.label }}</span>
|
|
128
|
-
<component :is="s.icon" class="w-3.5 h-3.5" :class="s.color" />
|
|
129
|
-
</div>
|
|
130
|
-
<div class="text-2xl font-semibold tabular-nums">{{ s.value }}</div>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
<div v-if="hasData">
|
|
135
|
-
<h2 class="text-sm font-medium text-zinc-300 uppercase tracking-wide mb-3">Apps</h2>
|
|
136
|
-
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
137
|
-
<div
|
|
138
|
-
v-for="app in cache.apps"
|
|
139
|
-
:key="app.name"
|
|
140
|
-
class="rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3"
|
|
141
|
-
data-testid="overview-app-card"
|
|
142
|
-
>
|
|
143
|
-
<div class="flex items-center justify-between">
|
|
144
|
-
<span class="font-medium">{{ app.name }}</span>
|
|
145
|
-
<span class="text-xs text-zinc-500 tabular-nums">
|
|
146
|
-
{{ app.plugins.length }} plugin(s)
|
|
147
|
-
</span>
|
|
148
|
-
</div>
|
|
149
|
-
<div v-if="app.description" class="text-sm text-zinc-400 mt-1">
|
|
150
|
-
{{ app.description }}
|
|
151
|
-
</div>
|
|
152
|
-
<div class="mt-2 flex flex-wrap gap-1">
|
|
153
|
-
<KindBadge v-for="p in app.plugins" :key="p" variant="neutral">
|
|
154
|
-
{{ p }}
|
|
155
|
-
</KindBadge>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
</template>
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Projections — CQRS read models. Each fold takes (state, event) → state;
|
|
4
|
-
* the projection store is keyed by projection name + tenant. Studio shows
|
|
5
|
-
* the registered projections and which events they listen to.
|
|
6
|
-
*/
|
|
7
|
-
import { computed, onMounted, ref, watch } from "vue";
|
|
8
|
-
import { useRoute, useRouter } from "vue-router";
|
|
9
|
-
import { useCache } from "@/lib/cache";
|
|
10
|
-
import { Database, ArrowRight } from "lucide-vue-next";
|
|
11
|
-
import {
|
|
12
|
-
PageHeader,
|
|
13
|
-
FilterInput,
|
|
14
|
-
EmptyState,
|
|
15
|
-
MasterDetail,
|
|
16
|
-
SourcePill,
|
|
17
|
-
SourceDrawer,
|
|
18
|
-
ListRow,
|
|
19
|
-
} from "@/components";
|
|
20
|
-
|
|
21
|
-
const route = useRoute();
|
|
22
|
-
const router = useRouter();
|
|
23
|
-
const { cache } = useCache();
|
|
24
|
-
const filter = ref("");
|
|
25
|
-
const selected = ref<string | null>(null);
|
|
26
|
-
const sourcePreview = ref<{ file: string; line: number; column?: number } | null>(null);
|
|
27
|
-
|
|
28
|
-
function applyQueryPreselect(): void {
|
|
29
|
-
const name = route.query.name;
|
|
30
|
-
if (typeof name !== "string" || name.length === 0) return;
|
|
31
|
-
const found = cache.value?.projections.find((p) => p.name === name);
|
|
32
|
-
if (found) selected.value = `${found.app}::${found.name}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
onMounted(applyQueryPreselect);
|
|
36
|
-
watch(() => route.query.name, applyQueryPreselect);
|
|
37
|
-
watch(() => cache.value, applyQueryPreselect);
|
|
38
|
-
|
|
39
|
-
const filtered = computed(() => {
|
|
40
|
-
if (!cache.value) return [];
|
|
41
|
-
const q = filter.value.toLowerCase();
|
|
42
|
-
return cache.value.projections.filter(
|
|
43
|
-
(p) => !q || p.name.toLowerCase().includes(q) || p.app.toLowerCase().includes(q),
|
|
44
|
-
);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const key = (p: { app: string; name: string }) => `${p.app}::${p.name}`;
|
|
48
|
-
const detail = computed(() => filtered.value.find((p) => key(p) === selected.value) ?? null);
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Cross-reference: which queries read from this projection? Studio
|
|
52
|
-
* doesn't yet know the projection→query edge directly (the scanner
|
|
53
|
-
* emits queries with their projection name when set), so we filter by
|
|
54
|
-
* name match.
|
|
55
|
-
*/
|
|
56
|
-
const queriesForDetail = computed(() => {
|
|
57
|
-
if (!detail.value || !cache.value) return [];
|
|
58
|
-
return cache.value.queries.filter(
|
|
59
|
-
(q) => (q as { projection?: string }).projection === detail.value!.name,
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
</script>
|
|
63
|
-
|
|
64
|
-
<template>
|
|
65
|
-
<div v-if="cache" class="h-full flex flex-col" data-testid="projections-page">
|
|
66
|
-
<div class="p-6 pb-3 border-b border-zinc-800">
|
|
67
|
-
<PageHeader
|
|
68
|
-
title="Projections"
|
|
69
|
-
subtitle="CQRS read models — every fold from event stream to materialised state."
|
|
70
|
-
:icon="Database"
|
|
71
|
-
icon-color="text-cyan-400"
|
|
72
|
-
:count="filtered.length"
|
|
73
|
-
:total="cache.projections.length"
|
|
74
|
-
/>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<EmptyState
|
|
78
|
-
v-if="cache.projections.length === 0"
|
|
79
|
-
title="No projections in cache"
|
|
80
|
-
hint="Projections are declared via defineProjection(name, { listens, initial, on }). Run `nwire cache` after adding one."
|
|
81
|
-
:icon="Database"
|
|
82
|
-
/>
|
|
83
|
-
|
|
84
|
-
<MasterDetail v-else class="flex-1">
|
|
85
|
-
<template #listHeader>
|
|
86
|
-
<FilterInput v-model="filter" placeholder="filter by name or app…" />
|
|
87
|
-
</template>
|
|
88
|
-
|
|
89
|
-
<template #list>
|
|
90
|
-
<ListRow
|
|
91
|
-
v-for="p in filtered"
|
|
92
|
-
:key="key(p)"
|
|
93
|
-
:selected="selected === key(p)"
|
|
94
|
-
@click="selected = key(p)"
|
|
95
|
-
>
|
|
96
|
-
<template #title>
|
|
97
|
-
<Database class="w-3 h-3 text-cyan-400 shrink-0" />
|
|
98
|
-
<span class="font-mono text-sm truncate">{{ p.name }}</span>
|
|
99
|
-
</template>
|
|
100
|
-
<template #meta>
|
|
101
|
-
<span class="text-[10px] text-zinc-500">{{ p.app }}</span>
|
|
102
|
-
</template>
|
|
103
|
-
</ListRow>
|
|
104
|
-
</template>
|
|
105
|
-
|
|
106
|
-
<template #empty
|
|
107
|
-
>Select a projection to view its event subscriptions and reading queries.</template
|
|
108
|
-
>
|
|
109
|
-
|
|
110
|
-
<template v-if="detail" #detail>
|
|
111
|
-
<div class="p-6 space-y-5" data-testid="projection-detail">
|
|
112
|
-
<div>
|
|
113
|
-
<div class="text-[10px] uppercase tracking-wide text-zinc-500">
|
|
114
|
-
{{ detail.app }}
|
|
115
|
-
</div>
|
|
116
|
-
<h2 class="font-mono text-xl mt-1">{{ detail.name }}</h2>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div class="space-y-3">
|
|
120
|
-
<h3 class="text-xs uppercase tracking-wide text-zinc-500">
|
|
121
|
-
Queries on this projection
|
|
122
|
-
</h3>
|
|
123
|
-
<div v-if="queriesForDetail.length === 0" class="text-xs text-zinc-600">
|
|
124
|
-
No queries registered against this projection yet.
|
|
125
|
-
</div>
|
|
126
|
-
<div v-else class="space-y-1">
|
|
127
|
-
<button
|
|
128
|
-
v-for="q in queriesForDetail"
|
|
129
|
-
:key="q.name"
|
|
130
|
-
type="button"
|
|
131
|
-
class="flex items-center gap-2 font-mono text-sm text-left hover:text-emerald-300"
|
|
132
|
-
@click="router.push({ path: '/queries', query: { name: q.name } })"
|
|
133
|
-
>
|
|
134
|
-
<ArrowRight class="w-3.5 h-3.5 text-emerald-400" />
|
|
135
|
-
<span class="underline-offset-2 hover:underline">{{ q.name }}</span>
|
|
136
|
-
</button>
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
<div v-if="detail.source" class="pt-2">
|
|
141
|
-
<SourcePill :source="detail.source" @click="sourcePreview = detail.source!" />
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
</template>
|
|
145
|
-
</MasterDetail>
|
|
146
|
-
<SourceDrawer :source="sourcePreview" @close="sourcePreview = null" />
|
|
147
|
-
</div>
|
|
148
|
-
</template>
|