sql-dev-inspector 0.0.0-stage → 1.0.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 +3 -0
- package/LICENSE +21 -0
- package/README.VI.md +341 -0
- package/README.md +267 -2
- package/dist/client/nuxt-plugin.client.d.ts +2 -0
- package/dist/client/nuxt-plugin.client.js +11 -0
- package/dist/client/widget.d.ts +40 -0
- package/dist/client/widget.js +485 -0
- package/dist/i18n/index.d.ts +26 -0
- package/dist/i18n/index.js +48 -0
- package/dist/i18n/messages.d.ts +119 -0
- package/dist/i18n/messages.js +331 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/inspector-server.d.ts +11 -0
- package/dist/inspector-server.js +95 -0
- package/dist/nitro-plugin.d.ts +19 -0
- package/dist/nitro-plugin.js +30 -0
- package/dist/node.d.ts +27 -0
- package/dist/node.js +52 -0
- package/dist/nuxt.d.ts +20 -0
- package/dist/nuxt.js +21 -0
- package/dist/report.d.ts +24 -0
- package/dist/report.js +182 -0
- package/dist/runtime.d.ts +21 -0
- package/dist/runtime.js +42 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +44 -0
- package/dist/sql-analyzer.d.ts +12 -0
- package/dist/sql-analyzer.js +214 -0
- package/dist/store.d.ts +36 -0
- package/dist/store.js +86 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.js +64 -0
- package/dist/tracer.d.ts +36 -0
- package/dist/tracer.js +143 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +1 -0
- package/dist/ui/client.d.ts +1 -0
- package/dist/ui/client.js +472 -0
- package/dist/ui/page.d.ts +6 -0
- package/dist/ui/page.js +73 -0
- package/dist/ui/styles.d.ts +7 -0
- package/dist/ui/styles.js +207 -0
- package/docs/PUBLISH-AND-INSTALL.md +267 -0
- package/package.json +80 -4
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// The inspector page's script, as a string: it runs in the browser, not in Node, and must stay
|
|
2
|
+
// dependency-free. `String.raw` keeps regex backslashes intact; keep backticks and dollar-braces out of it.
|
|
3
|
+
export const CLIENT_SCRIPT = String.raw `
|
|
4
|
+
(function () {
|
|
5
|
+
"use strict";
|
|
6
|
+
var CFG = __SQLI__;
|
|
7
|
+
var MESSAGES = CFG.messages, LOCALES = CFG.locales;
|
|
8
|
+
var KEY_LANG = "sqlDevInspector.lang", KEY_THEME = "sqlDevInspector.theme", KEY_FOLLOW = "sqlDevInspector.follow";
|
|
9
|
+
|
|
10
|
+
// ── tiny helpers ───────────────────────────────────────────────────
|
|
11
|
+
function $(id) { return document.getElementById(id); }
|
|
12
|
+
var storage = {
|
|
13
|
+
get: function (k) { try { return localStorage.getItem(k); } catch (e) { return null; } },
|
|
14
|
+
set: function (k, v) { try { localStorage.setItem(k, v); } catch (e) { /* storage may be blocked */ } }
|
|
15
|
+
};
|
|
16
|
+
function h(tag, props, kids) {
|
|
17
|
+
var el = document.createElement(tag);
|
|
18
|
+
if (props) {
|
|
19
|
+
Object.keys(props).forEach(function (k) {
|
|
20
|
+
var v = props[k];
|
|
21
|
+
if (v === null || v === undefined || v === false) { return; }
|
|
22
|
+
if (k === "class") { el.className = v; }
|
|
23
|
+
else if (k === "text") { el.textContent = v; }
|
|
24
|
+
else if (k.slice(0, 2) === "on") { el.addEventListener(k.slice(2), v); }
|
|
25
|
+
else { el.setAttribute(k, v === true ? "" : String(v)); }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
(kids || []).forEach(function (c) {
|
|
29
|
+
if (c === null || c === undefined || c === false) { return; }
|
|
30
|
+
el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
|
|
31
|
+
});
|
|
32
|
+
return el;
|
|
33
|
+
}
|
|
34
|
+
var ICONS = {
|
|
35
|
+
db: "<svg viewBox='0 0 24 24'><ellipse cx='12' cy='5' rx='8' ry='3'/><path d='M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6'/></svg>",
|
|
36
|
+
search: "<svg viewBox='0 0 24 24'><circle cx='11' cy='11' r='7'/><path d='m20 20-3.5-3.5'/></svg>",
|
|
37
|
+
chev: "<svg viewBox='0 0 24 24'><path d='m9 6 6 6-6 6'/></svg>",
|
|
38
|
+
down: "<svg viewBox='0 0 24 24'><path d='m6 9 6 6 6-6'/></svg>",
|
|
39
|
+
back: "<svg viewBox='0 0 24 24'><path d='m15 6-6 6 6 6'/></svg>",
|
|
40
|
+
trash: "<svg viewBox='0 0 24 24'><path d='M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3'/></svg>",
|
|
41
|
+
copy: "<svg viewBox='0 0 24 24'><rect x='9' y='9' width='11' height='11' rx='2'/><path d='M5 15V6a2 2 0 0 1 2-2h9'/></svg>",
|
|
42
|
+
ok: "<svg viewBox='0 0 24 24'><circle cx='12' cy='12' r='9'/><path d='m8 12.5 2.8 2.8L16 9.5'/></svg>",
|
|
43
|
+
warn: "<svg viewBox='0 0 24 24'><path d='M12 3 2.5 20h19L12 3Z'/><path d='M12 10v4M12 17.2v.1'/></svg>",
|
|
44
|
+
error: "<svg viewBox='0 0 24 24'><circle cx='12' cy='12' r='9'/><path d='m9 9 6 6M15 9l-6 6'/></svg>",
|
|
45
|
+
info: "<svg viewBox='0 0 24 24'><circle cx='12' cy='12' r='9'/><path d='M12 11v5M12 7.8v.1'/></svg>",
|
|
46
|
+
layers: "<svg viewBox='0 0 24 24'><path d='m12 3 9 5-9 5-9-5 9-5Z'/><path d='m3 13 9 5 9-5'/></svg>",
|
|
47
|
+
flag: "<svg viewBox='0 0 24 24'><path d='M5 21V4M5 4h11l-2 4 2 4H5'/></svg>",
|
|
48
|
+
sun: "<svg viewBox='0 0 24 24'><circle cx='12' cy='12' r='4'/><path d='M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4'/></svg>",
|
|
49
|
+
moon: "<svg viewBox='0 0 24 24'><path d='M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z'/></svg>",
|
|
50
|
+
monitor: "<svg viewBox='0 0 24 24'><rect x='3' y='4' width='18' height='12' rx='2'/><path d='M8 20h8M12 16v4'/></svg>",
|
|
51
|
+
globe: "<svg viewBox='0 0 24 24'><circle cx='12' cy='12' r='9'/><path d='M3 12h18M12 3c2.5 2.7 3.8 5.7 3.8 9s-1.3 6.3-3.8 9c-2.5-2.7-3.8-5.7-3.8-9S9.5 5.7 12 3Z'/></svg>",
|
|
52
|
+
check: "<svg viewBox='0 0 24 24'><path d='m5 12.5 4.5 4.5L19 7.5'/></svg>",
|
|
53
|
+
inbox: "<svg viewBox='0 0 24 24'><path d='M3 13h5l1.5 3h5L16 13h5'/><path d='M5 5h14l2 8v6H3v-6l2-8Z'/></svg>"
|
|
54
|
+
};
|
|
55
|
+
function icon(name) { var s = document.createElement("span"); s.innerHTML = ICONS[name] || ""; return s.firstChild; }
|
|
56
|
+
|
|
57
|
+
// ── i18n ──────────────────────────────────────────────────────────
|
|
58
|
+
function matchLocale(tag) {
|
|
59
|
+
var m = /^([a-z]{2,3})/i.exec(tag || "");
|
|
60
|
+
var code = m && m[1].toLowerCase();
|
|
61
|
+
for (var i = 0; i < LOCALES.length; i++) { if (LOCALES[i].code === code) { return code; } }
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function initialLocale() {
|
|
65
|
+
var fromQuery = matchLocale(new URLSearchParams(location.search).get("lang"));
|
|
66
|
+
var langs = (navigator.languages && navigator.languages.length) ? navigator.languages : [navigator.language];
|
|
67
|
+
var fromBrowser = null;
|
|
68
|
+
for (var i = 0; i < langs.length && !fromBrowser; i++) { fromBrowser = matchLocale(langs[i]); }
|
|
69
|
+
return fromQuery || matchLocale(storage.get(KEY_LANG)) || CFG.hint || fromBrowser || "en";
|
|
70
|
+
}
|
|
71
|
+
var locale = initialLocale();
|
|
72
|
+
function t(key, params) {
|
|
73
|
+
var dict = MESSAGES[locale] || {};
|
|
74
|
+
var one = params && String(params.count) === "1" ? (dict[key + ".one"] || MESSAGES.en[key + ".one"]) : null;
|
|
75
|
+
var s = one || dict[key] || MESSAGES.en[key] || key;
|
|
76
|
+
return s.replace(/\{(\w+)\}/g, function (whole, name) { return params && name in params ? String(params[name]) : whole; });
|
|
77
|
+
}
|
|
78
|
+
function num(n, digits) { try { return new Intl.NumberFormat(locale, { maximumFractionDigits: digits }).format(n); } catch (e) { return String(n); } }
|
|
79
|
+
function fmtMs(v) {
|
|
80
|
+
if (v === null || v === undefined) { return "…"; }
|
|
81
|
+
if (v < 1) { return num(v, 2) + " ms"; }
|
|
82
|
+
if (v < 10) { return num(v, 2) + " ms"; }
|
|
83
|
+
if (v < 1000) { return num(v, 1) + " ms"; }
|
|
84
|
+
return num(v / 1000, 2) + " s";
|
|
85
|
+
}
|
|
86
|
+
function fmtTime(ts) { try { return new Date(ts).toLocaleTimeString(locale); } catch (e) { return ""; } }
|
|
87
|
+
|
|
88
|
+
// ── state ─────────────────────────────────────────────────────────
|
|
89
|
+
var state = {
|
|
90
|
+
requests: [], selectedId: null, report: null, term: "", mode: "all",
|
|
91
|
+
follow: location.hash === "#latest" || storage.get(KEY_FOLLOW) === "1",
|
|
92
|
+
open: {}, collapsed: {}, highlight: [], expandAll: false, rawSql: {},
|
|
93
|
+
theme: storage.get(KEY_THEME) || "auto", live: false, loadToken: 0
|
|
94
|
+
};
|
|
95
|
+
var appEl = $("app"), listBody = $("list-body"), detailEl = $("detail");
|
|
96
|
+
|
|
97
|
+
// ── theme / language ──────────────────────────────────────────────
|
|
98
|
+
function applyTheme() {
|
|
99
|
+
if (state.theme === "light" || state.theme === "dark") { document.documentElement.setAttribute("data-theme", state.theme); }
|
|
100
|
+
else { document.documentElement.removeAttribute("data-theme"); }
|
|
101
|
+
var btn = $("theme"); btn.textContent = "";
|
|
102
|
+
btn.appendChild(icon(state.theme === "dark" ? "moon" : state.theme === "light" ? "sun" : "monitor"));
|
|
103
|
+
var label = t("theme.label") + ": " + t("theme." + state.theme);
|
|
104
|
+
btn.setAttribute("title", label); btn.setAttribute("aria-label", label);
|
|
105
|
+
}
|
|
106
|
+
function applyI18n() {
|
|
107
|
+
document.documentElement.lang = locale;
|
|
108
|
+
document.title = t("app.title");
|
|
109
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-i18n]"), function (el) { el.textContent = t(el.getAttribute("data-i18n")); });
|
|
110
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-i18n-title]"), function (el) { el.setAttribute("title", t(el.getAttribute("data-i18n-title"))); });
|
|
111
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-i18n-placeholder]"), function (el) { el.setAttribute("placeholder", t(el.getAttribute("data-i18n-placeholder"))); });
|
|
112
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-i18n-aria]"), function (el) { el.setAttribute("aria-label", t(el.getAttribute("data-i18n-aria"))); });
|
|
113
|
+
updateLangMenu();
|
|
114
|
+
applyTheme(); renderLive();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── SQL highlighting (DOM nodes only: SQL text is never parsed as HTML) ──
|
|
118
|
+
var KEYWORDS = {};
|
|
119
|
+
("select from where and or not in is null like between join left right inner outer cross on group by order having limit offset union all distinct as insert into values update set delete exists case when then else end asc desc with begin commit rollback savepoint release start transaction for true false default duplicate key lateral using interval to").split(" ").forEach(function (w) { KEYWORDS[w] = true; });
|
|
120
|
+
var TOKEN = /('(?:[^'\\]|\\.|'')*'|"(?:[^"\\]|\\.|"")*")|(` + "`" + String.raw `[^` + "`" + String.raw `]*` + "`" + String.raw `)|(\/\*[\s\S]*?\*\/|--[^\n]*)|(\b\d+(?:\.\d+)?\b)|([A-Za-z_][A-Za-z0-9_]*)|(\s+|[\s\S])/g;
|
|
121
|
+
function highlight(sql) {
|
|
122
|
+
var frag = document.createDocumentFragment(), m;
|
|
123
|
+
TOKEN.lastIndex = 0;
|
|
124
|
+
while ((m = TOKEN.exec(sql)) !== null) {
|
|
125
|
+
var text = m[0], cls = null;
|
|
126
|
+
if (m[1]) { cls = "str"; } else if (m[2]) { cls = "id"; } else if (m[3]) { cls = "com"; } else if (m[4]) { cls = "num"; }
|
|
127
|
+
else if (m[5]) {
|
|
128
|
+
if (KEYWORDS[text.toLowerCase()]) { cls = "kw"; }
|
|
129
|
+
else if (sql.charAt(m.index + text.length) === "(") { cls = "fn"; }
|
|
130
|
+
}
|
|
131
|
+
frag.appendChild(cls ? h("span", { class: cls, text: text }) : document.createTextNode(text));
|
|
132
|
+
}
|
|
133
|
+
return frag;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── derived data ──────────────────────────────────────────────────
|
|
137
|
+
function isControl(kind) { return /^(tx-|savepoint)/.test(kind); }
|
|
138
|
+
function countNodes(nodes, type) {
|
|
139
|
+
var n = 0;
|
|
140
|
+
nodes.forEach(function (node) { if (node.type === type) { n++; } if (node.children) { n += countNodes(node.children, type); } });
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
function visibleRequests() {
|
|
144
|
+
var term = state.term.trim().toLowerCase();
|
|
145
|
+
return state.requests.filter(function (r) {
|
|
146
|
+
if (state.mode === "issues" && !(r.warnings || r.errors)) { return false; }
|
|
147
|
+
if (state.mode === "errors" && !r.errors) { return false; }
|
|
148
|
+
return !term || (r.method + " " + r.url + " " + (r.status === null ? "" : r.status)).toLowerCase().indexOf(term) >= 0;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function urlLabel(r) { return r.id === "background" ? t("requests.background") : r.url; }
|
|
152
|
+
|
|
153
|
+
// ── request list ──────────────────────────────────────────────────
|
|
154
|
+
function statusEl(status) {
|
|
155
|
+
return h("span", { class: "status" + (status ? " s" + String(status).charAt(0) : ""), text: status === null ? "…" : String(status) });
|
|
156
|
+
}
|
|
157
|
+
function renderList() {
|
|
158
|
+
var rows = visibleRequests();
|
|
159
|
+
$("count").textContent = String(state.requests.length);
|
|
160
|
+
listBody.textContent = "";
|
|
161
|
+
if (!state.requests.length) {
|
|
162
|
+
listBody.appendChild(h("div", { class: "empty" }, [icon("inbox"), h("h3", { text: t("requests.empty.title") }), h("p", { text: t("requests.empty.body") })]));
|
|
163
|
+
listBody.firstChild.querySelector("svg").setAttribute("class", "big");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!rows.length) { listBody.appendChild(h("div", { class: "empty" }, [h("p", { text: t("requests.noMatch") })])); return; }
|
|
167
|
+
rows.forEach(function (r) {
|
|
168
|
+
var meta = [
|
|
169
|
+
h("span", { text: t("term.queries", { count: r.queryCount }) }),
|
|
170
|
+
h("i", { class: "dot" }),
|
|
171
|
+
h("span", { text: fmtMs(r.sqlTimeMs) }),
|
|
172
|
+
h("i", { class: "dot" }),
|
|
173
|
+
h("span", { text: fmtTime(r.startedAt) })
|
|
174
|
+
];
|
|
175
|
+
if (r.errors) { meta.push(h("span", { class: "chip err", text: String(r.errors) }, [])); }
|
|
176
|
+
if (r.warnings) { meta.push(h("span", { class: "chip warn", text: String(r.warnings) }, [])); }
|
|
177
|
+
var row = h("button", {
|
|
178
|
+
class: "req" + (r.warnings ? " has-warn" : "") + (r.errors ? " has-err" : ""),
|
|
179
|
+
role: "option", "aria-selected": r.id === state.selectedId ? "true" : "false", "data-id": r.id,
|
|
180
|
+
onclick: function () { select(r.id); }
|
|
181
|
+
}, [
|
|
182
|
+
h("div", { class: "req-top" }, [h("span", { class: "method " + r.method, text: r.method }), statusEl(r.status), h("span", { class: "req-url", title: urlLabel(r), text: urlLabel(r) })]),
|
|
183
|
+
h("div", { class: "req-meta" }, meta)
|
|
184
|
+
]);
|
|
185
|
+
listBody.appendChild(row);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── detail ────────────────────────────────────────────────────────
|
|
190
|
+
function kpi(label, value, sub) {
|
|
191
|
+
return h("div", { class: "kpi" }, [h("div", { class: "kpi-label", text: label }), h("div", { class: "kpi-value", text: value }), sub ? h("div", { class: "kpi-sub", text: sub }) : null]);
|
|
192
|
+
}
|
|
193
|
+
function renderDetail() {
|
|
194
|
+
var keep = detailEl.scrollTop;
|
|
195
|
+
detailEl.textContent = "";
|
|
196
|
+
var rep = state.report;
|
|
197
|
+
if (!rep) {
|
|
198
|
+
detailEl.appendChild(h("div", { class: "empty" }, [icon("layers"), h("h3", { text: t("detail.empty.title") }), h("p", { text: t("detail.empty.body") })]));
|
|
199
|
+
detailEl.firstChild.querySelector("svg").setAttribute("class", "big");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
var tr = rep.trace, tot = rep.totals;
|
|
203
|
+
var share = tr.durationMs ? Math.min(100, Math.round(tot.sqlTimeMs / tr.durationMs * 100)) : null;
|
|
204
|
+
var inner = h("div", { class: "detail-inner" });
|
|
205
|
+
inner.appendChild(h("div", { class: "detail-head" }, [
|
|
206
|
+
h("button", { class: "btn sm back", onclick: function () { appEl.setAttribute("data-view", "list"); } }, [icon("back"), h("span", { text: t("detail.back") })]),
|
|
207
|
+
h("span", { class: "method " + tr.method, text: tr.method }),
|
|
208
|
+
h("h1", { text: tr.id === "background" ? t("requests.background") : tr.url }),
|
|
209
|
+
statusEl(tr.status)
|
|
210
|
+
]));
|
|
211
|
+
inner.appendChild(h("div", { class: "kpis" }, [
|
|
212
|
+
kpi(t("kpi.total"), fmtMs(tr.durationMs)),
|
|
213
|
+
kpi(t("kpi.queries"), String(tot.queryCount)),
|
|
214
|
+
kpi(t("kpi.sqlTime"), fmtMs(tot.sqlTimeMs), share === null ? "" : t("kpi.share", { percent: share })),
|
|
215
|
+
kpi(t("kpi.transactions"), String(countNodes(rep.tree, "transaction"))),
|
|
216
|
+
kpi(t("kpi.depth"), String(tot.maxSubqueryDepth))
|
|
217
|
+
]));
|
|
218
|
+
|
|
219
|
+
// insights
|
|
220
|
+
var slowSeqs = {};
|
|
221
|
+
rep.insights.forEach(function (i) { if (i.type === "slow") { i.seqs.forEach(function (s) { slowSeqs[s] = true; }); } });
|
|
222
|
+
var insightsCard = h("div", { class: "card" });
|
|
223
|
+
if (!rep.insights.length) { insightsCard.appendChild(h("div", { class: "empty-ok" }, [icon("ok"), h("span", { text: t("insights.none") })])); }
|
|
224
|
+
rep.insights.forEach(function (i) {
|
|
225
|
+
var ico = i.severity === "error" ? "error" : i.severity === "warn" ? "warn" : "info";
|
|
226
|
+
insightsCard.appendChild(h("button", { class: "insight " + i.severity, onclick: function () { focusSeqs(i.seqs); } }, [
|
|
227
|
+
h("span", { class: "ico" }, [icon(ico)]),
|
|
228
|
+
h("div", {}, [h("div", { class: "insight-title", text: t("insight.title." + i.type) }), h("div", { class: "insight-msg", text: t("insight.msg." + i.type, i.params) })])
|
|
229
|
+
]));
|
|
230
|
+
});
|
|
231
|
+
inner.appendChild(h("section", { class: "section" }, [
|
|
232
|
+
h("div", { class: "section-head" }, [h("h2", { text: t("insights.title") }), h("span", { class: "count", text: String(rep.insights.length) })]),
|
|
233
|
+
insightsCard
|
|
234
|
+
]));
|
|
235
|
+
|
|
236
|
+
// tree
|
|
237
|
+
var span = Math.max(tr.durationMs || 0, 1);
|
|
238
|
+
tr.queries.forEach(function (q) { span = Math.max(span, q.startedAt - tr.startedAt + (q.durationMs || 0)); });
|
|
239
|
+
var ctx = { t0: tr.startedAt, span: span, slow: slowSeqs, rep: rep };
|
|
240
|
+
inner.appendChild(h("section", { class: "section" }, [
|
|
241
|
+
h("div", { class: "section-head" }, [
|
|
242
|
+
h("h2", { text: t("tree.title") }), h("span", { class: "spacer" }),
|
|
243
|
+
h("button", { class: "btn sm", onclick: function () { setExpandAll(true); }, text: t("tree.expandAll") }),
|
|
244
|
+
h("button", { class: "btn sm", onclick: function () { setExpandAll(false); }, text: t("tree.collapseAll") })
|
|
245
|
+
]),
|
|
246
|
+
tr.droppedQueries ? h("div", { class: "dim", style: "margin-bottom:8px", text: t("tree.dropped", { count: tr.droppedQueries }) }) : null,
|
|
247
|
+
h("div", { class: "axis" }, [h("span", { text: "0" }), h("span", { text: fmtMs(span) })]),
|
|
248
|
+
treeNodes(rep.tree, ctx)
|
|
249
|
+
]));
|
|
250
|
+
detailEl.appendChild(inner);
|
|
251
|
+
detailEl.scrollTop = keep;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function treeNodes(nodes, ctx) {
|
|
255
|
+
var frag = document.createDocumentFragment();
|
|
256
|
+
nodes.forEach(function (n) { frag.appendChild(n.type === "query" ? queryNode(n.query, ctx) : txNode(n, ctx)); });
|
|
257
|
+
return frag;
|
|
258
|
+
}
|
|
259
|
+
function txNode(n, ctx) {
|
|
260
|
+
var key = "tx:" + n.connectionId + ":" + n.startedAt + ":" + (n.name || "");
|
|
261
|
+
var collapsed = !!state.collapsed[key];
|
|
262
|
+
var statements = countNodes(n.children, "query");
|
|
263
|
+
var box = h("div", { class: "tx " + n.status + (collapsed ? "" : " expanded") });
|
|
264
|
+
var statusCls = n.status === "committed" ? "ok" : n.status === "rolled-back" ? "err" : "warn";
|
|
265
|
+
box.appendChild(h("button", { class: "tx-head", "aria-expanded": collapsed ? "false" : "true", onclick: function () { state.collapsed[key] = !collapsed; renderDetail(); } }, [
|
|
266
|
+
h("span", { class: "chev" }, [icon("chev")]),
|
|
267
|
+
icon(n.type === "transaction" ? "layers" : "flag"),
|
|
268
|
+
h("span", { text: n.type === "transaction" ? t("tx.transaction") : t("tx.savepoint", { name: n.name || "" }) }),
|
|
269
|
+
h("span", { class: "chip " + statusCls, text: t("status." + n.status) }),
|
|
270
|
+
h("span", { class: "dim", text: t("sql.connection", { id: n.connectionId }) + " · " + t("tx.statements", { count: statements }) }),
|
|
271
|
+
h("span", { class: "spacer" }),
|
|
272
|
+
h("span", { class: "num", text: fmtMs(n.durationMs) })
|
|
273
|
+
]));
|
|
274
|
+
if (!collapsed) { box.appendChild(h("div", { class: "tx-children" }, [treeNodes(n.children, ctx)])); }
|
|
275
|
+
return box;
|
|
276
|
+
}
|
|
277
|
+
function queryNode(q, ctx) {
|
|
278
|
+
var a = q.analysis;
|
|
279
|
+
var isOpen = state.expandAll ? state.open[q.seq] !== false : !!state.open[q.seq];
|
|
280
|
+
var box = h("div", { class: "q" + (isOpen ? " open" : "") + (q.error ? " err" : "") + (state.highlight.indexOf(q.seq) >= 0 ? " hl" : ""), "data-seq": q.seq });
|
|
281
|
+
var rows = q.rowCount !== null ? t("sql.rows", { count: num(q.rowCount) }) : q.affectedRows !== null ? t("sql.affected", { count: num(q.affectedRows) }) : "";
|
|
282
|
+
var wf = h("span", { class: "wf" }, [h("i", {
|
|
283
|
+
class: q.error ? "err" : ctx.slow[q.seq] ? "slow" : "",
|
|
284
|
+
style: "left:" + Math.min(99.5, Math.max(0, (q.startedAt - ctx.t0) / ctx.span * 100)) + "%;width:" + Math.max(0.6, (q.durationMs || 0) / ctx.span * 100) + "%"
|
|
285
|
+
})]);
|
|
286
|
+
var line = [
|
|
287
|
+
h("span", { class: "chev" }, [icon("chev")]),
|
|
288
|
+
h("span", { class: "kind " + a.kind, text: a.kind.toUpperCase() }),
|
|
289
|
+
h("span", { class: "tables", text: a.tables.join(", ") || "—" })
|
|
290
|
+
];
|
|
291
|
+
if (a.subqueryCount) { line.push(h("span", { class: "chip warn", text: t("sql.subquery", { count: a.subqueryCount, depth: a.subqueryDepth }) })); }
|
|
292
|
+
if (q.error) { line.push(h("span", { class: "chip err", text: t("insight.title.error") })); }
|
|
293
|
+
line.push(h("span", { class: "spacer" }), rows ? h("span", { class: "num", text: rows }) : null, h("span", { class: "num", text: fmtMs(q.durationMs) }));
|
|
294
|
+
box.appendChild(h("button", { class: "q-head", "aria-expanded": isOpen ? "true" : "false", onclick: function () {
|
|
295
|
+
var next = !isOpen; state.open[q.seq] = next; isOpen = next;
|
|
296
|
+
box.classList.toggle("open", next); box.firstChild.setAttribute("aria-expanded", next ? "true" : "false");
|
|
297
|
+
if (next && !body.firstChild) { fillBody(); }
|
|
298
|
+
} }, [
|
|
299
|
+
h("div", { class: "q-main" }, [h("div", { class: "q-line" }, line), h("div", { class: "preview", text: q.sql.replace(/\s+/g, " ").slice(0, 220) })]),
|
|
300
|
+
wf
|
|
301
|
+
]));
|
|
302
|
+
var body = h("div", { class: "q-body" });
|
|
303
|
+
function fillBody() {
|
|
304
|
+
body.textContent = "";
|
|
305
|
+
var raw = !!state.rawSql[q.seq];
|
|
306
|
+
var pre = h("pre", { class: "sql" });
|
|
307
|
+
pre.appendChild(highlight(raw ? q.sql : (ctx.rep.formattedSql[q.seq] || q.sql)));
|
|
308
|
+
var fmtBtn = h("button", { class: "seg-btn", "aria-pressed": raw ? "false" : "true", text: t("sql.formatted") });
|
|
309
|
+
var rawBtn = h("button", { class: "seg-btn", "aria-pressed": raw ? "true" : "false", text: t("sql.raw") });
|
|
310
|
+
function setRaw(v) { state.rawSql[q.seq] = v; fillBody(); }
|
|
311
|
+
fmtBtn.addEventListener("click", function () { setRaw(false); });
|
|
312
|
+
rawBtn.addEventListener("click", function () { setRaw(true); });
|
|
313
|
+
body.appendChild(h("div", { class: "q-tools" }, [
|
|
314
|
+
h("span", { class: "seg" }, [fmtBtn, rawBtn]),
|
|
315
|
+
h("span", { text: t("sql.connection", { id: q.connectionId }) + " · " + q.api + " · " + t("sql.startedAt", { ms: fmtMs(Math.max(0, q.startedAt - ctx.t0)) }) }),
|
|
316
|
+
h("span", { class: "spacer" }),
|
|
317
|
+
h("button", { class: "btn sm", onclick: function () { copy(q.sql); } }, [icon("copy"), h("span", { text: t("sql.copy") })])
|
|
318
|
+
]));
|
|
319
|
+
body.appendChild(pre);
|
|
320
|
+
if (q.params && q.params.length) { body.appendChild(h("div", { class: "params", text: t("sql.params") + ": " + JSON.stringify(q.params) })); }
|
|
321
|
+
if (q.error) { body.appendChild(h("div", { class: "error-box", text: q.error })); }
|
|
322
|
+
}
|
|
323
|
+
if (isOpen) { fillBody(); }
|
|
324
|
+
box.appendChild(body);
|
|
325
|
+
return box;
|
|
326
|
+
}
|
|
327
|
+
function setExpandAll(open) {
|
|
328
|
+
state.expandAll = open; state.open = {};
|
|
329
|
+
if (!open) { state.expandAll = false; }
|
|
330
|
+
renderDetail();
|
|
331
|
+
}
|
|
332
|
+
function focusSeqs(seqs) {
|
|
333
|
+
state.highlight = seqs; renderDetail();
|
|
334
|
+
var first = detailEl.querySelector(".q.hl");
|
|
335
|
+
if (first && first.scrollIntoView) { first.scrollIntoView({ block: "center" }); }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── language menu: a custom listbox, because the OS draws a native <select>'s popup ──
|
|
339
|
+
function langItems() { return Array.prototype.slice.call($("lang-menu").children); }
|
|
340
|
+
function updateLangMenu() {
|
|
341
|
+
var current = LOCALES.filter(function (l) { return l.code === locale; })[0];
|
|
342
|
+
$("lang-label").textContent = current ? current.name : locale;
|
|
343
|
+
langItems().forEach(function (li) { li.setAttribute("aria-selected", li.getAttribute("data-code") === locale ? "true" : "false"); });
|
|
344
|
+
}
|
|
345
|
+
function setLangMenu(open, restoreFocus) {
|
|
346
|
+
var menu = $("lang-menu"), btn = $("lang");
|
|
347
|
+
menu.hidden = !open; btn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
348
|
+
if (open) { var items = langItems(); var sel = items.filter(function (li) { return li.getAttribute("aria-selected") === "true"; })[0] || items[0]; sel.focus(); }
|
|
349
|
+
else if (restoreFocus) { btn.focus(); }
|
|
350
|
+
}
|
|
351
|
+
function chooseLocale(code) {
|
|
352
|
+
locale = code; storage.set(KEY_LANG, locale); applyI18n(); renderList(); renderDetail(); setLangMenu(false, true);
|
|
353
|
+
}
|
|
354
|
+
function setupLangMenu() {
|
|
355
|
+
var menu = $("lang-menu"), btn = $("lang");
|
|
356
|
+
LOCALES.forEach(function (l) {
|
|
357
|
+
var li = h("li", { class: "dd-item", role: "option", tabindex: "-1", "data-code": l.code }, [icon("check"), h("span", { text: l.name }), h("span", { class: "code", text: l.code })]);
|
|
358
|
+
li.firstChild.setAttribute("class", "check");
|
|
359
|
+
li.addEventListener("click", function () { chooseLocale(l.code); });
|
|
360
|
+
menu.appendChild(li);
|
|
361
|
+
});
|
|
362
|
+
btn.addEventListener("click", function () { setLangMenu(menu.hidden, false); });
|
|
363
|
+
btn.addEventListener("keydown", function (e) { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setLangMenu(true, false); } });
|
|
364
|
+
menu.addEventListener("keydown", function (e) {
|
|
365
|
+
var items = langItems(), i = items.indexOf(document.activeElement);
|
|
366
|
+
if (e.key === "ArrowDown") { e.preventDefault(); items[Math.min(items.length - 1, i + 1)].focus(); }
|
|
367
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); items[Math.max(0, i - 1)].focus(); }
|
|
368
|
+
else if (e.key === "Home") { e.preventDefault(); items[0].focus(); }
|
|
369
|
+
else if (e.key === "End") { e.preventDefault(); items[items.length - 1].focus(); }
|
|
370
|
+
else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (i >= 0) { chooseLocale(items[i].getAttribute("data-code")); } }
|
|
371
|
+
else if (e.key === "Escape") { e.preventDefault(); setLangMenu(false, true); }
|
|
372
|
+
else if (e.key === "Tab") { setLangMenu(false, false); }
|
|
373
|
+
});
|
|
374
|
+
document.addEventListener("click", function (e) { if (!menu.hidden && !$("lang-dd").contains(e.target)) { setLangMenu(false, false); } });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── actions ───────────────────────────────────────────────────────
|
|
378
|
+
var toastTimer = null;
|
|
379
|
+
function toast(text) {
|
|
380
|
+
var el = $("toast"); el.textContent = text; el.classList.add("show");
|
|
381
|
+
clearTimeout(toastTimer); toastTimer = setTimeout(function () { el.classList.remove("show"); }, 1600);
|
|
382
|
+
}
|
|
383
|
+
function copy(text) {
|
|
384
|
+
function fallback() {
|
|
385
|
+
var ta = h("textarea", { style: "position:fixed;opacity:0" }); ta.value = text; document.body.appendChild(ta); ta.select();
|
|
386
|
+
var ok = false; try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
|
|
387
|
+
document.body.removeChild(ta); toast(t(ok ? "toast.copied" : "toast.copyFailed"));
|
|
388
|
+
}
|
|
389
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
390
|
+
navigator.clipboard.writeText(text).then(function () { toast(t("toast.copied")); }, fallback);
|
|
391
|
+
} else { fallback(); }
|
|
392
|
+
}
|
|
393
|
+
function api(path, init) { return fetch("./api/" + path, init).then(function (r) { return r.json(); }); }
|
|
394
|
+
function loadDetail(id) {
|
|
395
|
+
var token = ++state.loadToken;
|
|
396
|
+
return api("requests/" + id).then(function (d) {
|
|
397
|
+
if (token !== state.loadToken || id !== state.selectedId) { return; }
|
|
398
|
+
state.report = d && d.trace ? d : null; renderDetail();
|
|
399
|
+
}).catch(function () { /* server restarting; the next change event retries */ });
|
|
400
|
+
}
|
|
401
|
+
function select(id) {
|
|
402
|
+
state.selectedId = id; state.highlight = []; state.open = {}; state.expandAll = false;
|
|
403
|
+
appEl.setAttribute("data-view", "detail");
|
|
404
|
+
renderList(); return loadDetail(id);
|
|
405
|
+
}
|
|
406
|
+
function refresh() {
|
|
407
|
+
return api("requests").then(function (list) {
|
|
408
|
+
state.requests = list; renderList();
|
|
409
|
+
if (state.follow && list.length && list[0].id !== state.selectedId) { return select(list[0].id); }
|
|
410
|
+
if (state.selectedId && list.some(function (r) { return r.id === state.selectedId; })) { return loadDetail(state.selectedId); }
|
|
411
|
+
if (state.selectedId) { state.selectedId = null; state.report = null; renderDetail(); }
|
|
412
|
+
}).catch(function () { /* server restarting */ });
|
|
413
|
+
}
|
|
414
|
+
function renderLive() {
|
|
415
|
+
var el = $("live"); el.classList.toggle("on", state.live);
|
|
416
|
+
$("live-text").textContent = t(state.live ? "live.on" : "live.off");
|
|
417
|
+
}
|
|
418
|
+
function connect() {
|
|
419
|
+
if (typeof EventSource === "undefined") { return; }
|
|
420
|
+
var es = new EventSource("./api/stream");
|
|
421
|
+
es.onopen = function () { state.live = true; renderLive(); refresh(); };
|
|
422
|
+
es.onmessage = function () { refresh(); };
|
|
423
|
+
es.onerror = function () { state.live = false; renderLive(); };
|
|
424
|
+
}
|
|
425
|
+
function move(delta) {
|
|
426
|
+
var rows = visibleRequests(); if (!rows.length) { return; }
|
|
427
|
+
var i = rows.findIndex(function (r) { return r.id === state.selectedId; });
|
|
428
|
+
var next = rows[Math.min(rows.length - 1, Math.max(0, i < 0 ? 0 : i + delta))];
|
|
429
|
+
select(next.id);
|
|
430
|
+
var el = listBody.querySelector("[data-id='" + next.id + "']"); if (el && el.scrollIntoView) { el.scrollIntoView({ block: "nearest" }); }
|
|
431
|
+
}
|
|
432
|
+
function setFollow(v) {
|
|
433
|
+
state.follow = v; storage.set(KEY_FOLLOW, v ? "1" : "0");
|
|
434
|
+
$("follow").setAttribute("aria-checked", v ? "true" : "false");
|
|
435
|
+
if (v) { refresh(); }
|
|
436
|
+
}
|
|
437
|
+
function setMode(mode) {
|
|
438
|
+
state.mode = mode;
|
|
439
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-mode]"), function (b) { b.setAttribute("aria-pressed", b.getAttribute("data-mode") === mode ? "true" : "false"); });
|
|
440
|
+
renderList();
|
|
441
|
+
}
|
|
442
|
+
function fitLayout() { appEl.classList.toggle("narrow", window.innerWidth < 760); document.documentElement.style.setProperty("--wf", window.innerWidth < 1100 ? "140px" : "220px"); }
|
|
443
|
+
|
|
444
|
+
// ── wiring ────────────────────────────────────────────────────────
|
|
445
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-icon]"), function (el) { el.replaceWith(icon(el.getAttribute("data-icon"))); });
|
|
446
|
+
setupLangMenu();
|
|
447
|
+
$("theme").addEventListener("click", function () {
|
|
448
|
+
state.theme = state.theme === "auto" ? "light" : state.theme === "light" ? "dark" : "auto";
|
|
449
|
+
storage.set(KEY_THEME, state.theme); applyTheme();
|
|
450
|
+
});
|
|
451
|
+
$("clear").addEventListener("click", function () {
|
|
452
|
+
api("clear", { method: "POST" }).then(function () { state.selectedId = null; state.report = null; renderDetail(); refresh(); });
|
|
453
|
+
});
|
|
454
|
+
$("follow").addEventListener("click", function () { setFollow(!state.follow); });
|
|
455
|
+
var search = $("search");
|
|
456
|
+
search.addEventListener("input", function () { state.term = search.value; renderList(); });
|
|
457
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-mode]"), function (b) { b.addEventListener("click", function () { setMode(b.getAttribute("data-mode")); }); });
|
|
458
|
+
document.addEventListener("keydown", function (e) {
|
|
459
|
+
var tag = (e.target && e.target.tagName) || "";
|
|
460
|
+
if (e.target && e.target.closest && e.target.closest(".dd")) { return; }
|
|
461
|
+
if (e.key === "Escape" && e.target === search) { search.value = ""; state.term = ""; renderList(); search.blur(); return; }
|
|
462
|
+
if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA" || e.metaKey || e.ctrlKey || e.altKey) { return; }
|
|
463
|
+
if (e.key === "/") { e.preventDefault(); search.focus(); }
|
|
464
|
+
else if (e.key === "ArrowDown" || e.key === "j") { e.preventDefault(); move(1); }
|
|
465
|
+
else if (e.key === "ArrowUp" || e.key === "k") { e.preventDefault(); move(-1); }
|
|
466
|
+
else if (e.key === "e") { setExpandAll(!state.expandAll); }
|
|
467
|
+
});
|
|
468
|
+
window.addEventListener("resize", fitLayout);
|
|
469
|
+
|
|
470
|
+
fitLayout(); applyI18n(); setFollow(state.follow); setMode("all"); renderList(); renderDetail(); refresh(); connect();
|
|
471
|
+
})();
|
|
472
|
+
`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Locale } from "../i18n/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The whole inspector page as one document: no build step, no external requests (no CDN, no web
|
|
4
|
+
* fonts). `localeHint` is the language the server was explicitly configured with, if any.
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderPage(localeHint?: Locale | null): string;
|
package/dist/ui/page.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LOCALES, MESSAGES } from "../i18n/index.js";
|
|
2
|
+
import { CLIENT_SCRIPT } from "./client.js";
|
|
3
|
+
import { STYLES } from "./styles.js";
|
|
4
|
+
/** Keeps a JSON blob inert inside an inline <script>: no `</script>`, no `<!--`. */
|
|
5
|
+
// Built from a string: a literal U+2028 / U+2029 inside a regex literal is itself a line terminator in older engines.
|
|
6
|
+
// eslint-disable-next-line prefer-regex-literals
|
|
7
|
+
const LINE_SEPARATORS = new RegExp("[\\u2028\\u2029]", "g");
|
|
8
|
+
const LESS_THAN = /</g;
|
|
9
|
+
const toInlineJson = (value) => JSON.stringify(value).replace(LESS_THAN, "\\u003c").replace(LINE_SEPARATORS, " ");
|
|
10
|
+
/**
|
|
11
|
+
* The whole inspector page as one document: no build step, no external requests (no CDN, no web
|
|
12
|
+
* fonts). `localeHint` is the language the server was explicitly configured with, if any.
|
|
13
|
+
*/
|
|
14
|
+
export function renderPage(localeHint = null) {
|
|
15
|
+
const config = toInlineJson({ messages: MESSAGES, locales: LOCALES, hint: localeHint });
|
|
16
|
+
return `<!doctype html>
|
|
17
|
+
<html lang="en">
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="utf-8">
|
|
20
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
21
|
+
<meta name="color-scheme" content="light dark">
|
|
22
|
+
<meta name="referrer" content="no-referrer">
|
|
23
|
+
<title>SQL Inspector</title>
|
|
24
|
+
<style>${STYLES}</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<div class="app" id="app" data-view="list">
|
|
28
|
+
<header class="topbar">
|
|
29
|
+
<div class="brand"><span class="brand-mark"><span data-icon="db"></span></span><span class="name" data-i18n="app.title"></span></div>
|
|
30
|
+
<span class="live" id="live"><i></i><span id="live-text"></span></span>
|
|
31
|
+
<span class="spacer"></span>
|
|
32
|
+
<label class="field">
|
|
33
|
+
<span data-icon="search"></span>
|
|
34
|
+
<input id="search" class="input" type="text" autocomplete="off" spellcheck="false" data-i18n-placeholder="search.placeholder" data-i18n-aria="search.aria">
|
|
35
|
+
<span class="kbd" aria-hidden="true">/</span>
|
|
36
|
+
</label>
|
|
37
|
+
<span class="seg" role="group">
|
|
38
|
+
<button type="button" data-mode="all" aria-pressed="true" data-i18n="filter.all"></button>
|
|
39
|
+
<button type="button" data-mode="issues" aria-pressed="false" data-i18n="filter.issues"></button>
|
|
40
|
+
<button type="button" data-mode="errors" aria-pressed="false" data-i18n="filter.errors"></button>
|
|
41
|
+
</span>
|
|
42
|
+
<span class="dd" id="lang-dd">
|
|
43
|
+
<button type="button" class="dd-btn" id="lang" aria-haspopup="listbox" aria-expanded="false" data-i18n-aria="lang.label" data-i18n-title="lang.label"><span data-icon="globe"></span><span id="lang-label"></span><span data-icon="down"></span></button>
|
|
44
|
+
<ul class="dd-menu" id="lang-menu" role="listbox" tabindex="-1" hidden></ul>
|
|
45
|
+
</span>
|
|
46
|
+
<button type="button" class="btn icon ghost" id="theme"></button>
|
|
47
|
+
<button type="button" class="btn" id="clear" data-i18n-title="action.clear.title"><span data-icon="trash"></span><span class="label" data-i18n="action.clear"></span></button>
|
|
48
|
+
</header>
|
|
49
|
+
<div class="layout">
|
|
50
|
+
<aside class="list">
|
|
51
|
+
<div class="list-head">
|
|
52
|
+
<h2 data-i18n="requests.title"></h2><span class="count" id="count">0</span><span class="spacer"></span>
|
|
53
|
+
<button type="button" class="switch" id="follow" role="switch" aria-checked="false"><span class="track"></span><span data-i18n="follow.label"></span></button>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="list-body" id="list-body" role="listbox"></div>
|
|
56
|
+
</aside>
|
|
57
|
+
<main class="detail" id="detail"></main>
|
|
58
|
+
</div>
|
|
59
|
+
<footer class="footer">
|
|
60
|
+
<span class="kbd">/</span><span data-i18n="shortcuts.search"></span>
|
|
61
|
+
<span class="kbd">↑</span><span class="kbd">↓</span><span data-i18n="shortcuts.navigate"></span>
|
|
62
|
+
<span class="kbd">e</span><span data-i18n="shortcuts.expand"></span>
|
|
63
|
+
</footer>
|
|
64
|
+
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
|
65
|
+
</div>
|
|
66
|
+
<script>
|
|
67
|
+
var __SQLI__ = ${config};
|
|
68
|
+
${CLIENT_SCRIPT}
|
|
69
|
+
</script>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styles of the inspector page. The page runs on its own document (served by the inspector,
|
|
3
|
+
* shown standalone or inside the floating panel's iframe), so nothing here can reach the host
|
|
4
|
+
* application. Everything is still scoped to our own elements and every native control is
|
|
5
|
+
* restyled, so no browser-default widget shows through.
|
|
6
|
+
*/
|
|
7
|
+
export declare const STYLES = "\n:root {\n --font: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", \"Hiragino Sans\", \"Yu Gothic UI\", \"Apple SD Gothic Neo\", sans-serif;\n --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n --radius: 8px; --radius-sm: 6px;\n --bg: #f5f7fa; --surface: #ffffff; --surface-2: #f1f4f8; --surface-3: #e6ebf1;\n --border: #e2e7ee; --border-strong: #cdd5df;\n --text: #121821; --text-2: #4a5666; --text-3: #6e7a8a;\n --accent: #1f6feb; --accent-weak: #e7f0ff; --accent-text: #ffffff;\n --ok: #1a7f37; --ok-weak: #e6f5ea;\n --warn: #9a6700; --warn-weak: #fff4d6;\n --err: #cf222e; --err-weak: #ffebe9;\n --shadow: 0 8px 24px rgba(20, 30, 50, .14);\n --syn-kw: #8250df; --syn-str: #0a7a3d; --syn-num: #b35900; --syn-id: #0550ae; --syn-fn: #8a5a00; --syn-com: #6e7a8a;\n color-scheme: light;\n}\n@media (prefers-color-scheme: dark) { :root:not([data-theme=\"light\"]) { \n --bg: #0d1117; --surface: #151b23; --surface-2: #1b232d; --surface-3: #242e3a;\n --border: #283241; --border-strong: #3a4757;\n --text: #e6edf3; --text-2: #aab5c3; --text-3: #7d8896;\n --accent: #5aa2ff; --accent-weak: rgba(90, 162, 255, .14); --accent-text: #0d1117;\n --ok: #4ac26b; --ok-weak: rgba(74, 194, 107, .14);\n --warn: #e3a008; --warn-weak: rgba(227, 160, 8, .14);\n --err: #ff6b6b; --err-weak: rgba(255, 107, 107, .15);\n --shadow: 0 8px 24px rgba(0, 0, 0, .45);\n --syn-kw: #c792ea; --syn-str: #8bd49c; --syn-num: #f2a65e; --syn-id: #7cb7ff; --syn-fn: #e6c07b; --syn-com: #7d8896;\n color-scheme: dark; } }\n:root[data-theme=\"dark\"] { \n --bg: #0d1117; --surface: #151b23; --surface-2: #1b232d; --surface-3: #242e3a;\n --border: #283241; --border-strong: #3a4757;\n --text: #e6edf3; --text-2: #aab5c3; --text-3: #7d8896;\n --accent: #5aa2ff; --accent-weak: rgba(90, 162, 255, .14); --accent-text: #0d1117;\n --ok: #4ac26b; --ok-weak: rgba(74, 194, 107, .14);\n --warn: #e3a008; --warn-weak: rgba(227, 160, 8, .14);\n --err: #ff6b6b; --err-weak: rgba(255, 107, 107, .15);\n --shadow: 0 8px 24px rgba(0, 0, 0, .45);\n --syn-kw: #c792ea; --syn-str: #8bd49c; --syn-num: #f2a65e; --syn-id: #7cb7ff; --syn-fn: #e6c07b; --syn-com: #7d8896;\n color-scheme: dark; }\n\n*, *::before, *::after { box-sizing: border-box; }\nhtml, body { height: 100%; }\nbody { margin: 0; background: var(--bg); color: var(--text); font: 13px/1.5 var(--font); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; overflow: hidden; }\n[hidden] { display: none !important; }\nbutton, input, select { font: inherit; color: inherit; }\nbutton { appearance: none; background: none; border: 0; padding: 0; cursor: pointer; }\nsvg { width: 16px; height: 16px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }\n:focus { outline: none; }\n:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px; }\n.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }\n* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }\n::-webkit-scrollbar { width: 10px; height: 10px; }\n::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; border: 2px solid transparent; background-clip: content-box; }\n::-webkit-scrollbar-track { background: transparent; }\n\n/* \u2500\u2500 Shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.app { display: flex; flex-direction: column; height: 100%; }\n.topbar { display: flex; align-items: center; gap: 12px; height: 52px; padding: 0 16px; background: var(--surface); border-bottom: 1px solid var(--border); flex: none; }\n.brand { display: flex; align-items: center; gap: 10px; font-weight: 650; font-size: 14px; letter-spacing: -.01em; white-space: nowrap; }\n.brand-mark { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; background: linear-gradient(135deg, #1f6feb, #7a5af8); color: #fff; }\n.brand-mark svg { width: 16px; height: 16px; stroke: #fff; }\n.live { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px 2px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 600; color: var(--text-2); background: var(--surface-2); border: 1px solid var(--border); white-space: nowrap; }\n.live i { width: 7px; height: 7px; border-radius: 50%; background: var(--err); }\n.live.on i { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-weak); }\n.spacer { flex: 1; }\n.field { position: relative; display: flex; align-items: center; }\n.field > svg { position: absolute; left: 10px; color: var(--text-3); pointer-events: none; }\n.input { height: 32px; width: 260px; padding: 0 34px 0 32px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-2); color: var(--text); transition: border-color .15s, background .15s, box-shadow .15s; }\n.input::placeholder { color: var(--text-3); }\n.input:hover { border-color: var(--border-strong); }\n.input:focus { border-color: var(--accent); background: var(--surface); box-shadow: 0 0 0 3px var(--accent-weak); outline: none; }\n.kbd { font: 600 11px/1 var(--mono); min-width: 18px; padding: 3px 5px; text-align: center; border: 1px solid var(--border-strong); border-bottom-width: 2px; border-radius: 4px; background: var(--surface); color: var(--text-2); }\n.field .kbd { position: absolute; right: 8px; }\n.btn { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); font-weight: 550; white-space: nowrap; transition: background .15s, border-color .15s; }\n.btn:hover { background: var(--surface-2); border-color: var(--border-strong); }\n.btn:active { background: var(--surface-3); }\n.btn.icon { width: 32px; padding: 0; justify-content: center; }\n.btn.ghost { border-color: transparent; background: transparent; }\n.btn.ghost:hover { background: var(--surface-2); }\n.btn.sm { height: 26px; padding: 0 9px; font-size: 12px; }\n.dd { position: relative; display: inline-flex; }\n.dd-btn { display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); font-weight: 550; white-space: nowrap; transition: background .15s, border-color .15s; }\n.dd-btn:hover { background: var(--surface-2); border-color: var(--border-strong); }\n.dd-btn[aria-expanded=\"true\"] { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }\n.dd-btn svg { color: var(--text-3); } .dd-btn svg:last-child { width: 12px; height: 12px; }\n.dd-menu { position: absolute; z-index: 40; right: 0; top: calc(100% + 6px); min-width: 188px; margin: 0; padding: 4px; list-style: none; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow); }\n.dd-item { display: flex; align-items: center; gap: 10px; height: 34px; padding: 0 10px; border-radius: 6px; color: var(--text); cursor: pointer; user-select: none; }\n.dd-item:hover, .dd-item:focus-visible { background: var(--surface-2); outline: none; }\n.dd-item .check { width: 14px; height: 14px; color: var(--accent); visibility: hidden; }\n.dd-item[aria-selected=\"true\"] { font-weight: 650; }\n.dd-item[aria-selected=\"true\"] .check { visibility: visible; }\n.dd-item .code { margin-left: auto; color: var(--text-3); font: 600 11px var(--mono); text-transform: uppercase; }\n.seg { display: inline-flex; padding: 2px; gap: 2px; border-radius: var(--radius-sm); background: var(--surface-2); border: 1px solid var(--border); }\n.seg button { height: 26px; padding: 0 10px; border-radius: 4px; color: var(--text-2); font-weight: 550; font-size: 12px; }\n.seg button:hover { color: var(--text); }\n.seg button[aria-pressed=\"true\"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }\n.switch { display: inline-flex; align-items: center; gap: 8px; color: var(--text-2); font-weight: 550; font-size: 12px; cursor: pointer; }\n.switch .track { position: relative; width: 28px; height: 16px; border-radius: 999px; background: var(--border-strong); transition: background .15s; }\n.switch .track::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .3); transition: transform .15s; }\n.switch[aria-checked=\"true\"] .track { background: var(--accent); }\n.switch[aria-checked=\"true\"] .track::after { transform: translateX(12px); }\n\n.layout { flex: 1; display: grid; grid-template-columns: minmax(280px, 360px) 1fr; min-height: 0; }\n.list { display: flex; flex-direction: column; min-height: 0; background: var(--surface); border-right: 1px solid var(--border); }\n.list-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); }\n.list-head h2 { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); font-weight: 650; }\n.count { padding: 0 7px; border-radius: 999px; background: var(--surface-3); color: var(--text-2); font-size: 11px; font-weight: 650; line-height: 18px; }\n.list-body { flex: 1; overflow: auto; }\n.req { position: relative; display: block; width: 100%; text-align: left; padding: 10px 12px 10px 15px; border-bottom: 1px solid var(--border); }\n.req::before { content: \"\"; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: transparent; }\n.req.has-warn::before { background: var(--warn); } .req.has-err::before { background: var(--err); }\n.req:hover { background: var(--surface-2); }\n.req[aria-selected=\"true\"] { background: var(--accent-weak); }\n.req-top { display: flex; align-items: center; gap: 8px; min-width: 0; }\n.req-url { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 12px; }\n.req-meta { display: flex; align-items: center; gap: 10px; margin-top: 3px; color: var(--text-3); font-size: 11.5px; }\n.req-meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--border-strong); }\n\n.method { font: 700 10.5px/1 var(--mono); letter-spacing: .02em; padding: 3px 6px; border-radius: 4px; background: var(--surface-3); color: var(--text-2); }\n.method.GET { background: var(--ok-weak); color: var(--ok); } .method.POST { background: var(--accent-weak); color: var(--accent); }\n.method.PUT, .method.PATCH { background: var(--warn-weak); color: var(--warn); } .method.DELETE { background: var(--err-weak); color: var(--err); }\n.status { font: 650 11.5px/1 var(--mono); color: var(--text-2); }\n.status.s2 { color: var(--ok); } .status.s3 { color: var(--accent); } .status.s4 { color: var(--warn); } .status.s5 { color: var(--err); }\n.chip { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 8px; border-radius: 999px; font-size: 11px; font-weight: 650; background: var(--surface-3); color: var(--text-2); white-space: nowrap; }\n.chip.ok { background: var(--ok-weak); color: var(--ok); } .chip.warn { background: var(--warn-weak); color: var(--warn); } .chip.err { background: var(--err-weak); color: var(--err); } .chip.accent { background: var(--accent-weak); color: var(--accent); }\n.chip svg { width: 12px; height: 12px; }\n\n.detail { overflow: auto; min-width: 0; }\n.detail-inner { max-width: 1180px; margin: 0 auto; padding: 20px 24px 40px; }\n.detail-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }\n.detail-head h1 { margin: 0; font: 600 15px/1.4 var(--mono); word-break: break-all; flex: 1; min-width: 200px; }\n.back { display: none; }\n.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 20px; }\n.kpi { padding: 12px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); }\n.kpi-label { color: var(--text-3); font-size: 11.5px; font-weight: 600; }\n.kpi-value { margin-top: 2px; font-size: 22px; font-weight: 650; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }\n.kpi-sub { color: var(--text-3); font-size: 11.5px; }\n.section { margin-bottom: 22px; }\n.section-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }\n.section-head h2 { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); font-weight: 650; }\n.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }\n.insight { display: flex; gap: 10px; width: 100%; text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--border); }\n.insight:last-child { border-bottom: 0; }\n.insight:hover { background: var(--surface-2); }\n.insight .ico { margin-top: 1px; }\n.insight.warn .ico { color: var(--warn); } .insight.error .ico { color: var(--err); } .insight.info .ico { color: var(--text-3); }\n.insight-title { font-weight: 650; }\n.insight-msg { color: var(--text-2); font-size: 12.5px; word-break: break-word; }\n.insight-msg code { font: 12px var(--mono); }\n.empty-ok { padding: 14px; color: var(--text-2); display: flex; gap: 8px; align-items: center; }\n.empty-ok svg { color: var(--ok); }\n.empty { display: grid; place-items: center; align-content: center; gap: 6px; height: 100%; min-height: 240px; padding: 32px; text-align: center; color: var(--text-2); }\n.empty svg.big { width: 44px; height: 44px; stroke-width: 1.3; color: var(--text-3); }\n.empty h3 { margin: 6px 0 0; font-size: 14px; color: var(--text); }\n.empty p { margin: 0; max-width: 340px; color: var(--text-3); }\n\n/* \u2500\u2500 Query tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.tree-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }\n.axis { display: flex; justify-content: space-between; margin: 0 12px 4px auto; width: var(--wf, 220px); color: var(--text-3); font: 11px var(--mono); }\n.tx { margin: 6px 0; border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius); background: color-mix(in srgb, var(--accent-weak) 35%, var(--surface)); }\n.tx.rolled-back { border-left-color: var(--err); } .tx.committed { border-left-color: var(--ok); } .tx.open { border-left-color: var(--warn); }\n.tx > .tx-head { display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 10px; text-align: left; font-weight: 650; }\n.tx-head .dim { font-weight: 500; }\n.tx-children { padding: 0 8px 8px; }\n.q { margin: 4px 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }\n.q.hl { box-shadow: 0 0 0 2px var(--accent); border-color: var(--accent); }\n.q.err { border-color: var(--err); }\n.q-head { display: grid; grid-template-columns: 1fr var(--wf, 220px); align-items: center; gap: 12px; width: 100%; padding: 7px 12px; text-align: left; }\n.q-main { min-width: 0; }\n.q-line { display: flex; align-items: center; gap: 8px; min-width: 0; }\n.chev { transition: transform .15s; color: var(--text-3); }\n.q.open .chev, .tx.expanded > .tx-head .chev { transform: rotate(90deg); }\n.kind { font: 700 10.5px/1 var(--mono); padding: 3px 6px; border-radius: 4px; background: var(--surface-3); color: var(--text-2); min-width: 54px; text-align: center; }\n.kind.select { background: var(--accent-weak); color: var(--accent); } .kind.insert { background: var(--ok-weak); color: var(--ok); }\n.kind.update { background: var(--warn-weak); color: var(--warn); } .kind.delete { background: var(--err-weak); color: var(--err); }\n.tables { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }\n.dim { color: var(--text-3); }\n.num { font: 12px var(--mono); font-variant-numeric: tabular-nums; color: var(--text-2); white-space: nowrap; }\n.preview { margin-top: 3px; font: 11.5px/1.4 var(--mono); color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.wf { position: relative; height: 8px; border-radius: 4px; background: var(--surface-3); }\n.wf i { position: absolute; top: 0; bottom: 0; min-width: 2px; border-radius: 4px; background: var(--accent); }\n.wf i.slow { background: var(--warn); } .wf i.err { background: var(--err); }\n.q-body { display: none; padding: 10px 12px 12px; border-top: 1px solid var(--border); background: var(--surface-2); border-radius: 0 0 var(--radius) var(--radius); }\n.q.open .q-body { display: block; }\n.q-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; color: var(--text-3); font-size: 12px; }\n.q-tools .spacer { flex: 1; }\n.sql { margin: 0; padding: 12px 14px; max-height: 380px; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); font: 12px/1.6 var(--mono); white-space: pre-wrap; word-break: break-word; tab-size: 2; }\n.sql .kw { color: var(--syn-kw); font-weight: 600; } .sql .str { color: var(--syn-str); } .sql .num { color: var(--syn-num); font: inherit; }\n.sql .id { color: var(--syn-id); } .sql .fn { color: var(--syn-fn); } .sql .com { color: var(--syn-com); font-style: italic; }\n.params { margin-top: 8px; font: 12px var(--mono); color: var(--text-2); word-break: break-all; }\n.error-box { margin-top: 8px; padding: 8px 10px; border-radius: var(--radius-sm); background: var(--err-weak); color: var(--err); font: 12px var(--mono); }\n\n.toast { position: fixed; left: 50%; bottom: 20px; transform: translate(-50%, 12px); padding: 8px 14px; border-radius: 999px; background: var(--text); color: var(--bg); font-weight: 600; font-size: 12.5px; box-shadow: var(--shadow); opacity: 0; pointer-events: none; transition: opacity .18s, transform .18s; }\n.toast.show { opacity: 1; transform: translate(-50%, 0); }\n.footer { display: flex; align-items: center; gap: 14px; height: 28px; padding: 0 16px; border-top: 1px solid var(--border); background: var(--surface); color: var(--text-3); font-size: 11.5px; flex: none; overflow: hidden; white-space: nowrap; }\n.footer span { display: inline-flex; align-items: center; gap: 5px; }\n\n/* Narrow (the docked panel can be ~400px wide): one pane at a time */\n.narrow .topbar { gap: 8px; padding: 0 10px; }\n.narrow .brand span.name, .narrow .footer, .narrow .live span, .narrow .btn .label { display: none; }\n.narrow .input { width: 150px; }\n.narrow .layout { display: block; }\n.narrow .list, .narrow .detail { height: 100%; border: 0; }\n.narrow[data-view=\"detail\"] .list { display: none; } .narrow[data-view=\"list\"] .detail { display: none; }\n.narrow .back { display: inline-flex; }\n.narrow .detail-inner { padding: 14px 12px 28px; }\n.narrow .kpis { grid-template-columns: repeat(2, 1fr); }\n.narrow .q-head { grid-template-columns: 1fr; } .narrow .wf, .narrow .axis { display: none; }\n\n@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }\n";
|