@warlock.js/devtools 5.25.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/cjs/index.cjs +694 -0
- package/cjs/index.cjs.map +1 -0
- package/cjs/server-DHYSVive.cjs +286 -0
- package/cjs/server-DHYSVive.cjs.map +1 -0
- package/esm/caller-location.mjs +33 -0
- package/esm/caller-location.mjs.map +1 -0
- package/esm/collector.mjs +185 -0
- package/esm/collector.mjs.map +1 -0
- package/esm/current-request-id.mjs +24 -0
- package/esm/current-request-id.mjs.map +1 -0
- package/esm/devtools-connector.d.mts +26 -0
- package/esm/devtools-connector.d.mts.map +1 -0
- package/esm/devtools-connector.mjs +112 -0
- package/esm/devtools-connector.mjs.map +1 -0
- package/esm/index.d.mts +3 -0
- package/esm/index.mjs +3 -0
- package/esm/n-plus-one.mjs +36 -0
- package/esm/n-plus-one.mjs.map +1 -0
- package/esm/options.mjs +19 -0
- package/esm/options.mjs.map +1 -0
- package/esm/query-shape.mjs +27 -0
- package/esm/query-shape.mjs.map +1 -0
- package/esm/safe-value.mjs +63 -0
- package/esm/safe-value.mjs.map +1 -0
- package/esm/server/background-routes.mjs +9 -0
- package/esm/server/background-routes.mjs.map +1 -0
- package/esm/server/explain-route.mjs +37 -0
- package/esm/server/explain-route.mjs.map +1 -0
- package/esm/server/guard.mjs +30 -0
- package/esm/server/guard.mjs.map +1 -0
- package/esm/server/index.mjs +37 -0
- package/esm/server/index.mjs.map +1 -0
- package/esm/server/log-routes.mjs +12 -0
- package/esm/server/log-routes.mjs.map +1 -0
- package/esm/server/mail-routes.mjs +27 -0
- package/esm/server/mail-routes.mjs.map +1 -0
- package/esm/server/requests-routes.mjs +21 -0
- package/esm/server/requests-routes.mjs.map +1 -0
- package/esm/server/route-routes.mjs +9 -0
- package/esm/server/route-routes.mjs.map +1 -0
- package/esm/server/stream-route.mjs +32 -0
- package/esm/server/stream-route.mjs.map +1 -0
- package/esm/server/ui-assets.mjs +38 -0
- package/esm/server/ui-assets.mjs.map +1 -0
- package/esm/sources/cache-source.mjs +51 -0
- package/esm/sources/cache-source.mjs.map +1 -0
- package/esm/sources/log-source.mjs +46 -0
- package/esm/sources/log-source.mjs.map +1 -0
- package/esm/sources/mail-source.mjs +48 -0
- package/esm/sources/mail-source.mjs.map +1 -0
- package/esm/sources/query-source.mjs +52 -0
- package/esm/sources/query-source.mjs.map +1 -0
- package/esm/sources/tracing-source.mjs +47 -0
- package/esm/sources/tracing-source.mjs.map +1 -0
- package/esm/types.d.mts +213 -0
- package/esm/types.d.mts.map +1 -0
- package/esm/ui-root.mjs +40 -0
- package/esm/ui-root.mjs.map +1 -0
- package/llms-full.txt +78 -0
- package/llms.txt +9 -0
- package/package.json +52 -0
- package/skills/devtools-overview/SKILL.md +68 -0
- package/ui/app.css +354 -0
- package/ui/app.js +520 -0
- package/ui/index.html +24 -0
package/ui/app.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
if (!location.pathname.endsWith("/"))
|
|
2
|
+
location.replace(location.pathname + "/" + location.search + location.hash);
|
|
3
|
+
|
|
4
|
+
const app = document.querySelector("#app");
|
|
5
|
+
const tabs = document.querySelectorAll(".tab");
|
|
6
|
+
const state = {
|
|
7
|
+
tab: "requests",
|
|
8
|
+
requests: [],
|
|
9
|
+
details: new Map(),
|
|
10
|
+
mails: [],
|
|
11
|
+
logs: [],
|
|
12
|
+
routes: [],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function api(path, init) {
|
|
16
|
+
const response = await fetch(path, init);
|
|
17
|
+
if (!response.ok && response.status !== 204) {
|
|
18
|
+
let message = response.statusText || `Request failed (${response.status})`;
|
|
19
|
+
try {
|
|
20
|
+
message = (await response.json()).error || message;
|
|
21
|
+
} catch {
|
|
22
|
+
/* response has no JSON error */
|
|
23
|
+
}
|
|
24
|
+
throw new Error(message);
|
|
25
|
+
}
|
|
26
|
+
return response.status === 204 ? undefined : response.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function el(tag, attrs = {}, children = []) {
|
|
30
|
+
const node = document.createElement(tag);
|
|
31
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
32
|
+
if (value == null) continue;
|
|
33
|
+
if (key === "class") node.className = value;
|
|
34
|
+
else if (key === "text") node.textContent = value;
|
|
35
|
+
else if (key.startsWith("on")) node.addEventListener(key.slice(2), value);
|
|
36
|
+
else node.setAttribute(key, value);
|
|
37
|
+
}
|
|
38
|
+
for (const child of Array.isArray(children) ? children : [children]) node.append(child);
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function errorView(error) {
|
|
43
|
+
return el("p", { class: "error", text: `Could not load this view: ${error.message}` });
|
|
44
|
+
}
|
|
45
|
+
function empty(message) {
|
|
46
|
+
return el("p", { class: "empty", text: message });
|
|
47
|
+
}
|
|
48
|
+
function time(value) {
|
|
49
|
+
return value ? new Date(value).toLocaleTimeString() : "—";
|
|
50
|
+
}
|
|
51
|
+
function value(value) {
|
|
52
|
+
return value == null ? "—" : String(value);
|
|
53
|
+
}
|
|
54
|
+
function sql(query) {
|
|
55
|
+
return (
|
|
56
|
+
query.sql ||
|
|
57
|
+
`${query.collection || "collection"}.${query.command || "command"}\n${JSON.stringify(query.pipeline ?? {}, null, 2)}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
function canExplain(query) {
|
|
61
|
+
return query.driver === "postgres" && /^(select|with)\b/i.test((query.sql || "").trim());
|
|
62
|
+
}
|
|
63
|
+
function setTab(tab) {
|
|
64
|
+
state.tab = tab;
|
|
65
|
+
tabs.forEach((button) => button.classList.toggle("is-active", button.dataset.tab === tab));
|
|
66
|
+
render();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requestRow(request, selected) {
|
|
70
|
+
const status = request.status == null ? "—" : String(request.status);
|
|
71
|
+
return el(
|
|
72
|
+
"button",
|
|
73
|
+
{
|
|
74
|
+
class: `row${selected ? " is-selected" : ""}`,
|
|
75
|
+
type: "button",
|
|
76
|
+
onclick: () => showRequest(request.id),
|
|
77
|
+
},
|
|
78
|
+
[
|
|
79
|
+
el("div", { class: "row-top" }, [
|
|
80
|
+
el("span", { class: "method", text: request.method }),
|
|
81
|
+
el("span", { class: `status-${status[0]}`, text: status }),
|
|
82
|
+
]),
|
|
83
|
+
el("div", { class: "path", text: request.path }),
|
|
84
|
+
el("div", { class: "meta" }, [
|
|
85
|
+
el("span", { text: `${value(request.durationMs)} ms` }),
|
|
86
|
+
el("span", { text: `${request.queryCount} queries` }),
|
|
87
|
+
request.warningCount
|
|
88
|
+
? el("span", { class: "badge", text: `⚠ ${request.warningCount}` })
|
|
89
|
+
: document.createTextNode(""),
|
|
90
|
+
]),
|
|
91
|
+
],
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function showRequest(id) {
|
|
96
|
+
try {
|
|
97
|
+
state.details.set(id, await api(`api/requests/${encodeURIComponent(id)}`));
|
|
98
|
+
state.selectedRequest = id;
|
|
99
|
+
render();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
app.replaceChildren(errorView(error));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function waterfall(request) {
|
|
106
|
+
const entries = [
|
|
107
|
+
...request.phases.map((item) => ({
|
|
108
|
+
label: item.name,
|
|
109
|
+
at: item.startedAt,
|
|
110
|
+
duration: item.durationMs,
|
|
111
|
+
})),
|
|
112
|
+
...request.queries.map((item) => ({
|
|
113
|
+
label: sql(item).replace(/\s+/g, " ").slice(0, 90),
|
|
114
|
+
at: item.startedAt,
|
|
115
|
+
duration: item.durationMs,
|
|
116
|
+
})),
|
|
117
|
+
...request.cache.map((item) => ({
|
|
118
|
+
label: `${item.type} ${item.key || item.keys?.join(", ") || ""}`.trim(),
|
|
119
|
+
at: item.at,
|
|
120
|
+
duration: item.durationMs || 0,
|
|
121
|
+
})),
|
|
122
|
+
];
|
|
123
|
+
if (!entries.length) return empty("No timed work recorded for this request.");
|
|
124
|
+
const duration = Math.max(request.durationMs || 0, 1);
|
|
125
|
+
const box = el("div", { class: "waterfall" });
|
|
126
|
+
entries.forEach((entry) => {
|
|
127
|
+
const start = Math.max(0, Math.min(100, ((entry.at - request.startedAt) / duration) * 100));
|
|
128
|
+
const width = Math.max(0, Math.min(100 - start, (entry.duration / duration) * 100));
|
|
129
|
+
const bar = el("span", { class: "bar" });
|
|
130
|
+
bar.style.setProperty("--start", `${start}%`);
|
|
131
|
+
bar.style.setProperty("--width", `${width}%`);
|
|
132
|
+
box.append(
|
|
133
|
+
el("div", { class: "waterfall-row" }, [
|
|
134
|
+
el("div", { class: "waterfall-label", text: entry.label }),
|
|
135
|
+
el("div", { class: "track" }, bar),
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
return box;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function queryRow(query, requestId) {
|
|
143
|
+
const details = el("div", { class: "query" });
|
|
144
|
+
const head = el("div", { class: "query-head" }, [
|
|
145
|
+
el("span", {
|
|
146
|
+
text: `${query.driver} · ${query.durationMs} ms · ${value(query.rowCount)} rows${query.error ? ` · ${query.error}` : ""}`,
|
|
147
|
+
}),
|
|
148
|
+
]);
|
|
149
|
+
if (canExplain(query) && requestId) {
|
|
150
|
+
const button = el("button", {
|
|
151
|
+
class: "explain",
|
|
152
|
+
type: "button",
|
|
153
|
+
text: "Explain",
|
|
154
|
+
onclick: async () => {
|
|
155
|
+
button.disabled = true;
|
|
156
|
+
try {
|
|
157
|
+
const result = await api(
|
|
158
|
+
`api/requests/${encodeURIComponent(requestId)}/queries/${encodeURIComponent(query.id)}/explain`,
|
|
159
|
+
{ method: "POST" },
|
|
160
|
+
);
|
|
161
|
+
details.append(el("pre", { text: JSON.stringify(result.plan, null, 2) }));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
details.append(el("p", { class: "error", text: error.message }));
|
|
164
|
+
} finally {
|
|
165
|
+
button.disabled = false;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
head.append(button);
|
|
170
|
+
}
|
|
171
|
+
details.append(head, el("pre", { text: sql(query) }));
|
|
172
|
+
if (query.bindings)
|
|
173
|
+
details.append(el("pre", { text: `bindings: ${JSON.stringify(query.bindings, null, 2)}` }));
|
|
174
|
+
return details;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requestDetail(request) {
|
|
178
|
+
const section = el("section", { class: "detail" }, [
|
|
179
|
+
el("h2", {
|
|
180
|
+
text: `${request.method} ${request.path} · ${value(request.status)} · ${value(request.durationMs)} ms`,
|
|
181
|
+
}),
|
|
182
|
+
]);
|
|
183
|
+
if (request.warnings.length) {
|
|
184
|
+
section.append(el("h3", { text: "Warnings" }));
|
|
185
|
+
request.warnings.forEach((warning) => {
|
|
186
|
+
const children = [el("span", { text: warning.message })];
|
|
187
|
+
if (warning.caller)
|
|
188
|
+
children.push(
|
|
189
|
+
document.createTextNode(" "),
|
|
190
|
+
el("a", {
|
|
191
|
+
href: `vscode://file/${warning.caller.file}:${warning.caller.line}:${warning.caller.column}`,
|
|
192
|
+
text: `${warning.caller.file}:${warning.caller.line}`,
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
section.append(el("div", { class: "warning" }, children));
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
section.append(
|
|
199
|
+
el("h3", { text: "Waterfall" }),
|
|
200
|
+
waterfall(request),
|
|
201
|
+
el("h3", { text: "Queries" }),
|
|
202
|
+
);
|
|
203
|
+
section.append(
|
|
204
|
+
request.queries.length
|
|
205
|
+
? request.queries.map((query) => queryRow(query, request.id))
|
|
206
|
+
: empty("No queries recorded."),
|
|
207
|
+
);
|
|
208
|
+
section.append(
|
|
209
|
+
el("h3", { text: "Request logs" }),
|
|
210
|
+
request.logs.length ? logsList(request.logs) : empty("No logs recorded."),
|
|
211
|
+
);
|
|
212
|
+
section.append(el("h3", { text: "Request mail" }));
|
|
213
|
+
if (request.mails.length)
|
|
214
|
+
request.mails.forEach((id) =>
|
|
215
|
+
section.append(
|
|
216
|
+
el("button", {
|
|
217
|
+
class: "row",
|
|
218
|
+
type: "button",
|
|
219
|
+
text: `Mail ${id}`,
|
|
220
|
+
onclick: () => {
|
|
221
|
+
setTab("mail");
|
|
222
|
+
showMail(id);
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
else section.append(empty("No mail captured."));
|
|
228
|
+
return section;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function requestsView() {
|
|
232
|
+
const list = el("div", { class: "panel list" });
|
|
233
|
+
try {
|
|
234
|
+
state.requests = await api("api/requests");
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return errorView(error);
|
|
237
|
+
}
|
|
238
|
+
if (!state.requests.length) list.append(empty("No requests yet — make a request to your app"));
|
|
239
|
+
else
|
|
240
|
+
state.requests.forEach((item) =>
|
|
241
|
+
list.append(requestRow(item, item.id === state.selectedRequest)),
|
|
242
|
+
);
|
|
243
|
+
const detail =
|
|
244
|
+
state.selectedRequest && state.details.get(state.selectedRequest)
|
|
245
|
+
? requestDetail(state.details.get(state.selectedRequest))
|
|
246
|
+
: el("div", { class: "panel" }, empty("Select a request to inspect it."));
|
|
247
|
+
return el("div", { class: "layout" }, [list, detail]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function queriesView() {
|
|
251
|
+
const requests = [...state.details.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
252
|
+
// This session-only tab intentionally lists queries from request details fetched so far.
|
|
253
|
+
const items = requests
|
|
254
|
+
.flatMap((request) => request.queries.map((query) => ({ query, request })))
|
|
255
|
+
.sort((a, b) => b.query.startedAt - a.query.startedAt);
|
|
256
|
+
return el("section", { class: "card detail" }, [
|
|
257
|
+
el("h2", { text: "Queries from inspected requests" }),
|
|
258
|
+
items.length
|
|
259
|
+
? items.map(({ query, request }) => queryRow(query, request.id))
|
|
260
|
+
: empty("Select requests to collect their queries in this session."),
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function mailDetail(mail) {
|
|
265
|
+
const section = el("section", { class: "detail" }, [
|
|
266
|
+
el("h2", { text: mail.subject || "(No subject)" }),
|
|
267
|
+
]);
|
|
268
|
+
const table = el("table");
|
|
269
|
+
const body = el("tbody");
|
|
270
|
+
[
|
|
271
|
+
["From", mail.from],
|
|
272
|
+
["To", mail.to?.join(", ")],
|
|
273
|
+
["Cc", mail.cc?.join(", ")],
|
|
274
|
+
["Bcc", mail.bcc?.join(", ")],
|
|
275
|
+
].forEach(([name, item]) =>
|
|
276
|
+
body.append(el("tr", {}, [el("th", { text: name }), el("td", { text: item || "—" })])),
|
|
277
|
+
);
|
|
278
|
+
Object.entries(mail.headers || {}).forEach(([name, item]) =>
|
|
279
|
+
body.append(el("tr", {}, [el("th", { text: name }), el("td", { text: item })])),
|
|
280
|
+
);
|
|
281
|
+
table.append(body);
|
|
282
|
+
section.append(table);
|
|
283
|
+
const content = el("div");
|
|
284
|
+
const htmlButton = el("button", { class: "toggle is-active", type: "button", text: "HTML" });
|
|
285
|
+
const textButton = el("button", { class: "toggle", type: "button", text: "Text" });
|
|
286
|
+
const show = (html) => {
|
|
287
|
+
htmlButton.classList.toggle("is-active", html);
|
|
288
|
+
textButton.classList.toggle("is-active", !html);
|
|
289
|
+
content.replaceChildren(
|
|
290
|
+
html
|
|
291
|
+
? el("iframe", {
|
|
292
|
+
class: "mail-frame",
|
|
293
|
+
sandbox: "",
|
|
294
|
+
src: `api/mails/${encodeURIComponent(mail.id)}/html`,
|
|
295
|
+
title: "HTML email body",
|
|
296
|
+
})
|
|
297
|
+
: el("pre", { text: mail.text || "No plain-text body." }),
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
htmlButton.addEventListener("click", () => show(true));
|
|
301
|
+
textButton.addEventListener("click", () => show(false));
|
|
302
|
+
section.append(
|
|
303
|
+
el("h3", { text: "Body" }),
|
|
304
|
+
el("div", { class: "toolbar" }, [htmlButton, textButton]),
|
|
305
|
+
content,
|
|
306
|
+
);
|
|
307
|
+
show(true);
|
|
308
|
+
section.append(el("h3", { text: "Attachments" }));
|
|
309
|
+
section.append(
|
|
310
|
+
mail.attachments?.length
|
|
311
|
+
? el(
|
|
312
|
+
"ul",
|
|
313
|
+
{ class: "attachments" },
|
|
314
|
+
mail.attachments.map((file) =>
|
|
315
|
+
el("li", {
|
|
316
|
+
text: `${file.filename} · ${file.contentType || "unknown type"} · ${value(file.size)} bytes`,
|
|
317
|
+
}),
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
: empty("No attachments."),
|
|
321
|
+
);
|
|
322
|
+
return section;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function showMail(id) {
|
|
326
|
+
try {
|
|
327
|
+
state.selectedMail = await api(`api/mails/${encodeURIComponent(id)}`);
|
|
328
|
+
render();
|
|
329
|
+
} catch (error) {
|
|
330
|
+
app.replaceChildren(errorView(error));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function mailView() {
|
|
334
|
+
try {
|
|
335
|
+
state.mails = await api("api/mails");
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return errorView(error);
|
|
338
|
+
}
|
|
339
|
+
const list = el("div", { class: "panel list" });
|
|
340
|
+
list.append(
|
|
341
|
+
state.mails.length
|
|
342
|
+
? state.mails.map((mail) =>
|
|
343
|
+
el(
|
|
344
|
+
"button",
|
|
345
|
+
{
|
|
346
|
+
class: `row${state.selectedMail?.id === mail.id ? " is-selected" : ""}`,
|
|
347
|
+
type: "button",
|
|
348
|
+
onclick: () => showMail(mail.id),
|
|
349
|
+
},
|
|
350
|
+
[
|
|
351
|
+
el("div", { class: "path", text: mail.subject || "(No subject)" }),
|
|
352
|
+
el("div", {
|
|
353
|
+
class: "meta",
|
|
354
|
+
text: `${mail.to.join(", ")} · ${time(mail.capturedAt)}`,
|
|
355
|
+
}),
|
|
356
|
+
],
|
|
357
|
+
),
|
|
358
|
+
)
|
|
359
|
+
: empty("No mail captured."),
|
|
360
|
+
);
|
|
361
|
+
return el("div", { class: "layout" }, [
|
|
362
|
+
list,
|
|
363
|
+
state.selectedMail
|
|
364
|
+
? mailDetail(state.selectedMail)
|
|
365
|
+
: el("div", { class: "panel" }, empty("Select a message to inspect it.")),
|
|
366
|
+
]);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function logsList(logs) {
|
|
370
|
+
const box = el("div");
|
|
371
|
+
logs.forEach((log) => {
|
|
372
|
+
const row = el("div", { class: "query" }, [
|
|
373
|
+
el("div", { class: "query-head" }, [
|
|
374
|
+
el("span", { class: `log-level log-${log.level}`, text: log.level }),
|
|
375
|
+
el("span", { text: `${log.module} · ${log.action} · ${time(log.at)}` }),
|
|
376
|
+
]),
|
|
377
|
+
el("div", { text: log.message }),
|
|
378
|
+
]);
|
|
379
|
+
if (log.context !== undefined)
|
|
380
|
+
row.append(
|
|
381
|
+
el("details", {}, [
|
|
382
|
+
el("summary", { text: "Context" }),
|
|
383
|
+
el("pre", { text: JSON.stringify(log.context, null, 2) }),
|
|
384
|
+
]),
|
|
385
|
+
);
|
|
386
|
+
box.append(row);
|
|
387
|
+
});
|
|
388
|
+
return box;
|
|
389
|
+
}
|
|
390
|
+
async function logsView() {
|
|
391
|
+
const section = el("section", { class: "card detail" }, [el("h2", { text: "Logs" })]);
|
|
392
|
+
const level = el("select");
|
|
393
|
+
["", "debug", "info", "warn", "error", "success"].forEach((name) =>
|
|
394
|
+
level.append(el("option", { value: name, text: name || "All levels" })),
|
|
395
|
+
);
|
|
396
|
+
const module = el("input", { placeholder: "Filter module" });
|
|
397
|
+
const output = el("div");
|
|
398
|
+
const load = async () => {
|
|
399
|
+
try {
|
|
400
|
+
const params = new URLSearchParams();
|
|
401
|
+
if (level.value) params.set("level", level.value);
|
|
402
|
+
if (module.value) params.set("module", module.value);
|
|
403
|
+
state.logs = await api(`api/logs?${params}`);
|
|
404
|
+
output.replaceChildren(state.logs.length ? logsList(state.logs) : empty("No logs found."));
|
|
405
|
+
} catch (error) {
|
|
406
|
+
output.replaceChildren(errorView(error));
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
level.addEventListener("change", load);
|
|
410
|
+
module.addEventListener("input", load);
|
|
411
|
+
section.append(el("div", { class: "toolbar" }, [level, module]), output);
|
|
412
|
+
await load();
|
|
413
|
+
return section;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function routesView() {
|
|
417
|
+
try {
|
|
418
|
+
state.routes = await api("api/routes");
|
|
419
|
+
} catch (error) {
|
|
420
|
+
return errorView(error);
|
|
421
|
+
}
|
|
422
|
+
const section = el("section", { class: "card detail" }, [el("h2", { text: "Routes" })]);
|
|
423
|
+
const filter = el("input", { placeholder: "Filter routes" });
|
|
424
|
+
const output = el("div");
|
|
425
|
+
const draw = () => {
|
|
426
|
+
const needle = filter.value.toLowerCase();
|
|
427
|
+
const routes = state.routes.filter((route) =>
|
|
428
|
+
`${route.method} ${route.path} ${route.name || ""} ${(route.middleware || []).join(" ")}`
|
|
429
|
+
.toLowerCase()
|
|
430
|
+
.includes(needle),
|
|
431
|
+
);
|
|
432
|
+
if (!routes.length) {
|
|
433
|
+
output.replaceChildren(empty("No routes found."));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const table = el("table");
|
|
437
|
+
table.append(
|
|
438
|
+
el(
|
|
439
|
+
"thead",
|
|
440
|
+
{},
|
|
441
|
+
el(
|
|
442
|
+
"tr",
|
|
443
|
+
{},
|
|
444
|
+
["Method", "Path", "Name", "Middleware"].map((name) => el("th", { text: name })),
|
|
445
|
+
),
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
table.append(
|
|
449
|
+
el(
|
|
450
|
+
"tbody",
|
|
451
|
+
{},
|
|
452
|
+
routes.map((route) =>
|
|
453
|
+
el("tr", {}, [
|
|
454
|
+
el("td", { text: route.method }),
|
|
455
|
+
el("td", { text: route.path }),
|
|
456
|
+
el("td", { text: route.name || "—" }),
|
|
457
|
+
el("td", { text: (route.middleware || []).join(", ") || "—" }),
|
|
458
|
+
]),
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
output.replaceChildren(table);
|
|
463
|
+
};
|
|
464
|
+
filter.addEventListener("input", draw);
|
|
465
|
+
section.append(el("div", { class: "toolbar" }, filter), output);
|
|
466
|
+
draw();
|
|
467
|
+
return section;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function render() {
|
|
471
|
+
try {
|
|
472
|
+
const view =
|
|
473
|
+
state.tab === "requests"
|
|
474
|
+
? await requestsView()
|
|
475
|
+
: state.tab === "queries"
|
|
476
|
+
? queriesView()
|
|
477
|
+
: state.tab === "mail"
|
|
478
|
+
? await mailView()
|
|
479
|
+
: state.tab === "logs"
|
|
480
|
+
? await logsView()
|
|
481
|
+
: await routesView();
|
|
482
|
+
app.replaceChildren(view);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
app.replaceChildren(errorView(error));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
tabs.forEach((button) => button.addEventListener("click", () => setTab(button.dataset.tab)));
|
|
489
|
+
document.querySelector("#clear").addEventListener("click", async () => {
|
|
490
|
+
try {
|
|
491
|
+
await api("api/requests", { method: "DELETE" });
|
|
492
|
+
state.requests = [];
|
|
493
|
+
state.details.clear();
|
|
494
|
+
state.selectedRequest = undefined;
|
|
495
|
+
render();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
app.prepend(errorView(error));
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
const events = new EventSource("api/stream");
|
|
501
|
+
for (const type of ["request", "mail", "log"])
|
|
502
|
+
events.addEventListener(type, (event) => {
|
|
503
|
+
try {
|
|
504
|
+
const data = JSON.parse(event.data);
|
|
505
|
+
if (type === "request") {
|
|
506
|
+
state.requests = [data, ...state.requests.filter((item) => item.id !== data.id)].slice(
|
|
507
|
+
0,
|
|
508
|
+
200,
|
|
509
|
+
);
|
|
510
|
+
if (state.tab === "requests") render();
|
|
511
|
+
}
|
|
512
|
+
if (type === "mail") {
|
|
513
|
+
state.mails = [data, ...state.mails.filter((item) => item.id !== data.id)].slice(0, 200);
|
|
514
|
+
}
|
|
515
|
+
if (type === "log") state.logs = [data, ...state.logs].slice(0, 200);
|
|
516
|
+
} catch {
|
|
517
|
+
/* malformed stream data is ignored */
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
render();
|
package/ui/index.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Warlock Devtools</title>
|
|
7
|
+
<link rel="stylesheet" href="assets/app.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="topbar">
|
|
11
|
+
<h1>Warlock Devtools</h1>
|
|
12
|
+
<nav aria-label="Dashboard sections">
|
|
13
|
+
<button class="tab is-active" data-tab="requests">Requests</button>
|
|
14
|
+
<button class="tab" data-tab="queries">Queries</button>
|
|
15
|
+
<button class="tab" data-tab="mail">Mail</button>
|
|
16
|
+
<button class="tab" data-tab="logs">Logs</button>
|
|
17
|
+
<button class="tab" data-tab="routes">Routes</button>
|
|
18
|
+
</nav>
|
|
19
|
+
<button id="clear" class="clear" type="button">Clear</button>
|
|
20
|
+
</header>
|
|
21
|
+
<main id="app" aria-live="polite"></main>
|
|
22
|
+
<script type="module" src="assets/app.js"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|