@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/Plugins.vue
CHANGED
|
@@ -1,175 +1,82 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Plugins — every plugin
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
3
|
+
* Plugins — every plugin composed into the app, read natively from the deep
|
|
4
|
+
* manifest's runtime topology (`topology.plugins`) via `useManifest`.
|
|
5
|
+
*
|
|
6
|
+
* Detail pane shows the plugin's INTERNALS Inspect can't carry: its
|
|
7
|
+
* boot/shutdown lifecycle hooks (`plugin.boot:<n>` / `plugin.shutdown:<n>` from
|
|
8
|
+
* `topology.hooks`), what it contributes (`contributes` / `mounts` graph edges),
|
|
9
|
+
* a same-named capability's ctx-by-kind, and a live lifecycle timeline from
|
|
10
|
+
* `hook.step` telemetry (`useTelemetry`). A `/plugins?name=<n>` deep-link
|
|
11
|
+
* preselects; each lifecycle hook cross-links to `/hooks?name=<hookName>`.
|
|
9
12
|
*/
|
|
10
|
-
import { computed, onMounted,
|
|
13
|
+
import { computed, onMounted, ref, watch } from "vue";
|
|
11
14
|
import { useRoute, useRouter } from "vue-router";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface HookStepRecord {
|
|
25
|
-
readonly kind: "hook.step";
|
|
26
|
-
readonly hookName: string;
|
|
27
|
-
readonly hookId: string;
|
|
28
|
-
readonly runId: string;
|
|
29
|
-
readonly stepId: number;
|
|
30
|
-
readonly stepKind: "chain" | "listener";
|
|
31
|
-
readonly stepName?: string;
|
|
32
|
-
readonly phase: "start" | "end" | "error";
|
|
33
|
-
readonly durationMs?: number;
|
|
34
|
-
readonly ts: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type PluginTelemetry = LifecycleRecord | HookStepRecord;
|
|
15
|
+
import { Activity, Anchor, Puzzle, RefreshCw, Search } from "lucide-vue-next";
|
|
16
|
+
import { useManifest } from "@/composables/useManifest";
|
|
17
|
+
import { useProject } from "@/composables/useProject";
|
|
18
|
+
import { useTelemetry } from "@/composables/useTelemetry";
|
|
19
|
+
import {
|
|
20
|
+
pluginRegistry,
|
|
21
|
+
pluginInternals,
|
|
22
|
+
liveFireTally,
|
|
23
|
+
type PluginRow,
|
|
24
|
+
} from "@/lib/topology-view";
|
|
38
25
|
|
|
39
26
|
const route = useRoute();
|
|
40
27
|
const router = useRouter();
|
|
41
|
-
const {
|
|
28
|
+
const { activeCwd } = useProject();
|
|
29
|
+
const { view, isError, error, refetch } = useManifest(activeCwd);
|
|
30
|
+
const { records, status: streamStatus, recent } = useTelemetry(activeCwd);
|
|
31
|
+
|
|
42
32
|
const filter = ref("");
|
|
43
|
-
const kindFilter = ref<"all" | "plugin" | "module">("all");
|
|
44
33
|
const selected = ref<string | null>(null);
|
|
45
|
-
const sourcePreview = ref<{ file: string; line: number; column?: number } | null>(null);
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
const name = route.query.name;
|
|
49
|
-
if (typeof name !== "string" || name.length === 0) return;
|
|
50
|
-
const found = cache.value?.plugins.find((p) => p.name === name);
|
|
51
|
-
if (found) selected.value = `${found.app}::${found.kind}::${found.name}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
onMounted(applyQueryPreselect);
|
|
55
|
-
watch(() => route.query.name, applyQueryPreselect);
|
|
56
|
-
watch(() => cache.value, applyQueryPreselect);
|
|
35
|
+
const plugins = computed<PluginRow[]>(() => pluginRegistry(view.value));
|
|
57
36
|
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
let es: EventSource | null = null;
|
|
62
|
-
|
|
63
|
-
onMounted(() => {
|
|
64
|
-
connect();
|
|
65
|
-
});
|
|
66
|
-
onUnmounted(() => {
|
|
67
|
-
es?.close();
|
|
37
|
+
const filteredPlugins = computed<PluginRow[]>(() => {
|
|
38
|
+
const q = filter.value.toLowerCase();
|
|
39
|
+
return plugins.value.filter((p) => !q || p.name.toLowerCase().includes(q));
|
|
68
40
|
});
|
|
69
41
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
es = new EventSource("/_nwire/telemetry/stream");
|
|
74
|
-
es.onopen = () => {
|
|
75
|
-
streamStatus.value = "open";
|
|
76
|
-
};
|
|
77
|
-
es.onerror = () => {
|
|
78
|
-
streamStatus.value = "error";
|
|
79
|
-
};
|
|
80
|
-
es.onmessage = (m) => {
|
|
81
|
-
try {
|
|
82
|
-
const rec = JSON.parse(m.data) as { kind?: string };
|
|
83
|
-
if (rec.kind === "lifecycle" || rec.kind === "hook.step") {
|
|
84
|
-
liveRecords.value.push(rec as PluginTelemetry);
|
|
85
|
-
if (liveRecords.value.length > 1000) liveRecords.value.splice(0, 500);
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
/* ignore non-JSON frames */
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Helpers ───────────────────────────────────────────────────────────
|
|
94
|
-
const key = (p: PluginEntry) => `${p.app}::${p.kind}::${p.name}`;
|
|
42
|
+
const detail = computed<PluginRow | null>(
|
|
43
|
+
() => plugins.value.find((p) => p.id === selected.value) ?? null,
|
|
44
|
+
);
|
|
95
45
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (colon === -1) return null;
|
|
99
|
-
const prefix = hookName.slice(0, colon);
|
|
100
|
-
if (prefix !== "plugin.boot" && prefix !== "plugin.shutdown") return null;
|
|
101
|
-
return hookName.slice(colon + 1);
|
|
102
|
-
}
|
|
46
|
+
const internals = computed(() => pluginInternals(view.value, detail.value?.name));
|
|
47
|
+
const ctxByKindEntries = computed(() => Object.entries(internals.value?.ctxByKind ?? {}));
|
|
103
48
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
49
|
+
// ── Deep-link preselect: /plugins?name=<n>. ───────────────────────────────
|
|
50
|
+
function applyQueryPreselect(): void {
|
|
51
|
+
const name = route.query.name;
|
|
52
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
53
|
+
const found = plugins.value.find((p) => p.name === name);
|
|
54
|
+
if (found) selected.value = found.id;
|
|
107
55
|
}
|
|
56
|
+
onMounted(applyQueryPreselect);
|
|
57
|
+
watch(() => route.query.name, applyQueryPreselect);
|
|
58
|
+
watch(plugins, applyQueryPreselect);
|
|
108
59
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// e.g. nwire.plugin.booted → "booted"
|
|
112
|
-
const parts = rec.event.split(".");
|
|
113
|
-
return parts[parts.length - 1] ?? rec.event;
|
|
114
|
-
}
|
|
115
|
-
const colon = rec.hookName.indexOf(":");
|
|
116
|
-
const head = colon === -1 ? rec.hookName : rec.hookName.slice(0, colon);
|
|
117
|
-
return `${head.replace(/^plugin\./, "")} ${rec.phase}`;
|
|
118
|
-
}
|
|
60
|
+
// ── Live lifecycle: fires per plugin, from `plugin.boot/shutdown` hook.step. ─
|
|
61
|
+
const fireTally = computed(() => liveFireTally(records.value));
|
|
119
62
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return rec.durationMs;
|
|
63
|
+
function pluginFires(name: string): number {
|
|
64
|
+
let n = 0;
|
|
65
|
+
n += fireTally.value.get(`plugin.boot:${name}`)?.fires ?? 0;
|
|
66
|
+
n += fireTally.value.get(`plugin.shutdown:${name}`)?.fires ?? 0;
|
|
67
|
+
return n;
|
|
126
68
|
}
|
|
127
69
|
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const out: Record<string, number> = {};
|
|
131
|
-
for (const rec of liveRecords.value) {
|
|
132
|
-
const name = recordPluginName(rec);
|
|
133
|
-
if (!name) continue;
|
|
134
|
-
if (rec.kind === "lifecycle" && rec.event.endsWith(".booted")) {
|
|
135
|
-
const d = recordDuration(rec);
|
|
136
|
-
if (typeof d === "number") out[name] = d;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return out;
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// ── Filtered list ─────────────────────────────────────────────────────
|
|
143
|
-
const filteredPlugins = computed<PluginEntry[]>(() => {
|
|
144
|
-
if (!cache.value) return [];
|
|
145
|
-
const q = filter.value.toLowerCase();
|
|
146
|
-
return cache.value.plugins.filter((p) => {
|
|
147
|
-
if (kindFilter.value !== "all" && p.kind !== kindFilter.value) return false;
|
|
148
|
-
if (!q) return true;
|
|
149
|
-
return p.name.toLowerCase().includes(q) || p.app.toLowerCase().includes(q);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const detail = computed<PluginEntry | null>(() => {
|
|
154
|
-
if (!cache.value) return null;
|
|
155
|
-
return cache.value.plugins.find((p) => key(p) === selected.value) ?? null;
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// ── Contributed hooks for the selected plugin ─────────────────────────
|
|
159
|
-
const contributedHooks = computed<HookEntry[]>(() => {
|
|
160
|
-
if (!detail.value || !cache.value) return [];
|
|
161
|
-
const name = detail.value.name;
|
|
162
|
-
const bootName = `plugin.boot:${name}`;
|
|
163
|
-
const downName = `plugin.shutdown:${name}`;
|
|
164
|
-
return cache.value.hooks.filter((h) => h.name === bootName || h.name === downName);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ── Lifecycle timeline for the selected plugin (last 50) ──────────────
|
|
168
|
-
const detailRecords = computed<PluginTelemetry[]>(() => {
|
|
70
|
+
// The selected plugin's recent lifecycle steps, newest first.
|
|
71
|
+
const detailSteps = computed(() => {
|
|
169
72
|
if (!detail.value) return [];
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
73
|
+
const boot = `plugin.boot:${detail.value.name}`;
|
|
74
|
+
const down = `plugin.shutdown:${detail.value.name}`;
|
|
75
|
+
return recent("hook.step")
|
|
76
|
+
.filter((r) => {
|
|
77
|
+
const hn = (r as Record<string, unknown>).hookName;
|
|
78
|
+
return hn === boot || hn === down;
|
|
79
|
+
})
|
|
173
80
|
.slice(-50)
|
|
174
81
|
.reverse();
|
|
175
82
|
});
|
|
@@ -177,10 +84,28 @@ const detailRecords = computed<PluginTelemetry[]>(() => {
|
|
|
177
84
|
function openHook(hookName: string): void {
|
|
178
85
|
void router.push({ path: "/hooks", query: { name: hookName } });
|
|
179
86
|
}
|
|
87
|
+
function fmtTime(ts: unknown): string {
|
|
88
|
+
if (typeof ts !== "string") return "—";
|
|
89
|
+
const d = new Date(ts);
|
|
90
|
+
return Number.isNaN(d.getTime()) ? "—" : d.toLocaleTimeString(undefined, { hour12: false });
|
|
91
|
+
}
|
|
92
|
+
function stepField(rec: unknown, key: string): string {
|
|
93
|
+
const v = (rec as Record<string, unknown>)[key];
|
|
94
|
+
return v == null ? "" : String(v);
|
|
95
|
+
}
|
|
96
|
+
function lifecyclePhase(rec: unknown): string {
|
|
97
|
+
const hn = stepField(rec, "hookName");
|
|
98
|
+
const head = hn.startsWith("plugin.boot") ? "boot" : "shutdown";
|
|
99
|
+
return `${head} ${stepField(rec, "phase")}`.trim();
|
|
100
|
+
}
|
|
101
|
+
function durationMsOf(rec: unknown): number | null {
|
|
102
|
+
const v = (rec as Record<string, unknown>).durationMs;
|
|
103
|
+
return typeof v === "number" ? v : null;
|
|
104
|
+
}
|
|
180
105
|
</script>
|
|
181
106
|
|
|
182
107
|
<template>
|
|
183
|
-
<div
|
|
108
|
+
<div class="h-full flex flex-col" data-testid="plugins-page">
|
|
184
109
|
<!-- Header -->
|
|
185
110
|
<div class="p-6 pb-3 border-b border-zinc-800 flex items-center justify-between">
|
|
186
111
|
<div>
|
|
@@ -188,41 +113,46 @@ function openHook(hookName: string): void {
|
|
|
188
113
|
<Puzzle class="w-4 h-4 text-fuchsia-400" />
|
|
189
114
|
<h1 class="text-lg font-medium">Plugins</h1>
|
|
190
115
|
<span class="text-[10px] text-zinc-500">
|
|
191
|
-
{{ filteredPlugins.length }} / {{
|
|
116
|
+
{{ filteredPlugins.length }} / {{ plugins.length }}
|
|
192
117
|
</span>
|
|
193
118
|
<span
|
|
194
119
|
class="ml-2 text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-zinc-900 text-zinc-400"
|
|
195
120
|
data-testid="stream-status"
|
|
196
121
|
>
|
|
197
|
-
|
|
122
|
+
tap {{ streamStatus }}
|
|
198
123
|
</span>
|
|
199
124
|
</div>
|
|
200
125
|
<p class="text-xs text-zinc-500 mt-1">
|
|
201
|
-
Plugins
|
|
202
|
-
live from the running wire.
|
|
126
|
+
Plugins composed into the app — what each contributes, its boot/shutdown lifecycle, and
|
|
127
|
+
live timing from the running wire.
|
|
203
128
|
</p>
|
|
204
129
|
</div>
|
|
205
130
|
<button
|
|
206
131
|
class="p-1 rounded hover:bg-zinc-800 text-zinc-400"
|
|
207
|
-
title="
|
|
208
|
-
@click="
|
|
132
|
+
title="Refetch manifest"
|
|
133
|
+
@click="() => refetch()"
|
|
209
134
|
>
|
|
210
135
|
<RefreshCw class="w-3.5 h-3.5" />
|
|
211
136
|
</button>
|
|
212
137
|
</div>
|
|
213
138
|
|
|
139
|
+
<!-- Error -->
|
|
140
|
+
<div v-if="isError" class="p-6 text-xs text-zinc-500" data-testid="plugins-error">
|
|
141
|
+
{{ error ?? "Couldn't load the manifest. Run `nwire cache` to build it." }}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
214
144
|
<!-- Empty (no plugins at all) -->
|
|
215
145
|
<div
|
|
216
|
-
v-if="
|
|
146
|
+
v-else-if="plugins.length === 0"
|
|
217
147
|
class="flex-1 flex items-center justify-center p-12"
|
|
218
148
|
data-testid="plugins-empty"
|
|
219
149
|
>
|
|
220
150
|
<div class="text-center max-w-md">
|
|
221
151
|
<Puzzle class="w-8 h-8 text-zinc-700 mx-auto mb-3" />
|
|
222
|
-
<div class="text-sm text-zinc-400 font-medium">No plugins in
|
|
152
|
+
<div class="text-sm text-zinc-400 font-medium">No plugins in the manifest</div>
|
|
223
153
|
<div class="text-xs text-zinc-500 mt-2">
|
|
224
|
-
|
|
225
|
-
|
|
154
|
+
The runtime topology is captured at scan time. Run
|
|
155
|
+
<span class="font-mono">nwire cache</span> after registering a plugin.
|
|
226
156
|
</div>
|
|
227
157
|
</div>
|
|
228
158
|
</div>
|
|
@@ -231,60 +161,41 @@ function openHook(hookName: string): void {
|
|
|
231
161
|
<div v-else class="flex-1 flex min-h-0">
|
|
232
162
|
<!-- Master -->
|
|
233
163
|
<aside class="w-[280px] border-r border-zinc-800 flex flex-col shrink-0">
|
|
234
|
-
<div class="px-3 py-2 border-b border-zinc-800
|
|
164
|
+
<div class="px-3 py-2 border-b border-zinc-800">
|
|
235
165
|
<div class="relative">
|
|
236
166
|
<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-zinc-500" />
|
|
237
167
|
<input
|
|
238
168
|
v-model="filter"
|
|
239
|
-
placeholder="filter name
|
|
169
|
+
placeholder="filter name…"
|
|
170
|
+
data-testid="plugins-filter"
|
|
240
171
|
class="w-full bg-zinc-900 border border-zinc-800 rounded pl-7 pr-2 py-1 text-xs placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600"
|
|
241
172
|
/>
|
|
242
173
|
</div>
|
|
243
|
-
<div class="flex gap-1">
|
|
244
|
-
<button
|
|
245
|
-
v-for="opt in ['all', 'plugin', 'module'] as const"
|
|
246
|
-
:key="opt"
|
|
247
|
-
class="flex-1 px-2 py-0.5 text-[10px] uppercase tracking-wide rounded border transition-colors"
|
|
248
|
-
:class="
|
|
249
|
-
kindFilter === opt
|
|
250
|
-
? 'bg-zinc-800 border-zinc-700 text-zinc-100'
|
|
251
|
-
: 'bg-zinc-950 border-zinc-900 text-zinc-500 hover:text-zinc-300'
|
|
252
|
-
"
|
|
253
|
-
@click="kindFilter = opt"
|
|
254
|
-
>
|
|
255
|
-
{{ opt === "all" ? "All" : opt + "s" }}
|
|
256
|
-
</button>
|
|
257
|
-
</div>
|
|
258
174
|
</div>
|
|
259
175
|
|
|
260
176
|
<div v-if="filteredPlugins.length === 0" class="p-4 text-xs text-zinc-500">
|
|
261
177
|
No plugins match the filter.
|
|
262
178
|
</div>
|
|
263
179
|
<ul v-else class="flex-1 overflow-auto" data-testid="plugins-list">
|
|
264
|
-
<li v-for="p in filteredPlugins" :key="
|
|
180
|
+
<li v-for="p in filteredPlugins" :key="p.id">
|
|
265
181
|
<button
|
|
266
182
|
class="w-full text-left px-3 py-2 border-b border-zinc-900 hover:bg-zinc-900/50 transition-colors"
|
|
267
|
-
:class="{ 'bg-zinc-900/70':
|
|
268
|
-
@click="selected =
|
|
183
|
+
:class="{ 'bg-zinc-900/70': p.id === selected }"
|
|
184
|
+
@click="selected = p.id"
|
|
269
185
|
>
|
|
270
186
|
<div class="flex items-center gap-2">
|
|
271
|
-
<
|
|
272
|
-
:is="p.kind === 'plugin' ? Anchor : Boxes"
|
|
273
|
-
class="w-3 h-3 shrink-0"
|
|
274
|
-
:class="p.kind === 'plugin' ? 'text-fuchsia-400' : 'text-orange-400'"
|
|
275
|
-
/>
|
|
187
|
+
<Anchor class="w-3 h-3 shrink-0 text-fuchsia-400" />
|
|
276
188
|
<span class="font-mono text-xs text-zinc-100 truncate flex-1">{{ p.name }}</span>
|
|
277
189
|
<span
|
|
278
|
-
v-if="
|
|
279
|
-
class="text-[10px] text-emerald-400 tabular-nums shrink-0"
|
|
280
|
-
|
|
190
|
+
v-if="pluginFires(p.name) > 0"
|
|
191
|
+
class="inline-flex items-center gap-1 text-[10px] text-emerald-400 tabular-nums shrink-0"
|
|
192
|
+
:data-testid="`plugin-fires-${p.name}`"
|
|
193
|
+
title="live lifecycle fires observed"
|
|
281
194
|
>
|
|
282
|
-
|
|
195
|
+
<Activity class="w-3 h-3" />
|
|
196
|
+
{{ pluginFires(p.name) }}
|
|
283
197
|
</span>
|
|
284
198
|
</div>
|
|
285
|
-
<div class="text-[10px] text-zinc-500 mt-0.5 pl-5">
|
|
286
|
-
{{ p.app }}
|
|
287
|
-
</div>
|
|
288
199
|
</button>
|
|
289
200
|
</li>
|
|
290
201
|
</ul>
|
|
@@ -292,52 +203,49 @@ function openHook(hookName: string): void {
|
|
|
292
203
|
|
|
293
204
|
<!-- Detail -->
|
|
294
205
|
<main class="flex-1 flex flex-col min-w-0" data-testid="plugins-detail">
|
|
295
|
-
<div v-if="!detail" class="p-8 text-sm text-zinc-500 italic">
|
|
296
|
-
Select a plugin to view its
|
|
206
|
+
<div v-if="!detail || !internals" class="p-8 text-sm text-zinc-500 italic">
|
|
207
|
+
Select a plugin to view its internals + live lifecycle.
|
|
297
208
|
</div>
|
|
298
209
|
<div v-else class="flex-1 flex flex-col overflow-hidden">
|
|
299
210
|
<!-- Header -->
|
|
300
211
|
<div class="px-6 py-5 border-b border-zinc-800">
|
|
301
212
|
<div class="flex items-center gap-3 flex-wrap">
|
|
302
|
-
<
|
|
303
|
-
:is="detail.kind === 'plugin' ? Anchor : Boxes"
|
|
304
|
-
class="w-4 h-4"
|
|
305
|
-
:class="detail.kind === 'plugin' ? 'text-fuchsia-400' : 'text-orange-400'"
|
|
306
|
-
/>
|
|
213
|
+
<Anchor class="w-4 h-4 text-fuchsia-400" />
|
|
307
214
|
<h2 class="font-mono text-xl">{{ detail.name }}</h2>
|
|
308
|
-
<
|
|
309
|
-
{{ detail.kind }}
|
|
310
|
-
</KindBadge>
|
|
311
|
-
<span class="text-[10px] text-zinc-500 font-mono">{{ detail.app }}</span>
|
|
312
|
-
<span v-if="detail.source" class="ml-auto inline-flex">
|
|
313
|
-
<SourcePill :source="detail.source" @click="sourcePreview = detail.source!" />
|
|
314
|
-
</span>
|
|
215
|
+
<span class="text-[10px] text-zinc-500 font-mono">{{ detail.id }}</span>
|
|
315
216
|
</div>
|
|
316
217
|
</div>
|
|
317
218
|
|
|
318
219
|
<div class="flex-1 overflow-auto">
|
|
319
|
-
<!--
|
|
220
|
+
<!-- Lifecycle hooks -->
|
|
320
221
|
<section class="px-6 py-4 border-b border-zinc-900">
|
|
321
222
|
<h3 class="text-xs uppercase tracking-wide text-zinc-400 mb-3">
|
|
322
|
-
|
|
223
|
+
Lifecycle hooks
|
|
323
224
|
<span class="text-zinc-600 normal-case tracking-normal ml-1">
|
|
324
|
-
({{
|
|
225
|
+
({{ internals.lifecycleHooks.length }})
|
|
325
226
|
</span>
|
|
326
227
|
</h3>
|
|
327
|
-
<div
|
|
228
|
+
<div
|
|
229
|
+
v-if="internals.lifecycleHooks.length === 0"
|
|
230
|
+
class="text-xs text-zinc-500 italic"
|
|
231
|
+
>
|
|
328
232
|
No <span class="font-mono">plugin.boot:{{ detail.name }}</span> or
|
|
329
233
|
<span class="font-mono">plugin.shutdown:{{ detail.name }}</span>
|
|
330
234
|
hooks registered.
|
|
331
235
|
</div>
|
|
332
236
|
<ul v-else class="space-y-1">
|
|
333
237
|
<li
|
|
334
|
-
v-for="h in
|
|
335
|
-
:key="h.
|
|
238
|
+
v-for="h in internals.lifecycleHooks"
|
|
239
|
+
:key="h.name"
|
|
336
240
|
class="flex items-center gap-3 px-3 py-1.5 rounded hover:bg-zinc-900/50 cursor-pointer"
|
|
337
241
|
:data-testid="`hook-link-${h.name}`"
|
|
338
242
|
@click="openHook(h.name)"
|
|
339
243
|
>
|
|
340
|
-
<
|
|
244
|
+
<span
|
|
245
|
+
class="text-[10px] uppercase tracking-wide shrink-0 w-16"
|
|
246
|
+
:class="h.phase === 'boot' ? 'text-emerald-400' : 'text-amber-400'"
|
|
247
|
+
>{{ h.phase }}</span
|
|
248
|
+
>
|
|
341
249
|
<span class="font-mono text-xs flex-1 truncate">{{ h.name }}</span>
|
|
342
250
|
<span class="text-[10px] text-zinc-500 tabular-nums shrink-0">
|
|
343
251
|
chain {{ h.chain }} · {{ h.listeners }} listener{{
|
|
@@ -348,42 +256,72 @@ function openHook(hookName: string): void {
|
|
|
348
256
|
</ul>
|
|
349
257
|
</section>
|
|
350
258
|
|
|
351
|
-
<!--
|
|
259
|
+
<!-- Contributes -->
|
|
260
|
+
<section class="px-6 py-4 border-b border-zinc-900">
|
|
261
|
+
<h3 class="text-xs uppercase tracking-wide text-zinc-400 mb-3">
|
|
262
|
+
Contributes
|
|
263
|
+
<span class="text-zinc-600 normal-case tracking-normal ml-1">
|
|
264
|
+
({{ internals.contributes.length }})
|
|
265
|
+
</span>
|
|
266
|
+
</h3>
|
|
267
|
+
<div v-if="internals.contributes.length === 0" class="text-xs text-zinc-500 italic">
|
|
268
|
+
No attributed contributions in the topology.
|
|
269
|
+
</div>
|
|
270
|
+
<div v-else class="flex flex-wrap gap-1.5" data-testid="plugins-contributes">
|
|
271
|
+
<span
|
|
272
|
+
v-for="c in internals.contributes"
|
|
273
|
+
:key="c"
|
|
274
|
+
class="font-mono text-[11px] px-2 py-0.5 rounded border border-zinc-800 bg-zinc-900 text-zinc-300"
|
|
275
|
+
>
|
|
276
|
+
{{ c }}
|
|
277
|
+
</span>
|
|
278
|
+
</div>
|
|
279
|
+
</section>
|
|
280
|
+
|
|
281
|
+
<!-- ctx-by-kind -->
|
|
282
|
+
<section v-if="ctxByKindEntries.length" class="px-6 py-4 border-b border-zinc-900">
|
|
283
|
+
<h3 class="text-xs uppercase tracking-wide text-zinc-400 mb-3">Ctx by kind</h3>
|
|
284
|
+
<div class="space-y-2" data-testid="plugins-ctx-by-kind">
|
|
285
|
+
<div v-for="[kind, keys] in ctxByKindEntries" :key="kind" class="flex gap-2">
|
|
286
|
+
<span class="font-mono text-[11px] text-zinc-500 w-20 shrink-0">{{ kind }}</span>
|
|
287
|
+
<div class="flex flex-wrap gap-1">
|
|
288
|
+
<span
|
|
289
|
+
v-for="k in keys"
|
|
290
|
+
:key="k"
|
|
291
|
+
class="font-mono text-[10px] px-1.5 py-0.5 rounded bg-zinc-900 text-cyan-300"
|
|
292
|
+
>
|
|
293
|
+
{{ k }}
|
|
294
|
+
</span>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</section>
|
|
299
|
+
|
|
300
|
+
<!-- Live lifecycle timeline -->
|
|
352
301
|
<section class="px-6 py-4">
|
|
353
302
|
<div class="flex items-center gap-2 mb-3">
|
|
354
303
|
<Activity class="w-3.5 h-3.5 text-emerald-400" />
|
|
355
|
-
<h3 class="text-xs uppercase tracking-wide text-zinc-400">
|
|
304
|
+
<h3 class="text-xs uppercase tracking-wide text-zinc-400">Live lifecycle</h3>
|
|
356
305
|
<span class="text-[10px] text-zinc-500 tabular-nums">
|
|
357
|
-
{{
|
|
306
|
+
{{ detailSteps.length }} recent
|
|
358
307
|
</span>
|
|
359
308
|
</div>
|
|
360
|
-
<div v-if="
|
|
361
|
-
No lifecycle data yet. Boot or restart the wire and
|
|
309
|
+
<div v-if="detailSteps.length === 0" class="text-xs text-zinc-500 italic">
|
|
310
|
+
No lifecycle data yet. Boot or restart the wire and steps for
|
|
362
311
|
<span class="font-mono">{{ detail.name }}</span> will stream here.
|
|
363
312
|
</div>
|
|
364
313
|
<ul v-else class="divide-y divide-zinc-900" data-testid="plugins-tap-list">
|
|
365
314
|
<li
|
|
366
|
-
v-for="(r, i) in
|
|
315
|
+
v-for="(r, i) in detailSteps"
|
|
367
316
|
:key="i"
|
|
368
317
|
class="px-3 py-1.5 flex items-center gap-3 font-mono text-[11px] hover:bg-zinc-900/30"
|
|
369
318
|
>
|
|
370
319
|
<span class="text-zinc-600 tabular-nums w-20 shrink-0">
|
|
371
|
-
{{
|
|
320
|
+
{{ fmtTime(stepField(r, "ts")) }}
|
|
372
321
|
</span>
|
|
373
|
-
<span
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
'text-emerald-400': r.kind === 'lifecycle',
|
|
377
|
-
'text-cyan-400': r.kind === 'hook.step',
|
|
378
|
-
}"
|
|
379
|
-
>{{ r.kind === "lifecycle" ? "event" : "hook" }}</span
|
|
380
|
-
>
|
|
381
|
-
<span class="text-zinc-200 truncate flex-1">{{ recordPhase(r) }}</span>
|
|
382
|
-
<span
|
|
383
|
-
v-if="recordDuration(r) !== undefined"
|
|
384
|
-
class="text-zinc-500 tabular-nums shrink-0"
|
|
385
|
-
>
|
|
386
|
-
{{ recordDuration(r)!.toFixed(1) }} ms
|
|
322
|
+
<span class="text-zinc-200 truncate flex-1">{{ lifecyclePhase(r) }}</span>
|
|
323
|
+
<span v-if="durationMsOf(r) !== null" class="text-zinc-500 tabular-nums shrink-0">
|
|
324
|
+
{{ durationMsOf(r)!.toFixed(1) }} ms
|
|
387
325
|
</span>
|
|
388
326
|
</li>
|
|
389
327
|
</ul>
|
|
@@ -392,7 +330,5 @@ function openHook(hookName: string): void {
|
|
|
392
330
|
</div>
|
|
393
331
|
</main>
|
|
394
332
|
</div>
|
|
395
|
-
|
|
396
|
-
<SourceDrawer :source="sourcePreview" @close="sourcePreview = null" />
|
|
397
333
|
</div>
|
|
398
334
|
</template>
|