@nwire/studio 0.12.1 → 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
package/src/pages/Run.vue
DELETED
|
@@ -1,618 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* Studio-as-runner — the Processes panel.
|
|
4
|
-
*
|
|
5
|
-
* Three columns: topology picker (left), running processes (middle),
|
|
6
|
-
* live stdout for the selected process (right). Drives the `/__nwire/run/*`
|
|
7
|
-
* supervisor surface served by Studio's Vite middleware.
|
|
8
|
-
*/
|
|
9
|
-
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
10
|
-
import {
|
|
11
|
-
Play,
|
|
12
|
-
Square,
|
|
13
|
-
RefreshCw,
|
|
14
|
-
Trash2,
|
|
15
|
-
CircleDot,
|
|
16
|
-
Circle,
|
|
17
|
-
Loader2,
|
|
18
|
-
AlertTriangle,
|
|
19
|
-
CheckCircle2,
|
|
20
|
-
Terminal,
|
|
21
|
-
} from "lucide-vue-next";
|
|
22
|
-
|
|
23
|
-
type ProcessStatus = "idle" | "starting" | "running" | "stopping" | "exited" | "crashed";
|
|
24
|
-
|
|
25
|
-
interface ManagedProcess {
|
|
26
|
-
id: string;
|
|
27
|
-
topology: string;
|
|
28
|
-
port?: number;
|
|
29
|
-
startedAt: string;
|
|
30
|
-
status: ProcessStatus;
|
|
31
|
-
pid?: number;
|
|
32
|
-
exitCode?: number | null;
|
|
33
|
-
signal?: string | null;
|
|
34
|
-
errorMessage?: string;
|
|
35
|
-
/** "studio" — spawned via this Studio session.
|
|
36
|
-
* "external" — discovered from .nwire/processes/*.json (e.g. nwire dev). */
|
|
37
|
-
source?: "studio" | "external";
|
|
38
|
-
env?: Record<string, string>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface LogLine {
|
|
42
|
-
seq: number;
|
|
43
|
-
ts: string;
|
|
44
|
-
stream: "stdout" | "stderr";
|
|
45
|
-
line: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const topologies = ref<string[]>([]);
|
|
49
|
-
const scripts = ref<Array<{ name: string; command: string }>>([]);
|
|
50
|
-
const scriptBusy = ref<string | null>(null);
|
|
51
|
-
const processes = ref<ManagedProcess[]>([]);
|
|
52
|
-
const selectedId = ref<string | null>(null);
|
|
53
|
-
const startTopology = ref<string>("");
|
|
54
|
-
const startPort = ref<number>(3000);
|
|
55
|
-
const startBusy = ref(false);
|
|
56
|
-
const startError = ref<string | null>(null);
|
|
57
|
-
|
|
58
|
-
/** KEY=value lines the operator typed; parsed on submit. */
|
|
59
|
-
const envInput = ref<string>("");
|
|
60
|
-
const envOpen = ref(false);
|
|
61
|
-
|
|
62
|
-
function parseEnvInput(raw: string): Record<string, string> {
|
|
63
|
-
const out: Record<string, string> = {};
|
|
64
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
65
|
-
const trimmed = line.trim();
|
|
66
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
67
|
-
const eq = trimmed.indexOf("=");
|
|
68
|
-
if (eq < 1) continue;
|
|
69
|
-
const k = trimmed.slice(0, eq).trim();
|
|
70
|
-
const v = trimmed.slice(eq + 1).trim();
|
|
71
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) continue;
|
|
72
|
-
// Strip matching surrounding quotes — "value" or 'value'.
|
|
73
|
-
const dequoted =
|
|
74
|
-
(v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))
|
|
75
|
-
? v.slice(1, -1)
|
|
76
|
-
: v;
|
|
77
|
-
out[k] = dequoted;
|
|
78
|
-
}
|
|
79
|
-
return out;
|
|
80
|
-
}
|
|
81
|
-
const autoScroll = ref(true);
|
|
82
|
-
const filterStream = ref<"all" | "stdout" | "stderr">("all");
|
|
83
|
-
|
|
84
|
-
const logs = ref<LogLine[]>([]);
|
|
85
|
-
let es: EventSource | null = null;
|
|
86
|
-
let processesPoll: ReturnType<typeof setInterval> | null = null;
|
|
87
|
-
|
|
88
|
-
async function loadTopologies() {
|
|
89
|
-
try {
|
|
90
|
-
const res = await fetch("/__nwire/run/topologies");
|
|
91
|
-
const body = (await res.json()) as { topologies: string[] };
|
|
92
|
-
topologies.value = body.topologies;
|
|
93
|
-
if (!startTopology.value && body.topologies[0]) startTopology.value = body.topologies[0];
|
|
94
|
-
} catch {
|
|
95
|
-
topologies.value = [];
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function loadScripts() {
|
|
100
|
-
try {
|
|
101
|
-
const res = await fetch("/__nwire/run/scripts");
|
|
102
|
-
const body = (await res.json()) as { scripts: Array<{ name: string; command: string }> };
|
|
103
|
-
scripts.value = body.scripts;
|
|
104
|
-
} catch {
|
|
105
|
-
scripts.value = [];
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Display ordering for package.json scripts:
|
|
111
|
-
*
|
|
112
|
-
* 1. `nwire`-prefixed first (alphabetical) — these are the framework's own
|
|
113
|
-
* shorthands (`nwire:dev`, `nwire:cache`, etc.) and operators reach for
|
|
114
|
-
* them most often.
|
|
115
|
-
* 2. Everything else (alphabetical).
|
|
116
|
-
*
|
|
117
|
-
* Computed off `scripts.value` so refresh updates the order without churn.
|
|
118
|
-
*/
|
|
119
|
-
const sortedScripts = computed(() => {
|
|
120
|
-
const nwirePrefixed = scripts.value
|
|
121
|
-
.filter((s) => s.name.startsWith("nwire"))
|
|
122
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
123
|
-
const others = scripts.value
|
|
124
|
-
.filter((s) => !s.name.startsWith("nwire"))
|
|
125
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
126
|
-
return [...nwirePrefixed, ...others];
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
async function runScript(name: string) {
|
|
130
|
-
scriptBusy.value = name;
|
|
131
|
-
startError.value = null;
|
|
132
|
-
try {
|
|
133
|
-
const env = parseEnvInput(envInput.value);
|
|
134
|
-
const port = startPort.value > 0 ? startPort.value : undefined;
|
|
135
|
-
const res = await fetch("/__nwire/run/exec-script", {
|
|
136
|
-
method: "POST",
|
|
137
|
-
headers: { "Content-Type": "application/json" },
|
|
138
|
-
body: JSON.stringify({ script: name, port, env: Object.keys(env).length ? env : undefined }),
|
|
139
|
-
});
|
|
140
|
-
const body = (await res.json()) as { process?: ManagedProcess; error?: string };
|
|
141
|
-
if (!res.ok) {
|
|
142
|
-
startError.value = body.error ?? `run failed (${res.status})`;
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (body.process) {
|
|
146
|
-
processes.value = [...processes.value, body.process];
|
|
147
|
-
selectedId.value = body.process.id;
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
startError.value = (err as Error).message;
|
|
151
|
-
} finally {
|
|
152
|
-
scriptBusy.value = null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function loadProcesses() {
|
|
157
|
-
try {
|
|
158
|
-
const res = await fetch("/__nwire/run/processes");
|
|
159
|
-
const body = (await res.json()) as { processes: ManagedProcess[] };
|
|
160
|
-
processes.value = body.processes;
|
|
161
|
-
// If selection died, keep the id but show last logs.
|
|
162
|
-
if (selectedId.value && !body.processes.some((p) => p.id === selectedId.value)) {
|
|
163
|
-
selectedId.value = null;
|
|
164
|
-
}
|
|
165
|
-
} catch {
|
|
166
|
-
/* ignore */
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function start() {
|
|
171
|
-
if (!startTopology.value) return;
|
|
172
|
-
startBusy.value = true;
|
|
173
|
-
startError.value = null;
|
|
174
|
-
try {
|
|
175
|
-
const res = await fetch("/__nwire/run/start", {
|
|
176
|
-
method: "POST",
|
|
177
|
-
headers: { "Content-Type": "application/json" },
|
|
178
|
-
body: JSON.stringify({
|
|
179
|
-
topology: startTopology.value,
|
|
180
|
-
port: startPort.value,
|
|
181
|
-
env: (() => {
|
|
182
|
-
const e = parseEnvInput(envInput.value);
|
|
183
|
-
return Object.keys(e).length ? e : undefined;
|
|
184
|
-
})(),
|
|
185
|
-
}),
|
|
186
|
-
});
|
|
187
|
-
const body = (await res.json()) as { process?: ManagedProcess; error?: string };
|
|
188
|
-
if (!res.ok) {
|
|
189
|
-
startError.value = body.error ?? `start failed (${res.status})`;
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (body.process) {
|
|
193
|
-
processes.value = [...processes.value, body.process];
|
|
194
|
-
selectedId.value = body.process.id;
|
|
195
|
-
}
|
|
196
|
-
} catch (err) {
|
|
197
|
-
startError.value = (err as Error).message;
|
|
198
|
-
} finally {
|
|
199
|
-
startBusy.value = false;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function stop(id: string) {
|
|
204
|
-
try {
|
|
205
|
-
await fetch(`/__nwire/run/stop/${id}`, { method: "POST" });
|
|
206
|
-
} catch {
|
|
207
|
-
/* ignore */
|
|
208
|
-
}
|
|
209
|
-
await loadProcesses();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function forget(id: string) {
|
|
213
|
-
try {
|
|
214
|
-
await fetch(`/__nwire/run/forget/${id}`, { method: "POST" });
|
|
215
|
-
if (selectedId.value === id) selectedId.value = null;
|
|
216
|
-
} catch {
|
|
217
|
-
/* ignore */
|
|
218
|
-
}
|
|
219
|
-
await loadProcesses();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function connectLogStream(id: string | null) {
|
|
223
|
-
es?.close();
|
|
224
|
-
es = null;
|
|
225
|
-
logs.value = [];
|
|
226
|
-
if (!id) return;
|
|
227
|
-
es = new EventSource(`/__nwire/run/logs/${id}/stream`);
|
|
228
|
-
es.onmessage = (msg) => {
|
|
229
|
-
try {
|
|
230
|
-
const line = JSON.parse(msg.data) as LogLine;
|
|
231
|
-
logs.value.push(line);
|
|
232
|
-
if (logs.value.length > 5000) logs.value.shift();
|
|
233
|
-
} catch {
|
|
234
|
-
/* ignore */
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
|
-
es.onerror = () => {
|
|
238
|
-
// SSE auto-reconnects; nothing to do.
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
watch(selectedId, (id) => connectLogStream(id));
|
|
243
|
-
|
|
244
|
-
onMounted(() => {
|
|
245
|
-
void loadTopologies();
|
|
246
|
-
void loadScripts();
|
|
247
|
-
void loadProcesses();
|
|
248
|
-
processesPoll = setInterval(loadProcesses, 2000);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
onUnmounted(() => {
|
|
252
|
-
es?.close();
|
|
253
|
-
if (processesPoll) clearInterval(processesPoll);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const filteredLogs = computed(() => {
|
|
257
|
-
if (filterStream.value === "all") return logs.value;
|
|
258
|
-
return logs.value.filter((l) => l.stream === filterStream.value);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const selectedProcess = computed(() => processes.value.find((p) => p.id === selectedId.value));
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* The process Studio's `/_nwire/*` proxy currently routes to — the most
|
|
265
|
-
* recently started running process. Surface it in the header so the user
|
|
266
|
-
* knows which wire Live / Dispatch / EventStorm are pointing at.
|
|
267
|
-
*/
|
|
268
|
-
const activeProcess = computed<ManagedProcess | undefined>(() => {
|
|
269
|
-
const running = processes.value.filter((p) => p.status === "running");
|
|
270
|
-
if (running.length === 0) return undefined;
|
|
271
|
-
return [...running].sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0];
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
function shortId(id: string): string {
|
|
275
|
-
return id.split("-")[0] ?? id.slice(0, 8);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function timeAgo(iso: string): string {
|
|
279
|
-
const s = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000));
|
|
280
|
-
if (s < 60) return `${s}s ago`;
|
|
281
|
-
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
282
|
-
return `${Math.floor(s / 3600)}h ago`;
|
|
283
|
-
}
|
|
284
|
-
</script>
|
|
285
|
-
|
|
286
|
-
<template>
|
|
287
|
-
<div class="h-full flex">
|
|
288
|
-
<!-- Topology picker -->
|
|
289
|
-
<div class="w-72 border-r border-zinc-800 flex flex-col">
|
|
290
|
-
<div class="border-b border-zinc-800 px-4 py-3">
|
|
291
|
-
<h1 class="font-semibold text-lg tracking-tight">Run</h1>
|
|
292
|
-
<p class="text-xs text-zinc-500 mt-0.5">Spawn a wire from a topology manifest.</p>
|
|
293
|
-
<div v-if="activeProcess" class="mt-2 flex items-center gap-1.5 text-[10px] font-mono">
|
|
294
|
-
<span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
|
295
|
-
<span class="text-emerald-300">/_nwire/* → :{{ activeProcess.port }}</span>
|
|
296
|
-
<span class="text-zinc-600">({{ activeProcess.topology }})</span>
|
|
297
|
-
</div>
|
|
298
|
-
<div v-else class="mt-2 text-[10px] font-mono text-zinc-600">
|
|
299
|
-
/_nwire/* → static fallback
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
<div
|
|
303
|
-
v-if="topologies.length === 0 && scripts.length === 0"
|
|
304
|
-
class="p-4 text-xs text-zinc-400 space-y-2 border-b border-zinc-800"
|
|
305
|
-
>
|
|
306
|
-
<div class="flex items-start gap-2 text-amber-300">
|
|
307
|
-
<AlertTriangle class="w-3.5 h-3.5 mt-0.5 shrink-0" />
|
|
308
|
-
<div class="space-y-1">
|
|
309
|
-
<div class="font-medium">No way to start this project</div>
|
|
310
|
-
<div class="text-zinc-500">
|
|
311
|
-
Studio looks for two things:
|
|
312
|
-
<ul class="list-disc list-inside mt-1 space-y-0.5">
|
|
313
|
-
<li>
|
|
314
|
-
<code class="text-zinc-300">apps/topologies/*.topology.ts</code> — multi-app
|
|
315
|
-
projects
|
|
316
|
-
</li>
|
|
317
|
-
<li>
|
|
318
|
-
<code class="text-zinc-300">package.json</code> scripts (<code>dev</code>,
|
|
319
|
-
<code>start</code>) — single-app projects
|
|
320
|
-
</li>
|
|
321
|
-
</ul>
|
|
322
|
-
</div>
|
|
323
|
-
<div class="text-zinc-500 mt-2">
|
|
324
|
-
Most projects use <code>pnpm dev</code>. Add a <code>"dev"</code> script to your
|
|
325
|
-
package.json and refresh.
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
<div
|
|
331
|
-
v-else-if="topologies.length === 0 && scripts.length > 0"
|
|
332
|
-
class="p-3 text-[11px] text-zinc-500 border-b border-zinc-800"
|
|
333
|
-
>
|
|
334
|
-
<div class="flex items-start gap-1.5">
|
|
335
|
-
<CircleDot class="w-3 h-3 mt-0.5 text-emerald-400 shrink-0" />
|
|
336
|
-
<div>
|
|
337
|
-
No topology files. Use the
|
|
338
|
-
<span class="text-zinc-300">package.json scripts</span> below — usually
|
|
339
|
-
<code class="text-zinc-300">dev</code> is what you want.
|
|
340
|
-
</div>
|
|
341
|
-
</div>
|
|
342
|
-
</div>
|
|
343
|
-
<div class="p-4 space-y-3" v-if="topologies.length > 0">
|
|
344
|
-
<div>
|
|
345
|
-
<label class="text-[10px] uppercase tracking-wide text-zinc-500">Topology</label>
|
|
346
|
-
<select
|
|
347
|
-
v-model="startTopology"
|
|
348
|
-
class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-zinc-600"
|
|
349
|
-
>
|
|
350
|
-
<option v-for="t in topologies" :key="t" :value="t">{{ t }}</option>
|
|
351
|
-
</select>
|
|
352
|
-
</div>
|
|
353
|
-
<div>
|
|
354
|
-
<label class="text-[10px] uppercase tracking-wide text-zinc-500">Port</label>
|
|
355
|
-
<input
|
|
356
|
-
v-model.number="startPort"
|
|
357
|
-
type="number"
|
|
358
|
-
class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-zinc-600"
|
|
359
|
-
/>
|
|
360
|
-
</div>
|
|
361
|
-
<button
|
|
362
|
-
class="w-full px-3 py-2 rounded bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 text-sm font-medium flex items-center justify-center gap-2"
|
|
363
|
-
:disabled="startBusy || !startTopology"
|
|
364
|
-
@click="start"
|
|
365
|
-
>
|
|
366
|
-
<Loader2 v-if="startBusy" class="w-4 h-4 animate-spin" />
|
|
367
|
-
<Play v-else class="w-4 h-4" />
|
|
368
|
-
{{ startBusy ? "Starting…" : "Start" }}
|
|
369
|
-
</button>
|
|
370
|
-
<div
|
|
371
|
-
v-if="startError"
|
|
372
|
-
class="text-xs text-rose-300 bg-rose-950/40 border border-rose-900 rounded p-2 flex items-start gap-2"
|
|
373
|
-
>
|
|
374
|
-
<AlertTriangle class="w-3 h-3 mt-0.5 shrink-0" />
|
|
375
|
-
{{ startError }}
|
|
376
|
-
</div>
|
|
377
|
-
<button
|
|
378
|
-
class="w-full text-[10px] text-zinc-500 hover:text-zinc-300 flex items-center justify-center gap-1"
|
|
379
|
-
@click="loadTopologies"
|
|
380
|
-
>
|
|
381
|
-
<RefreshCw class="w-3 h-3" />
|
|
382
|
-
refresh topologies
|
|
383
|
-
</button>
|
|
384
|
-
</div>
|
|
385
|
-
<!-- Env vars + port shared between topology start and script run.
|
|
386
|
-
Collapsed by default. Lines like `PORT=4000`, `LOG_LEVEL=debug`
|
|
387
|
-
are forwarded to the child process. -->
|
|
388
|
-
<div
|
|
389
|
-
class="border-t border-zinc-800 px-4 py-3"
|
|
390
|
-
v-if="scripts.length > 0 || topologies.length > 0"
|
|
391
|
-
>
|
|
392
|
-
<button
|
|
393
|
-
class="w-full flex items-center justify-between text-[10px] uppercase tracking-wide text-zinc-500 hover:text-zinc-300"
|
|
394
|
-
@click="envOpen = !envOpen"
|
|
395
|
-
>
|
|
396
|
-
<span class="flex items-center gap-1.5">
|
|
397
|
-
<span
|
|
398
|
-
:class="
|
|
399
|
-
envOpen
|
|
400
|
-
? 'rotate-90 inline-block transition-transform'
|
|
401
|
-
: 'inline-block transition-transform'
|
|
402
|
-
"
|
|
403
|
-
>▸</span
|
|
404
|
-
>
|
|
405
|
-
env + port overrides
|
|
406
|
-
</span>
|
|
407
|
-
<span
|
|
408
|
-
v-if="Object.keys(parseEnvInput(envInput)).length > 0"
|
|
409
|
-
class="text-emerald-300 normal-case"
|
|
410
|
-
>
|
|
411
|
-
{{ Object.keys(parseEnvInput(envInput)).length }} var(s) set
|
|
412
|
-
</span>
|
|
413
|
-
</button>
|
|
414
|
-
<div v-if="envOpen" class="mt-2 space-y-2">
|
|
415
|
-
<div>
|
|
416
|
-
<label class="text-[10px] uppercase tracking-wide text-zinc-500">Port (PORT env)</label>
|
|
417
|
-
<input
|
|
418
|
-
v-model.number="startPort"
|
|
419
|
-
type="number"
|
|
420
|
-
placeholder="auto"
|
|
421
|
-
class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-zinc-600"
|
|
422
|
-
/>
|
|
423
|
-
</div>
|
|
424
|
-
<div>
|
|
425
|
-
<label class="text-[10px] uppercase tracking-wide text-zinc-500"
|
|
426
|
-
>Other env (KEY=value, one per line)</label
|
|
427
|
-
>
|
|
428
|
-
<textarea
|
|
429
|
-
v-model="envInput"
|
|
430
|
-
rows="4"
|
|
431
|
-
placeholder="LOG_LEVEL=debug NODE_ENV=development"
|
|
432
|
-
class="w-full mt-1 bg-zinc-900 border border-zinc-800 rounded px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-zinc-600"
|
|
433
|
-
></textarea>
|
|
434
|
-
<div class="text-[10px] text-zinc-600 mt-1">
|
|
435
|
-
Applied to both topology starts and script runs.
|
|
436
|
-
</div>
|
|
437
|
-
</div>
|
|
438
|
-
</div>
|
|
439
|
-
</div>
|
|
440
|
-
|
|
441
|
-
<!-- package.json scripts — fallback for single-app projects without
|
|
442
|
-
apps/topologies/. Always shown so multi-app projects can also
|
|
443
|
-
use it for one-shot `pnpm test`, `pnpm build`, etc. -->
|
|
444
|
-
<div class="border-t border-zinc-800 px-4 py-3" v-if="scripts.length > 0">
|
|
445
|
-
<div class="flex items-center justify-between mb-2">
|
|
446
|
-
<label class="text-[10px] uppercase tracking-wide text-zinc-500">
|
|
447
|
-
package.json scripts
|
|
448
|
-
</label>
|
|
449
|
-
<button
|
|
450
|
-
class="text-zinc-500 hover:text-zinc-200 p-0.5 rounded"
|
|
451
|
-
@click="loadScripts"
|
|
452
|
-
title="Refresh"
|
|
453
|
-
>
|
|
454
|
-
<RefreshCw class="w-3 h-3" />
|
|
455
|
-
</button>
|
|
456
|
-
</div>
|
|
457
|
-
<div class="space-y-1">
|
|
458
|
-
<button
|
|
459
|
-
v-for="s in sortedScripts"
|
|
460
|
-
:key="s.name"
|
|
461
|
-
class="w-full text-left px-2 py-1.5 rounded hover:bg-zinc-900/70 disabled:opacity-50 flex items-center gap-2 group"
|
|
462
|
-
:disabled="scriptBusy === s.name"
|
|
463
|
-
@click="runScript(s.name)"
|
|
464
|
-
:title="s.command"
|
|
465
|
-
>
|
|
466
|
-
<Loader2 v-if="scriptBusy === s.name" class="w-3 h-3 animate-spin text-emerald-400" />
|
|
467
|
-
<Play v-else class="w-3 h-3 text-zinc-600 group-hover:text-emerald-400" />
|
|
468
|
-
<span class="text-xs font-mono text-zinc-200">{{ s.name }}</span>
|
|
469
|
-
<span class="text-[10px] text-zinc-600 truncate flex-1 text-right">
|
|
470
|
-
{{ s.command }}
|
|
471
|
-
</span>
|
|
472
|
-
</button>
|
|
473
|
-
</div>
|
|
474
|
-
</div>
|
|
475
|
-
</div>
|
|
476
|
-
|
|
477
|
-
<!-- Processes -->
|
|
478
|
-
<div class="w-96 border-r border-zinc-800 flex flex-col">
|
|
479
|
-
<div class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between">
|
|
480
|
-
<div>
|
|
481
|
-
<h2 class="font-medium text-sm tracking-tight">Processes</h2>
|
|
482
|
-
<p class="text-[10px] text-zinc-500">{{ processes.length }} managed</p>
|
|
483
|
-
</div>
|
|
484
|
-
<button
|
|
485
|
-
class="text-zinc-500 hover:text-zinc-200 p-1 rounded"
|
|
486
|
-
@click="loadProcesses"
|
|
487
|
-
title="Refresh"
|
|
488
|
-
>
|
|
489
|
-
<RefreshCw class="w-3.5 h-3.5" />
|
|
490
|
-
</button>
|
|
491
|
-
</div>
|
|
492
|
-
<div class="flex-1 overflow-auto">
|
|
493
|
-
<div v-if="processes.length === 0" class="p-6 text-sm text-zinc-500">
|
|
494
|
-
No processes. Pick a topology and click <span class="text-zinc-300">Start</span>.
|
|
495
|
-
</div>
|
|
496
|
-
<button
|
|
497
|
-
v-for="p in processes"
|
|
498
|
-
:key="p.id"
|
|
499
|
-
class="w-full text-left px-4 py-3 border-b border-zinc-900 hover:bg-zinc-900/50 transition-colors"
|
|
500
|
-
:class="{ 'bg-zinc-900/70': selectedId === p.id }"
|
|
501
|
-
@click="selectedId = p.id"
|
|
502
|
-
>
|
|
503
|
-
<div class="flex items-center justify-between">
|
|
504
|
-
<div class="flex items-center gap-2 min-w-0">
|
|
505
|
-
<CircleDot v-if="p.status === 'running'" class="w-3 h-3 text-emerald-400 shrink-0" />
|
|
506
|
-
<Loader2
|
|
507
|
-
v-else-if="p.status === 'starting' || p.status === 'stopping'"
|
|
508
|
-
class="w-3 h-3 text-amber-400 animate-spin shrink-0"
|
|
509
|
-
/>
|
|
510
|
-
<AlertTriangle
|
|
511
|
-
v-else-if="p.status === 'crashed'"
|
|
512
|
-
class="w-3 h-3 text-rose-400 shrink-0"
|
|
513
|
-
/>
|
|
514
|
-
<CheckCircle2
|
|
515
|
-
v-else-if="p.status === 'exited'"
|
|
516
|
-
class="w-3 h-3 text-zinc-400 shrink-0"
|
|
517
|
-
/>
|
|
518
|
-
<Circle v-else class="w-3 h-3 text-zinc-500 shrink-0" />
|
|
519
|
-
<span class="font-mono text-sm truncate">{{ p.topology }}</span>
|
|
520
|
-
<span v-if="p.port" class="text-[10px] text-zinc-500">:{{ p.port }}</span>
|
|
521
|
-
</div>
|
|
522
|
-
<span class="text-[10px] text-zinc-500">{{ timeAgo(p.startedAt) }}</span>
|
|
523
|
-
</div>
|
|
524
|
-
<div class="text-[10px] text-zinc-500 mt-0.5 font-mono flex items-center gap-2 flex-wrap">
|
|
525
|
-
<span class="uppercase">{{ p.status }}</span>
|
|
526
|
-
<span v-if="p.pid">pid {{ p.pid }}</span>
|
|
527
|
-
<span class="text-zinc-600">{{ shortId(p.id) }}</span>
|
|
528
|
-
<span
|
|
529
|
-
v-if="p.source === 'external'"
|
|
530
|
-
class="text-[9px] uppercase tracking-wider px-1 py-0 rounded bg-amber-950/40 text-amber-300 border border-amber-900"
|
|
531
|
-
title="Discovered from .nwire/processes/*.json — started outside Studio"
|
|
532
|
-
>external</span
|
|
533
|
-
>
|
|
534
|
-
</div>
|
|
535
|
-
<div v-if="p.errorMessage" class="text-[10px] text-rose-300 mt-1">
|
|
536
|
-
{{ p.errorMessage }}
|
|
537
|
-
</div>
|
|
538
|
-
</button>
|
|
539
|
-
</div>
|
|
540
|
-
</div>
|
|
541
|
-
|
|
542
|
-
<!-- Logs -->
|
|
543
|
-
<div class="flex-1 flex flex-col min-w-0">
|
|
544
|
-
<div class="border-b border-zinc-800 px-4 py-3 flex items-center justify-between gap-3">
|
|
545
|
-
<div class="flex items-center gap-3 min-w-0">
|
|
546
|
-
<Terminal class="w-4 h-4 text-zinc-400 shrink-0" />
|
|
547
|
-
<h2 class="font-medium text-sm tracking-tight truncate">
|
|
548
|
-
<template v-if="selectedProcess">
|
|
549
|
-
{{ selectedProcess.topology }}
|
|
550
|
-
<span class="text-zinc-500 font-mono text-xs">{{ shortId(selectedProcess.id) }}</span>
|
|
551
|
-
</template>
|
|
552
|
-
<template v-else>
|
|
553
|
-
<span class="text-zinc-500">Select a process</span>
|
|
554
|
-
</template>
|
|
555
|
-
</h2>
|
|
556
|
-
</div>
|
|
557
|
-
<div class="flex items-center gap-2">
|
|
558
|
-
<select
|
|
559
|
-
v-model="filterStream"
|
|
560
|
-
class="bg-zinc-900 border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-200"
|
|
561
|
-
>
|
|
562
|
-
<option value="all">all</option>
|
|
563
|
-
<option value="stdout">stdout</option>
|
|
564
|
-
<option value="stderr">stderr</option>
|
|
565
|
-
</select>
|
|
566
|
-
<label class="flex items-center gap-1 text-[10px] text-zinc-400">
|
|
567
|
-
<input type="checkbox" v-model="autoScroll" class="accent-emerald-400" />
|
|
568
|
-
tail
|
|
569
|
-
</label>
|
|
570
|
-
<button
|
|
571
|
-
v-if="
|
|
572
|
-
selectedProcess &&
|
|
573
|
-
(selectedProcess.status === 'running' || selectedProcess.status === 'starting')
|
|
574
|
-
"
|
|
575
|
-
class="text-xs px-2 py-1 rounded bg-rose-900/40 border border-rose-900 hover:bg-rose-900/70 text-rose-200 flex items-center gap-1"
|
|
576
|
-
@click="stop(selectedProcess.id)"
|
|
577
|
-
>
|
|
578
|
-
<Square class="w-3 h-3" /> stop
|
|
579
|
-
</button>
|
|
580
|
-
<button
|
|
581
|
-
v-if="
|
|
582
|
-
selectedProcess &&
|
|
583
|
-
(selectedProcess.status === 'exited' || selectedProcess.status === 'crashed')
|
|
584
|
-
"
|
|
585
|
-
class="text-xs px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-300 flex items-center gap-1"
|
|
586
|
-
@click="forget(selectedProcess.id)"
|
|
587
|
-
>
|
|
588
|
-
<Trash2 class="w-3 h-3" /> clear
|
|
589
|
-
</button>
|
|
590
|
-
</div>
|
|
591
|
-
</div>
|
|
592
|
-
<div
|
|
593
|
-
ref="(el) => el && autoScroll && (el.scrollTop = el.scrollHeight)"
|
|
594
|
-
class="flex-1 overflow-auto bg-zinc-950 font-mono text-[11px] leading-tight"
|
|
595
|
-
>
|
|
596
|
-
<div v-if="!selectedId" class="p-6 text-zinc-500 text-sm font-sans">
|
|
597
|
-
Pick a process on the left to view its stdout/stderr.
|
|
598
|
-
</div>
|
|
599
|
-
<div v-else-if="filteredLogs.length === 0" class="p-6 text-zinc-500 text-sm font-sans">
|
|
600
|
-
Waiting for output…
|
|
601
|
-
</div>
|
|
602
|
-
<div v-else class="p-3 space-y-0.5">
|
|
603
|
-
<div
|
|
604
|
-
v-for="log in filteredLogs"
|
|
605
|
-
:key="log.seq"
|
|
606
|
-
class="flex gap-2"
|
|
607
|
-
:class="log.stream === 'stderr' ? 'text-rose-200' : 'text-zinc-300'"
|
|
608
|
-
>
|
|
609
|
-
<span class="text-zinc-600 tabular-nums shrink-0">
|
|
610
|
-
{{ new Date(log.ts).toLocaleTimeString() }}
|
|
611
|
-
</span>
|
|
612
|
-
<span class="whitespace-pre-wrap break-all">{{ log.line }}</span>
|
|
613
|
-
</div>
|
|
614
|
-
</div>
|
|
615
|
-
</div>
|
|
616
|
-
</div>
|
|
617
|
-
</div>
|
|
618
|
-
</template>
|