@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.
- 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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
|
2
|
+
import { createRouter, createMemoryHistory } from "vue-router";
|
|
3
|
+
import Hooks from "./Hooks.vue";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storybook for the Hooks page. The manifest fetch + telemetry stream fail
|
|
7
|
+
* inside Storybook, so the page degrades to its empty / no-live-tap state —
|
|
8
|
+
* exactly the surface we want to document.
|
|
9
|
+
*/
|
|
10
|
+
const router = createRouter({
|
|
11
|
+
history: createMemoryHistory(),
|
|
12
|
+
routes: [
|
|
13
|
+
{ path: "/hooks", name: "hooks", component: Hooks },
|
|
14
|
+
{ path: "/plugins", name: "plugins", component: { template: "<div/>" } },
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const meta: Meta<typeof Hooks> = {
|
|
19
|
+
title: "Pages/Hooks",
|
|
20
|
+
component: Hooks,
|
|
21
|
+
decorators: [
|
|
22
|
+
(story) => ({
|
|
23
|
+
components: { story },
|
|
24
|
+
template: '<div class="h-screen bg-zinc-950 text-zinc-100"><story /></div>',
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
parameters: {
|
|
28
|
+
layout: "fullscreen",
|
|
29
|
+
vueRouter: { router },
|
|
30
|
+
},
|
|
31
|
+
render: () => ({
|
|
32
|
+
components: { Hooks },
|
|
33
|
+
setup() {
|
|
34
|
+
return {};
|
|
35
|
+
},
|
|
36
|
+
template: "<Hooks />",
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
export default meta;
|
|
40
|
+
|
|
41
|
+
type Story = StoryObj<typeof Hooks>;
|
|
42
|
+
|
|
43
|
+
/** Default: no wire running — empty registry + idle tap. */
|
|
44
|
+
export const NoLiveData: Story = {};
|
package/src/pages/Hooks.vue
CHANGED
|
@@ -1,127 +1,108 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Hooks — the unified extension-point primitive every nwire substrate
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Hooks — the unified extension-point primitive every nwire substrate collapses
|
|
4
|
+
* onto. Each row is one named hook from the deep manifest's runtime topology
|
|
5
|
+
* (`topology.hooks`: name + chain/listener counts), read natively via
|
|
6
|
+
* `useManifest`.
|
|
6
7
|
*
|
|
7
|
-
* Detail pane shows steady-state metadata
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Detail pane shows steady-state metadata (MetadataInspector) plus the LIVE
|
|
9
|
+
* hook-tap: per-hook fire activity sourced from `hook.step` telemetry
|
|
10
|
+
* (`useTelemetry`), keyed by hook name. A `plugin.boot:<n>` / `plugin.shutdown:<n>`
|
|
11
|
+
* hook cross-links to `/plugins?name=<n>` ("Registered by").
|
|
10
12
|
*/
|
|
11
|
-
import { computed, onMounted,
|
|
13
|
+
import { computed, onMounted, ref, watch } from "vue";
|
|
12
14
|
import { useRoute, useRouter } from "vue-router";
|
|
13
|
-
import { useCache, type HookEntry } from "@/lib/cache";
|
|
14
15
|
import { Activity, Boxes, Puzzle, RefreshCw, Search } from "lucide-vue-next";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
readonly hookId: string;
|
|
21
|
-
readonly runId: string;
|
|
22
|
-
readonly parentRunId?: string;
|
|
23
|
-
readonly stepId: number;
|
|
24
|
-
readonly stepKind: "chain" | "listener";
|
|
25
|
-
readonly stepName?: string;
|
|
26
|
-
readonly phase: "start" | "end" | "error";
|
|
27
|
-
readonly durationMs?: number;
|
|
28
|
-
readonly error?: { message?: string };
|
|
29
|
-
readonly ts: string;
|
|
30
|
-
}
|
|
16
|
+
import { useManifest } from "@/composables/useManifest";
|
|
17
|
+
import { useProject } from "@/composables/useProject";
|
|
18
|
+
import { useTelemetry } from "@/composables/useTelemetry";
|
|
19
|
+
import { hookRegistry, registeringPlugin, liveFireTally, type HookRow } from "@/lib/topology-view";
|
|
20
|
+
import { MetadataInspector, SourcePill, SourceDrawer } from "@/components";
|
|
31
21
|
|
|
32
22
|
const route = useRoute();
|
|
33
23
|
const router = useRouter();
|
|
34
|
-
const {
|
|
24
|
+
const { activeCwd } = useProject();
|
|
25
|
+
const { view, isError, error, refetch } = useManifest(activeCwd);
|
|
26
|
+
const { records, status: streamStatus, recent } = useTelemetry(activeCwd);
|
|
27
|
+
|
|
35
28
|
const filter = ref("");
|
|
36
29
|
const selected = ref<string | null>(null);
|
|
37
|
-
const sourcePreview = ref<{ file: string; line
|
|
30
|
+
const sourcePreview = ref<{ file: string; line?: number; column?: number } | null>(null);
|
|
31
|
+
|
|
32
|
+
const hooks = computed<HookRow[]>(() => hookRegistry(view.value));
|
|
33
|
+
|
|
34
|
+
const filteredHooks = computed<HookRow[]>(() => {
|
|
35
|
+
const q = filter.value.toLowerCase();
|
|
36
|
+
return hooks.value.filter((h) => !q || h.name.toLowerCase().includes(q));
|
|
37
|
+
});
|
|
38
38
|
|
|
39
|
+
const detail = computed<HookRow | null>(
|
|
40
|
+
() => hooks.value.find((h) => h.id === selected.value) ?? null,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// ── Deep-link preselect: /hooks?name=<hookName> → first name match. ───────
|
|
39
44
|
function applyQueryPreselect(): void {
|
|
40
45
|
const name = route.query.name;
|
|
41
46
|
if (typeof name !== "string" || name.length === 0) return;
|
|
42
|
-
|
|
43
|
-
const found = cache.value?.hooks.find((h) => h.name === name);
|
|
47
|
+
const found = hooks.value.find((h) => h.name === name);
|
|
44
48
|
if (found) selected.value = found.id;
|
|
45
49
|
}
|
|
46
|
-
|
|
47
50
|
onMounted(applyQueryPreselect);
|
|
48
51
|
watch(() => route.query.name, applyQueryPreselect);
|
|
49
|
-
watch(
|
|
52
|
+
watch(hooks, applyQueryPreselect);
|
|
50
53
|
|
|
51
|
-
// ── Live tap
|
|
52
|
-
const
|
|
53
|
-
const streamStatus = ref<"connecting" | "open" | "closed" | "error">("connecting");
|
|
54
|
-
let es: EventSource | null = null;
|
|
55
|
-
|
|
56
|
-
onMounted(() => {
|
|
57
|
-
connect();
|
|
58
|
-
});
|
|
59
|
-
onUnmounted(() => {
|
|
60
|
-
es?.close();
|
|
61
|
-
});
|
|
54
|
+
// ── Live hook-tap: fire tally per hook name, from `hook.step` telemetry. ──
|
|
55
|
+
const fireTally = computed(() => liveFireTally(records.value));
|
|
62
56
|
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
es?.close();
|
|
66
|
-
es = new EventSource("/_nwire/telemetry/stream");
|
|
67
|
-
es.onopen = () => {
|
|
68
|
-
streamStatus.value = "open";
|
|
69
|
-
};
|
|
70
|
-
es.onerror = () => {
|
|
71
|
-
streamStatus.value = "error";
|
|
72
|
-
};
|
|
73
|
-
es.onmessage = (m) => {
|
|
74
|
-
try {
|
|
75
|
-
const rec = JSON.parse(m.data) as { kind?: string };
|
|
76
|
-
if (rec.kind !== "hook.step") return;
|
|
77
|
-
liveSteps.value.push(rec as StepRecord);
|
|
78
|
-
if (liveSteps.value.length > 500) liveSteps.value.splice(0, 200);
|
|
79
|
-
} catch {
|
|
80
|
-
/* ignore non-JSON frames */
|
|
81
|
-
}
|
|
82
|
-
};
|
|
57
|
+
function firesFor(name: string): number {
|
|
58
|
+
return fireTally.value.get(name)?.fires ?? 0;
|
|
83
59
|
}
|
|
60
|
+
function lastFiredFor(name: string): string | undefined {
|
|
61
|
+
return fireTally.value.get(name)?.lastTs;
|
|
62
|
+
}
|
|
63
|
+
const totalFires = computed(() => recent("hook.step").length);
|
|
84
64
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const q = filter.value.toLowerCase();
|
|
88
|
-
return cache.value.hooks.filter(
|
|
89
|
-
(h) => !q || h.name.toLowerCase().includes(q) || h.id.toLowerCase().includes(q),
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const detail = computed<HookEntry | null>(
|
|
94
|
-
() => cache.value?.hooks.find((h) => h.id === selected.value) ?? null,
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const detailSteps = computed<StepRecord[]>(() => {
|
|
65
|
+
// The selected hook's recent live steps, newest first.
|
|
66
|
+
const detailSteps = computed(() => {
|
|
98
67
|
if (!detail.value) return [];
|
|
99
|
-
|
|
100
|
-
|
|
68
|
+
const name = detail.value.name;
|
|
69
|
+
return recent("hook.step")
|
|
70
|
+
.filter((r) => (r as Record<string, unknown>).hookName === name)
|
|
101
71
|
.slice(-50)
|
|
102
72
|
.reverse();
|
|
103
73
|
});
|
|
104
74
|
|
|
105
|
-
|
|
106
|
-
* Parse `plugin.boot:<name>` / `plugin.shutdown:<name>` → `<name>`.
|
|
107
|
-
* Used by the "Registered by" cross-link to /plugins?name=…
|
|
108
|
-
*/
|
|
109
|
-
const registeringPlugin = computed<string | null>(() => {
|
|
110
|
-
const n = detail.value?.name;
|
|
111
|
-
if (!n) return null;
|
|
112
|
-
for (const prefix of ["plugin.boot:", "plugin.shutdown:"]) {
|
|
113
|
-
if (n.startsWith(prefix)) return n.slice(prefix.length);
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
});
|
|
75
|
+
const registeredBy = computed<string | null>(() => registeringPlugin(detail.value?.name));
|
|
117
76
|
|
|
118
|
-
|
|
119
|
-
|
|
77
|
+
const detailMeta = computed(() => {
|
|
78
|
+
if (!detail.value) return null;
|
|
79
|
+
return {
|
|
80
|
+
name: detail.value.name,
|
|
81
|
+
id: detail.value.id,
|
|
82
|
+
chainSteps: detail.value.chain,
|
|
83
|
+
listeners: detail.value.listeners,
|
|
84
|
+
liveFires: firesFor(detail.value.name),
|
|
85
|
+
lastFired: lastFiredFor(detail.value.name) ?? null,
|
|
86
|
+
};
|
|
120
87
|
});
|
|
88
|
+
|
|
89
|
+
function fmtTime(ts: string | undefined): string {
|
|
90
|
+
if (!ts) return "—";
|
|
91
|
+
const d = new Date(ts);
|
|
92
|
+
return Number.isNaN(d.getTime()) ? "—" : d.toLocaleTimeString(undefined, { hour12: false });
|
|
93
|
+
}
|
|
94
|
+
function stepField(rec: unknown, key: string): string {
|
|
95
|
+
const v = (rec as Record<string, unknown>)[key];
|
|
96
|
+
return v == null ? "" : String(v);
|
|
97
|
+
}
|
|
98
|
+
function durationMsOf(rec: unknown): number | null {
|
|
99
|
+
const v = (rec as Record<string, unknown>).durationMs;
|
|
100
|
+
return typeof v === "number" ? v : null;
|
|
101
|
+
}
|
|
121
102
|
</script>
|
|
122
103
|
|
|
123
104
|
<template>
|
|
124
|
-
<div
|
|
105
|
+
<div class="h-full flex flex-col" data-testid="hooks-page">
|
|
125
106
|
<!-- Header -->
|
|
126
107
|
<div class="p-6 pb-3 border-b border-zinc-800 flex items-center justify-between">
|
|
127
108
|
<div>
|
|
@@ -129,13 +110,20 @@ watch(detail, () => {
|
|
|
129
110
|
<Boxes class="w-4 h-4 text-orange-400" />
|
|
130
111
|
<h1 class="text-lg font-medium">Hooks</h1>
|
|
131
112
|
<span class="text-[10px] text-zinc-500">
|
|
132
|
-
{{ filteredHooks.length }} / {{
|
|
113
|
+
{{ filteredHooks.length }} / {{ hooks.length }}
|
|
133
114
|
</span>
|
|
134
115
|
<span
|
|
135
116
|
class="ml-2 text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-zinc-900 text-zinc-400"
|
|
136
117
|
data-testid="stream-status"
|
|
137
118
|
>
|
|
138
|
-
|
|
119
|
+
tap {{ streamStatus }}
|
|
120
|
+
</span>
|
|
121
|
+
<span
|
|
122
|
+
v-if="totalFires > 0"
|
|
123
|
+
class="text-[9px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-950 text-emerald-300"
|
|
124
|
+
data-testid="hooks-total-fires"
|
|
125
|
+
>
|
|
126
|
+
{{ totalFires }} live fires
|
|
139
127
|
</span>
|
|
140
128
|
</div>
|
|
141
129
|
<p class="text-xs text-zinc-500 mt-1">
|
|
@@ -145,15 +133,20 @@ watch(detail, () => {
|
|
|
145
133
|
</div>
|
|
146
134
|
<button
|
|
147
135
|
class="p-1 rounded hover:bg-zinc-800 text-zinc-400"
|
|
148
|
-
title="
|
|
149
|
-
@click="
|
|
136
|
+
title="Refetch manifest"
|
|
137
|
+
@click="() => refetch()"
|
|
150
138
|
>
|
|
151
139
|
<RefreshCw class="w-3.5 h-3.5" />
|
|
152
140
|
</button>
|
|
153
141
|
</div>
|
|
154
142
|
|
|
143
|
+
<!-- Error -->
|
|
144
|
+
<div v-if="isError" class="p-6 text-xs text-zinc-500" data-testid="hooks-error">
|
|
145
|
+
{{ error ?? "Couldn't load the manifest. Run `nwire cache` to build it." }}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
155
148
|
<!-- Master / detail -->
|
|
156
|
-
<div class="flex-1 flex min-h-0">
|
|
149
|
+
<div v-else class="flex-1 flex min-h-0">
|
|
157
150
|
<!-- Master -->
|
|
158
151
|
<aside class="w-80 border-r border-zinc-800 flex flex-col shrink-0">
|
|
159
152
|
<div class="px-3 py-2 border-b border-zinc-800">
|
|
@@ -162,6 +155,7 @@ watch(detail, () => {
|
|
|
162
155
|
<input
|
|
163
156
|
v-model="filter"
|
|
164
157
|
placeholder="filter…"
|
|
158
|
+
data-testid="hooks-filter"
|
|
165
159
|
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"
|
|
166
160
|
/>
|
|
167
161
|
</div>
|
|
@@ -169,12 +163,12 @@ watch(detail, () => {
|
|
|
169
163
|
|
|
170
164
|
<div v-if="filteredHooks.length === 0" class="p-4 text-xs text-zinc-500">
|
|
171
165
|
{{
|
|
172
|
-
|
|
173
|
-
? "No hooks in
|
|
166
|
+
hooks.length === 0
|
|
167
|
+
? "No hooks in the manifest. Did you rebuild after adding hook() calls?"
|
|
174
168
|
: "No hooks match the filter."
|
|
175
169
|
}}
|
|
176
170
|
</div>
|
|
177
|
-
<div v-else class="flex-1 overflow-auto">
|
|
171
|
+
<div v-else class="flex-1 overflow-auto" data-testid="hooks-list">
|
|
178
172
|
<button
|
|
179
173
|
v-for="h in filteredHooks"
|
|
180
174
|
:key="h.id"
|
|
@@ -182,7 +176,18 @@ watch(detail, () => {
|
|
|
182
176
|
:class="{ 'bg-zinc-900/70': h.id === selected }"
|
|
183
177
|
@click="selected = h.id"
|
|
184
178
|
>
|
|
185
|
-
<div class="
|
|
179
|
+
<div class="flex items-center gap-2">
|
|
180
|
+
<span class="font-mono text-xs text-zinc-100 truncate flex-1">{{ h.name }}</span>
|
|
181
|
+
<span
|
|
182
|
+
v-if="firesFor(h.name) > 0"
|
|
183
|
+
class="inline-flex items-center gap-1 text-[10px] text-emerald-400 tabular-nums shrink-0"
|
|
184
|
+
:data-testid="`hook-fires-${h.name}`"
|
|
185
|
+
title="live fires observed"
|
|
186
|
+
>
|
|
187
|
+
<Activity class="w-3 h-3" />
|
|
188
|
+
{{ firesFor(h.name) }}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
186
191
|
<div class="text-[10px] text-zinc-500 tabular-nums mt-0.5 flex gap-2">
|
|
187
192
|
<span>chain {{ h.chain }}</span>
|
|
188
193
|
<span>·</span>
|
|
@@ -195,93 +200,89 @@ watch(detail, () => {
|
|
|
195
200
|
<!-- Detail -->
|
|
196
201
|
<main class="flex-1 flex flex-col min-w-0">
|
|
197
202
|
<div v-if="!detail" class="p-8 text-sm text-zinc-500 italic">
|
|
198
|
-
Select a hook to view its surface + live tap
|
|
203
|
+
Select a hook to view its surface + live tap.
|
|
199
204
|
</div>
|
|
200
205
|
<div v-else class="flex-1 flex flex-col overflow-hidden">
|
|
201
206
|
<div class="px-6 py-5 border-b border-zinc-800">
|
|
202
207
|
<div class="flex items-center gap-3 flex-wrap">
|
|
203
208
|
<h2 class="font-mono text-xl">{{ detail.name }}</h2>
|
|
204
209
|
<span class="text-[10px] text-zinc-500 font-mono">{{ detail.id }}</span>
|
|
205
|
-
<span v-if="detail.source" class="ml-auto inline-flex">
|
|
206
|
-
<SourcePill :source="detail.source" @click="sourcePreview = detail.source!" />
|
|
207
|
-
</span>
|
|
208
210
|
</div>
|
|
209
|
-
<div class="text-[11px] text-zinc-500 mt-2 flex gap-4 tabular-nums">
|
|
211
|
+
<div class="text-[11px] text-zinc-500 mt-2 flex gap-4 items-center tabular-nums">
|
|
210
212
|
<span>{{ detail.chain }} chain step{{ detail.chain === 1 ? "" : "s" }}</span>
|
|
211
213
|
<span>{{ detail.listeners }} listener{{ detail.listeners === 1 ? "" : "s" }}</span>
|
|
214
|
+
<SourcePill
|
|
215
|
+
v-if="detail.source"
|
|
216
|
+
:source="detail.source"
|
|
217
|
+
compact
|
|
218
|
+
@click="sourcePreview = detail.source ?? null"
|
|
219
|
+
/>
|
|
212
220
|
</div>
|
|
213
|
-
<div v-if="
|
|
221
|
+
<div v-if="registeredBy" class="mt-2 flex items-center gap-2 text-xs">
|
|
214
222
|
<span class="text-zinc-500">Registered by</span>
|
|
215
223
|
<button
|
|
216
224
|
type="button"
|
|
217
225
|
class="inline-flex items-center gap-1 font-mono text-fuchsia-300 hover:underline"
|
|
218
|
-
:data-testid="`plugin-link-${
|
|
219
|
-
@click="router.push({ path: '/plugins', query: { name:
|
|
226
|
+
:data-testid="`plugin-link-${registeredBy}`"
|
|
227
|
+
@click="router.push({ path: '/plugins', query: { name: registeredBy } })"
|
|
220
228
|
>
|
|
221
229
|
<Puzzle class="w-3 h-3" />
|
|
222
|
-
{{
|
|
230
|
+
{{ registeredBy }}
|
|
223
231
|
</button>
|
|
224
232
|
</div>
|
|
225
233
|
</div>
|
|
226
234
|
|
|
227
|
-
<!-- Live tap stream -->
|
|
228
235
|
<div class="flex-1 overflow-auto">
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
appear here.
|
|
243
|
-
</div>
|
|
244
|
-
<ul v-else class="divide-y divide-zinc-900" data-testid="hooks-tap-list">
|
|
245
|
-
<li
|
|
246
|
-
v-for="(s, i) in detailSteps"
|
|
247
|
-
:key="`${s.runId}:${s.stepId}:${s.phase}:${i}`"
|
|
248
|
-
class="px-6 py-1.5 flex items-center gap-3 font-mono text-[11px] hover:bg-zinc-900/30"
|
|
249
|
-
>
|
|
250
|
-
<span class="text-zinc-600 tabular-nums w-20 shrink-0">
|
|
251
|
-
{{ new Date(s.ts).toLocaleTimeString(undefined, { hour12: false }) }}
|
|
252
|
-
</span>
|
|
253
|
-
<span
|
|
254
|
-
class="w-12 text-[10px] uppercase tracking-wide shrink-0"
|
|
255
|
-
:class="{
|
|
256
|
-
'text-zinc-400': s.phase === 'start',
|
|
257
|
-
'text-emerald-400': s.phase === 'end',
|
|
258
|
-
'text-red-400': s.phase === 'error',
|
|
259
|
-
}"
|
|
260
|
-
>{{ s.phase }}</span
|
|
261
|
-
>
|
|
262
|
-
<span
|
|
263
|
-
class="w-16 text-[10px] uppercase tracking-wide shrink-0"
|
|
264
|
-
:class="s.stepKind === 'chain' ? 'text-cyan-400' : 'text-amber-400'"
|
|
265
|
-
>{{ s.stepKind }}</span
|
|
266
|
-
>
|
|
267
|
-
<span class="text-zinc-200 truncate flex-1">
|
|
268
|
-
{{ s.stepName ?? `#${s.stepId}` }}
|
|
269
|
-
</span>
|
|
270
|
-
<span v-if="s.durationMs !== undefined" class="text-zinc-500 tabular-nums shrink-0">
|
|
271
|
-
{{ s.durationMs.toFixed(1) }} ms
|
|
236
|
+
<!-- Steady-state metadata -->
|
|
237
|
+
<section class="border-b border-zinc-900">
|
|
238
|
+
<MetadataInspector :data="detailMeta" label="Hook" />
|
|
239
|
+
</section>
|
|
240
|
+
|
|
241
|
+
<!-- Live hook-tap -->
|
|
242
|
+
<section>
|
|
243
|
+
<div class="px-6 py-3 border-b border-zinc-900 flex items-center gap-2">
|
|
244
|
+
<Activity class="w-3.5 h-3.5 text-emerald-400" />
|
|
245
|
+
<h3 class="text-xs uppercase tracking-wide text-zinc-400">Live tap</h3>
|
|
246
|
+
<span class="text-[10px] text-zinc-500 tabular-nums">
|
|
247
|
+
{{ firesFor(detail.name) }} fire{{ firesFor(detail.name) === 1 ? "" : "s" }} ·
|
|
248
|
+
last {{ fmtTime(lastFiredFor(detail.name)) }}
|
|
272
249
|
</span>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
250
|
+
</div>
|
|
251
|
+
<div
|
|
252
|
+
v-if="detailSteps.length === 0"
|
|
253
|
+
class="p-6 text-xs text-zinc-500 italic"
|
|
254
|
+
data-testid="hooks-empty-tap"
|
|
255
|
+
>
|
|
256
|
+
No tap data yet. Trigger an action through the running wire and fire activity will
|
|
257
|
+
appear here.
|
|
258
|
+
</div>
|
|
259
|
+
<ul v-else class="divide-y divide-zinc-900" data-testid="hooks-tap-list">
|
|
260
|
+
<li
|
|
261
|
+
v-for="(s, i) in detailSteps"
|
|
262
|
+
:key="i"
|
|
263
|
+
class="px-6 py-1.5 flex items-center gap-3 font-mono text-[11px] hover:bg-zinc-900/30"
|
|
277
264
|
>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
265
|
+
<span class="text-zinc-600 tabular-nums w-20 shrink-0">
|
|
266
|
+
{{ fmtTime(stepField(s, "ts")) }}
|
|
267
|
+
</span>
|
|
268
|
+
<span
|
|
269
|
+
class="w-12 text-[10px] uppercase tracking-wide shrink-0"
|
|
270
|
+
:class="{
|
|
271
|
+
'text-zinc-400': stepField(s, 'phase') === 'start',
|
|
272
|
+
'text-emerald-400': stepField(s, 'phase') === 'end',
|
|
273
|
+
'text-red-400': stepField(s, 'phase') === 'error',
|
|
274
|
+
}"
|
|
275
|
+
>{{ stepField(s, "phase") || "step" }}</span
|
|
276
|
+
>
|
|
277
|
+
<span class="text-zinc-200 truncate flex-1">
|
|
278
|
+
{{ stepField(s, "stepName") || `#${stepField(s, "stepId")}` }}
|
|
279
|
+
</span>
|
|
280
|
+
<span v-if="durationMsOf(s) !== null" class="text-zinc-500 tabular-nums shrink-0">
|
|
281
|
+
{{ durationMsOf(s)!.toFixed(1) }} ms
|
|
282
|
+
</span>
|
|
283
|
+
</li>
|
|
284
|
+
</ul>
|
|
285
|
+
</section>
|
|
285
286
|
</div>
|
|
286
287
|
</div>
|
|
287
288
|
</main>
|