@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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topology view — pure selectors that shape the deep manifest's runtime
|
|
3
|
+
* `topology` layer into the rows the specialized Hooks + Plugins pages render,
|
|
4
|
+
* and tally the live hook-tap from telemetry. Reads `ManifestView` (the
|
|
5
|
+
* `topology.hooks` registry, the `topology.plugins` list, capabilities,
|
|
6
|
+
* `ctxByKind`) and `TelemetryRecord`s.
|
|
7
|
+
*
|
|
8
|
+
* Kept pure (no DOM, no fetch) so it unit-tests in isolation; the pages bind
|
|
9
|
+
* `useManifest` + `useTelemetry` to these.
|
|
10
|
+
*/
|
|
11
|
+
import type { ManifestView } from "./manifest";
|
|
12
|
+
import type { TelemetryRecord } from "./telemetry";
|
|
13
|
+
|
|
14
|
+
/** A `plugin.boot:<n>` / `plugin.shutdown:<n>` lifecycle prefix. */
|
|
15
|
+
const LIFECYCLE_PREFIXES = ["plugin.boot:", "plugin.shutdown:"] as const;
|
|
16
|
+
|
|
17
|
+
/** One named hook in the registry — the Hooks master row. */
|
|
18
|
+
export interface HookRow {
|
|
19
|
+
/** Stable id for selection (`hook:<name>` when the topology omits its own). */
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly chain: number;
|
|
23
|
+
readonly listeners: number;
|
|
24
|
+
/** Where the hook was authored — drives the SourcePill. */
|
|
25
|
+
readonly source?: { readonly file: string; readonly line?: number; readonly column?: number };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One plugin in the registry — the Plugins master row. */
|
|
29
|
+
export interface PluginRow {
|
|
30
|
+
/** Stable id for selection (`plugin:<name>`). */
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A boot/shutdown hook attributed to a plugin (the lifecycle internals). */
|
|
36
|
+
export interface PluginLifecycleHook {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly phase: "boot" | "shutdown";
|
|
39
|
+
readonly chain: number;
|
|
40
|
+
readonly listeners: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The deep internals assembled for a selected plugin. */
|
|
44
|
+
export interface PluginInternals {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
/** Boot/shutdown hooks the plugin registered (`plugin.boot:<n>` / `…shutdown:<n>`). */
|
|
47
|
+
readonly lifecycleHooks: readonly PluginLifecycleHook[];
|
|
48
|
+
/** Names this plugin contributes via `contributes` / `mounts` graph edges. */
|
|
49
|
+
readonly contributes: readonly string[];
|
|
50
|
+
/** A same-named capability's ctx-by-kind, when one exists. */
|
|
51
|
+
readonly ctxByKind: Readonly<Record<string, readonly string[]>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A live-fire entry for one hook, tallied from `hook.step` telemetry. */
|
|
55
|
+
export interface HookFire {
|
|
56
|
+
/** Count of `start`-phase steps seen for this hook (one per fire). */
|
|
57
|
+
readonly fires: number;
|
|
58
|
+
/** ISO timestamp of the most recent step, if any. */
|
|
59
|
+
readonly lastTs?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Read a string field off a loosely-typed record, or undefined. */
|
|
63
|
+
function str(rec: TelemetryRecord, key: string): string | undefined {
|
|
64
|
+
const v = (rec as Record<string, unknown>)[key];
|
|
65
|
+
return typeof v === "string" ? v : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The hook registry from native topology, name-sorted. Tolerant of missing
|
|
70
|
+
* `chain` / `listeners` (older topology) and a missing topology entirely.
|
|
71
|
+
*/
|
|
72
|
+
export function hookRegistry(view: ManifestView | null): HookRow[] {
|
|
73
|
+
const hooks = view?.manifest.topology?.hooks ?? [];
|
|
74
|
+
return hooks
|
|
75
|
+
.filter((h) => typeof h.name === "string" && h.name.length > 0)
|
|
76
|
+
.map((h) => ({
|
|
77
|
+
id: h.id ?? `hook:${h.name}`,
|
|
78
|
+
name: h.name,
|
|
79
|
+
chain: h.chain ?? 0,
|
|
80
|
+
listeners: h.listeners ?? 0,
|
|
81
|
+
source: h.source,
|
|
82
|
+
}))
|
|
83
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The plugin name that registered a `plugin.boot:<n>` / `plugin.shutdown:<n>`
|
|
88
|
+
* hook, or `null` for a non-lifecycle hook. Powers the Hooks "Registered by"
|
|
89
|
+
* cross-link.
|
|
90
|
+
*/
|
|
91
|
+
export function registeringPlugin(hookName: string | undefined | null): string | null {
|
|
92
|
+
if (!hookName) return null;
|
|
93
|
+
for (const prefix of LIFECYCLE_PREFIXES) {
|
|
94
|
+
if (hookName.startsWith(prefix)) return hookName.slice(prefix.length);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The plugin registry from native topology, name-sorted. */
|
|
100
|
+
export function pluginRegistry(view: ManifestView | null): PluginRow[] {
|
|
101
|
+
const plugins = view?.manifest.topology?.plugins ?? [];
|
|
102
|
+
return plugins
|
|
103
|
+
.filter((p) => typeof p.name === "string" && p.name.length > 0)
|
|
104
|
+
.map((p) => ({ id: `plugin:${p.name}`, name: p.name }))
|
|
105
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Assemble a plugin's deep internals: its boot/shutdown lifecycle hooks (parsed
|
|
110
|
+
* back from `topology.hooks`), what it contributes (graph `contributes` /
|
|
111
|
+
* `mounts` edges out of its node), and a same-named capability's ctx-by-kind.
|
|
112
|
+
* Pure; tolerant of a missing topology / model.
|
|
113
|
+
*/
|
|
114
|
+
export function pluginInternals(
|
|
115
|
+
view: ManifestView | null,
|
|
116
|
+
name: string | null | undefined,
|
|
117
|
+
): PluginInternals | null {
|
|
118
|
+
if (!view || !name) return null;
|
|
119
|
+
|
|
120
|
+
const hooks = view.manifest.topology?.hooks ?? [];
|
|
121
|
+
const lifecycleHooks: PluginLifecycleHook[] = [];
|
|
122
|
+
for (const h of hooks) {
|
|
123
|
+
if (h.name === `plugin.boot:${name}`)
|
|
124
|
+
lifecycleHooks.push({
|
|
125
|
+
name: h.name,
|
|
126
|
+
phase: "boot",
|
|
127
|
+
chain: h.chain ?? 0,
|
|
128
|
+
listeners: h.listeners ?? 0,
|
|
129
|
+
});
|
|
130
|
+
else if (h.name === `plugin.shutdown:${name}`)
|
|
131
|
+
lifecycleHooks.push({
|
|
132
|
+
name: h.name,
|
|
133
|
+
phase: "shutdown",
|
|
134
|
+
chain: h.chain ?? 0,
|
|
135
|
+
listeners: h.listeners ?? 0,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Boot before shutdown.
|
|
139
|
+
lifecycleHooks.sort((a, b) => (a.phase === b.phase ? 0 : a.phase === "boot" ? -1 : 1));
|
|
140
|
+
|
|
141
|
+
// `contributes` / `mounts` edges out of this plugin node (emitted by richer
|
|
142
|
+
// topology captures; absent ones simply yield no rows).
|
|
143
|
+
const id = `plugin:${name}`;
|
|
144
|
+
const contributes = view
|
|
145
|
+
.edgesOf(id, "out")
|
|
146
|
+
.filter((e) => e.type === "contributes" || e.type === "mounts")
|
|
147
|
+
.map((e) => {
|
|
148
|
+
const node = view.node(e.to);
|
|
149
|
+
return node?.name ?? e.to;
|
|
150
|
+
})
|
|
151
|
+
.sort((a, b) => a.localeCompare(b));
|
|
152
|
+
|
|
153
|
+
// A capability registered under the same name carries the ctx-by-kind it adds.
|
|
154
|
+
const capByKind: Record<string, readonly string[]> = {};
|
|
155
|
+
const capNode = view.node(`capability:${name}`);
|
|
156
|
+
const capKinds = capNode?.data?.kinds;
|
|
157
|
+
const capCtxKeys = capNode?.data?.ctxKeys;
|
|
158
|
+
if (Array.isArray(capCtxKeys)) {
|
|
159
|
+
const kinds = Array.isArray(capKinds) && capKinds.length ? (capKinds as string[]) : ["*"];
|
|
160
|
+
for (const k of kinds) capByKind[k] = capCtxKeys as string[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { name, lifecycleHooks, contributes, ctxByKind: capByKind };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Tally live hook fires from `hook.step` telemetry, keyed by hook name. Counts
|
|
168
|
+
* one fire per `start`-phase step (a fire's first observation); records the most
|
|
169
|
+
* recent step's timestamp regardless of phase. Records of other kinds are
|
|
170
|
+
* ignored, so it degrades to an empty map when no stream/records exist.
|
|
171
|
+
*/
|
|
172
|
+
export function liveFireTally(records: readonly TelemetryRecord[]): Map<string, HookFire> {
|
|
173
|
+
const out = new Map<string, HookFire>();
|
|
174
|
+
for (const rec of records) {
|
|
175
|
+
if (rec.kind !== "hook.step") continue;
|
|
176
|
+
const name = str(rec, "hookName");
|
|
177
|
+
if (!name) continue;
|
|
178
|
+
const prev = out.get(name);
|
|
179
|
+
const phase = str(rec, "phase");
|
|
180
|
+
const fires = (prev?.fires ?? 0) + (phase === "start" || phase === undefined ? 1 : 0);
|
|
181
|
+
const ts = rec.ts ?? prev?.lastTs;
|
|
182
|
+
out.set(name, { fires, lastTs: ts });
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waterfall layout — pure geometry for the Flow / Trace view.
|
|
3
|
+
*
|
|
4
|
+
* Takes a causation forest (`TraceNode[]` from `buildCorrelationTree`) and lays
|
|
5
|
+
* it out as time-positioned rows: depth-indented, each with an offset + width as
|
|
6
|
+
* a percentage of the trace span, plus the critical path (the longest-duration
|
|
7
|
+
* root→leaf chain). No DOM, no Vue — unit-tested in isolation; the component
|
|
8
|
+
* just renders the rows.
|
|
9
|
+
*/
|
|
10
|
+
import { isFailure, type TelemetryRecord, type TraceNode } from "./telemetry";
|
|
11
|
+
|
|
12
|
+
/** Smallest bar a row gets so zero-duration spans stay visible as a tick. */
|
|
13
|
+
const MIN_WIDTH_PCT = 1.5;
|
|
14
|
+
|
|
15
|
+
/** One laid-out row of the waterfall. */
|
|
16
|
+
export interface WaterfallRow {
|
|
17
|
+
readonly record: TelemetryRecord;
|
|
18
|
+
/** Tree depth (0 = root) — drives the gutter indent. */
|
|
19
|
+
readonly depth: number;
|
|
20
|
+
/** ms from the trace start to this row's start. */
|
|
21
|
+
readonly offsetMs: number;
|
|
22
|
+
/** This row's own duration in ms (0 if instantaneous). */
|
|
23
|
+
readonly durationMs: number;
|
|
24
|
+
/** Left edge as a percentage [0,100] of the trace span. */
|
|
25
|
+
readonly offsetPct: number;
|
|
26
|
+
/** Bar width as a percentage of the trace span (clamped to a visible min). */
|
|
27
|
+
readonly widthPct: number;
|
|
28
|
+
/** On the critical (longest-duration) path. */
|
|
29
|
+
readonly critical: boolean;
|
|
30
|
+
/** A failure record (drives the red row treatment). */
|
|
31
|
+
readonly failed: boolean;
|
|
32
|
+
/** Human label — handler/event name, falling back to the kind. */
|
|
33
|
+
readonly label: string;
|
|
34
|
+
/** Stable key for `v-for` + selection. */
|
|
35
|
+
readonly key: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The full layout: rows in pre-order plus the resolved time window. */
|
|
39
|
+
export interface WaterfallLayout {
|
|
40
|
+
readonly rows: WaterfallRow[];
|
|
41
|
+
readonly startMs: number;
|
|
42
|
+
readonly endMs: number;
|
|
43
|
+
readonly spanMs: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
47
|
+
return Math.max(lo, Math.min(hi, n));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseTs(r: TelemetryRecord): number | undefined {
|
|
51
|
+
if (!r.ts) return undefined;
|
|
52
|
+
const t = Date.parse(r.ts);
|
|
53
|
+
return Number.isNaN(t) ? undefined : t;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The display name carried on a record, by convention, else the kind. */
|
|
57
|
+
export function recordLabel(r: TelemetryRecord): string {
|
|
58
|
+
for (const field of ["name", "action", "event", "query", "actor", "workflow", "handler"]) {
|
|
59
|
+
const v = r[field];
|
|
60
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
61
|
+
}
|
|
62
|
+
return r.kind;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface Flat {
|
|
66
|
+
readonly node: TraceNode;
|
|
67
|
+
readonly depth: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function flatten(forest: readonly TraceNode[], depth = 0, out: Flat[] = []): Flat[] {
|
|
71
|
+
for (const node of forest) {
|
|
72
|
+
out.push({ node, depth });
|
|
73
|
+
flatten(node.children, depth + 1, out);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The longest downward (root→leaf) path by total duration, with its total. */
|
|
79
|
+
function bestPath(node: TraceNode): { total: number; nodes: TraceNode[] } {
|
|
80
|
+
const self = node.record.durationMs ?? 0;
|
|
81
|
+
if (node.children.length === 0) return { total: self, nodes: [node] };
|
|
82
|
+
let best = { total: -1, nodes: [] as TraceNode[] };
|
|
83
|
+
for (const child of node.children) {
|
|
84
|
+
const p = bestPath(child);
|
|
85
|
+
if (p.total > best.total) best = p;
|
|
86
|
+
}
|
|
87
|
+
return { total: self + best.total, nodes: [node, ...best.nodes] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The critical path across a forest — the single root→leaf chain with the
|
|
92
|
+
* greatest summed duration. Returns the set of nodes on that chain.
|
|
93
|
+
*/
|
|
94
|
+
export function criticalPath(forest: readonly TraceNode[]): Set<TraceNode> {
|
|
95
|
+
let best = { total: -1, nodes: [] as TraceNode[] };
|
|
96
|
+
for (const root of forest) {
|
|
97
|
+
const p = bestPath(root);
|
|
98
|
+
if (p.total > best.total) best = p;
|
|
99
|
+
}
|
|
100
|
+
return new Set(best.nodes);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Lay a causation forest out as a waterfall. Rows are returned in pre-order
|
|
105
|
+
* (parents before children). When records carry `ts`, bars are time-positioned
|
|
106
|
+
* against the trace span; when none do, every bar spans full width so the tree
|
|
107
|
+
* still reads (depth + kind + duration label) without a timeline.
|
|
108
|
+
*/
|
|
109
|
+
export function buildWaterfall(forest: readonly TraceNode[]): WaterfallLayout {
|
|
110
|
+
const flat = flatten(forest);
|
|
111
|
+
const crit = criticalPath(forest);
|
|
112
|
+
|
|
113
|
+
const starts = flat
|
|
114
|
+
.map((f) => parseTs(f.node.record))
|
|
115
|
+
.filter((t): t is number => t !== undefined);
|
|
116
|
+
const haveTime = starts.length > 0;
|
|
117
|
+
const startMs = haveTime ? Math.min(...starts) : 0;
|
|
118
|
+
const endMs = haveTime
|
|
119
|
+
? Math.max(
|
|
120
|
+
...flat.map((f) => (parseTs(f.node.record) ?? startMs) + (f.node.record.durationMs ?? 0)),
|
|
121
|
+
)
|
|
122
|
+
: 0;
|
|
123
|
+
const spanMs = Math.max(endMs - startMs, 1);
|
|
124
|
+
|
|
125
|
+
const rows: WaterfallRow[] = flat.map((f, i) => {
|
|
126
|
+
const rec = f.node.record;
|
|
127
|
+
const start = parseTs(rec) ?? startMs;
|
|
128
|
+
const offsetMs = haveTime ? start - startMs : 0;
|
|
129
|
+
const durationMs = rec.durationMs ?? 0;
|
|
130
|
+
const offsetPct = haveTime ? clamp((offsetMs / spanMs) * 100, 0, 100) : 0;
|
|
131
|
+
const rawWidth = haveTime ? (durationMs / spanMs) * 100 : 100;
|
|
132
|
+
const widthPct = clamp(Math.max(rawWidth, MIN_WIDTH_PCT), MIN_WIDTH_PCT, 100 - offsetPct);
|
|
133
|
+
return {
|
|
134
|
+
record: rec,
|
|
135
|
+
depth: f.depth,
|
|
136
|
+
offsetMs,
|
|
137
|
+
durationMs,
|
|
138
|
+
offsetPct,
|
|
139
|
+
widthPct,
|
|
140
|
+
critical: crit.has(f.node),
|
|
141
|
+
failed: isFailure(rec),
|
|
142
|
+
label: recordLabel(rec),
|
|
143
|
+
key: rec.envelope?.messageId ?? `${rec.kind}-${i}`,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return { rows, startMs, endMs, spanMs };
|
|
148
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createApp } from "vue";
|
|
2
2
|
import { createRouter, createWebHistory } from "vue-router";
|
|
3
|
+
import { VueQueryPlugin } from "@tanstack/vue-query";
|
|
3
4
|
import App from "./App.vue";
|
|
4
5
|
import "./style.css";
|
|
5
6
|
import { getActiveProjectCwd, loadCatalog } from "./lib/project-catalog";
|
|
@@ -47,23 +48,16 @@ class ScopedEventSource extends OriginalEventSource {
|
|
|
47
48
|
(window as unknown as { EventSource: typeof OriginalEventSource }).EventSource = ScopedEventSource;
|
|
48
49
|
|
|
49
50
|
import Home from "./pages/Home.vue";
|
|
50
|
-
import
|
|
51
|
+
import MapView from "./pages/Map.vue";
|
|
51
52
|
import Topology from "./pages/Topology.vue";
|
|
52
53
|
import Trace from "./pages/Trace.vue";
|
|
53
|
-
import Actions from "./pages/Actions.vue";
|
|
54
|
-
import Events from "./pages/Events.vue";
|
|
55
|
-
import Workflows from "./pages/Workflows.vue";
|
|
56
54
|
import Hooks from "./pages/Hooks.vue";
|
|
57
55
|
import Plugins from "./pages/Plugins.vue";
|
|
58
|
-
import
|
|
59
|
-
import
|
|
60
|
-
import Run from "./pages/Run.vue";
|
|
61
|
-
import Commands from "./pages/Commands.vue";
|
|
56
|
+
import Streams from "./pages/Streams.vue";
|
|
57
|
+
import Operate from "./pages/Operate.vue";
|
|
62
58
|
import Projects from "./pages/Projects.vue";
|
|
63
|
-
import
|
|
64
|
-
import
|
|
65
|
-
import Apps from "./pages/Apps.vue";
|
|
66
|
-
import Sinks from "./pages/Sinks.vue";
|
|
59
|
+
import Errors from "./pages/Errors.vue";
|
|
60
|
+
import Inspect from "./pages/Inspect.vue";
|
|
67
61
|
|
|
68
62
|
/**
|
|
69
63
|
* Routes are namespaced under `/projects/:slug` so every URL is
|
|
@@ -78,22 +72,37 @@ import Sinks from "./pages/Sinks.vue";
|
|
|
78
72
|
*/
|
|
79
73
|
const pageRoutes = [
|
|
80
74
|
{ path: "", component: Home, name: "home" },
|
|
81
|
-
{ path: "
|
|
75
|
+
{ path: "map", component: MapView, name: "map" },
|
|
76
|
+
{ path: "inspect", component: Inspect, name: "inspect" },
|
|
82
77
|
{ path: "topology", component: Topology, name: "topology" },
|
|
83
78
|
{ path: "trace", component: Trace, name: "trace" },
|
|
84
|
-
{ path: "
|
|
85
|
-
{ path: "actions", component: Actions, name: "actions" },
|
|
86
|
-
{ path: "events", component: Events, name: "events" },
|
|
87
|
-
{ path: "workflows", component: Workflows, name: "workflows" },
|
|
79
|
+
{ path: "errors", component: Errors, name: "errors" },
|
|
88
80
|
{ path: "hooks", component: Hooks, name: "hooks" },
|
|
89
81
|
{ path: "plugins", component: Plugins, name: "plugins" },
|
|
90
|
-
{ path: "
|
|
91
|
-
{ path: "
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
{ path: "streams", component: Streams, name: "streams" },
|
|
83
|
+
{ path: "operate", component: Operate, name: "operate" },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Per-kind pages collapsed into the unified Inspect view. Old routes (bare +
|
|
87
|
+
// project-pinned) redirect to `/inspect?kind=<kind>` so bookmarks + CI keep
|
|
88
|
+
// working.
|
|
89
|
+
const KIND_REDIRECTS: ReadonlyArray<readonly [string, string]> = [
|
|
90
|
+
["apps", "app"],
|
|
91
|
+
["actions", "action"],
|
|
92
|
+
["events", "event"],
|
|
93
|
+
["workflows", "workflow"],
|
|
94
|
+
["projections", "projection"],
|
|
95
|
+
["queries", "query"],
|
|
96
|
+
["sinks", "sinkStage"],
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Dispatch / Run / Commands collapsed into the unified Operate view. Old routes
|
|
100
|
+
// (bare + project-pinned) redirect to `/operate?mode=<mode>` so bookmarks + CI
|
|
101
|
+
// keep working.
|
|
102
|
+
const OPERATE_REDIRECTS: ReadonlyArray<readonly [string, string]> = [
|
|
103
|
+
["dispatch", "dispatch"],
|
|
104
|
+
["run", "run"],
|
|
105
|
+
["commands", "commands"],
|
|
97
106
|
];
|
|
98
107
|
|
|
99
108
|
const router = createRouter({
|
|
@@ -105,19 +114,44 @@ const router = createRouter({
|
|
|
105
114
|
// collide with the bare-path variants below.
|
|
106
115
|
{
|
|
107
116
|
path: "/projects/:slug",
|
|
108
|
-
children:
|
|
117
|
+
children: [
|
|
118
|
+
...pageRoutes.map((r) => ({ ...r, name: `p-${r.name}` })),
|
|
119
|
+
...KIND_REDIRECTS.map(([path, kind]) => ({
|
|
120
|
+
path,
|
|
121
|
+
redirect: (to: { params: Record<string, unknown> }) => ({
|
|
122
|
+
path: `/projects/${to.params.slug}/inspect`,
|
|
123
|
+
query: { kind },
|
|
124
|
+
}),
|
|
125
|
+
})),
|
|
126
|
+
...OPERATE_REDIRECTS.map(([path, mode]) => ({
|
|
127
|
+
path,
|
|
128
|
+
redirect: (to: { params: Record<string, unknown> }) => ({
|
|
129
|
+
path: `/projects/${to.params.slug}/operate`,
|
|
130
|
+
query: { mode },
|
|
131
|
+
}),
|
|
132
|
+
})),
|
|
133
|
+
],
|
|
109
134
|
},
|
|
110
135
|
// Bare paths render the same pages. App.vue upgrades the URL to
|
|
111
136
|
// /projects/<slug>/... once the active project slug is known, so a
|
|
112
|
-
// user who lands on /
|
|
137
|
+
// user who lands on /trace ends up bookmark-friendly without
|
|
113
138
|
// breaking the load. Old bookmarks + CI test suites continue to
|
|
114
139
|
// work unchanged.
|
|
115
140
|
{ path: "/", component: Home, name: "home" },
|
|
116
141
|
...pageRoutes
|
|
117
142
|
.filter((r) => r.path !== "")
|
|
118
143
|
.map((r) => ({ path: `/${r.path}`, component: r.component, name: r.name })),
|
|
144
|
+
...KIND_REDIRECTS.map(([path, kind]) => ({
|
|
145
|
+
path: `/${path}`,
|
|
146
|
+
redirect: { path: "/inspect", query: { kind } },
|
|
147
|
+
})),
|
|
148
|
+
...OPERATE_REDIRECTS.map(([path, mode]) => ({
|
|
149
|
+
path: `/${path}`,
|
|
150
|
+
redirect: { path: "/operate", query: { mode } },
|
|
151
|
+
})),
|
|
119
152
|
{ path: "/eventstorm", redirect: "/trace" },
|
|
120
|
-
{ path: "/
|
|
153
|
+
{ path: "/live", redirect: "/streams" },
|
|
154
|
+
{ path: "/modules", redirect: { path: "/inspect", query: { kind: "app" } } },
|
|
121
155
|
],
|
|
122
156
|
});
|
|
123
157
|
|
|
@@ -125,7 +159,7 @@ const router = createRouter({
|
|
|
125
159
|
* Register every catalog cwd with the running Studio process BEFORE mounting
|
|
126
160
|
* Vue. The fetch shim above appends `?project=<cwd>` to every nwire request,
|
|
127
161
|
* and the server's allowlist only includes `NWIRE_CWD` by default — so the
|
|
128
|
-
* first
|
|
162
|
+
* first manifest fetch from useManifest() would 400 without this hand-shake.
|
|
129
163
|
*/
|
|
130
164
|
async function bootstrap() {
|
|
131
165
|
const catalog = loadCatalog();
|
|
@@ -143,6 +177,6 @@ async function bootstrap() {
|
|
|
143
177
|
}),
|
|
144
178
|
),
|
|
145
179
|
);
|
|
146
|
-
createApp(App).use(router).mount("#app");
|
|
180
|
+
createApp(App).use(router).use(VueQueryPlugin).mount("#app");
|
|
147
181
|
}
|
|
148
182
|
void bootstrap();
|