@youtyan/code-viewer 0.1.50 → 0.1.52
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/dist/code-viewer.js +3007 -271
- package/package.json +1 -1
- package/skills/code-viewer-query/SKILL.md +59 -0
- package/web/app.js +3864 -207
- package/web/index.html +21 -0
- package/web/style.css +2111 -42
package/web/app.js
CHANGED
|
@@ -516,6 +516,19 @@
|
|
|
516
516
|
range
|
|
517
517
|
};
|
|
518
518
|
}
|
|
519
|
+
case "/database": {
|
|
520
|
+
const db = params.get("db") || undefined;
|
|
521
|
+
const table = params.get("table") || undefined;
|
|
522
|
+
const tabRaw = params.get("tab");
|
|
523
|
+
const tab = tabRaw === "data" || tabRaw === "query" || tabRaw === "schema" || tabRaw === "er" || tabRaw === "search" || tabRaw === "snapshot" ? tabRaw : undefined;
|
|
524
|
+
return {
|
|
525
|
+
screen: "database",
|
|
526
|
+
...db ? { db } : {},
|
|
527
|
+
...table ? { table } : {},
|
|
528
|
+
...tab ? { tab } : {},
|
|
529
|
+
range
|
|
530
|
+
};
|
|
531
|
+
}
|
|
519
532
|
default:
|
|
520
533
|
return {
|
|
521
534
|
screen: "unknown",
|
|
@@ -562,6 +575,17 @@
|
|
|
562
575
|
const qs = params.toString();
|
|
563
576
|
return `/history${qs ? `?${qs}` : ""}`;
|
|
564
577
|
}
|
|
578
|
+
case "database": {
|
|
579
|
+
const params = new URLSearchParams;
|
|
580
|
+
if (route.db)
|
|
581
|
+
params.set("db", route.db);
|
|
582
|
+
if (route.table)
|
|
583
|
+
params.set("table", route.table);
|
|
584
|
+
if (route.tab)
|
|
585
|
+
params.set("tab", route.tab);
|
|
586
|
+
const qs = params.toString();
|
|
587
|
+
return `/database${qs ? `?${qs}` : ""}`;
|
|
588
|
+
}
|
|
565
589
|
case "unknown":
|
|
566
590
|
return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
|
|
567
591
|
default:
|
|
@@ -7138,8 +7162,13 @@ ${frontmatter.yaml}
|
|
|
7138
7162
|
function setAnnotationPanelOpen(open) {
|
|
7139
7163
|
annotationPanel.hidden = !open;
|
|
7140
7164
|
document.body.classList.toggle("annotation-panel-open", open);
|
|
7141
|
-
if (open)
|
|
7165
|
+
if (open) {
|
|
7142
7166
|
annotationPanelDismissed = false;
|
|
7167
|
+
const qhPanel = document.getElementById("query-history-panel");
|
|
7168
|
+
if (qhPanel)
|
|
7169
|
+
qhPanel.hidden = true;
|
|
7170
|
+
document.body.classList.remove("query-history-panel-open");
|
|
7171
|
+
}
|
|
7143
7172
|
localStorage.setItem("gdp:annotation-panel", open ? "1" : "0");
|
|
7144
7173
|
}
|
|
7145
7174
|
function annotationLineTarget(entry) {
|
|
@@ -7771,177 +7800,3099 @@ ${frontmatter.yaml}
|
|
|
7771
7800
|
};
|
|
7772
7801
|
}
|
|
7773
7802
|
|
|
7774
|
-
// web-src/views/
|
|
7775
|
-
var
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7803
|
+
// web-src/views/database/er-diagram.ts
|
|
7804
|
+
var mermaidPromise2 = null;
|
|
7805
|
+
var mermaidInitialized2 = false;
|
|
7806
|
+
async function loadMermaid2() {
|
|
7807
|
+
if (!mermaidPromise2) {
|
|
7808
|
+
mermaidPromise2 = import("/mermaid.js").then((mod) => {
|
|
7809
|
+
const typed = mod;
|
|
7810
|
+
const mermaid = typed.default;
|
|
7811
|
+
if (!mermaidInitialized2) {
|
|
7812
|
+
mermaid.initialize({
|
|
7813
|
+
startOnLoad: false,
|
|
7814
|
+
securityLevel: "strict",
|
|
7815
|
+
theme: "default",
|
|
7816
|
+
er: { useMaxWidth: false }
|
|
7817
|
+
});
|
|
7818
|
+
mermaidInitialized2 = true;
|
|
7819
|
+
}
|
|
7820
|
+
return mermaid;
|
|
7821
|
+
}).catch(() => null);
|
|
7791
7822
|
}
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
const
|
|
7796
|
-
|
|
7797
|
-
|
|
7823
|
+
return mermaidPromise2;
|
|
7824
|
+
}
|
|
7825
|
+
function mermaidType(sqlType) {
|
|
7826
|
+
const upper = sqlType.toUpperCase();
|
|
7827
|
+
if (upper.includes("INT"))
|
|
7828
|
+
return "int";
|
|
7829
|
+
if (upper.includes("TEXT") || upper.includes("VARCHAR") || upper.includes("CHAR"))
|
|
7830
|
+
return "string";
|
|
7831
|
+
if (upper.includes("REAL") || upper.includes("FLOAT") || upper.includes("DOUBLE"))
|
|
7832
|
+
return "float";
|
|
7833
|
+
if (upper.includes("BLOB"))
|
|
7834
|
+
return "blob";
|
|
7835
|
+
if (upper.includes("BOOL"))
|
|
7836
|
+
return "bool";
|
|
7837
|
+
if (upper.includes("DATE") || upper.includes("TIME"))
|
|
7838
|
+
return "datetime";
|
|
7839
|
+
if (upper.includes("NUMERIC") || upper.includes("DECIMAL"))
|
|
7840
|
+
return "decimal";
|
|
7841
|
+
return sqlType.replace(/[^a-zA-Z0-9]/g, "_") || "text";
|
|
7842
|
+
}
|
|
7843
|
+
function sanitizeMermaidId(name) {
|
|
7844
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
7845
|
+
}
|
|
7846
|
+
function buildErMarkup(schema, columnsMap) {
|
|
7847
|
+
const lines = ["erDiagram"];
|
|
7848
|
+
for (const table2 of schema.tables) {
|
|
7849
|
+
if (table2.type === "view")
|
|
7850
|
+
continue;
|
|
7851
|
+
if (!columnsMap.has(table2.name))
|
|
7852
|
+
continue;
|
|
7853
|
+
const id = sanitizeMermaidId(table2.name);
|
|
7854
|
+
const cols = columnsMap.get(table2.name) || [];
|
|
7855
|
+
lines.push(` ${id} {`);
|
|
7856
|
+
for (const col of cols) {
|
|
7857
|
+
const markers = [];
|
|
7858
|
+
if (col.primaryKey)
|
|
7859
|
+
markers.push("PK");
|
|
7860
|
+
const fk = schema.foreignKeys.find((f2) => f2.fromTable === table2.name && f2.fromColumn === col.name);
|
|
7861
|
+
if (fk)
|
|
7862
|
+
markers.push("FK");
|
|
7863
|
+
const comment2 = markers.length ? `"${markers.join(",")}"` : "";
|
|
7864
|
+
lines.push(` ${mermaidType(col.type)} ${sanitizeMermaidId(col.name)}${comment2 ? ` ${comment2}` : ""}`);
|
|
7865
|
+
}
|
|
7866
|
+
lines.push(" }");
|
|
7867
|
+
}
|
|
7868
|
+
const seen = new Set;
|
|
7869
|
+
for (const fk of schema.foreignKeys) {
|
|
7870
|
+
const fromId = sanitizeMermaidId(fk.fromTable);
|
|
7871
|
+
const toId = sanitizeMermaidId(fk.toTable);
|
|
7872
|
+
const key = `${fromId}--${toId}`;
|
|
7873
|
+
if (seen.has(key))
|
|
7874
|
+
continue;
|
|
7875
|
+
seen.add(key);
|
|
7876
|
+
if (!columnsMap.has(fk.fromTable) || !columnsMap.has(fk.toTable))
|
|
7877
|
+
continue;
|
|
7878
|
+
const toTable = schema.tables.find((t2) => t2.name === fk.toTable);
|
|
7879
|
+
if (!toTable || toTable.type === "view")
|
|
7880
|
+
continue;
|
|
7881
|
+
lines.push(` ${toId} ||--o{ ${fromId} : "${fk.fromColumn}"`);
|
|
7882
|
+
}
|
|
7883
|
+
return lines.join(`
|
|
7884
|
+
`);
|
|
7798
7885
|
}
|
|
7799
|
-
function
|
|
7800
|
-
const
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7886
|
+
function createErDiagram() {
|
|
7887
|
+
const el = document.createElement("div");
|
|
7888
|
+
el.className = "db-er-diagram";
|
|
7889
|
+
el.hidden = true;
|
|
7890
|
+
const toolbar = document.createElement("div");
|
|
7891
|
+
toolbar.className = "db-er-toolbar";
|
|
7892
|
+
const zoomIn = document.createElement("button");
|
|
7893
|
+
zoomIn.type = "button";
|
|
7894
|
+
zoomIn.className = "db-btn db-er-zoom-btn";
|
|
7895
|
+
zoomIn.textContent = "+";
|
|
7896
|
+
zoomIn.title = "Zoom in";
|
|
7897
|
+
const zoomOut = document.createElement("button");
|
|
7898
|
+
zoomOut.type = "button";
|
|
7899
|
+
zoomOut.className = "db-btn db-er-zoom-btn";
|
|
7900
|
+
zoomOut.textContent = "−";
|
|
7901
|
+
zoomOut.title = "Zoom out";
|
|
7902
|
+
const zoomReset = document.createElement("button");
|
|
7903
|
+
zoomReset.type = "button";
|
|
7904
|
+
zoomReset.className = "db-btn db-er-zoom-btn";
|
|
7905
|
+
zoomReset.textContent = "1:1";
|
|
7906
|
+
zoomReset.title = "Reset zoom";
|
|
7907
|
+
const copyBtn = document.createElement("button");
|
|
7908
|
+
copyBtn.type = "button";
|
|
7909
|
+
copyBtn.className = "db-btn db-er-zoom-btn";
|
|
7910
|
+
copyBtn.textContent = "Copy Mermaid";
|
|
7911
|
+
copyBtn.title = "Copy mermaid source to clipboard";
|
|
7912
|
+
toolbar.append(zoomIn, zoomOut, zoomReset, copyBtn);
|
|
7913
|
+
const container = document.createElement("div");
|
|
7914
|
+
container.className = "db-er-container";
|
|
7915
|
+
const svgWrap = document.createElement("div");
|
|
7916
|
+
svgWrap.className = "db-er-svg-wrap";
|
|
7917
|
+
container.appendChild(svgWrap);
|
|
7918
|
+
el.append(toolbar, container);
|
|
7919
|
+
let scale = 1;
|
|
7920
|
+
let lastMarkup = "";
|
|
7921
|
+
function applyZoom() {
|
|
7922
|
+
svgWrap.style.transform = `scale(${scale})`;
|
|
7923
|
+
svgWrap.style.transformOrigin = "top left";
|
|
7924
|
+
}
|
|
7925
|
+
zoomIn.addEventListener("click", () => {
|
|
7926
|
+
scale = Math.min(3, scale + 0.2);
|
|
7927
|
+
applyZoom();
|
|
7928
|
+
});
|
|
7929
|
+
zoomOut.addEventListener("click", () => {
|
|
7930
|
+
scale = Math.max(0.2, scale - 0.2);
|
|
7931
|
+
applyZoom();
|
|
7932
|
+
});
|
|
7933
|
+
zoomReset.addEventListener("click", () => {
|
|
7934
|
+
scale = 1;
|
|
7935
|
+
applyZoom();
|
|
7936
|
+
});
|
|
7937
|
+
copyBtn.addEventListener("click", () => {
|
|
7938
|
+
if (lastMarkup) {
|
|
7939
|
+
navigator.clipboard.writeText(lastMarkup).then(() => {
|
|
7940
|
+
copyBtn.textContent = "Copied!";
|
|
7941
|
+
setTimeout(() => {
|
|
7942
|
+
copyBtn.textContent = "Copy Mermaid";
|
|
7943
|
+
}, 1500);
|
|
7944
|
+
}, () => {});
|
|
7945
|
+
}
|
|
7946
|
+
});
|
|
7947
|
+
let dragState = null;
|
|
7948
|
+
container.addEventListener("mousedown", (e2) => {
|
|
7949
|
+
if (e2.button !== 0)
|
|
7804
7950
|
return;
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7951
|
+
dragState = {
|
|
7952
|
+
x: e2.clientX,
|
|
7953
|
+
y: e2.clientY,
|
|
7954
|
+
sl: container.scrollLeft,
|
|
7955
|
+
st: container.scrollTop
|
|
7956
|
+
};
|
|
7957
|
+
container.style.cursor = "grabbing";
|
|
7958
|
+
e2.preventDefault();
|
|
7808
7959
|
});
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
function createDiffLineSelect(deps) {
|
|
7812
|
-
let drag = null;
|
|
7813
|
-
let selection = null;
|
|
7814
|
-
function clearHighlights() {
|
|
7815
|
-
document.querySelectorAll(`.${SELECTED_CLASS}`).forEach((row) => {
|
|
7816
|
-
row.classList.remove(SELECTED_CLASS);
|
|
7817
|
-
});
|
|
7818
|
-
}
|
|
7819
|
-
function applySelection(next) {
|
|
7820
|
-
selection = next;
|
|
7821
|
-
clearHighlights();
|
|
7822
|
-
if (!next) {
|
|
7823
|
-
deps.pill.hide();
|
|
7960
|
+
window.addEventListener("mousemove", (e2) => {
|
|
7961
|
+
if (!dragState)
|
|
7824
7962
|
return;
|
|
7963
|
+
container.scrollLeft = dragState.sl - (e2.clientX - dragState.x);
|
|
7964
|
+
container.scrollTop = dragState.st - (e2.clientY - dragState.y);
|
|
7965
|
+
});
|
|
7966
|
+
window.addEventListener("mouseup", () => {
|
|
7967
|
+
if (dragState) {
|
|
7968
|
+
dragState = null;
|
|
7969
|
+
container.style.cursor = "";
|
|
7825
7970
|
}
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7971
|
+
});
|
|
7972
|
+
container.addEventListener("wheel", (e2) => {
|
|
7973
|
+
if (e2.ctrlKey || e2.metaKey) {
|
|
7974
|
+
e2.preventDefault();
|
|
7975
|
+
const delta = e2.deltaY > 0 ? -0.1 : 0.1;
|
|
7976
|
+
scale = Math.max(0.2, Math.min(3, scale + delta));
|
|
7977
|
+
applyZoom();
|
|
7978
|
+
}
|
|
7979
|
+
}, { passive: false });
|
|
7980
|
+
async function render(schema, columnsMap) {
|
|
7981
|
+
el.hidden = false;
|
|
7982
|
+
svgWrap.innerHTML = "";
|
|
7983
|
+
scale = 1;
|
|
7984
|
+
applyZoom();
|
|
7985
|
+
const tables = schema.tables.filter((t2) => t2.type === "table");
|
|
7986
|
+
if (tables.length === 0) {
|
|
7987
|
+
svgWrap.textContent = "No tables to display.";
|
|
7988
|
+
return;
|
|
7989
|
+
}
|
|
7990
|
+
const markup = buildErMarkup(schema, columnsMap);
|
|
7991
|
+
lastMarkup = markup;
|
|
7992
|
+
const mermaid = await loadMermaid2();
|
|
7993
|
+
if (!mermaid) {
|
|
7994
|
+
svgWrap.textContent = "Failed to load mermaid.js";
|
|
7995
|
+
return;
|
|
7996
|
+
}
|
|
7997
|
+
const node = document.createElement("div");
|
|
7998
|
+
node.className = "mermaid";
|
|
7999
|
+
node.textContent = markup;
|
|
8000
|
+
svgWrap.appendChild(node);
|
|
8001
|
+
try {
|
|
8002
|
+
await mermaid.run({ nodes: [node], suppressErrors: true });
|
|
8003
|
+
} catch {
|
|
8004
|
+
svgWrap.textContent = "Failed to render ER diagram.";
|
|
7834
8005
|
}
|
|
7835
|
-
deps.pill.show(next.path, start, end);
|
|
7836
8006
|
}
|
|
7837
8007
|
function clear() {
|
|
7838
|
-
|
|
7839
|
-
|
|
8008
|
+
el.hidden = true;
|
|
8009
|
+
svgWrap.innerHTML = "";
|
|
8010
|
+
lastMarkup = "";
|
|
7840
8011
|
}
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
8012
|
+
return { el, render, clear };
|
|
8013
|
+
}
|
|
8014
|
+
|
|
8015
|
+
// web-src/views/database/global-search-view.ts
|
|
8016
|
+
function createGlobalSearchView(deps) {
|
|
8017
|
+
const el = document.createElement("div");
|
|
8018
|
+
el.className = "db-global-search";
|
|
8019
|
+
const header = document.createElement("div");
|
|
8020
|
+
header.className = "db-global-search-header";
|
|
8021
|
+
const input = document.createElement("input");
|
|
8022
|
+
input.type = "text";
|
|
8023
|
+
input.className = "db-global-search-input";
|
|
8024
|
+
input.placeholder = "全テーブルを横断検索...";
|
|
8025
|
+
const searchBtn = document.createElement("button");
|
|
8026
|
+
searchBtn.type = "button";
|
|
8027
|
+
searchBtn.className = "db-global-search-btn";
|
|
8028
|
+
searchBtn.textContent = "検索";
|
|
8029
|
+
const cancelBtn = document.createElement("button");
|
|
8030
|
+
cancelBtn.type = "button";
|
|
8031
|
+
cancelBtn.className = "db-global-search-cancel";
|
|
8032
|
+
cancelBtn.textContent = "キャンセル";
|
|
8033
|
+
cancelBtn.hidden = true;
|
|
8034
|
+
const optionsRow = document.createElement("div");
|
|
8035
|
+
optionsRow.className = "db-global-search-options";
|
|
8036
|
+
const nonTextLabel = document.createElement("label");
|
|
8037
|
+
const nonTextCheck = document.createElement("input");
|
|
8038
|
+
nonTextCheck.type = "checkbox";
|
|
8039
|
+
nonTextLabel.append(nonTextCheck, " 数値・日付カラムも検索する");
|
|
8040
|
+
optionsRow.appendChild(nonTextLabel);
|
|
8041
|
+
header.append(input, searchBtn, cancelBtn);
|
|
8042
|
+
const progress = document.createElement("div");
|
|
8043
|
+
progress.className = "db-global-search-progress";
|
|
8044
|
+
progress.hidden = true;
|
|
8045
|
+
const results = document.createElement("div");
|
|
8046
|
+
results.className = "db-global-search-results";
|
|
8047
|
+
el.append(header, optionsRow, progress, results);
|
|
8048
|
+
let currentJobId = null;
|
|
8049
|
+
let pollTimer = null;
|
|
8050
|
+
function stopPolling() {
|
|
8051
|
+
if (pollTimer) {
|
|
8052
|
+
clearInterval(pollTimer);
|
|
8053
|
+
pollTimer = null;
|
|
8054
|
+
}
|
|
8055
|
+
cancelBtn.hidden = true;
|
|
8056
|
+
searchBtn.disabled = false;
|
|
8057
|
+
input.disabled = false;
|
|
8058
|
+
}
|
|
8059
|
+
async function startSearch() {
|
|
8060
|
+
const dbId = deps.getDbId();
|
|
8061
|
+
if (!dbId)
|
|
7848
8062
|
return;
|
|
7849
|
-
const
|
|
7850
|
-
|
|
7851
|
-
if (line === null || !path) {
|
|
7852
|
-
if (selection)
|
|
7853
|
-
clear();
|
|
8063
|
+
const term = input.value.trim();
|
|
8064
|
+
if (!term)
|
|
7854
8065
|
return;
|
|
8066
|
+
results.innerHTML = "";
|
|
8067
|
+
progress.hidden = false;
|
|
8068
|
+
progress.textContent = "検索を開始しています...";
|
|
8069
|
+
searchBtn.disabled = true;
|
|
8070
|
+
input.disabled = true;
|
|
8071
|
+
cancelBtn.hidden = false;
|
|
8072
|
+
try {
|
|
8073
|
+
const res = await fetch("/_db/search/start", {
|
|
8074
|
+
method: "POST",
|
|
8075
|
+
headers: {
|
|
8076
|
+
"Content-Type": "application/json",
|
|
8077
|
+
"X-Code-Viewer-Action": "1"
|
|
8078
|
+
},
|
|
8079
|
+
body: JSON.stringify({
|
|
8080
|
+
db: dbId,
|
|
8081
|
+
term,
|
|
8082
|
+
includeNonText: nonTextCheck.checked
|
|
8083
|
+
})
|
|
8084
|
+
});
|
|
8085
|
+
if (!res.ok) {
|
|
8086
|
+
progress.textContent = `エラー: ${await res.text()}`;
|
|
8087
|
+
stopPolling();
|
|
8088
|
+
return;
|
|
8089
|
+
}
|
|
8090
|
+
const data = await res.json();
|
|
8091
|
+
currentJobId = data.jobId;
|
|
8092
|
+
pollTimer = setInterval(() => pollStatus(), 500);
|
|
8093
|
+
} catch (err) {
|
|
8094
|
+
progress.textContent = `エラー: ${err instanceof Error ? err.message : String(err)}`;
|
|
8095
|
+
stopPolling();
|
|
7855
8096
|
}
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
});
|
|
7860
|
-
diff.addEventListener("mouseover", (e2) => {
|
|
7861
|
-
if (!drag)
|
|
8097
|
+
}
|
|
8098
|
+
async function pollStatus() {
|
|
8099
|
+
if (!currentJobId)
|
|
7862
8100
|
return;
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
8101
|
+
try {
|
|
8102
|
+
const res = await fetch(`/_db/search/status?id=${encodeURIComponent(currentJobId)}`);
|
|
8103
|
+
if (!res.ok) {
|
|
8104
|
+
stopPolling();
|
|
8105
|
+
return;
|
|
8106
|
+
}
|
|
8107
|
+
const data = await res.json();
|
|
8108
|
+
if (data.error) {
|
|
8109
|
+
progress.textContent = `エラー: ${data.error}`;
|
|
8110
|
+
stopPolling();
|
|
8111
|
+
return;
|
|
8112
|
+
}
|
|
8113
|
+
const pct = data.totalTables > 0 ? Math.round(data.scannedTables / data.totalTables * 100) : 0;
|
|
8114
|
+
progress.textContent = data.done ? `完了。${data.scannedTables}テーブルから ${data.hits.length}件見つかりました。` : `検索中... ${data.currentTable || "対象テーブルを確認中"} (${pct}% - ${data.scannedTables}/${data.totalTables}テーブル、${data.hits.length}件)`;
|
|
8115
|
+
renderHits(data.hits);
|
|
8116
|
+
if (data.done) {
|
|
8117
|
+
stopPolling();
|
|
8118
|
+
progress.hidden = false;
|
|
8119
|
+
}
|
|
8120
|
+
} catch {}
|
|
8121
|
+
}
|
|
8122
|
+
async function cancelSearch() {
|
|
8123
|
+
if (!currentJobId)
|
|
7866
8124
|
return;
|
|
7867
|
-
|
|
7868
|
-
|
|
8125
|
+
try {
|
|
8126
|
+
await fetch("/_db/search/cancel", {
|
|
8127
|
+
method: "POST",
|
|
8128
|
+
headers: {
|
|
8129
|
+
"Content-Type": "application/json",
|
|
8130
|
+
"X-Code-Viewer-Action": "1"
|
|
8131
|
+
},
|
|
8132
|
+
body: JSON.stringify({ id: currentJobId })
|
|
8133
|
+
});
|
|
8134
|
+
} catch {}
|
|
8135
|
+
stopPolling();
|
|
8136
|
+
progress.textContent = "検索をキャンセルしました。";
|
|
8137
|
+
}
|
|
8138
|
+
function renderHits(hits) {
|
|
8139
|
+
results.innerHTML = "";
|
|
8140
|
+
if (hits.length === 0)
|
|
7869
8141
|
return;
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
8142
|
+
const grouped = new Map;
|
|
8143
|
+
for (const h of hits) {
|
|
8144
|
+
const existing = grouped.get(h.table) || [];
|
|
8145
|
+
existing.push(h);
|
|
8146
|
+
grouped.set(h.table, existing);
|
|
8147
|
+
}
|
|
8148
|
+
for (const [table2, tableHits] of grouped) {
|
|
8149
|
+
const section = document.createElement("div");
|
|
8150
|
+
section.className = "db-search-table-section";
|
|
8151
|
+
const tableHeader = document.createElement("div");
|
|
8152
|
+
tableHeader.className = "db-search-table-header";
|
|
8153
|
+
tableHeader.textContent = `${table2} (${tableHits.length}件)`;
|
|
8154
|
+
section.appendChild(tableHeader);
|
|
8155
|
+
const hitsList = document.createElement("div");
|
|
8156
|
+
hitsList.className = "db-search-hits-list";
|
|
8157
|
+
for (const hit of tableHits.slice(0, 100)) {
|
|
8158
|
+
const row = document.createElement("div");
|
|
8159
|
+
row.className = "db-search-hit-row";
|
|
8160
|
+
const colSpan = document.createElement("span");
|
|
8161
|
+
colSpan.className = "db-search-hit-col";
|
|
8162
|
+
colSpan.textContent = hit.column;
|
|
8163
|
+
const valSpan = document.createElement("span");
|
|
8164
|
+
valSpan.className = "db-search-hit-val";
|
|
8165
|
+
valSpan.textContent = hit.valuePreview;
|
|
8166
|
+
row.append(colSpan, valSpan);
|
|
8167
|
+
hitsList.appendChild(row);
|
|
8168
|
+
}
|
|
8169
|
+
if (tableHits.length > 100) {
|
|
8170
|
+
const more = document.createElement("div");
|
|
8171
|
+
more.className = "db-search-more";
|
|
8172
|
+
more.textContent = `ほか ${tableHits.length - 100}件`;
|
|
8173
|
+
hitsList.appendChild(more);
|
|
8174
|
+
}
|
|
8175
|
+
section.appendChild(hitsList);
|
|
8176
|
+
results.appendChild(section);
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
searchBtn.addEventListener("click", startSearch);
|
|
8180
|
+
input.addEventListener("keydown", (e2) => {
|
|
8181
|
+
if (e2.key === "Enter")
|
|
8182
|
+
startSearch();
|
|
7878
8183
|
});
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
// web-src/core/file-path-copy.ts
|
|
7883
|
-
function filePathClipboardText(path) {
|
|
7884
|
-
return path || "";
|
|
7885
|
-
}
|
|
7886
|
-
function fileNameClipboardText(path) {
|
|
7887
|
-
if (!path)
|
|
7888
|
-
return "";
|
|
7889
|
-
const parts = path.split("/").filter(Boolean);
|
|
7890
|
-
return parts[parts.length - 1] || "";
|
|
7891
|
-
}
|
|
7892
|
-
function fileReferenceClipboardText(path, start, end) {
|
|
7893
|
-
if (!path)
|
|
7894
|
-
return "";
|
|
7895
|
-
const a2 = Math.max(1, Math.floor(Math.min(start, end)));
|
|
7896
|
-
const b2 = Math.max(1, Math.floor(Math.max(start, end)));
|
|
7897
|
-
return a2 === b2 ? `@${path}#${a2}` : `@${path}#${a2}-${b2}`;
|
|
8184
|
+
cancelBtn.addEventListener("click", cancelSearch);
|
|
8185
|
+
return { el };
|
|
7898
8186
|
}
|
|
7899
8187
|
|
|
7900
|
-
// web-src/
|
|
7901
|
-
|
|
7902
|
-
|
|
8188
|
+
// web-src/views/database/query-editor.ts
|
|
8189
|
+
var shikiPromise2 = null;
|
|
8190
|
+
function loadShikiSql() {
|
|
8191
|
+
if (!shikiPromise2) {
|
|
8192
|
+
shikiPromise2 = import("/shiki.js").then((mod) => {
|
|
8193
|
+
const typed = mod;
|
|
8194
|
+
return typed.createHighlighter({
|
|
8195
|
+
themes: ["github-light", "github-dark"],
|
|
8196
|
+
langs: ["sql"]
|
|
8197
|
+
});
|
|
8198
|
+
}).catch(() => null);
|
|
8199
|
+
}
|
|
8200
|
+
return shikiPromise2;
|
|
7903
8201
|
}
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
8202
|
+
var HISTORY_KEY = "db:query-history";
|
|
8203
|
+
var MAX_HISTORY = 50;
|
|
8204
|
+
function loadHistory() {
|
|
8205
|
+
try {
|
|
8206
|
+
const raw = localStorage.getItem(HISTORY_KEY);
|
|
8207
|
+
if (!raw)
|
|
8208
|
+
return [];
|
|
8209
|
+
const parsed = JSON.parse(raw);
|
|
8210
|
+
return Array.isArray(parsed) ? parsed.filter((s2) => typeof s2 === "string") : [];
|
|
8211
|
+
} catch {
|
|
8212
|
+
return [];
|
|
8213
|
+
}
|
|
8214
|
+
}
|
|
8215
|
+
function saveToHistory(sql) {
|
|
8216
|
+
const history2 = loadHistory();
|
|
8217
|
+
const idx = history2.indexOf(sql);
|
|
8218
|
+
if (idx >= 0)
|
|
8219
|
+
history2.splice(idx, 1);
|
|
8220
|
+
history2.unshift(sql);
|
|
8221
|
+
if (history2.length > MAX_HISTORY)
|
|
8222
|
+
history2.length = MAX_HISTORY;
|
|
8223
|
+
localStorage.setItem(HISTORY_KEY, JSON.stringify(history2));
|
|
8224
|
+
}
|
|
8225
|
+
function createQueryEditor(callbacks) {
|
|
8226
|
+
const el = document.createElement("div");
|
|
8227
|
+
el.className = "db-query-editor";
|
|
8228
|
+
const inputArea = document.createElement("div");
|
|
8229
|
+
inputArea.className = "db-query-input";
|
|
8230
|
+
const editorWrap = document.createElement("div");
|
|
8231
|
+
editorWrap.className = "db-query-editor-wrap";
|
|
8232
|
+
const highlight = document.createElement("pre");
|
|
8233
|
+
highlight.className = "db-query-highlight";
|
|
8234
|
+
highlight.setAttribute("aria-hidden", "true");
|
|
8235
|
+
const textarea = document.createElement("textarea");
|
|
8236
|
+
textarea.className = "db-query-textarea";
|
|
8237
|
+
textarea.placeholder = "SELECT * FROM ...";
|
|
8238
|
+
textarea.spellcheck = false;
|
|
8239
|
+
textarea.rows = 3;
|
|
8240
|
+
editorWrap.append(highlight, textarea);
|
|
8241
|
+
let shiki = null;
|
|
8242
|
+
loadShikiSql().then((h) => {
|
|
8243
|
+
shiki = h;
|
|
8244
|
+
syncHighlight();
|
|
8245
|
+
});
|
|
8246
|
+
function syncEditorHeight() {
|
|
8247
|
+
textarea.style.height = "auto";
|
|
8248
|
+
const h = Math.max(60, Math.min(textarea.scrollHeight, 300));
|
|
8249
|
+
textarea.style.height = `${h}px`;
|
|
8250
|
+
editorWrap.style.height = `${h}px`;
|
|
8251
|
+
}
|
|
8252
|
+
function syncHighlight() {
|
|
8253
|
+
const code2 = textarea.value;
|
|
8254
|
+
if (!code2) {
|
|
8255
|
+
highlight.innerHTML = "";
|
|
8256
|
+
syncEditorHeight();
|
|
7907
8257
|
return;
|
|
7908
|
-
|
|
7909
|
-
if (!
|
|
8258
|
+
}
|
|
8259
|
+
if (!shiki) {
|
|
8260
|
+
highlight.textContent = code2;
|
|
8261
|
+
syncEditorHeight();
|
|
7910
8262
|
return;
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
function isVideo(p2) {
|
|
7927
|
-
return VIDEO_RE.test(p2);
|
|
7928
|
-
}
|
|
7929
|
-
function isAudio(p2) {
|
|
7930
|
-
return AUDIO_RE.test(p2);
|
|
7931
|
-
}
|
|
7932
|
-
function fileURL(path, ref) {
|
|
7933
|
-
return `/_file?path=${encodeURIComponent(path)}&ref=${ref}`;
|
|
7934
|
-
}
|
|
7935
|
-
function mediaTag(path, ref) {
|
|
7936
|
-
const url = fileURL(path, ref);
|
|
7937
|
-
if (isVideo(path)) {
|
|
7938
|
-
return `<video src="${url}" controls preload="metadata"></video>`;
|
|
8263
|
+
}
|
|
8264
|
+
const html = shiki.codeToHtml(code2, {
|
|
8265
|
+
lang: "sql",
|
|
8266
|
+
themes: { light: "github-light", dark: "github-dark" },
|
|
8267
|
+
defaultColor: false
|
|
8268
|
+
});
|
|
8269
|
+
const template = document.createElement("template");
|
|
8270
|
+
template.innerHTML = html;
|
|
8271
|
+
const pre = template.content.querySelector("pre");
|
|
8272
|
+
if (pre) {
|
|
8273
|
+
highlight.innerHTML = pre.innerHTML;
|
|
8274
|
+
} else {
|
|
8275
|
+
highlight.textContent = code2;
|
|
8276
|
+
}
|
|
8277
|
+
syncEditorHeight();
|
|
7939
8278
|
}
|
|
7940
|
-
|
|
7941
|
-
|
|
8279
|
+
textarea.addEventListener("input", syncHighlight);
|
|
8280
|
+
textarea.addEventListener("scroll", () => {
|
|
8281
|
+
highlight.scrollTop = textarea.scrollTop;
|
|
8282
|
+
highlight.scrollLeft = textarea.scrollLeft;
|
|
8283
|
+
});
|
|
8284
|
+
const toolbar = document.createElement("div");
|
|
8285
|
+
toolbar.className = "db-query-toolbar";
|
|
8286
|
+
const runBtn = document.createElement("button");
|
|
8287
|
+
runBtn.className = "db-btn db-btn-primary db-query-run";
|
|
8288
|
+
runBtn.type = "button";
|
|
8289
|
+
runBtn.textContent = "Run";
|
|
8290
|
+
runBtn.title = "Execute query (Ctrl+Enter)";
|
|
8291
|
+
const explainBtn = document.createElement("button");
|
|
8292
|
+
explainBtn.className = "db-btn db-query-explain";
|
|
8293
|
+
explainBtn.type = "button";
|
|
8294
|
+
explainBtn.textContent = "Explain";
|
|
8295
|
+
explainBtn.title = "Show query execution plan";
|
|
8296
|
+
const historyBtn = document.createElement("button");
|
|
8297
|
+
historyBtn.className = "db-btn db-query-history-btn";
|
|
8298
|
+
historyBtn.type = "button";
|
|
8299
|
+
historyBtn.textContent = "History";
|
|
8300
|
+
historyBtn.title = "Query history";
|
|
8301
|
+
const statusSpan = document.createElement("span");
|
|
8302
|
+
statusSpan.className = "db-query-status";
|
|
8303
|
+
const historyDropdown = document.createElement("div");
|
|
8304
|
+
historyDropdown.className = "db-query-history-dropdown";
|
|
8305
|
+
historyDropdown.hidden = true;
|
|
8306
|
+
toolbar.append(runBtn, explainBtn, historyBtn, statusSpan);
|
|
8307
|
+
inputArea.append(editorWrap, toolbar, historyDropdown);
|
|
8308
|
+
const resultArea = document.createElement("div");
|
|
8309
|
+
resultArea.className = "db-query-result";
|
|
8310
|
+
resultArea.hidden = true;
|
|
8311
|
+
el.append(inputArea, resultArea);
|
|
8312
|
+
async function run() {
|
|
8313
|
+
const sql = textarea.value.trim();
|
|
8314
|
+
if (!sql)
|
|
8315
|
+
return;
|
|
8316
|
+
runBtn.disabled = true;
|
|
8317
|
+
statusSpan.textContent = "Running…";
|
|
8318
|
+
resultArea.hidden = true;
|
|
8319
|
+
try {
|
|
8320
|
+
const result = await callbacks.executeQuery(sql);
|
|
8321
|
+
if (result.error) {
|
|
8322
|
+
statusSpan.textContent = `Error (${result.elapsedMs}ms)`;
|
|
8323
|
+
resultArea.hidden = false;
|
|
8324
|
+
resultArea.innerHTML = "";
|
|
8325
|
+
const errEl = document.createElement("pre");
|
|
8326
|
+
errEl.className = "db-query-error";
|
|
8327
|
+
errEl.textContent = result.error;
|
|
8328
|
+
resultArea.appendChild(errEl);
|
|
8329
|
+
return;
|
|
8330
|
+
}
|
|
8331
|
+
saveToHistory(sql);
|
|
8332
|
+
const suffix = result.truncated ? "+" : "";
|
|
8333
|
+
statusSpan.textContent = `${result.rowCount}${suffix} rows (${result.elapsedMs}ms)`;
|
|
8334
|
+
renderResultTable(result);
|
|
8335
|
+
} catch (err) {
|
|
8336
|
+
statusSpan.textContent = "Failed";
|
|
8337
|
+
resultArea.hidden = false;
|
|
8338
|
+
resultArea.innerHTML = "";
|
|
8339
|
+
const errEl = document.createElement("pre");
|
|
8340
|
+
errEl.className = "db-query-error";
|
|
8341
|
+
errEl.textContent = err instanceof Error ? err.message : String(err);
|
|
8342
|
+
resultArea.appendChild(errEl);
|
|
8343
|
+
} finally {
|
|
8344
|
+
runBtn.disabled = false;
|
|
8345
|
+
}
|
|
7942
8346
|
}
|
|
7943
|
-
|
|
7944
|
-
|
|
8347
|
+
function renderResultTable(result) {
|
|
8348
|
+
resultArea.hidden = false;
|
|
8349
|
+
resultArea.innerHTML = "";
|
|
8350
|
+
if (result.columns.length === 0) {
|
|
8351
|
+
resultArea.textContent = "Query returned no columns.";
|
|
8352
|
+
return;
|
|
8353
|
+
}
|
|
8354
|
+
const table2 = document.createElement("table");
|
|
8355
|
+
table2.className = "db-query-table";
|
|
8356
|
+
const thead = document.createElement("thead");
|
|
8357
|
+
const headRow = document.createElement("tr");
|
|
8358
|
+
const thNum = document.createElement("th");
|
|
8359
|
+
thNum.textContent = "#";
|
|
8360
|
+
thNum.className = "db-grid-rownum";
|
|
8361
|
+
headRow.appendChild(thNum);
|
|
8362
|
+
for (const col of result.columns) {
|
|
8363
|
+
const th = document.createElement("th");
|
|
8364
|
+
th.textContent = col;
|
|
8365
|
+
headRow.appendChild(th);
|
|
8366
|
+
}
|
|
8367
|
+
thead.appendChild(headRow);
|
|
8368
|
+
const tbody = document.createElement("tbody");
|
|
8369
|
+
for (let i2 = 0;i2 < result.rows.length; i2++) {
|
|
8370
|
+
const row = result.rows[i2];
|
|
8371
|
+
const tr = document.createElement("tr");
|
|
8372
|
+
if (i2 % 2 === 1)
|
|
8373
|
+
tr.classList.add("alt");
|
|
8374
|
+
const tdNum = document.createElement("td");
|
|
8375
|
+
tdNum.className = "db-grid-rownum";
|
|
8376
|
+
tdNum.textContent = String(i2 + 1);
|
|
8377
|
+
tr.appendChild(tdNum);
|
|
8378
|
+
for (const value of row) {
|
|
8379
|
+
const td = document.createElement("td");
|
|
8380
|
+
td.textContent = formatValue(value);
|
|
8381
|
+
if (value === null)
|
|
8382
|
+
td.classList.add("null");
|
|
8383
|
+
tr.appendChild(td);
|
|
8384
|
+
}
|
|
8385
|
+
tbody.appendChild(tr);
|
|
8386
|
+
}
|
|
8387
|
+
table2.append(thead, tbody);
|
|
8388
|
+
const wrapper = document.createElement("div");
|
|
8389
|
+
wrapper.className = "db-query-table-wrap";
|
|
8390
|
+
wrapper.appendChild(table2);
|
|
8391
|
+
resultArea.appendChild(wrapper);
|
|
8392
|
+
}
|
|
8393
|
+
async function runExplain() {
|
|
8394
|
+
const sql = textarea.value.trim();
|
|
8395
|
+
if (!sql)
|
|
8396
|
+
return;
|
|
8397
|
+
explainBtn.disabled = true;
|
|
8398
|
+
runBtn.disabled = true;
|
|
8399
|
+
statusSpan.textContent = "Explaining…";
|
|
8400
|
+
resultArea.hidden = true;
|
|
8401
|
+
try {
|
|
8402
|
+
const result = await callbacks.executeQuery(`EXPLAIN QUERY PLAN ${sql}`);
|
|
8403
|
+
if (result.error) {
|
|
8404
|
+
statusSpan.textContent = `Error (${result.elapsedMs}ms)`;
|
|
8405
|
+
resultArea.hidden = false;
|
|
8406
|
+
resultArea.innerHTML = "";
|
|
8407
|
+
const errEl = document.createElement("pre");
|
|
8408
|
+
errEl.className = "db-query-error";
|
|
8409
|
+
errEl.textContent = result.error;
|
|
8410
|
+
resultArea.appendChild(errEl);
|
|
8411
|
+
return;
|
|
8412
|
+
}
|
|
8413
|
+
statusSpan.textContent = `Explain (${result.elapsedMs}ms)`;
|
|
8414
|
+
renderResultTable(result);
|
|
8415
|
+
} catch (err) {
|
|
8416
|
+
statusSpan.textContent = "Failed";
|
|
8417
|
+
resultArea.hidden = false;
|
|
8418
|
+
resultArea.innerHTML = "";
|
|
8419
|
+
const errEl = document.createElement("pre");
|
|
8420
|
+
errEl.className = "db-query-error";
|
|
8421
|
+
errEl.textContent = err instanceof Error ? err.message : String(err);
|
|
8422
|
+
resultArea.appendChild(errEl);
|
|
8423
|
+
} finally {
|
|
8424
|
+
explainBtn.disabled = false;
|
|
8425
|
+
runBtn.disabled = false;
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
runBtn.addEventListener("click", run);
|
|
8429
|
+
explainBtn.addEventListener("click", runExplain);
|
|
8430
|
+
historyBtn.addEventListener("click", () => {
|
|
8431
|
+
if (!historyDropdown.hidden) {
|
|
8432
|
+
historyDropdown.hidden = true;
|
|
8433
|
+
return;
|
|
8434
|
+
}
|
|
8435
|
+
const history2 = loadHistory();
|
|
8436
|
+
historyDropdown.innerHTML = "";
|
|
8437
|
+
if (history2.length === 0) {
|
|
8438
|
+
const empty = document.createElement("div");
|
|
8439
|
+
empty.className = "db-query-history-empty";
|
|
8440
|
+
empty.textContent = "No history";
|
|
8441
|
+
historyDropdown.appendChild(empty);
|
|
8442
|
+
} else {
|
|
8443
|
+
for (const sql of history2) {
|
|
8444
|
+
const item = document.createElement("div");
|
|
8445
|
+
item.className = "db-query-history-item";
|
|
8446
|
+
item.textContent = sql.length > 100 ? `${sql.slice(0, 100)}...` : sql;
|
|
8447
|
+
item.title = sql;
|
|
8448
|
+
item.addEventListener("click", () => {
|
|
8449
|
+
setSql(sql);
|
|
8450
|
+
historyDropdown.hidden = true;
|
|
8451
|
+
});
|
|
8452
|
+
historyDropdown.appendChild(item);
|
|
8453
|
+
}
|
|
8454
|
+
}
|
|
8455
|
+
historyDropdown.hidden = false;
|
|
8456
|
+
});
|
|
8457
|
+
document.addEventListener("click", (e2) => {
|
|
8458
|
+
if (!historyDropdown.hidden && !historyBtn.contains(e2.target) && !historyDropdown.contains(e2.target)) {
|
|
8459
|
+
historyDropdown.hidden = true;
|
|
8460
|
+
}
|
|
8461
|
+
});
|
|
8462
|
+
textarea.addEventListener("keydown", (e2) => {
|
|
8463
|
+
if ((e2.ctrlKey || e2.metaKey) && e2.key === "Enter") {
|
|
8464
|
+
e2.preventDefault();
|
|
8465
|
+
run();
|
|
8466
|
+
return;
|
|
8467
|
+
}
|
|
8468
|
+
if (e2.key === "Tab") {
|
|
8469
|
+
e2.preventDefault();
|
|
8470
|
+
const start = textarea.selectionStart;
|
|
8471
|
+
const end = textarea.selectionEnd;
|
|
8472
|
+
if (e2.shiftKey) {
|
|
8473
|
+
const before = textarea.value.slice(0, start);
|
|
8474
|
+
const lineStart = before.lastIndexOf(`
|
|
8475
|
+
`) + 1;
|
|
8476
|
+
const linePrefix = textarea.value.slice(lineStart, start);
|
|
8477
|
+
const spaces = linePrefix.match(/^ {1,2}/);
|
|
8478
|
+
if (spaces) {
|
|
8479
|
+
textarea.setRangeText("", lineStart, lineStart + spaces[0].length, "end");
|
|
8480
|
+
}
|
|
8481
|
+
} else {
|
|
8482
|
+
textarea.setRangeText(" ", start, end, "end");
|
|
8483
|
+
}
|
|
8484
|
+
syncHighlight();
|
|
8485
|
+
}
|
|
8486
|
+
});
|
|
8487
|
+
function focus() {
|
|
8488
|
+
textarea.focus();
|
|
8489
|
+
}
|
|
8490
|
+
function setSql(sql) {
|
|
8491
|
+
textarea.value = sql;
|
|
8492
|
+
syncHighlight();
|
|
8493
|
+
}
|
|
8494
|
+
return { el, focus, setSql };
|
|
8495
|
+
}
|
|
8496
|
+
function formatValue(value) {
|
|
8497
|
+
if (value === null)
|
|
8498
|
+
return "NULL";
|
|
8499
|
+
if (value instanceof Uint8Array)
|
|
8500
|
+
return `<blob ${value.byteLength} bytes>`;
|
|
8501
|
+
if (typeof value === "boolean")
|
|
8502
|
+
return value ? "true" : "false";
|
|
8503
|
+
if (typeof value === "object")
|
|
8504
|
+
return JSON.stringify(value);
|
|
8505
|
+
return String(value);
|
|
8506
|
+
}
|
|
8507
|
+
|
|
8508
|
+
// web-src/views/database/query-history-view.ts
|
|
8509
|
+
function createQueryHistoryView(callbacks) {
|
|
8510
|
+
const el = document.createElement("div");
|
|
8511
|
+
el.className = "db-query-history";
|
|
8512
|
+
const toolbar = document.createElement("div");
|
|
8513
|
+
toolbar.className = "db-query-history-toolbar";
|
|
8514
|
+
const refreshBtn = document.createElement("button");
|
|
8515
|
+
refreshBtn.className = "db-query-history-action";
|
|
8516
|
+
refreshBtn.type = "button";
|
|
8517
|
+
refreshBtn.textContent = "Refresh";
|
|
8518
|
+
refreshBtn.title = "Refresh history";
|
|
8519
|
+
const clearBtn = document.createElement("button");
|
|
8520
|
+
clearBtn.className = "db-query-history-action db-query-history-danger";
|
|
8521
|
+
clearBtn.type = "button";
|
|
8522
|
+
clearBtn.textContent = "Clear All";
|
|
8523
|
+
clearBtn.title = "Delete all query history";
|
|
8524
|
+
toolbar.append(refreshBtn, clearBtn);
|
|
8525
|
+
const body = document.createElement("div");
|
|
8526
|
+
body.className = "db-query-history-body-split";
|
|
8527
|
+
const listCol = document.createElement("div");
|
|
8528
|
+
listCol.className = "db-query-history-list-col";
|
|
8529
|
+
const listEl = document.createElement("div");
|
|
8530
|
+
listEl.className = "db-query-history-list";
|
|
8531
|
+
listCol.appendChild(listEl);
|
|
8532
|
+
const detailCol = document.createElement("div");
|
|
8533
|
+
detailCol.className = "db-query-history-detail-col";
|
|
8534
|
+
const detailPlaceholder = document.createElement("div");
|
|
8535
|
+
detailPlaceholder.className = "db-query-history-detail-placeholder";
|
|
8536
|
+
detailPlaceholder.textContent = "Select a query to view details";
|
|
8537
|
+
detailCol.appendChild(detailPlaceholder);
|
|
8538
|
+
body.append(listCol, detailCol);
|
|
8539
|
+
el.append(toolbar, body);
|
|
8540
|
+
let entries = [];
|
|
8541
|
+
const expandedIds = new Set;
|
|
8542
|
+
async function refresh() {
|
|
8543
|
+
const dbId = callbacks.getDbId();
|
|
8544
|
+
const params = dbId ? `?db=${encodeURIComponent(dbId)}` : "";
|
|
8545
|
+
try {
|
|
8546
|
+
const res = await fetch(`/_db/history${params}`);
|
|
8547
|
+
if (!res.ok)
|
|
8548
|
+
return;
|
|
8549
|
+
const state = await res.json();
|
|
8550
|
+
entries = state.entries;
|
|
8551
|
+
render();
|
|
8552
|
+
} catch {}
|
|
8553
|
+
}
|
|
8554
|
+
function render() {
|
|
8555
|
+
listEl.innerHTML = "";
|
|
8556
|
+
if (entries.length === 0) {
|
|
8557
|
+
const empty = document.createElement("div");
|
|
8558
|
+
empty.className = "db-query-history-empty";
|
|
8559
|
+
empty.textContent = "No query history";
|
|
8560
|
+
listEl.appendChild(empty);
|
|
8561
|
+
return;
|
|
8562
|
+
}
|
|
8563
|
+
for (const entry of entries) {
|
|
8564
|
+
listEl.appendChild(renderEntry(entry));
|
|
8565
|
+
}
|
|
8566
|
+
}
|
|
8567
|
+
let selectedEntryId = null;
|
|
8568
|
+
function selectEntry(entry) {
|
|
8569
|
+
selectedEntryId = entry.id;
|
|
8570
|
+
listEl.querySelectorAll(".db-query-history-entry").forEach((el2) => {
|
|
8571
|
+
el2.classList.toggle("selected", el2.dataset.id === entry.id);
|
|
8572
|
+
});
|
|
8573
|
+
renderDetail(entry);
|
|
8574
|
+
}
|
|
8575
|
+
function renderDetail(entry) {
|
|
8576
|
+
detailCol.innerHTML = "";
|
|
8577
|
+
const actions = document.createElement("div");
|
|
8578
|
+
actions.className = "db-query-history-detail-actions";
|
|
8579
|
+
const useBtn = document.createElement("button");
|
|
8580
|
+
useBtn.className = "db-btn db-btn-primary";
|
|
8581
|
+
useBtn.type = "button";
|
|
8582
|
+
useBtn.textContent = "Use in Editor";
|
|
8583
|
+
useBtn.addEventListener("click", () => callbacks.copySqlToQuery(entry.sql));
|
|
8584
|
+
const copyBtn = document.createElement("button");
|
|
8585
|
+
copyBtn.className = "db-btn";
|
|
8586
|
+
copyBtn.type = "button";
|
|
8587
|
+
copyBtn.textContent = "Copy SQL";
|
|
8588
|
+
copyBtn.addEventListener("click", () => {
|
|
8589
|
+
navigator.clipboard.writeText(entry.sql).then(() => {
|
|
8590
|
+
copyBtn.textContent = "Copied!";
|
|
8591
|
+
setTimeout(() => {
|
|
8592
|
+
copyBtn.textContent = "Copy SQL";
|
|
8593
|
+
}, 1500);
|
|
8594
|
+
}, () => {});
|
|
8595
|
+
});
|
|
8596
|
+
const deleteBtn = document.createElement("button");
|
|
8597
|
+
deleteBtn.className = "db-btn db-query-history-danger";
|
|
8598
|
+
deleteBtn.type = "button";
|
|
8599
|
+
deleteBtn.textContent = "Delete";
|
|
8600
|
+
deleteBtn.addEventListener("click", () => {
|
|
8601
|
+
deleteEntry(entry.id);
|
|
8602
|
+
});
|
|
8603
|
+
actions.append(useBtn, copyBtn, deleteBtn);
|
|
8604
|
+
const sqlBlock = document.createElement("pre");
|
|
8605
|
+
sqlBlock.className = "db-query-history-sql";
|
|
8606
|
+
sqlBlock.textContent = entry.sql;
|
|
8607
|
+
detailCol.append(actions, sqlBlock);
|
|
8608
|
+
if (entry.body) {
|
|
8609
|
+
const bodyBlock = document.createElement("div");
|
|
8610
|
+
bodyBlock.className = "db-query-history-body";
|
|
8611
|
+
bodyBlock.textContent = entry.body;
|
|
8612
|
+
detailCol.appendChild(bodyBlock);
|
|
8613
|
+
}
|
|
8614
|
+
if (entry.columns.length > 0 && entry.rowsPreview.length > 0) {
|
|
8615
|
+
detailCol.appendChild(renderPreviewTable(entry));
|
|
8616
|
+
}
|
|
8617
|
+
if (entry.savedRows < entry.rowCount) {
|
|
8618
|
+
const note = document.createElement("div");
|
|
8619
|
+
note.className = "db-query-history-truncated";
|
|
8620
|
+
note.textContent = `Showing ${entry.savedRows} of ${entry.rowCount} rows`;
|
|
8621
|
+
detailCol.appendChild(note);
|
|
8622
|
+
}
|
|
8623
|
+
}
|
|
8624
|
+
function renderEntry(entry) {
|
|
8625
|
+
const item = document.createElement("div");
|
|
8626
|
+
item.className = "db-query-history-entry";
|
|
8627
|
+
if (entry.id === selectedEntryId)
|
|
8628
|
+
item.classList.add("selected");
|
|
8629
|
+
item.dataset.id = entry.id;
|
|
8630
|
+
const meta = document.createElement("div");
|
|
8631
|
+
meta.className = "db-query-history-entry-meta";
|
|
8632
|
+
const byIcon = document.createElement("span");
|
|
8633
|
+
byIcon.className = "db-query-history-by";
|
|
8634
|
+
byIcon.textContent = entry.executedBy === "ai" ? "[AI]" : "[User]";
|
|
8635
|
+
const time = document.createElement("span");
|
|
8636
|
+
time.className = "db-query-history-time";
|
|
8637
|
+
time.textContent = formatTime(entry.executedAt);
|
|
8638
|
+
time.title = entry.executedAt;
|
|
8639
|
+
const stats = document.createElement("span");
|
|
8640
|
+
stats.className = "db-query-history-stats";
|
|
8641
|
+
const truncMark = entry.truncated ? "+" : "";
|
|
8642
|
+
stats.textContent = `${entry.rowCount}${truncMark} rows, ${entry.elapsedMs}ms`;
|
|
8643
|
+
meta.append(byIcon, time, stats);
|
|
8644
|
+
const title = document.createElement("div");
|
|
8645
|
+
title.className = "db-query-history-entry-title";
|
|
8646
|
+
title.textContent = entry.title || (entry.sql.length > 80 ? `${entry.sql.slice(0, 80)}...` : entry.sql);
|
|
8647
|
+
item.append(meta, title);
|
|
8648
|
+
item.addEventListener("click", () => selectEntry(entry));
|
|
8649
|
+
return item;
|
|
8650
|
+
}
|
|
8651
|
+
function renderPreviewTable(entry) {
|
|
8652
|
+
const wrapper = document.createElement("div");
|
|
8653
|
+
wrapper.className = "db-query-table-wrap";
|
|
8654
|
+
const table2 = document.createElement("table");
|
|
8655
|
+
table2.className = "db-query-table";
|
|
8656
|
+
const thead = document.createElement("thead");
|
|
8657
|
+
const headRow = document.createElement("tr");
|
|
8658
|
+
const thNum = document.createElement("th");
|
|
8659
|
+
thNum.textContent = "#";
|
|
8660
|
+
thNum.className = "db-grid-rownum";
|
|
8661
|
+
headRow.appendChild(thNum);
|
|
8662
|
+
for (const col of entry.columns) {
|
|
8663
|
+
const th = document.createElement("th");
|
|
8664
|
+
th.textContent = col;
|
|
8665
|
+
headRow.appendChild(th);
|
|
8666
|
+
}
|
|
8667
|
+
thead.appendChild(headRow);
|
|
8668
|
+
const tbody = document.createElement("tbody");
|
|
8669
|
+
for (let i2 = 0;i2 < entry.rowsPreview.length; i2++) {
|
|
8670
|
+
const row = entry.rowsPreview[i2];
|
|
8671
|
+
const tr = document.createElement("tr");
|
|
8672
|
+
if (i2 % 2 === 1)
|
|
8673
|
+
tr.classList.add("alt");
|
|
8674
|
+
const tdNum = document.createElement("td");
|
|
8675
|
+
tdNum.className = "db-grid-rownum";
|
|
8676
|
+
tdNum.textContent = String(i2 + 1);
|
|
8677
|
+
tr.appendChild(tdNum);
|
|
8678
|
+
for (const value of row) {
|
|
8679
|
+
const td = document.createElement("td");
|
|
8680
|
+
td.textContent = formatValue2(value);
|
|
8681
|
+
if (value === null)
|
|
8682
|
+
td.classList.add("null");
|
|
8683
|
+
tr.appendChild(td);
|
|
8684
|
+
}
|
|
8685
|
+
tbody.appendChild(tr);
|
|
8686
|
+
}
|
|
8687
|
+
table2.append(thead, tbody);
|
|
8688
|
+
wrapper.appendChild(table2);
|
|
8689
|
+
if (entry.savedRows < entry.rowCount) {
|
|
8690
|
+
const note = document.createElement("div");
|
|
8691
|
+
note.className = "db-query-history-truncated";
|
|
8692
|
+
note.textContent = `Showing ${entry.savedRows} of ${entry.rowCount} rows`;
|
|
8693
|
+
wrapper.appendChild(note);
|
|
8694
|
+
}
|
|
8695
|
+
return wrapper;
|
|
8696
|
+
}
|
|
8697
|
+
function resetDetailCol() {
|
|
8698
|
+
detailCol.innerHTML = "";
|
|
8699
|
+
detailCol.appendChild(detailPlaceholder);
|
|
8700
|
+
selectedEntryId = null;
|
|
8701
|
+
}
|
|
8702
|
+
async function deleteEntry(id) {
|
|
8703
|
+
try {
|
|
8704
|
+
await fetch("/_db/history/delete", {
|
|
8705
|
+
method: "POST",
|
|
8706
|
+
headers: {
|
|
8707
|
+
"Content-Type": "application/json",
|
|
8708
|
+
"X-Code-Viewer-Action": "1"
|
|
8709
|
+
},
|
|
8710
|
+
body: JSON.stringify({ id })
|
|
8711
|
+
});
|
|
8712
|
+
entries = entries.filter((e2) => e2.id !== id);
|
|
8713
|
+
expandedIds.delete(id);
|
|
8714
|
+
if (selectedEntryId === id)
|
|
8715
|
+
resetDetailCol();
|
|
8716
|
+
const itemEl = listEl.querySelector(`.db-query-history-entry[data-id="${CSS.escape(id)}"]`);
|
|
8717
|
+
if (itemEl) {
|
|
8718
|
+
itemEl.remove();
|
|
8719
|
+
if (entries.length === 0)
|
|
8720
|
+
render();
|
|
8721
|
+
} else {
|
|
8722
|
+
render();
|
|
8723
|
+
}
|
|
8724
|
+
} catch {}
|
|
8725
|
+
}
|
|
8726
|
+
clearBtn.addEventListener("click", async () => {
|
|
8727
|
+
const dbId = callbacks.getDbId();
|
|
8728
|
+
try {
|
|
8729
|
+
await fetch("/_db/history/clear", {
|
|
8730
|
+
method: "POST",
|
|
8731
|
+
headers: {
|
|
8732
|
+
"Content-Type": "application/json",
|
|
8733
|
+
"X-Code-Viewer-Action": "1"
|
|
8734
|
+
},
|
|
8735
|
+
body: JSON.stringify(dbId ? { db: dbId } : {})
|
|
8736
|
+
});
|
|
8737
|
+
entries = [];
|
|
8738
|
+
render();
|
|
8739
|
+
} catch {}
|
|
8740
|
+
});
|
|
8741
|
+
refreshBtn.addEventListener("click", () => {
|
|
8742
|
+
refresh();
|
|
8743
|
+
});
|
|
8744
|
+
return { el, refresh };
|
|
8745
|
+
}
|
|
8746
|
+
function formatTime(iso) {
|
|
8747
|
+
try {
|
|
8748
|
+
const d2 = new Date(iso);
|
|
8749
|
+
const pad = (n2) => String(n2).padStart(2, "0");
|
|
8750
|
+
return `${d2.getFullYear()}-${pad(d2.getMonth() + 1)}-${pad(d2.getDate())} ${pad(d2.getHours())}:${pad(d2.getMinutes())}:${pad(d2.getSeconds())}`;
|
|
8751
|
+
} catch {
|
|
8752
|
+
return iso;
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
8755
|
+
function formatValue2(value) {
|
|
8756
|
+
if (value === null)
|
|
8757
|
+
return "NULL";
|
|
8758
|
+
if (value instanceof Uint8Array)
|
|
8759
|
+
return `<blob ${value.byteLength} bytes>`;
|
|
8760
|
+
if (typeof value === "boolean")
|
|
8761
|
+
return value ? "true" : "false";
|
|
8762
|
+
if (typeof value === "object")
|
|
8763
|
+
return JSON.stringify(value);
|
|
8764
|
+
return String(value);
|
|
8765
|
+
}
|
|
8766
|
+
|
|
8767
|
+
// web-src/views/database/schema-view.ts
|
|
8768
|
+
function createSchemaView() {
|
|
8769
|
+
const el = document.createElement("div");
|
|
8770
|
+
el.className = "db-schema-view";
|
|
8771
|
+
el.hidden = true;
|
|
8772
|
+
function render(table2, columns, indexes, extra) {
|
|
8773
|
+
el.hidden = false;
|
|
8774
|
+
el.innerHTML = "";
|
|
8775
|
+
const header = document.createElement("div");
|
|
8776
|
+
header.className = "db-schema-header";
|
|
8777
|
+
header.textContent = `Schema: ${table2}`;
|
|
8778
|
+
el.appendChild(header);
|
|
8779
|
+
const colSectionHeader = document.createElement("div");
|
|
8780
|
+
colSectionHeader.className = "db-schema-section-header";
|
|
8781
|
+
colSectionHeader.textContent = "Columns";
|
|
8782
|
+
el.appendChild(colSectionHeader);
|
|
8783
|
+
const colTable = document.createElement("table");
|
|
8784
|
+
colTable.className = "db-schema-table";
|
|
8785
|
+
const thead = document.createElement("thead");
|
|
8786
|
+
const headRow = document.createElement("tr");
|
|
8787
|
+
for (const label of ["Column", "Type", "Nullable", "PK", "Default"]) {
|
|
8788
|
+
const th = document.createElement("th");
|
|
8789
|
+
th.textContent = label;
|
|
8790
|
+
headRow.appendChild(th);
|
|
8791
|
+
}
|
|
8792
|
+
thead.appendChild(headRow);
|
|
8793
|
+
const tbody = document.createElement("tbody");
|
|
8794
|
+
for (const col of columns) {
|
|
8795
|
+
const tr = document.createElement("tr");
|
|
8796
|
+
if (col.primaryKey)
|
|
8797
|
+
tr.classList.add("pk-row");
|
|
8798
|
+
const tdName = document.createElement("td");
|
|
8799
|
+
tdName.textContent = col.name;
|
|
8800
|
+
tdName.className = "db-schema-col-name";
|
|
8801
|
+
const tdType = document.createElement("td");
|
|
8802
|
+
tdType.textContent = col.type;
|
|
8803
|
+
tdType.className = "db-schema-col-type";
|
|
8804
|
+
const tdNull = document.createElement("td");
|
|
8805
|
+
tdNull.textContent = col.nullable ? "YES" : "NO";
|
|
8806
|
+
const tdPk = document.createElement("td");
|
|
8807
|
+
tdPk.textContent = col.primaryKey ? "PK" : "";
|
|
8808
|
+
if (col.primaryKey)
|
|
8809
|
+
tdPk.className = "db-schema-pk";
|
|
8810
|
+
const tdDefault = document.createElement("td");
|
|
8811
|
+
tdDefault.textContent = col.defaultValue ?? "";
|
|
8812
|
+
tdDefault.className = "db-schema-default";
|
|
8813
|
+
tr.append(tdName, tdType, tdNull, tdPk, tdDefault);
|
|
8814
|
+
tbody.appendChild(tr);
|
|
8815
|
+
}
|
|
8816
|
+
colTable.append(thead, tbody);
|
|
8817
|
+
el.appendChild(colTable);
|
|
8818
|
+
const fks = extra?.foreignKeys?.filter((fk) => fk.fromTable === table2);
|
|
8819
|
+
if (fks && fks.length > 0) {
|
|
8820
|
+
const fkHeader = document.createElement("div");
|
|
8821
|
+
fkHeader.className = "db-schema-section-header";
|
|
8822
|
+
fkHeader.textContent = "Foreign Keys";
|
|
8823
|
+
el.appendChild(fkHeader);
|
|
8824
|
+
const fkTable = document.createElement("table");
|
|
8825
|
+
fkTable.className = "db-schema-table";
|
|
8826
|
+
const fkThead = document.createElement("thead");
|
|
8827
|
+
const fkHeadRow = document.createElement("tr");
|
|
8828
|
+
for (const label of ["Column", "References Table", "References Column"]) {
|
|
8829
|
+
const th = document.createElement("th");
|
|
8830
|
+
th.textContent = label;
|
|
8831
|
+
fkHeadRow.appendChild(th);
|
|
8832
|
+
}
|
|
8833
|
+
fkThead.appendChild(fkHeadRow);
|
|
8834
|
+
const fkTbody = document.createElement("tbody");
|
|
8835
|
+
for (const fk of fks) {
|
|
8836
|
+
const tr = document.createElement("tr");
|
|
8837
|
+
const tdFrom = document.createElement("td");
|
|
8838
|
+
tdFrom.textContent = fk.fromColumn;
|
|
8839
|
+
const tdToTable = document.createElement("td");
|
|
8840
|
+
tdToTable.textContent = fk.toTable;
|
|
8841
|
+
const tdToCol = document.createElement("td");
|
|
8842
|
+
tdToCol.textContent = fk.toColumn;
|
|
8843
|
+
tr.append(tdFrom, tdToTable, tdToCol);
|
|
8844
|
+
fkTbody.appendChild(tr);
|
|
8845
|
+
}
|
|
8846
|
+
fkTable.append(fkThead, fkTbody);
|
|
8847
|
+
el.appendChild(fkTable);
|
|
8848
|
+
}
|
|
8849
|
+
const tableIndexes = indexes.filter((idx) => idx.table === table2);
|
|
8850
|
+
if (tableIndexes.length > 0) {
|
|
8851
|
+
const idxHeader = document.createElement("div");
|
|
8852
|
+
idxHeader.className = "db-schema-section-header";
|
|
8853
|
+
idxHeader.textContent = "Indexes";
|
|
8854
|
+
el.appendChild(idxHeader);
|
|
8855
|
+
const idxTable = document.createElement("table");
|
|
8856
|
+
idxTable.className = "db-schema-table";
|
|
8857
|
+
const idxThead = document.createElement("thead");
|
|
8858
|
+
const idxHeadRow = document.createElement("tr");
|
|
8859
|
+
for (const label of ["Name", "Columns", "Unique"]) {
|
|
8860
|
+
const th = document.createElement("th");
|
|
8861
|
+
th.textContent = label;
|
|
8862
|
+
idxHeadRow.appendChild(th);
|
|
8863
|
+
}
|
|
8864
|
+
idxThead.appendChild(idxHeadRow);
|
|
8865
|
+
const idxTbody = document.createElement("tbody");
|
|
8866
|
+
for (const idx of tableIndexes) {
|
|
8867
|
+
const tr = document.createElement("tr");
|
|
8868
|
+
const tdName = document.createElement("td");
|
|
8869
|
+
tdName.textContent = idx.name;
|
|
8870
|
+
const tdCols = document.createElement("td");
|
|
8871
|
+
tdCols.textContent = idx.columns.join(", ");
|
|
8872
|
+
const tdUnique = document.createElement("td");
|
|
8873
|
+
tdUnique.textContent = idx.unique ? "YES" : "NO";
|
|
8874
|
+
tr.append(tdName, tdCols, tdUnique);
|
|
8875
|
+
idxTbody.appendChild(tr);
|
|
8876
|
+
}
|
|
8877
|
+
idxTable.append(idxThead, idxTbody);
|
|
8878
|
+
el.appendChild(idxTable);
|
|
8879
|
+
}
|
|
8880
|
+
const triggers = extra?.triggers;
|
|
8881
|
+
if (triggers && triggers.length > 0) {
|
|
8882
|
+
const trigHeader = document.createElement("div");
|
|
8883
|
+
trigHeader.className = "db-schema-section-header";
|
|
8884
|
+
trigHeader.textContent = "Triggers";
|
|
8885
|
+
el.appendChild(trigHeader);
|
|
8886
|
+
for (const trig of triggers) {
|
|
8887
|
+
const trigBlock = document.createElement("div");
|
|
8888
|
+
trigBlock.className = "db-schema-trigger-block";
|
|
8889
|
+
const trigName = document.createElement("div");
|
|
8890
|
+
trigName.className = "db-schema-trigger-name";
|
|
8891
|
+
trigName.textContent = trig.name;
|
|
8892
|
+
const trigPre = document.createElement("pre");
|
|
8893
|
+
trigPre.className = "db-schema-ddl-pre";
|
|
8894
|
+
trigPre.textContent = trig.sql;
|
|
8895
|
+
trigBlock.append(trigName, trigPre);
|
|
8896
|
+
el.appendChild(trigBlock);
|
|
8897
|
+
}
|
|
8898
|
+
}
|
|
8899
|
+
const ddl = extra?.ddl;
|
|
8900
|
+
if (ddl) {
|
|
8901
|
+
const ddlHeader = document.createElement("div");
|
|
8902
|
+
ddlHeader.className = "db-schema-section-header";
|
|
8903
|
+
ddlHeader.textContent = "DDL";
|
|
8904
|
+
el.appendChild(ddlHeader);
|
|
8905
|
+
const ddlWrap = document.createElement("div");
|
|
8906
|
+
ddlWrap.className = "db-schema-ddl-wrap";
|
|
8907
|
+
const copyBtn = document.createElement("button");
|
|
8908
|
+
copyBtn.type = "button";
|
|
8909
|
+
copyBtn.className = "db-btn db-btn-sm db-schema-copy-btn";
|
|
8910
|
+
copyBtn.textContent = "Copy DDL";
|
|
8911
|
+
copyBtn.addEventListener("click", () => {
|
|
8912
|
+
navigator.clipboard.writeText(ddl).then(() => {
|
|
8913
|
+
copyBtn.textContent = "Copied!";
|
|
8914
|
+
setTimeout(() => {
|
|
8915
|
+
copyBtn.textContent = "Copy DDL";
|
|
8916
|
+
}, 1500);
|
|
8917
|
+
}, () => {});
|
|
8918
|
+
});
|
|
8919
|
+
const ddlPre = document.createElement("pre");
|
|
8920
|
+
ddlPre.className = "db-schema-ddl-pre";
|
|
8921
|
+
ddlPre.textContent = ddl;
|
|
8922
|
+
ddlWrap.append(copyBtn, ddlPre);
|
|
8923
|
+
el.appendChild(ddlWrap);
|
|
8924
|
+
}
|
|
8925
|
+
}
|
|
8926
|
+
function clear() {
|
|
8927
|
+
el.hidden = true;
|
|
8928
|
+
el.innerHTML = "";
|
|
8929
|
+
}
|
|
8930
|
+
return { el, render, clear };
|
|
8931
|
+
}
|
|
8932
|
+
|
|
8933
|
+
// web-src/views/database/snapshot-view.ts
|
|
8934
|
+
function changeTypeLabel(type) {
|
|
8935
|
+
if (type === "inserted")
|
|
8936
|
+
return "追加";
|
|
8937
|
+
if (type === "updated")
|
|
8938
|
+
return "更新";
|
|
8939
|
+
return "削除";
|
|
8940
|
+
}
|
|
8941
|
+
function snapshotLabel(s2) {
|
|
8942
|
+
const date = new Date(s2.createdAt).toLocaleString();
|
|
8943
|
+
const note = s2.note ? ` — ${s2.note}` : "";
|
|
8944
|
+
return `${date} (${s2.tables.length}テーブル)${note}`;
|
|
8945
|
+
}
|
|
8946
|
+
function arraysEqual(a2, b2) {
|
|
8947
|
+
if (a2.length !== b2.length)
|
|
8948
|
+
return false;
|
|
8949
|
+
const sa = [...a2].sort();
|
|
8950
|
+
const sb = [...b2].sort();
|
|
8951
|
+
return sa.every((v, i2) => v === sb[i2]);
|
|
8952
|
+
}
|
|
8953
|
+
function postJson(path, body) {
|
|
8954
|
+
return fetch(path, {
|
|
8955
|
+
method: "POST",
|
|
8956
|
+
headers: {
|
|
8957
|
+
"Content-Type": "application/json",
|
|
8958
|
+
"X-Code-Viewer-Action": "1"
|
|
8959
|
+
},
|
|
8960
|
+
body: JSON.stringify(body)
|
|
8961
|
+
});
|
|
8962
|
+
}
|
|
8963
|
+
function createSnapshotView(deps) {
|
|
8964
|
+
const el = document.createElement("div");
|
|
8965
|
+
el.className = "db-snapshot-view";
|
|
8966
|
+
const guide = document.createElement("div");
|
|
8967
|
+
guide.className = "db-snapshot-guide";
|
|
8968
|
+
guide.innerHTML = '<div class="db-snapshot-guide-title">スナップショット差分</div>' + '<div class="db-snapshot-guide-body">' + "① スナップショット取得 → ② アプリやテストでDB操作 → ③ もう一度取得すると自動で差分表示されます" + "</div>";
|
|
8969
|
+
const toolbar = document.createElement("div");
|
|
8970
|
+
toolbar.className = "db-snapshot-toolbar";
|
|
8971
|
+
const createBtn = document.createElement("button");
|
|
8972
|
+
createBtn.type = "button";
|
|
8973
|
+
createBtn.className = "db-snapshot-create-btn";
|
|
8974
|
+
createBtn.textContent = "スナップショット取得";
|
|
8975
|
+
const refreshBtn = document.createElement("button");
|
|
8976
|
+
refreshBtn.type = "button";
|
|
8977
|
+
refreshBtn.className = "db-snapshot-refresh-btn";
|
|
8978
|
+
refreshBtn.textContent = "更新";
|
|
8979
|
+
toolbar.append(createBtn, refreshBtn);
|
|
8980
|
+
const tableSelector = document.createElement("div");
|
|
8981
|
+
tableSelector.className = "db-snapshot-table-selector";
|
|
8982
|
+
tableSelector.hidden = true;
|
|
8983
|
+
const tableSelectorHeader = document.createElement("div");
|
|
8984
|
+
tableSelectorHeader.className = "db-snapshot-table-selector-header";
|
|
8985
|
+
tableSelectorHeader.textContent = "対象テーブルを選択";
|
|
8986
|
+
const tableCheckboxes = document.createElement("div");
|
|
8987
|
+
tableCheckboxes.className = "db-snapshot-table-checkboxes";
|
|
8988
|
+
const selectActions = document.createElement("div");
|
|
8989
|
+
selectActions.className = "db-snapshot-select-actions";
|
|
8990
|
+
const selectAllBtn = document.createElement("button");
|
|
8991
|
+
selectAllBtn.type = "button";
|
|
8992
|
+
selectAllBtn.textContent = "すべて選択";
|
|
8993
|
+
const deselectAllBtn = document.createElement("button");
|
|
8994
|
+
deselectAllBtn.type = "button";
|
|
8995
|
+
deselectAllBtn.textContent = "選択解除";
|
|
8996
|
+
const noteInput = document.createElement("input");
|
|
8997
|
+
noteInput.type = "text";
|
|
8998
|
+
noteInput.className = "db-snapshot-note-input";
|
|
8999
|
+
noteInput.placeholder = "例: ユーザー登録テスト前";
|
|
9000
|
+
const noteField = document.createElement("label");
|
|
9001
|
+
noteField.className = "db-snapshot-note-field";
|
|
9002
|
+
const noteLabel = document.createElement("span");
|
|
9003
|
+
noteLabel.className = "db-snapshot-note-label";
|
|
9004
|
+
noteLabel.textContent = "メモ";
|
|
9005
|
+
noteField.append(noteLabel, noteInput);
|
|
9006
|
+
const confirmBtn = document.createElement("button");
|
|
9007
|
+
confirmBtn.type = "button";
|
|
9008
|
+
confirmBtn.className = "db-snapshot-confirm-btn";
|
|
9009
|
+
confirmBtn.textContent = "取得開始";
|
|
9010
|
+
const cancelBtn = document.createElement("button");
|
|
9011
|
+
cancelBtn.type = "button";
|
|
9012
|
+
cancelBtn.className = "db-snapshot-cancel-btn";
|
|
9013
|
+
cancelBtn.textContent = "キャンセル";
|
|
9014
|
+
selectActions.append(selectAllBtn, deselectAllBtn, noteField, confirmBtn, cancelBtn);
|
|
9015
|
+
tableSelector.append(tableSelectorHeader, tableCheckboxes, selectActions);
|
|
9016
|
+
const mainArea = document.createElement("div");
|
|
9017
|
+
mainArea.className = "db-snapshot-main-area";
|
|
9018
|
+
el.append(guide, toolbar, tableSelector, mainArea);
|
|
9019
|
+
let snapshots = [];
|
|
9020
|
+
function showTableSelector() {
|
|
9021
|
+
const tables = deps.getTables();
|
|
9022
|
+
const lastTables = getLastTables();
|
|
9023
|
+
tableCheckboxes.innerHTML = "";
|
|
9024
|
+
for (const t2 of tables) {
|
|
9025
|
+
if (t2.type !== "table")
|
|
9026
|
+
continue;
|
|
9027
|
+
const label = document.createElement("label");
|
|
9028
|
+
label.className = "db-snapshot-table-label";
|
|
9029
|
+
const cb = document.createElement("input");
|
|
9030
|
+
cb.type = "checkbox";
|
|
9031
|
+
cb.checked = lastTables.length === 0 || lastTables.includes(t2.name);
|
|
9032
|
+
cb.value = t2.name;
|
|
9033
|
+
const rowInfo = t2.rowCount != null ? ` (${t2.rowCount}件)` : "";
|
|
9034
|
+
label.append(cb, ` ${t2.name}${rowInfo}`);
|
|
9035
|
+
tableCheckboxes.appendChild(label);
|
|
9036
|
+
}
|
|
9037
|
+
tableSelector.hidden = false;
|
|
9038
|
+
noteInput.value = "";
|
|
9039
|
+
}
|
|
9040
|
+
function getLastTables() {
|
|
9041
|
+
const dbId = deps.getDbId();
|
|
9042
|
+
if (!dbId)
|
|
9043
|
+
return [];
|
|
9044
|
+
const done = snapshots.filter((s2) => s2.status === "done");
|
|
9045
|
+
return done.length > 0 ? done[0].tables : [];
|
|
9046
|
+
}
|
|
9047
|
+
function getSelectedTables() {
|
|
9048
|
+
return Array.from(tableCheckboxes.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => cb.value);
|
|
9049
|
+
}
|
|
9050
|
+
selectAllBtn.addEventListener("click", () => {
|
|
9051
|
+
tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
|
9052
|
+
cb.checked = true;
|
|
9053
|
+
});
|
|
9054
|
+
});
|
|
9055
|
+
deselectAllBtn.addEventListener("click", () => {
|
|
9056
|
+
tableCheckboxes.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
|
9057
|
+
cb.checked = false;
|
|
9058
|
+
});
|
|
9059
|
+
});
|
|
9060
|
+
createBtn.addEventListener("click", showTableSelector);
|
|
9061
|
+
cancelBtn.addEventListener("click", () => {
|
|
9062
|
+
tableSelector.hidden = true;
|
|
9063
|
+
});
|
|
9064
|
+
confirmBtn.addEventListener("click", async () => {
|
|
9065
|
+
const dbId = deps.getDbId();
|
|
9066
|
+
if (!dbId)
|
|
9067
|
+
return;
|
|
9068
|
+
const tables = getSelectedTables();
|
|
9069
|
+
if (tables.length === 0)
|
|
9070
|
+
return;
|
|
9071
|
+
confirmBtn.disabled = true;
|
|
9072
|
+
confirmBtn.textContent = "取得中...";
|
|
9073
|
+
try {
|
|
9074
|
+
await postJson("/_db/snapshot/create", {
|
|
9075
|
+
db: dbId,
|
|
9076
|
+
tables,
|
|
9077
|
+
note: noteInput.value.trim()
|
|
9078
|
+
});
|
|
9079
|
+
tableSelector.hidden = true;
|
|
9080
|
+
setTimeout(() => refreshAndAutoDiff(), 3000);
|
|
9081
|
+
} catch {} finally {
|
|
9082
|
+
confirmBtn.disabled = false;
|
|
9083
|
+
confirmBtn.textContent = "取得開始";
|
|
9084
|
+
}
|
|
9085
|
+
});
|
|
9086
|
+
refreshBtn.addEventListener("click", refresh);
|
|
9087
|
+
async function refresh() {
|
|
9088
|
+
const dbId = deps.getDbId();
|
|
9089
|
+
if (!dbId)
|
|
9090
|
+
return;
|
|
9091
|
+
try {
|
|
9092
|
+
const snapRes = await fetch(`/_db/snapshot/list?db=${encodeURIComponent(dbId)}`);
|
|
9093
|
+
if (snapRes.ok) {
|
|
9094
|
+
const data = await snapRes.json();
|
|
9095
|
+
snapshots = data.snapshots;
|
|
9096
|
+
}
|
|
9097
|
+
renderMain();
|
|
9098
|
+
} catch {}
|
|
9099
|
+
}
|
|
9100
|
+
async function refreshAndAutoDiff() {
|
|
9101
|
+
await refresh();
|
|
9102
|
+
const done = snapshots.filter((s2) => s2.status === "done");
|
|
9103
|
+
if (done.length < 2)
|
|
9104
|
+
return;
|
|
9105
|
+
const newest = done[0];
|
|
9106
|
+
const prev = done[1];
|
|
9107
|
+
if (!arraysEqual(newest.tables, prev.tables))
|
|
9108
|
+
return;
|
|
9109
|
+
showDiffInline(prev.id, newest.id);
|
|
9110
|
+
}
|
|
9111
|
+
function renderMain() {
|
|
9112
|
+
mainArea.innerHTML = "";
|
|
9113
|
+
const done = snapshots.filter((s2) => s2.status === "done");
|
|
9114
|
+
if (snapshots.length === 0) {
|
|
9115
|
+
mainArea.innerHTML = '<div class="db-snapshot-empty">まだスナップショットがありません。「スナップショット取得」で開始します。</div>';
|
|
9116
|
+
return;
|
|
9117
|
+
}
|
|
9118
|
+
const snapshotSection = document.createElement("div");
|
|
9119
|
+
snapshotSection.className = "db-snapshot-list-section";
|
|
9120
|
+
const snapTitle = document.createElement("h3");
|
|
9121
|
+
snapTitle.className = "db-snapshot-section-title";
|
|
9122
|
+
snapTitle.textContent = `スナップショット (${done.length}件)`;
|
|
9123
|
+
snapshotSection.appendChild(snapTitle);
|
|
9124
|
+
for (const snap of snapshots) {
|
|
9125
|
+
const item = document.createElement("div");
|
|
9126
|
+
item.className = "db-snapshot-item";
|
|
9127
|
+
const info = document.createElement("div");
|
|
9128
|
+
info.className = "db-snapshot-info";
|
|
9129
|
+
info.innerHTML = `<span class="db-snapshot-date">${new Date(snap.createdAt).toLocaleString()}</span>` + `<span class="db-snapshot-tables-count" title="${snap.tables.join(", ")}">${snap.tables.length}テーブル</span>` + (snap.note ? `<span class="db-snapshot-note">${snap.note}</span>` : "");
|
|
9130
|
+
const actions = document.createElement("div");
|
|
9131
|
+
actions.className = "db-snapshot-actions";
|
|
9132
|
+
const noteBtn = document.createElement("button");
|
|
9133
|
+
noteBtn.type = "button";
|
|
9134
|
+
noteBtn.textContent = "メモ";
|
|
9135
|
+
noteBtn.addEventListener("click", () => editNote(snap.id, snap.note));
|
|
9136
|
+
const deleteBtn = document.createElement("button");
|
|
9137
|
+
deleteBtn.type = "button";
|
|
9138
|
+
deleteBtn.textContent = "削除";
|
|
9139
|
+
deleteBtn.addEventListener("click", () => deleteSnap(snap.id));
|
|
9140
|
+
actions.append(noteBtn, deleteBtn);
|
|
9141
|
+
item.append(info, actions);
|
|
9142
|
+
snapshotSection.appendChild(item);
|
|
9143
|
+
}
|
|
9144
|
+
if (done.length >= 2) {
|
|
9145
|
+
const diffCreate = document.createElement("div");
|
|
9146
|
+
diffCreate.className = "db-snapshot-diff-create";
|
|
9147
|
+
const beforeSelect = document.createElement("select");
|
|
9148
|
+
beforeSelect.className = "db-snapshot-diff-select";
|
|
9149
|
+
const afterSelect = document.createElement("select");
|
|
9150
|
+
afterSelect.className = "db-snapshot-diff-select";
|
|
9151
|
+
const sorted = [...done].sort((a2, b2) => new Date(a2.createdAt).getTime() - new Date(b2.createdAt).getTime());
|
|
9152
|
+
for (const s2 of sorted) {
|
|
9153
|
+
const optB = document.createElement("option");
|
|
9154
|
+
optB.value = s2.id;
|
|
9155
|
+
optB.textContent = snapshotLabel(s2);
|
|
9156
|
+
beforeSelect.appendChild(optB);
|
|
9157
|
+
const optA = document.createElement("option");
|
|
9158
|
+
optA.value = s2.id;
|
|
9159
|
+
optA.textContent = snapshotLabel(s2);
|
|
9160
|
+
afterSelect.appendChild(optA);
|
|
9161
|
+
}
|
|
9162
|
+
if (sorted.length >= 2) {
|
|
9163
|
+
beforeSelect.selectedIndex = sorted.length - 2;
|
|
9164
|
+
afterSelect.selectedIndex = sorted.length - 1;
|
|
9165
|
+
}
|
|
9166
|
+
const diffBtn = document.createElement("button");
|
|
9167
|
+
diffBtn.type = "button";
|
|
9168
|
+
diffBtn.className = "db-snapshot-diff-btn";
|
|
9169
|
+
diffBtn.textContent = "手動で差分チェック";
|
|
9170
|
+
diffBtn.addEventListener("click", async () => {
|
|
9171
|
+
diffBtn.disabled = true;
|
|
9172
|
+
diffBtn.textContent = "比較中...";
|
|
9173
|
+
try {
|
|
9174
|
+
showDiffInline(beforeSelect.value, afterSelect.value);
|
|
9175
|
+
} finally {
|
|
9176
|
+
diffBtn.disabled = false;
|
|
9177
|
+
diffBtn.textContent = "手動で差分チェック";
|
|
9178
|
+
}
|
|
9179
|
+
});
|
|
9180
|
+
diffCreate.append(beforeSelect, document.createTextNode(" → "), afterSelect, diffBtn);
|
|
9181
|
+
snapshotSection.appendChild(diffCreate);
|
|
9182
|
+
}
|
|
9183
|
+
mainArea.appendChild(snapshotSection);
|
|
9184
|
+
}
|
|
9185
|
+
async function showDiffInline(beforeId, afterId) {
|
|
9186
|
+
const existing = mainArea.querySelector(".db-snapshot-diff-inline");
|
|
9187
|
+
if (existing)
|
|
9188
|
+
existing.remove();
|
|
9189
|
+
const loading = document.createElement("div");
|
|
9190
|
+
loading.className = "db-snapshot-diff-inline";
|
|
9191
|
+
loading.innerHTML = '<div class="db-snapshot-loading">差分を計算中...</div>';
|
|
9192
|
+
mainArea.appendChild(loading);
|
|
9193
|
+
try {
|
|
9194
|
+
const res = await fetch(`/_db/snapshot/diff/tables?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}`);
|
|
9195
|
+
if (!res.ok) {
|
|
9196
|
+
loading.innerHTML = '<div class="db-snapshot-error">差分の計算に失敗しました</div>';
|
|
9197
|
+
return;
|
|
9198
|
+
}
|
|
9199
|
+
const data = await res.json();
|
|
9200
|
+
loading.remove();
|
|
9201
|
+
renderDiffInline(beforeId, afterId, data.tables);
|
|
9202
|
+
} catch {
|
|
9203
|
+
loading.innerHTML = '<div class="db-snapshot-error">差分の計算に失敗しました</div>';
|
|
9204
|
+
}
|
|
9205
|
+
}
|
|
9206
|
+
function renderDiffInline(beforeId, afterId, tables) {
|
|
9207
|
+
const existing = mainArea.querySelector(".db-snapshot-diff-inline");
|
|
9208
|
+
if (existing)
|
|
9209
|
+
existing.remove();
|
|
9210
|
+
const section = document.createElement("div");
|
|
9211
|
+
section.className = "db-snapshot-diff-inline";
|
|
9212
|
+
const changedTables = tables.filter((t2) => t2.insertedCount + t2.updatedCount + t2.deletedCount > 0);
|
|
9213
|
+
const unchangedCount = tables.length - changedTables.length;
|
|
9214
|
+
const summary = document.createElement("div");
|
|
9215
|
+
summary.className = "db-snapshot-diff-summary";
|
|
9216
|
+
if (changedTables.length === 0) {
|
|
9217
|
+
summary.textContent = "変更は検出されませんでした。";
|
|
9218
|
+
section.appendChild(summary);
|
|
9219
|
+
mainArea.appendChild(section);
|
|
9220
|
+
return;
|
|
9221
|
+
}
|
|
9222
|
+
summary.textContent = `${changedTables.length}テーブルに変更あり` + (unchangedCount > 0 ? `、${unchangedCount}テーブルは変更なし` : "");
|
|
9223
|
+
section.appendChild(summary);
|
|
9224
|
+
for (const t2 of changedTables) {
|
|
9225
|
+
const tableEl = document.createElement("div");
|
|
9226
|
+
tableEl.className = "db-snapshot-diff-table";
|
|
9227
|
+
const tableHeader = document.createElement("div");
|
|
9228
|
+
tableHeader.className = "db-snapshot-diff-table-header";
|
|
9229
|
+
const nameSpan = document.createElement("span");
|
|
9230
|
+
nameSpan.className = "db-snapshot-diff-table-name";
|
|
9231
|
+
nameSpan.textContent = t2.tableName;
|
|
9232
|
+
const statsSpan = document.createElement("span");
|
|
9233
|
+
statsSpan.className = "db-snapshot-diff-table-stats";
|
|
9234
|
+
const parts = [];
|
|
9235
|
+
if (t2.insertedCount > 0)
|
|
9236
|
+
parts.push(`+${t2.insertedCount}`);
|
|
9237
|
+
if (t2.updatedCount > 0)
|
|
9238
|
+
parts.push(`~${t2.updatedCount}`);
|
|
9239
|
+
if (t2.deletedCount > 0)
|
|
9240
|
+
parts.push(`-${t2.deletedCount}`);
|
|
9241
|
+
statsSpan.textContent = parts.join(" ");
|
|
9242
|
+
tableHeader.append(nameSpan, statsSpan);
|
|
9243
|
+
tableHeader.style.cursor = "pointer";
|
|
9244
|
+
const rowsContainer = document.createElement("div");
|
|
9245
|
+
rowsContainer.className = "db-snapshot-diff-rows-container";
|
|
9246
|
+
loadDiffRows(beforeId, afterId, t2.tableName, rowsContainer);
|
|
9247
|
+
tableHeader.addEventListener("click", () => {
|
|
9248
|
+
rowsContainer.hidden = !rowsContainer.hidden;
|
|
9249
|
+
});
|
|
9250
|
+
tableEl.append(tableHeader, rowsContainer);
|
|
9251
|
+
section.appendChild(tableEl);
|
|
9252
|
+
}
|
|
9253
|
+
mainArea.appendChild(section);
|
|
9254
|
+
}
|
|
9255
|
+
async function loadDiffRows(beforeId, afterId, table2, container) {
|
|
9256
|
+
container.innerHTML = '<div class="db-snapshot-loading">読み込み中...</div>';
|
|
9257
|
+
try {
|
|
9258
|
+
const res = await fetch(`/_db/snapshot/diff/rows?before=${encodeURIComponent(beforeId)}&after=${encodeURIComponent(afterId)}&table=${encodeURIComponent(table2)}&limit=200`);
|
|
9259
|
+
if (!res.ok) {
|
|
9260
|
+
container.innerHTML = '<div class="db-snapshot-error">読み込みに失敗しました</div>';
|
|
9261
|
+
return;
|
|
9262
|
+
}
|
|
9263
|
+
const data = await res.json();
|
|
9264
|
+
renderDiffRows(container, data.rows, data.total);
|
|
9265
|
+
} catch {
|
|
9266
|
+
container.innerHTML = '<div class="db-snapshot-error">読み込みに失敗しました</div>';
|
|
9267
|
+
}
|
|
9268
|
+
}
|
|
9269
|
+
function renderDiffRows(container, rows, total) {
|
|
9270
|
+
container.innerHTML = "";
|
|
9271
|
+
if (rows.length === 0)
|
|
9272
|
+
return;
|
|
9273
|
+
const allCols = new Set;
|
|
9274
|
+
for (const row of rows) {
|
|
9275
|
+
if (row.beforeValues)
|
|
9276
|
+
for (const k of Object.keys(row.beforeValues))
|
|
9277
|
+
allCols.add(k);
|
|
9278
|
+
if (row.afterValues)
|
|
9279
|
+
for (const k of Object.keys(row.afterValues))
|
|
9280
|
+
allCols.add(k);
|
|
9281
|
+
}
|
|
9282
|
+
const columns = [...allCols];
|
|
9283
|
+
const table2 = document.createElement("table");
|
|
9284
|
+
table2.className = "db-snap-diff-grid";
|
|
9285
|
+
const thead = document.createElement("thead");
|
|
9286
|
+
const headRow = document.createElement("tr");
|
|
9287
|
+
const thType = document.createElement("th");
|
|
9288
|
+
thType.textContent = "";
|
|
9289
|
+
headRow.appendChild(thType);
|
|
9290
|
+
for (const col of columns) {
|
|
9291
|
+
const th = document.createElement("th");
|
|
9292
|
+
th.textContent = col;
|
|
9293
|
+
headRow.appendChild(th);
|
|
9294
|
+
}
|
|
9295
|
+
thead.appendChild(headRow);
|
|
9296
|
+
table2.appendChild(thead);
|
|
9297
|
+
const tbody = document.createElement("tbody");
|
|
9298
|
+
for (const row of rows) {
|
|
9299
|
+
if (row.changeType === "updated" && row.beforeValues && row.afterValues) {
|
|
9300
|
+
const trBefore = document.createElement("tr");
|
|
9301
|
+
trBefore.className = "snap-diff-del";
|
|
9302
|
+
const tdTypeBefore = document.createElement("td");
|
|
9303
|
+
tdTypeBefore.className = "snap-diff-type-cell";
|
|
9304
|
+
tdTypeBefore.textContent = "更新前";
|
|
9305
|
+
tdTypeBefore.rowSpan = 2;
|
|
9306
|
+
trBefore.appendChild(tdTypeBefore);
|
|
9307
|
+
for (const col of columns) {
|
|
9308
|
+
const td = document.createElement("td");
|
|
9309
|
+
const bs = row.beforeValues[col] == null ? "NULL" : String(row.beforeValues[col]);
|
|
9310
|
+
const as_ = row.afterValues[col] == null ? "NULL" : String(row.afterValues[col]);
|
|
9311
|
+
td.textContent = bs;
|
|
9312
|
+
if (bs !== as_)
|
|
9313
|
+
td.classList.add("snap-diff-changed-cell");
|
|
9314
|
+
trBefore.appendChild(td);
|
|
9315
|
+
}
|
|
9316
|
+
tbody.appendChild(trBefore);
|
|
9317
|
+
const trAfter = document.createElement("tr");
|
|
9318
|
+
trAfter.className = "snap-diff-add";
|
|
9319
|
+
for (const col of columns) {
|
|
9320
|
+
const td = document.createElement("td");
|
|
9321
|
+
const bs = row.beforeValues[col] == null ? "NULL" : String(row.beforeValues[col]);
|
|
9322
|
+
const as_ = row.afterValues[col] == null ? "NULL" : String(row.afterValues[col]);
|
|
9323
|
+
td.textContent = as_;
|
|
9324
|
+
if (bs !== as_)
|
|
9325
|
+
td.classList.add("snap-diff-changed-cell");
|
|
9326
|
+
trAfter.appendChild(td);
|
|
9327
|
+
}
|
|
9328
|
+
tbody.appendChild(trAfter);
|
|
9329
|
+
} else {
|
|
9330
|
+
const tr = document.createElement("tr");
|
|
9331
|
+
tr.className = row.changeType === "inserted" ? "snap-diff-add" : "snap-diff-del";
|
|
9332
|
+
const tdType = document.createElement("td");
|
|
9333
|
+
tdType.className = "snap-diff-type-cell";
|
|
9334
|
+
tdType.textContent = changeTypeLabel(row.changeType);
|
|
9335
|
+
tr.appendChild(tdType);
|
|
9336
|
+
const values = row.changeType === "inserted" ? row.afterValues : row.beforeValues;
|
|
9337
|
+
for (const col of columns) {
|
|
9338
|
+
const td = document.createElement("td");
|
|
9339
|
+
const v = values?.[col];
|
|
9340
|
+
td.textContent = v == null ? "NULL" : String(v);
|
|
9341
|
+
tr.appendChild(td);
|
|
9342
|
+
}
|
|
9343
|
+
tbody.appendChild(tr);
|
|
9344
|
+
}
|
|
9345
|
+
}
|
|
9346
|
+
table2.appendChild(tbody);
|
|
9347
|
+
container.appendChild(table2);
|
|
9348
|
+
if (total > rows.length) {
|
|
9349
|
+
const more = document.createElement("div");
|
|
9350
|
+
more.className = "db-snapshot-diff-more";
|
|
9351
|
+
more.textContent = `全${total}件中 ${rows.length}件を表示中`;
|
|
9352
|
+
container.appendChild(more);
|
|
9353
|
+
}
|
|
9354
|
+
}
|
|
9355
|
+
async function editNote(snapshotId, currentNote) {
|
|
9356
|
+
const existing = el.querySelector(".db-snapshot-inline-dialog");
|
|
9357
|
+
if (existing)
|
|
9358
|
+
existing.remove();
|
|
9359
|
+
const dialog = document.createElement("div");
|
|
9360
|
+
dialog.className = "db-snapshot-inline-dialog";
|
|
9361
|
+
const input = document.createElement("input");
|
|
9362
|
+
input.type = "text";
|
|
9363
|
+
input.className = "db-snapshot-note-input";
|
|
9364
|
+
input.value = currentNote || "";
|
|
9365
|
+
input.placeholder = "メモを入力";
|
|
9366
|
+
const saveBtn = document.createElement("button");
|
|
9367
|
+
saveBtn.type = "button";
|
|
9368
|
+
saveBtn.textContent = "保存";
|
|
9369
|
+
saveBtn.className = "db-snapshot-confirm-btn";
|
|
9370
|
+
const cancelDlgBtn = document.createElement("button");
|
|
9371
|
+
cancelDlgBtn.type = "button";
|
|
9372
|
+
cancelDlgBtn.textContent = "キャンセル";
|
|
9373
|
+
cancelDlgBtn.addEventListener("click", () => dialog.remove());
|
|
9374
|
+
saveBtn.addEventListener("click", async () => {
|
|
9375
|
+
await postJson("/_db/snapshot/update-note", {
|
|
9376
|
+
id: snapshotId,
|
|
9377
|
+
note: input.value
|
|
9378
|
+
});
|
|
9379
|
+
dialog.remove();
|
|
9380
|
+
refresh();
|
|
9381
|
+
});
|
|
9382
|
+
input.addEventListener("keydown", (e2) => {
|
|
9383
|
+
if (e2.key === "Enter")
|
|
9384
|
+
saveBtn.click();
|
|
9385
|
+
if (e2.key === "Escape")
|
|
9386
|
+
dialog.remove();
|
|
9387
|
+
});
|
|
9388
|
+
dialog.append(input, saveBtn, cancelDlgBtn);
|
|
9389
|
+
const item = el.querySelector(`.db-snapshot-item [title="${snapshotId}"]`)?.closest(".db-snapshot-item");
|
|
9390
|
+
if (item) {
|
|
9391
|
+
item.after(dialog);
|
|
9392
|
+
} else {
|
|
9393
|
+
mainArea.prepend(dialog);
|
|
9394
|
+
}
|
|
9395
|
+
input.focus();
|
|
9396
|
+
}
|
|
9397
|
+
async function deleteSnap(snapshotId) {
|
|
9398
|
+
await postJson("/_db/snapshot/delete", { id: snapshotId });
|
|
9399
|
+
refresh();
|
|
9400
|
+
}
|
|
9401
|
+
function handleSse(data) {
|
|
9402
|
+
try {
|
|
9403
|
+
const parsed = JSON.parse(data);
|
|
9404
|
+
if (parsed.action === "created" || parsed.action === "error") {
|
|
9405
|
+
refreshAndAutoDiff();
|
|
9406
|
+
}
|
|
9407
|
+
} catch {}
|
|
9408
|
+
}
|
|
9409
|
+
return { el, refresh, handleSse };
|
|
9410
|
+
}
|
|
9411
|
+
|
|
9412
|
+
// web-src/views/database/table-grid.ts
|
|
9413
|
+
var ROW_HEIGHT = 28;
|
|
9414
|
+
var OVERSCAN = 20;
|
|
9415
|
+
var PAGE_SIZE = 200;
|
|
9416
|
+
var FILTER_DEBOUNCE_MS = 300;
|
|
9417
|
+
var DEFAULT_COL_WIDTH = 180;
|
|
9418
|
+
function createTableGrid(callbacks) {
|
|
9419
|
+
const el = document.createElement("div");
|
|
9420
|
+
el.className = "db-grid";
|
|
9421
|
+
const filterBar = document.createElement("div");
|
|
9422
|
+
filterBar.className = "db-grid-filter-bar";
|
|
9423
|
+
const filterIcon = document.createElement("span");
|
|
9424
|
+
filterIcon.className = "db-grid-filter-icon";
|
|
9425
|
+
filterIcon.textContent = "\uD83D\uDD0D";
|
|
9426
|
+
const filterInput = document.createElement("input");
|
|
9427
|
+
filterInput.type = "search";
|
|
9428
|
+
filterInput.className = "db-grid-filter-input";
|
|
9429
|
+
filterInput.placeholder = "Search all columns…";
|
|
9430
|
+
filterInput.autocomplete = "off";
|
|
9431
|
+
const filterClear = document.createElement("button");
|
|
9432
|
+
filterClear.type = "button";
|
|
9433
|
+
filterClear.className = "db-btn db-btn-icon db-grid-filter-clear";
|
|
9434
|
+
filterClear.textContent = "×";
|
|
9435
|
+
filterClear.hidden = true;
|
|
9436
|
+
filterBar.append(filterIcon, filterInput, filterClear);
|
|
9437
|
+
const headerWrap = document.createElement("div");
|
|
9438
|
+
headerWrap.className = "db-grid-header-wrap";
|
|
9439
|
+
const headerRow = document.createElement("div");
|
|
9440
|
+
headerRow.className = "db-grid-header";
|
|
9441
|
+
headerWrap.appendChild(headerRow);
|
|
9442
|
+
const filterRowWrap = document.createElement("div");
|
|
9443
|
+
filterRowWrap.className = "db-grid-filter-row-wrap";
|
|
9444
|
+
const filterRow = document.createElement("div");
|
|
9445
|
+
filterRow.className = "db-grid-filter-row";
|
|
9446
|
+
filterRowWrap.appendChild(filterRow);
|
|
9447
|
+
const viewport = document.createElement("div");
|
|
9448
|
+
viewport.className = "db-grid-viewport";
|
|
9449
|
+
const spacer = document.createElement("div");
|
|
9450
|
+
spacer.className = "db-grid-spacer";
|
|
9451
|
+
const body = document.createElement("div");
|
|
9452
|
+
body.className = "db-grid-body";
|
|
9453
|
+
const detailPanel = document.createElement("div");
|
|
9454
|
+
detailPanel.className = "db-grid-detail-panel";
|
|
9455
|
+
detailPanel.hidden = true;
|
|
9456
|
+
viewport.append(spacer, body);
|
|
9457
|
+
el.append(filterBar, headerWrap, filterRowWrap, viewport, detailPanel);
|
|
9458
|
+
let currentTable = "";
|
|
9459
|
+
let columns = [];
|
|
9460
|
+
let columnNames = [];
|
|
9461
|
+
let totalRows = 0;
|
|
9462
|
+
const columnFilters = new Map;
|
|
9463
|
+
let globalSearchValue = "";
|
|
9464
|
+
let sort = null;
|
|
9465
|
+
let pageCache = new Map;
|
|
9466
|
+
let pendingPages = new Set;
|
|
9467
|
+
let loadGeneration = 0;
|
|
9468
|
+
let rafId = 0;
|
|
9469
|
+
let statusEl = null;
|
|
9470
|
+
let filterTimer = null;
|
|
9471
|
+
let selectedRowIndex = -1;
|
|
9472
|
+
const colWidths = new Map;
|
|
9473
|
+
function storageKey() {
|
|
9474
|
+
const project = callbacks.getProjectName?.() ?? "";
|
|
9475
|
+
if (!currentTable)
|
|
9476
|
+
return null;
|
|
9477
|
+
return `db:col-widths:${project}:${currentTable}`;
|
|
9478
|
+
}
|
|
9479
|
+
function saveColWidths() {
|
|
9480
|
+
const key = storageKey();
|
|
9481
|
+
if (!key)
|
|
9482
|
+
return;
|
|
9483
|
+
const obj = {};
|
|
9484
|
+
for (const [name, w] of colWidths) {
|
|
9485
|
+
obj[name] = w;
|
|
9486
|
+
}
|
|
9487
|
+
try {
|
|
9488
|
+
localStorage.setItem(key, JSON.stringify(obj));
|
|
9489
|
+
} catch {}
|
|
9490
|
+
}
|
|
9491
|
+
function loadColWidths() {
|
|
9492
|
+
colWidths.clear();
|
|
9493
|
+
const key = storageKey();
|
|
9494
|
+
if (!key)
|
|
9495
|
+
return;
|
|
9496
|
+
try {
|
|
9497
|
+
const raw = localStorage.getItem(key);
|
|
9498
|
+
if (raw) {
|
|
9499
|
+
const obj = JSON.parse(raw);
|
|
9500
|
+
for (const [name, w] of Object.entries(obj)) {
|
|
9501
|
+
if (typeof w === "number" && w > 0) {
|
|
9502
|
+
colWidths.set(name, w);
|
|
9503
|
+
}
|
|
9504
|
+
}
|
|
9505
|
+
}
|
|
9506
|
+
} catch {}
|
|
9507
|
+
}
|
|
9508
|
+
function getColWidth(colName) {
|
|
9509
|
+
return colWidths.get(colName) ?? DEFAULT_COL_WIDTH;
|
|
9510
|
+
}
|
|
9511
|
+
function applyColWidth(colName, index) {
|
|
9512
|
+
const w = getColWidth(colName);
|
|
9513
|
+
const headerCells = headerRow.querySelectorAll(".db-grid-header-cell");
|
|
9514
|
+
if (headerCells[index]) {
|
|
9515
|
+
headerCells[index].style.width = `${w}px`;
|
|
9516
|
+
}
|
|
9517
|
+
const filterCells = filterRow.querySelectorAll(".db-grid-filter-cell");
|
|
9518
|
+
if (filterCells[index]) {
|
|
9519
|
+
filterCells[index].style.width = `${w}px`;
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
function autoFitColumn(colName, colIndex) {
|
|
9523
|
+
const measure = document.createElement("span");
|
|
9524
|
+
measure.style.cssText = "position:absolute;visibility:hidden;white-space:nowrap;font:inherit;padding:0 8px;";
|
|
9525
|
+
document.body.appendChild(measure);
|
|
9526
|
+
const headerLabel = columns[colIndex]?.name || colName;
|
|
9527
|
+
const typeLabel = columns[colIndex]?.type || "";
|
|
9528
|
+
measure.textContent = `${headerLabel} ${typeLabel} ▲`;
|
|
9529
|
+
let maxW = measure.offsetWidth + 16;
|
|
9530
|
+
const rows = body.querySelectorAll(".db-grid-row");
|
|
9531
|
+
for (const row of rows) {
|
|
9532
|
+
const cells = row.querySelectorAll(".db-grid-cell:not(.db-grid-rownum)");
|
|
9533
|
+
const cell = cells[colIndex];
|
|
9534
|
+
if (cell) {
|
|
9535
|
+
measure.textContent = cell.textContent || "";
|
|
9536
|
+
maxW = Math.max(maxW, measure.offsetWidth);
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
9539
|
+
document.body.removeChild(measure);
|
|
9540
|
+
const fitted = Math.max(60, Math.min(600, maxW));
|
|
9541
|
+
colWidths.set(colName, fitted);
|
|
9542
|
+
applyColWidth(colName, colIndex);
|
|
9543
|
+
saveColWidths();
|
|
9544
|
+
syncContentWidth();
|
|
9545
|
+
renderViewport();
|
|
9546
|
+
}
|
|
9547
|
+
function collectFilters() {
|
|
9548
|
+
const filters = [];
|
|
9549
|
+
for (const [col, val] of columnFilters) {
|
|
9550
|
+
if (val)
|
|
9551
|
+
filters.push({ column: col, value: val });
|
|
9552
|
+
}
|
|
9553
|
+
if (globalSearchValue && filters.length === 0) {
|
|
9554
|
+
for (const col of columnNames) {
|
|
9555
|
+
filters.push({ column: col, value: globalSearchValue });
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
return filters;
|
|
9559
|
+
}
|
|
9560
|
+
function invalidateData() {
|
|
9561
|
+
pageCache = new Map;
|
|
9562
|
+
pendingPages = new Set;
|
|
9563
|
+
loadGeneration++;
|
|
9564
|
+
viewport.scrollTop = 0;
|
|
9565
|
+
ensurePage(0);
|
|
9566
|
+
}
|
|
9567
|
+
function clear() {
|
|
9568
|
+
currentTable = "";
|
|
9569
|
+
columns = [];
|
|
9570
|
+
columnNames = [];
|
|
9571
|
+
totalRows = 0;
|
|
9572
|
+
sort = null;
|
|
9573
|
+
columnFilters.clear();
|
|
9574
|
+
globalSearchValue = "";
|
|
9575
|
+
filterInput.value = "";
|
|
9576
|
+
filterClear.hidden = true;
|
|
9577
|
+
filterRow.innerHTML = "";
|
|
9578
|
+
pageCache = new Map;
|
|
9579
|
+
pendingPages = new Set;
|
|
9580
|
+
loadGeneration++;
|
|
9581
|
+
cancelAnimationFrame(rafId);
|
|
9582
|
+
if (filterTimer)
|
|
9583
|
+
clearTimeout(filterTimer);
|
|
9584
|
+
headerRow.innerHTML = "";
|
|
9585
|
+
body.innerHTML = "";
|
|
9586
|
+
spacer.style.height = "0px";
|
|
9587
|
+
statusEl?.remove();
|
|
9588
|
+
statusEl = null;
|
|
9589
|
+
detailPanel.hidden = true;
|
|
9590
|
+
detailPanel.innerHTML = "";
|
|
9591
|
+
colWidths.clear();
|
|
9592
|
+
}
|
|
9593
|
+
function showCellDetail(colIndex, value) {
|
|
9594
|
+
const colName = columnNames[colIndex];
|
|
9595
|
+
const colType = columns[colIndex]?.type || "";
|
|
9596
|
+
detailPanel.hidden = false;
|
|
9597
|
+
detailPanel.innerHTML = "";
|
|
9598
|
+
const header = document.createElement("div");
|
|
9599
|
+
header.className = "db-grid-detail-header";
|
|
9600
|
+
const title = document.createElement("span");
|
|
9601
|
+
title.className = "db-grid-detail-title";
|
|
9602
|
+
title.textContent = `${colName} (${colType})`;
|
|
9603
|
+
const closeBtn = document.createElement("button");
|
|
9604
|
+
closeBtn.type = "button";
|
|
9605
|
+
closeBtn.className = "db-btn db-btn-icon db-grid-detail-close";
|
|
9606
|
+
closeBtn.textContent = "×";
|
|
9607
|
+
closeBtn.addEventListener("click", () => {
|
|
9608
|
+
detailPanel.hidden = true;
|
|
9609
|
+
});
|
|
9610
|
+
header.append(title, closeBtn);
|
|
9611
|
+
const content = document.createElement("div");
|
|
9612
|
+
content.className = "db-grid-detail-content";
|
|
9613
|
+
if (value === null) {
|
|
9614
|
+
content.textContent = "NULL";
|
|
9615
|
+
content.classList.add("null");
|
|
9616
|
+
} else if (value instanceof Uint8Array) {
|
|
9617
|
+
content.textContent = `BLOB (${value.byteLength} bytes)`;
|
|
9618
|
+
content.classList.add("blob");
|
|
9619
|
+
} else {
|
|
9620
|
+
const str = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
9621
|
+
if (str.length > 0 && (str[0] === "{" || str[0] === "[")) {
|
|
9622
|
+
try {
|
|
9623
|
+
const parsed = JSON.parse(str);
|
|
9624
|
+
const pre = document.createElement("pre");
|
|
9625
|
+
pre.className = "db-grid-detail-json";
|
|
9626
|
+
pre.textContent = JSON.stringify(parsed, null, 2);
|
|
9627
|
+
content.appendChild(pre);
|
|
9628
|
+
} catch {
|
|
9629
|
+
content.textContent = str;
|
|
9630
|
+
}
|
|
9631
|
+
} else {
|
|
9632
|
+
content.textContent = str;
|
|
9633
|
+
}
|
|
9634
|
+
}
|
|
9635
|
+
detailPanel.append(header, content);
|
|
9636
|
+
}
|
|
9637
|
+
function renderHeader() {
|
|
9638
|
+
headerRow.innerHTML = "";
|
|
9639
|
+
const rowNum = document.createElement("div");
|
|
9640
|
+
rowNum.className = "db-grid-cell db-grid-rownum-header";
|
|
9641
|
+
rowNum.textContent = "#";
|
|
9642
|
+
headerRow.appendChild(rowNum);
|
|
9643
|
+
for (let i2 = 0;i2 < columns.length; i2++) {
|
|
9644
|
+
const col = columns[i2];
|
|
9645
|
+
const cell = document.createElement("div");
|
|
9646
|
+
cell.className = "db-grid-cell db-grid-header-cell";
|
|
9647
|
+
cell.style.width = `${getColWidth(col.name)}px`;
|
|
9648
|
+
if (sort?.column === col.name) {
|
|
9649
|
+
cell.classList.add("sorted");
|
|
9650
|
+
cell.dataset.dir = sort.direction;
|
|
9651
|
+
}
|
|
9652
|
+
const label = document.createElement("span");
|
|
9653
|
+
label.className = "db-grid-header-label";
|
|
9654
|
+
label.textContent = col.name;
|
|
9655
|
+
const typeTag = document.createElement("span");
|
|
9656
|
+
typeTag.className = "db-grid-header-type";
|
|
9657
|
+
typeTag.textContent = col.type;
|
|
9658
|
+
if (col.primaryKey)
|
|
9659
|
+
typeTag.classList.add("pk");
|
|
9660
|
+
const sortIcon = document.createElement("span");
|
|
9661
|
+
sortIcon.className = "db-grid-sort-icon";
|
|
9662
|
+
sortIcon.textContent = sort?.column === col.name ? sort.direction === "asc" ? "▲" : "▼" : "";
|
|
9663
|
+
const resizeHandle = document.createElement("div");
|
|
9664
|
+
resizeHandle.className = "db-grid-resize-handle";
|
|
9665
|
+
const colIndex = i2;
|
|
9666
|
+
resizeHandle.addEventListener("mousedown", (e2) => {
|
|
9667
|
+
e2.preventDefault();
|
|
9668
|
+
e2.stopPropagation();
|
|
9669
|
+
startResize(colIndex, e2);
|
|
9670
|
+
});
|
|
9671
|
+
cell.append(label, typeTag, sortIcon, resizeHandle);
|
|
9672
|
+
cell.addEventListener("click", (e2) => {
|
|
9673
|
+
if (e2.target.classList.contains("db-grid-resize-handle"))
|
|
9674
|
+
return;
|
|
9675
|
+
handleSort(col.name);
|
|
9676
|
+
});
|
|
9677
|
+
cell.addEventListener("dblclick", (e2) => {
|
|
9678
|
+
if (e2.target.classList.contains("db-grid-resize-handle")) {
|
|
9679
|
+
e2.preventDefault();
|
|
9680
|
+
e2.stopPropagation();
|
|
9681
|
+
autoFitColumn(col.name, colIndex);
|
|
9682
|
+
}
|
|
9683
|
+
});
|
|
9684
|
+
headerRow.appendChild(cell);
|
|
9685
|
+
}
|
|
9686
|
+
renderFilterRow();
|
|
9687
|
+
syncContentWidth();
|
|
9688
|
+
}
|
|
9689
|
+
function startResize(colIndex, startEvent) {
|
|
9690
|
+
const colName = columnNames[colIndex];
|
|
9691
|
+
const startX = startEvent.clientX;
|
|
9692
|
+
const startWidth = getColWidth(colName);
|
|
9693
|
+
document.body.classList.add("db-resizing");
|
|
9694
|
+
const onMouseMove = (e2) => {
|
|
9695
|
+
const delta = e2.clientX - startX;
|
|
9696
|
+
const newWidth = Math.max(60, startWidth + delta);
|
|
9697
|
+
colWidths.set(colName, newWidth);
|
|
9698
|
+
applyColWidth(colName, colIndex);
|
|
9699
|
+
syncContentWidth();
|
|
9700
|
+
renderViewport();
|
|
9701
|
+
};
|
|
9702
|
+
const onMouseUp = () => {
|
|
9703
|
+
document.body.classList.remove("db-resizing");
|
|
9704
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
9705
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
9706
|
+
saveColWidths();
|
|
9707
|
+
};
|
|
9708
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
9709
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
9710
|
+
}
|
|
9711
|
+
function renderFilterRow() {
|
|
9712
|
+
filterRow.innerHTML = "";
|
|
9713
|
+
const rowNumSpacer = document.createElement("div");
|
|
9714
|
+
rowNumSpacer.className = "db-grid-cell db-grid-rownum-header db-grid-filter-spacer";
|
|
9715
|
+
filterRow.appendChild(rowNumSpacer);
|
|
9716
|
+
for (let i2 = 0;i2 < columns.length; i2++) {
|
|
9717
|
+
const col = columns[i2];
|
|
9718
|
+
const cell = document.createElement("div");
|
|
9719
|
+
cell.className = "db-grid-cell db-grid-filter-cell";
|
|
9720
|
+
cell.style.width = `${getColWidth(col.name)}px`;
|
|
9721
|
+
const input = document.createElement("input");
|
|
9722
|
+
input.type = "search";
|
|
9723
|
+
input.className = "db-grid-col-filter";
|
|
9724
|
+
input.placeholder = `${col.name}…`;
|
|
9725
|
+
input.autocomplete = "off";
|
|
9726
|
+
input.value = columnFilters.get(col.name) || "";
|
|
9727
|
+
input.addEventListener("input", () => {
|
|
9728
|
+
const val = input.value.trim();
|
|
9729
|
+
if (val) {
|
|
9730
|
+
columnFilters.set(col.name, val);
|
|
9731
|
+
} else {
|
|
9732
|
+
columnFilters.delete(col.name);
|
|
9733
|
+
}
|
|
9734
|
+
scheduleFilter();
|
|
9735
|
+
});
|
|
9736
|
+
input.addEventListener("keydown", (e2) => {
|
|
9737
|
+
if (e2.key === "Escape") {
|
|
9738
|
+
input.value = "";
|
|
9739
|
+
columnFilters.delete(col.name);
|
|
9740
|
+
scheduleFilter();
|
|
9741
|
+
}
|
|
9742
|
+
});
|
|
9743
|
+
cell.appendChild(input);
|
|
9744
|
+
filterRow.appendChild(cell);
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9747
|
+
function scheduleFilter() {
|
|
9748
|
+
if (filterTimer)
|
|
9749
|
+
clearTimeout(filterTimer);
|
|
9750
|
+
filterTimer = setTimeout(() => {
|
|
9751
|
+
invalidateData();
|
|
9752
|
+
}, FILTER_DEBOUNCE_MS);
|
|
9753
|
+
}
|
|
9754
|
+
function syncContentWidth() {
|
|
9755
|
+
requestAnimationFrame(() => {
|
|
9756
|
+
const w = headerRow.scrollWidth;
|
|
9757
|
+
if (w > 0) {
|
|
9758
|
+
spacer.style.minWidth = `${w}px`;
|
|
9759
|
+
body.style.minWidth = `${w}px`;
|
|
9760
|
+
filterRow.style.minWidth = `${w}px`;
|
|
9761
|
+
}
|
|
9762
|
+
});
|
|
9763
|
+
}
|
|
9764
|
+
function handleSort(column) {
|
|
9765
|
+
if (sort?.column === column) {
|
|
9766
|
+
sort = sort.direction === "asc" ? { column, direction: "desc" } : null;
|
|
9767
|
+
} else {
|
|
9768
|
+
sort = { column, direction: "asc" };
|
|
9769
|
+
}
|
|
9770
|
+
pageCache = new Map;
|
|
9771
|
+
pendingPages = new Set;
|
|
9772
|
+
renderHeader();
|
|
9773
|
+
renderViewport();
|
|
9774
|
+
}
|
|
9775
|
+
function ensurePage(pageStart) {
|
|
9776
|
+
if (pageCache.has(pageStart) || pendingPages.has(pageStart))
|
|
9777
|
+
return;
|
|
9778
|
+
pendingPages.add(pageStart);
|
|
9779
|
+
const gen = loadGeneration;
|
|
9780
|
+
const filters = collectFilters();
|
|
9781
|
+
callbacks.fetchPage(currentTable, pageStart, PAGE_SIZE, sort, filters).then((data) => {
|
|
9782
|
+
if (gen !== loadGeneration)
|
|
9783
|
+
return;
|
|
9784
|
+
pendingPages.delete(pageStart);
|
|
9785
|
+
pageCache.set(pageStart, data.rows);
|
|
9786
|
+
totalRows = data.totalRows;
|
|
9787
|
+
spacer.style.height = `${totalRows * ROW_HEIGHT}px`;
|
|
9788
|
+
updateStatus();
|
|
9789
|
+
renderViewport();
|
|
9790
|
+
}).catch(() => {
|
|
9791
|
+
if (gen === loadGeneration)
|
|
9792
|
+
pendingPages.delete(pageStart);
|
|
9793
|
+
});
|
|
9794
|
+
}
|
|
9795
|
+
function renderViewport() {
|
|
9796
|
+
cancelAnimationFrame(rafId);
|
|
9797
|
+
rafId = requestAnimationFrame(() => {
|
|
9798
|
+
const scrollTop = viewport.scrollTop;
|
|
9799
|
+
const viewHeight = viewport.clientHeight;
|
|
9800
|
+
const startRow = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - OVERSCAN);
|
|
9801
|
+
const endRow = Math.min(totalRows, Math.ceil((scrollTop + viewHeight) / ROW_HEIGHT) + OVERSCAN);
|
|
9802
|
+
const neededPageStart = Math.floor(startRow / PAGE_SIZE) * PAGE_SIZE;
|
|
9803
|
+
const neededPageEnd = Math.floor(endRow / PAGE_SIZE) * PAGE_SIZE;
|
|
9804
|
+
for (let p2 = neededPageStart;p2 <= neededPageEnd; p2 += PAGE_SIZE) {
|
|
9805
|
+
ensurePage(p2);
|
|
9806
|
+
}
|
|
9807
|
+
body.innerHTML = "";
|
|
9808
|
+
body.style.transform = `translateY(${startRow * ROW_HEIGHT}px)`;
|
|
9809
|
+
for (let i2 = startRow;i2 < endRow; i2++) {
|
|
9810
|
+
const pageStart = Math.floor(i2 / PAGE_SIZE) * PAGE_SIZE;
|
|
9811
|
+
const pageRows = pageCache.get(pageStart);
|
|
9812
|
+
const rowData = pageRows ? pageRows[i2 - pageStart] : null;
|
|
9813
|
+
const row = document.createElement("div");
|
|
9814
|
+
row.className = "db-grid-row";
|
|
9815
|
+
if (i2 % 2 === 1)
|
|
9816
|
+
row.classList.add("alt");
|
|
9817
|
+
if (i2 === selectedRowIndex)
|
|
9818
|
+
row.classList.add("selected");
|
|
9819
|
+
const rowIndex = i2;
|
|
9820
|
+
row.addEventListener("click", () => {
|
|
9821
|
+
selectedRowIndex = rowIndex;
|
|
9822
|
+
body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
|
|
9823
|
+
r2.classList.remove("selected");
|
|
9824
|
+
});
|
|
9825
|
+
row.classList.add("selected");
|
|
9826
|
+
});
|
|
9827
|
+
const rowNum = document.createElement("div");
|
|
9828
|
+
rowNum.className = "db-grid-cell db-grid-rownum";
|
|
9829
|
+
rowNum.textContent = String(i2 + 1);
|
|
9830
|
+
row.appendChild(rowNum);
|
|
9831
|
+
if (rowData) {
|
|
9832
|
+
for (let c2 = 0;c2 < columnNames.length; c2++) {
|
|
9833
|
+
const cell = document.createElement("div");
|
|
9834
|
+
cell.className = "db-grid-cell";
|
|
9835
|
+
cell.style.width = `${getColWidth(columnNames[c2])}px`;
|
|
9836
|
+
const val = rowData[c2];
|
|
9837
|
+
cell.textContent = formatValue3(val);
|
|
9838
|
+
if (val === null)
|
|
9839
|
+
cell.classList.add("null");
|
|
9840
|
+
else if (val instanceof Uint8Array)
|
|
9841
|
+
cell.classList.add("blob");
|
|
9842
|
+
else if (typeof val === "string" && val === "")
|
|
9843
|
+
cell.classList.add("empty");
|
|
9844
|
+
cell.style.cursor = "pointer";
|
|
9845
|
+
const cellValue = val;
|
|
9846
|
+
const cellColIndex = c2;
|
|
9847
|
+
cell.addEventListener("click", (e2) => {
|
|
9848
|
+
e2.stopPropagation();
|
|
9849
|
+
selectedRowIndex = rowIndex;
|
|
9850
|
+
body.querySelectorAll(".db-grid-row.selected").forEach((r2) => {
|
|
9851
|
+
r2.classList.remove("selected");
|
|
9852
|
+
});
|
|
9853
|
+
row.classList.add("selected");
|
|
9854
|
+
showCellDetail(cellColIndex, cellValue);
|
|
9855
|
+
const text2 = formatValueForCopy(cellValue);
|
|
9856
|
+
navigator.clipboard.writeText(text2).then(() => {
|
|
9857
|
+
cell.classList.add("copied");
|
|
9858
|
+
setTimeout(() => cell.classList.remove("copied"), 600);
|
|
9859
|
+
}, () => {});
|
|
9860
|
+
});
|
|
9861
|
+
row.appendChild(cell);
|
|
9862
|
+
}
|
|
9863
|
+
} else {
|
|
9864
|
+
for (let c2 = 0;c2 < columnNames.length; c2++) {
|
|
9865
|
+
const cell = document.createElement("div");
|
|
9866
|
+
cell.className = "db-grid-cell loading";
|
|
9867
|
+
cell.style.width = `${getColWidth(columnNames[c2])}px`;
|
|
9868
|
+
cell.textContent = "…";
|
|
9869
|
+
row.appendChild(cell);
|
|
9870
|
+
}
|
|
9871
|
+
}
|
|
9872
|
+
body.appendChild(row);
|
|
9873
|
+
}
|
|
9874
|
+
});
|
|
9875
|
+
}
|
|
9876
|
+
function triggerExport(format2) {
|
|
9877
|
+
const dbId = callbacks.getDbId();
|
|
9878
|
+
if (!dbId || !currentTable)
|
|
9879
|
+
return;
|
|
9880
|
+
const params = new URLSearchParams({
|
|
9881
|
+
db: dbId,
|
|
9882
|
+
table: currentTable,
|
|
9883
|
+
format: format2
|
|
9884
|
+
});
|
|
9885
|
+
if (sort) {
|
|
9886
|
+
params.set("sort", sort.column);
|
|
9887
|
+
params.set("dir", sort.direction);
|
|
9888
|
+
}
|
|
9889
|
+
const filters = collectFilters();
|
|
9890
|
+
if (filters.length > 0) {
|
|
9891
|
+
params.set("filters", JSON.stringify(filters));
|
|
9892
|
+
}
|
|
9893
|
+
const a2 = document.createElement("a");
|
|
9894
|
+
a2.href = `/_db/export?${params}`;
|
|
9895
|
+
a2.download = `${currentTable}.${format2}`;
|
|
9896
|
+
document.body.appendChild(a2);
|
|
9897
|
+
a2.click();
|
|
9898
|
+
a2.remove();
|
|
9899
|
+
}
|
|
9900
|
+
let exportCsvBtn = null;
|
|
9901
|
+
let exportJsonBtn = null;
|
|
9902
|
+
function updateStatus() {
|
|
9903
|
+
if (!statusEl) {
|
|
9904
|
+
statusEl = document.createElement("div");
|
|
9905
|
+
statusEl.className = "db-grid-status";
|
|
9906
|
+
exportCsvBtn = document.createElement("button");
|
|
9907
|
+
exportCsvBtn.type = "button";
|
|
9908
|
+
exportCsvBtn.className = "db-btn db-btn-sm db-grid-export-btn";
|
|
9909
|
+
exportCsvBtn.textContent = "Export CSV";
|
|
9910
|
+
exportCsvBtn.addEventListener("click", () => triggerExport("csv"));
|
|
9911
|
+
exportJsonBtn = document.createElement("button");
|
|
9912
|
+
exportJsonBtn.type = "button";
|
|
9913
|
+
exportJsonBtn.className = "db-btn db-btn-sm db-grid-export-btn";
|
|
9914
|
+
exportJsonBtn.textContent = "Export JSON";
|
|
9915
|
+
exportJsonBtn.addEventListener("click", () => triggerExport("json"));
|
|
9916
|
+
statusEl.append(exportCsvBtn, exportJsonBtn);
|
|
9917
|
+
el.appendChild(statusEl);
|
|
9918
|
+
}
|
|
9919
|
+
const parts = [`${totalRows.toLocaleString()} rows`];
|
|
9920
|
+
if (sort)
|
|
9921
|
+
parts.push(`Sort: ${sort.column} ${sort.direction.toUpperCase()}`);
|
|
9922
|
+
const activeFilterCount = columnFilters.size + (globalSearchValue ? 1 : 0);
|
|
9923
|
+
if (activeFilterCount > 0)
|
|
9924
|
+
parts.push(`${activeFilterCount} filter(s)`);
|
|
9925
|
+
const textNode = statusEl.firstChild;
|
|
9926
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
9927
|
+
textNode.textContent = `${parts.join(" | ")} `;
|
|
9928
|
+
} else {
|
|
9929
|
+
statusEl.insertBefore(document.createTextNode(`${parts.join(" | ")} `), statusEl.firstChild);
|
|
9930
|
+
}
|
|
9931
|
+
}
|
|
9932
|
+
function load(table2, initialData) {
|
|
9933
|
+
clear();
|
|
9934
|
+
currentTable = table2;
|
|
9935
|
+
if (initialData) {
|
|
9936
|
+
columns = initialData.columns;
|
|
9937
|
+
columnNames = columns.map((c2) => c2.name);
|
|
9938
|
+
totalRows = initialData.totalRows;
|
|
9939
|
+
pageCache.set(0, initialData.rows);
|
|
9940
|
+
} else {
|
|
9941
|
+
columns = [];
|
|
9942
|
+
columnNames = [];
|
|
9943
|
+
totalRows = 0;
|
|
9944
|
+
}
|
|
9945
|
+
loadColWidths();
|
|
9946
|
+
spacer.style.height = `${totalRows * ROW_HEIGHT}px`;
|
|
9947
|
+
renderHeader();
|
|
9948
|
+
updateStatus();
|
|
9949
|
+
if (!initialData) {
|
|
9950
|
+
ensurePage(0);
|
|
9951
|
+
} else {
|
|
9952
|
+
renderViewport();
|
|
9953
|
+
}
|
|
9954
|
+
}
|
|
9955
|
+
filterInput.addEventListener("input", () => {
|
|
9956
|
+
globalSearchValue = filterInput.value.trim();
|
|
9957
|
+
filterClear.hidden = !globalSearchValue;
|
|
9958
|
+
scheduleFilter();
|
|
9959
|
+
});
|
|
9960
|
+
filterInput.addEventListener("keydown", (e2) => {
|
|
9961
|
+
if (e2.key === "Escape") {
|
|
9962
|
+
filterInput.value = "";
|
|
9963
|
+
globalSearchValue = "";
|
|
9964
|
+
filterClear.hidden = true;
|
|
9965
|
+
scheduleFilter();
|
|
9966
|
+
}
|
|
9967
|
+
});
|
|
9968
|
+
filterClear.addEventListener("click", () => {
|
|
9969
|
+
filterInput.value = "";
|
|
9970
|
+
globalSearchValue = "";
|
|
9971
|
+
filterClear.hidden = true;
|
|
9972
|
+
invalidateData();
|
|
9973
|
+
});
|
|
9974
|
+
viewport.addEventListener("scroll", () => {
|
|
9975
|
+
headerWrap.scrollLeft = viewport.scrollLeft;
|
|
9976
|
+
filterRowWrap.scrollLeft = viewport.scrollLeft;
|
|
9977
|
+
renderViewport();
|
|
9978
|
+
}, { passive: true });
|
|
9979
|
+
function destroy() {
|
|
9980
|
+
clear();
|
|
9981
|
+
viewport.removeEventListener("scroll", renderViewport);
|
|
9982
|
+
}
|
|
9983
|
+
return { el, load, clear, destroy };
|
|
9984
|
+
}
|
|
9985
|
+
function formatValue3(value) {
|
|
9986
|
+
if (value === null)
|
|
9987
|
+
return "NULL";
|
|
9988
|
+
if (value instanceof Uint8Array)
|
|
9989
|
+
return `<blob ${value.byteLength} bytes>`;
|
|
9990
|
+
if (typeof value === "boolean")
|
|
9991
|
+
return value ? "true" : "false";
|
|
9992
|
+
if (typeof value === "object")
|
|
9993
|
+
return JSON.stringify(value);
|
|
9994
|
+
const s2 = String(value);
|
|
9995
|
+
if (s2 === "")
|
|
9996
|
+
return "<empty>";
|
|
9997
|
+
return s2;
|
|
9998
|
+
}
|
|
9999
|
+
function formatValueForCopy(value) {
|
|
10000
|
+
if (value === null)
|
|
10001
|
+
return "";
|
|
10002
|
+
if (value instanceof Uint8Array)
|
|
10003
|
+
return `<blob ${value.byteLength} bytes>`;
|
|
10004
|
+
if (typeof value === "boolean")
|
|
10005
|
+
return value ? "true" : "false";
|
|
10006
|
+
if (typeof value === "object")
|
|
10007
|
+
return JSON.stringify(value);
|
|
10008
|
+
return String(value);
|
|
10009
|
+
}
|
|
10010
|
+
|
|
10011
|
+
// web-src/views/database/table-list.ts
|
|
10012
|
+
function createTableList(callbacks) {
|
|
10013
|
+
const wrapper = document.createElement("div");
|
|
10014
|
+
wrapper.className = "db-table-list-wrapper";
|
|
10015
|
+
wrapper.style.display = "flex";
|
|
10016
|
+
wrapper.style.flexDirection = "column";
|
|
10017
|
+
wrapper.style.flex = "1";
|
|
10018
|
+
wrapper.style.overflow = "hidden";
|
|
10019
|
+
const filterWrap = document.createElement("div");
|
|
10020
|
+
filterWrap.className = "db-table-filter-wrap";
|
|
10021
|
+
const filterInput = document.createElement("input");
|
|
10022
|
+
filterInput.className = "db-table-filter";
|
|
10023
|
+
filterInput.type = "text";
|
|
10024
|
+
filterInput.placeholder = "Filter tables...";
|
|
10025
|
+
filterWrap.appendChild(filterInput);
|
|
10026
|
+
const el = document.createElement("div");
|
|
10027
|
+
el.className = "db-table-list";
|
|
10028
|
+
wrapper.append(filterWrap, el);
|
|
10029
|
+
let activeTable = null;
|
|
10030
|
+
let allTables = [];
|
|
10031
|
+
const expandedTables = new Set;
|
|
10032
|
+
const columnCache = new Map;
|
|
10033
|
+
let contextMenu = null;
|
|
10034
|
+
function closeContextMenu() {
|
|
10035
|
+
if (contextMenu) {
|
|
10036
|
+
contextMenu.remove();
|
|
10037
|
+
contextMenu = null;
|
|
10038
|
+
}
|
|
10039
|
+
}
|
|
10040
|
+
function showContextMenu(e2, tableName) {
|
|
10041
|
+
e2.preventDefault();
|
|
10042
|
+
closeContextMenu();
|
|
10043
|
+
const menu = document.createElement("div");
|
|
10044
|
+
menu.className = "db-context-menu";
|
|
10045
|
+
menu.style.position = "absolute";
|
|
10046
|
+
menu.style.left = `${e2.pageX}px`;
|
|
10047
|
+
menu.style.top = `${e2.pageY}px`;
|
|
10048
|
+
const items = [
|
|
10049
|
+
{
|
|
10050
|
+
label: "Copy Table Name",
|
|
10051
|
+
action: () => {
|
|
10052
|
+
navigator.clipboard.writeText(tableName).catch(() => {});
|
|
10053
|
+
}
|
|
10054
|
+
},
|
|
10055
|
+
{
|
|
10056
|
+
label: "Copy SELECT Statement",
|
|
10057
|
+
action: () => {
|
|
10058
|
+
const sql = `SELECT * FROM "${tableName}" LIMIT 100`;
|
|
10059
|
+
navigator.clipboard.writeText(sql).catch(() => {});
|
|
10060
|
+
}
|
|
10061
|
+
},
|
|
10062
|
+
{
|
|
10063
|
+
label: "View CREATE TABLE",
|
|
10064
|
+
action: () => {
|
|
10065
|
+
callbacks.onViewCreateTable?.(tableName);
|
|
10066
|
+
}
|
|
10067
|
+
},
|
|
10068
|
+
{
|
|
10069
|
+
label: "View Table Definition",
|
|
10070
|
+
action: () => {
|
|
10071
|
+
callbacks.onViewDefinition?.(tableName);
|
|
10072
|
+
}
|
|
10073
|
+
}
|
|
10074
|
+
];
|
|
10075
|
+
for (const item of items) {
|
|
10076
|
+
const row = document.createElement("div");
|
|
10077
|
+
row.className = "db-context-menu-item";
|
|
10078
|
+
row.textContent = item.label;
|
|
10079
|
+
row.addEventListener("click", () => {
|
|
10080
|
+
item.action();
|
|
10081
|
+
closeContextMenu();
|
|
10082
|
+
});
|
|
10083
|
+
menu.appendChild(row);
|
|
10084
|
+
}
|
|
10085
|
+
document.body.appendChild(menu);
|
|
10086
|
+
contextMenu = menu;
|
|
10087
|
+
const onDocClick = (ev) => {
|
|
10088
|
+
if (!menu.contains(ev.target)) {
|
|
10089
|
+
closeContextMenu();
|
|
10090
|
+
document.removeEventListener("click", onDocClick, true);
|
|
10091
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
10092
|
+
}
|
|
10093
|
+
};
|
|
10094
|
+
const onKeyDown = (ev) => {
|
|
10095
|
+
if (ev.key === "Escape") {
|
|
10096
|
+
closeContextMenu();
|
|
10097
|
+
document.removeEventListener("click", onDocClick, true);
|
|
10098
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
10099
|
+
}
|
|
10100
|
+
};
|
|
10101
|
+
setTimeout(() => {
|
|
10102
|
+
document.addEventListener("click", onDocClick, true);
|
|
10103
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
10104
|
+
}, 0);
|
|
10105
|
+
}
|
|
10106
|
+
async function renderColumns(container, tableName) {
|
|
10107
|
+
let cols = columnCache.get(tableName);
|
|
10108
|
+
if (!cols && callbacks.getColumns) {
|
|
10109
|
+
cols = await callbacks.getColumns(tableName);
|
|
10110
|
+
columnCache.set(tableName, cols);
|
|
10111
|
+
}
|
|
10112
|
+
if (!cols || cols.length === 0)
|
|
10113
|
+
return;
|
|
10114
|
+
container.innerHTML = "";
|
|
10115
|
+
for (const col of cols) {
|
|
10116
|
+
const colRow = document.createElement("div");
|
|
10117
|
+
colRow.className = "db-table-col-item";
|
|
10118
|
+
if (col.primaryKey)
|
|
10119
|
+
colRow.classList.add("pk");
|
|
10120
|
+
const colName = document.createElement("span");
|
|
10121
|
+
colName.className = "db-table-col-name";
|
|
10122
|
+
colName.textContent = col.name;
|
|
10123
|
+
const colType = document.createElement("span");
|
|
10124
|
+
colType.className = "db-table-col-type";
|
|
10125
|
+
colType.textContent = col.type;
|
|
10126
|
+
colRow.append(colName, colType);
|
|
10127
|
+
container.appendChild(colRow);
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
function toggleExpand(tableName, _node, arrow, children) {
|
|
10131
|
+
const expanded = expandedTables.has(tableName);
|
|
10132
|
+
if (expanded) {
|
|
10133
|
+
expandedTables.delete(tableName);
|
|
10134
|
+
children.hidden = true;
|
|
10135
|
+
arrow.classList.remove("expanded");
|
|
10136
|
+
} else {
|
|
10137
|
+
expandedTables.add(tableName);
|
|
10138
|
+
children.hidden = false;
|
|
10139
|
+
arrow.classList.add("expanded");
|
|
10140
|
+
if (children.children.length === 0) {
|
|
10141
|
+
renderColumns(children, tableName);
|
|
10142
|
+
}
|
|
10143
|
+
}
|
|
10144
|
+
}
|
|
10145
|
+
function renderFiltered(tables, filter) {
|
|
10146
|
+
el.innerHTML = "";
|
|
10147
|
+
const filtered = filter ? tables.filter((t2) => t2.name.toLowerCase().includes(filter.toLowerCase())) : tables;
|
|
10148
|
+
if (filtered.length === 0) {
|
|
10149
|
+
const empty = document.createElement("div");
|
|
10150
|
+
empty.className = "db-table-list-empty";
|
|
10151
|
+
empty.textContent = filter ? "No matching tables" : "No tables found";
|
|
10152
|
+
el.appendChild(empty);
|
|
10153
|
+
return;
|
|
10154
|
+
}
|
|
10155
|
+
const groups = { table: [], view: [] };
|
|
10156
|
+
for (const t2 of filtered) {
|
|
10157
|
+
(groups[t2.type] || groups.table).push(t2);
|
|
10158
|
+
}
|
|
10159
|
+
for (const [type, items] of Object.entries(groups)) {
|
|
10160
|
+
if (items.length === 0)
|
|
10161
|
+
continue;
|
|
10162
|
+
const header = document.createElement("div");
|
|
10163
|
+
header.className = "db-table-group-header";
|
|
10164
|
+
header.textContent = type === "view" ? "Views" : "Tables";
|
|
10165
|
+
el.appendChild(header);
|
|
10166
|
+
for (const table2 of items) {
|
|
10167
|
+
const node = document.createElement("div");
|
|
10168
|
+
node.className = "db-table-node";
|
|
10169
|
+
node.dataset.table = table2.name;
|
|
10170
|
+
const row = document.createElement("div");
|
|
10171
|
+
row.className = "db-table-item";
|
|
10172
|
+
if (table2.name === activeTable)
|
|
10173
|
+
row.classList.add("active");
|
|
10174
|
+
row.dataset.table = table2.name;
|
|
10175
|
+
const arrow = document.createElement("span");
|
|
10176
|
+
arrow.className = "db-table-arrow";
|
|
10177
|
+
if (expandedTables.has(table2.name))
|
|
10178
|
+
arrow.classList.add("expanded");
|
|
10179
|
+
const icon = document.createElement("span");
|
|
10180
|
+
icon.className = "db-table-icon";
|
|
10181
|
+
icon.textContent = table2.type === "view" ? "V" : "T";
|
|
10182
|
+
icon.title = table2.type === "view" ? "View" : "Table";
|
|
10183
|
+
const name = document.createElement("span");
|
|
10184
|
+
name.className = "db-table-name";
|
|
10185
|
+
name.textContent = table2.name;
|
|
10186
|
+
const count = document.createElement("span");
|
|
10187
|
+
count.className = "db-table-count";
|
|
10188
|
+
count.textContent = table2.rowCount != null ? formatRowCount(table2.rowCount) : "";
|
|
10189
|
+
row.append(arrow, icon, name, count);
|
|
10190
|
+
const children = document.createElement("div");
|
|
10191
|
+
children.className = "db-table-children";
|
|
10192
|
+
children.hidden = !expandedTables.has(table2.name);
|
|
10193
|
+
if (expandedTables.has(table2.name)) {
|
|
10194
|
+
renderColumns(children, table2.name);
|
|
10195
|
+
}
|
|
10196
|
+
arrow.addEventListener("click", (e2) => {
|
|
10197
|
+
e2.stopPropagation();
|
|
10198
|
+
toggleExpand(table2.name, node, arrow, children);
|
|
10199
|
+
});
|
|
10200
|
+
row.addEventListener("click", () => callbacks.onSelectTable(table2.name));
|
|
10201
|
+
row.addEventListener("dblclick", () => callbacks.onSelectSchema(table2.name));
|
|
10202
|
+
row.addEventListener("contextmenu", (e2) => showContextMenu(e2, table2.name));
|
|
10203
|
+
node.append(row, children);
|
|
10204
|
+
el.appendChild(node);
|
|
10205
|
+
}
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
function render(tables) {
|
|
10209
|
+
allTables = tables;
|
|
10210
|
+
filterInput.value = "";
|
|
10211
|
+
renderFiltered(tables, "");
|
|
10212
|
+
}
|
|
10213
|
+
filterInput.addEventListener("input", () => {
|
|
10214
|
+
renderFiltered(allTables, filterInput.value);
|
|
10215
|
+
});
|
|
10216
|
+
function setActive(table2) {
|
|
10217
|
+
activeTable = table2;
|
|
10218
|
+
el.querySelectorAll(".db-table-item").forEach((item) => {
|
|
10219
|
+
item.classList.toggle("active", item.dataset.table === table2);
|
|
10220
|
+
});
|
|
10221
|
+
}
|
|
10222
|
+
return { el: wrapper, render, setActive };
|
|
10223
|
+
}
|
|
10224
|
+
function formatRowCount(n2) {
|
|
10225
|
+
if (n2 >= 1e6)
|
|
10226
|
+
return `${(n2 / 1e6).toFixed(1)}M`;
|
|
10227
|
+
if (n2 >= 1000)
|
|
10228
|
+
return `${(n2 / 1000).toFixed(1)}K`;
|
|
10229
|
+
return String(n2);
|
|
10230
|
+
}
|
|
10231
|
+
|
|
10232
|
+
// web-src/views/database/database-view.ts
|
|
10233
|
+
function createDatabaseView(deps) {
|
|
10234
|
+
let mounted = false;
|
|
10235
|
+
let currentDb = null;
|
|
10236
|
+
let schemaCache = null;
|
|
10237
|
+
let lastFiles = [];
|
|
10238
|
+
const dbSelect = document.createElement("select");
|
|
10239
|
+
dbSelect.className = "db-file-select";
|
|
10240
|
+
dbSelect.title = "Select database file";
|
|
10241
|
+
const dbToolbar = document.createElement("div");
|
|
10242
|
+
dbToolbar.className = "db-toolbar";
|
|
10243
|
+
dbToolbar.appendChild(dbSelect);
|
|
10244
|
+
const tabBar = document.createElement("div");
|
|
10245
|
+
tabBar.className = "db-tab-bar";
|
|
10246
|
+
const tabData = createTab("Data", true);
|
|
10247
|
+
const tabSchema = createTab("Schema", false);
|
|
10248
|
+
tabBar.append(tabData, tabSchema);
|
|
10249
|
+
let currentTab = "data";
|
|
10250
|
+
const tableList = createTableList({
|
|
10251
|
+
onSelectTable: (table2) => selectTable(table2),
|
|
10252
|
+
onSelectSchema: (table2) => showSchema(table2),
|
|
10253
|
+
onViewCreateTable: (table2) => showDdl(table2),
|
|
10254
|
+
onViewDefinition: (table2) => showSchema(table2),
|
|
10255
|
+
getColumns: (table2) => fetchColumns(table2)
|
|
10256
|
+
});
|
|
10257
|
+
const sidebar = document.createElement("div");
|
|
10258
|
+
sidebar.className = "db-sidebar";
|
|
10259
|
+
const savedWidth = localStorage.getItem("db:sidebar-width");
|
|
10260
|
+
if (savedWidth)
|
|
10261
|
+
sidebar.style.width = savedWidth;
|
|
10262
|
+
const toolsSection = document.createElement("div");
|
|
10263
|
+
toolsSection.className = "db-tools-section";
|
|
10264
|
+
const queryBtn = document.createElement("button");
|
|
10265
|
+
queryBtn.className = "db-tool-btn";
|
|
10266
|
+
queryBtn.type = "button";
|
|
10267
|
+
queryBtn.textContent = "Query";
|
|
10268
|
+
queryBtn.title = "SQL Query Editor";
|
|
10269
|
+
queryBtn.addEventListener("click", () => {
|
|
10270
|
+
setActiveTab("query");
|
|
10271
|
+
});
|
|
10272
|
+
const erBtn = document.createElement("button");
|
|
10273
|
+
erBtn.className = "db-tool-btn";
|
|
10274
|
+
erBtn.type = "button";
|
|
10275
|
+
erBtn.textContent = "ER Diagram";
|
|
10276
|
+
erBtn.title = "Entity Relationship Diagram";
|
|
10277
|
+
erBtn.addEventListener("click", () => {
|
|
10278
|
+
setActiveTab("er");
|
|
10279
|
+
if (schemaCache)
|
|
10280
|
+
renderErDiagram();
|
|
10281
|
+
});
|
|
10282
|
+
const searchBtn = document.createElement("button");
|
|
10283
|
+
searchBtn.className = "db-tool-btn";
|
|
10284
|
+
searchBtn.type = "button";
|
|
10285
|
+
searchBtn.textContent = "Search";
|
|
10286
|
+
searchBtn.title = "Search all tables";
|
|
10287
|
+
searchBtn.addEventListener("click", () => {
|
|
10288
|
+
setActiveTab("search");
|
|
10289
|
+
});
|
|
10290
|
+
const snapshotBtn = document.createElement("button");
|
|
10291
|
+
snapshotBtn.className = "db-tool-btn";
|
|
10292
|
+
snapshotBtn.type = "button";
|
|
10293
|
+
snapshotBtn.textContent = "Snapshot";
|
|
10294
|
+
snapshotBtn.title = "Snapshot & Diff";
|
|
10295
|
+
snapshotBtn.addEventListener("click", () => {
|
|
10296
|
+
setActiveTab("snapshot");
|
|
10297
|
+
snapshotView.refresh();
|
|
10298
|
+
});
|
|
10299
|
+
toolsSection.append(queryBtn, erBtn, searchBtn, snapshotBtn);
|
|
10300
|
+
sidebar.append(dbToolbar, tableList.el, toolsSection);
|
|
10301
|
+
const resizeHandle = document.createElement("div");
|
|
10302
|
+
resizeHandle.className = "db-sidebar-resize";
|
|
10303
|
+
sidebar.appendChild(resizeHandle);
|
|
10304
|
+
let resizing = false;
|
|
10305
|
+
resizeHandle.addEventListener("mousedown", (e2) => {
|
|
10306
|
+
e2.preventDefault();
|
|
10307
|
+
resizing = true;
|
|
10308
|
+
resizeHandle.classList.add("active");
|
|
10309
|
+
const onMove = (ev) => {
|
|
10310
|
+
if (!resizing)
|
|
10311
|
+
return;
|
|
10312
|
+
const w = Math.max(120, Math.min(600, ev.clientX));
|
|
10313
|
+
sidebar.style.width = `${w}px`;
|
|
10314
|
+
};
|
|
10315
|
+
const onUp = () => {
|
|
10316
|
+
resizing = false;
|
|
10317
|
+
resizeHandle.classList.remove("active");
|
|
10318
|
+
localStorage.setItem("db:sidebar-width", sidebar.style.width);
|
|
10319
|
+
window.removeEventListener("mousemove", onMove);
|
|
10320
|
+
window.removeEventListener("mouseup", onUp);
|
|
10321
|
+
};
|
|
10322
|
+
window.addEventListener("mousemove", onMove);
|
|
10323
|
+
window.addEventListener("mouseup", onUp);
|
|
10324
|
+
});
|
|
10325
|
+
const grid = createTableGrid({
|
|
10326
|
+
fetchPage: (table2, offset, limit, sort, filters) => fetchTablePage(table2, offset, limit, sort, filters),
|
|
10327
|
+
getDbId: () => currentDb?.id || null
|
|
10328
|
+
});
|
|
10329
|
+
const queryEditor = createQueryEditor({
|
|
10330
|
+
executeQuery: (sql) => executeQuery(sql)
|
|
10331
|
+
});
|
|
10332
|
+
const schemaView = createSchemaView();
|
|
10333
|
+
const erDiagram = createErDiagram();
|
|
10334
|
+
const globalSearchView = createGlobalSearchView({
|
|
10335
|
+
getDbId: () => currentDb?.id || null
|
|
10336
|
+
});
|
|
10337
|
+
const snapshotView = createSnapshotView({
|
|
10338
|
+
getDbId: () => currentDb?.id || null,
|
|
10339
|
+
getTables: () => schemaCache?.tables || []
|
|
10340
|
+
});
|
|
10341
|
+
const historyView = createQueryHistoryView({
|
|
10342
|
+
getDbId: () => currentDb?.id || null,
|
|
10343
|
+
copySqlToQuery: (sql) => {
|
|
10344
|
+
queryEditor.setSql(sql);
|
|
10345
|
+
setActiveTab("query");
|
|
10346
|
+
}
|
|
10347
|
+
});
|
|
10348
|
+
const mainContent = document.createElement("div");
|
|
10349
|
+
mainContent.className = "db-main-content";
|
|
10350
|
+
mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el);
|
|
10351
|
+
queryEditor.el.hidden = true;
|
|
10352
|
+
globalSearchView.el.hidden = true;
|
|
10353
|
+
snapshotView.el.hidden = true;
|
|
10354
|
+
const upperArea = document.createElement("div");
|
|
10355
|
+
upperArea.className = "db-upper-area";
|
|
10356
|
+
upperArea.append(sidebar, mainContent);
|
|
10357
|
+
const historyResizer = document.createElement("div");
|
|
10358
|
+
historyResizer.className = "db-history-resizer";
|
|
10359
|
+
const historyPane = document.createElement("div");
|
|
10360
|
+
historyPane.className = "db-history-pane";
|
|
10361
|
+
const savedHistoryHeight = localStorage.getItem("db:history-height");
|
|
10362
|
+
if (savedHistoryHeight)
|
|
10363
|
+
historyPane.style.height = savedHistoryHeight;
|
|
10364
|
+
historyPane.appendChild(historyView.el);
|
|
10365
|
+
let historyResizing = false;
|
|
10366
|
+
historyResizer.addEventListener("mousedown", (e2) => {
|
|
10367
|
+
e2.preventDefault();
|
|
10368
|
+
historyResizing = true;
|
|
10369
|
+
historyResizer.classList.add("active");
|
|
10370
|
+
const startY = e2.clientY;
|
|
10371
|
+
const startH = historyPane.offsetHeight;
|
|
10372
|
+
const onMove = (ev) => {
|
|
10373
|
+
if (!historyResizing)
|
|
10374
|
+
return;
|
|
10375
|
+
const maxH = container.offsetHeight - 60;
|
|
10376
|
+
const h = Math.max(60, Math.min(maxH, startH - (ev.clientY - startY)));
|
|
10377
|
+
historyPane.style.height = `${h}px`;
|
|
10378
|
+
};
|
|
10379
|
+
const onUp = () => {
|
|
10380
|
+
historyResizing = false;
|
|
10381
|
+
historyResizer.classList.remove("active");
|
|
10382
|
+
localStorage.setItem("db:history-height", historyPane.style.height);
|
|
10383
|
+
window.removeEventListener("mousemove", onMove);
|
|
10384
|
+
window.removeEventListener("mouseup", onUp);
|
|
10385
|
+
};
|
|
10386
|
+
window.addEventListener("mousemove", onMove);
|
|
10387
|
+
window.addEventListener("mouseup", onUp);
|
|
10388
|
+
});
|
|
10389
|
+
const historyToggle = document.createElement("button");
|
|
10390
|
+
historyToggle.className = "db-history-toggle";
|
|
10391
|
+
historyToggle.type = "button";
|
|
10392
|
+
historyToggle.textContent = "Query History";
|
|
10393
|
+
historyToggle.title = "Toggle query history panel";
|
|
10394
|
+
sidebar.appendChild(historyToggle);
|
|
10395
|
+
let historyOpen = localStorage.getItem("db:history-open") !== "false";
|
|
10396
|
+
function applyHistoryVisibility() {
|
|
10397
|
+
historyResizer.hidden = !historyOpen;
|
|
10398
|
+
historyPane.hidden = !historyOpen;
|
|
10399
|
+
historyToggle.classList.toggle("active", historyOpen);
|
|
10400
|
+
if (historyOpen)
|
|
10401
|
+
historyView.refresh();
|
|
10402
|
+
}
|
|
10403
|
+
applyHistoryVisibility();
|
|
10404
|
+
historyToggle.addEventListener("click", () => {
|
|
10405
|
+
historyOpen = !historyOpen;
|
|
10406
|
+
localStorage.setItem("db:history-open", String(historyOpen));
|
|
10407
|
+
applyHistoryVisibility();
|
|
10408
|
+
});
|
|
10409
|
+
const container = document.createElement("div");
|
|
10410
|
+
container.className = "db-container";
|
|
10411
|
+
container.append(upperArea, historyResizer, historyPane);
|
|
10412
|
+
function createTab(text2, active) {
|
|
10413
|
+
const btn = document.createElement("button");
|
|
10414
|
+
btn.className = `db-tab${active ? " active" : ""}`;
|
|
10415
|
+
btn.type = "button";
|
|
10416
|
+
btn.textContent = text2;
|
|
10417
|
+
return btn;
|
|
10418
|
+
}
|
|
10419
|
+
function clearDockerNotice() {
|
|
10420
|
+
const notice = mainContent.querySelector(".db-docker-notice");
|
|
10421
|
+
if (notice)
|
|
10422
|
+
notice.remove();
|
|
10423
|
+
tabBar.hidden = false;
|
|
10424
|
+
grid.el.hidden = false;
|
|
10425
|
+
}
|
|
10426
|
+
function mount() {
|
|
10427
|
+
if (mounted)
|
|
10428
|
+
return;
|
|
10429
|
+
const content = document.getElementById("content");
|
|
10430
|
+
if (!content)
|
|
10431
|
+
return;
|
|
10432
|
+
const diff = document.getElementById("diff");
|
|
10433
|
+
if (diff)
|
|
10434
|
+
diff.hidden = true;
|
|
10435
|
+
const empty = document.getElementById("empty");
|
|
10436
|
+
if (empty)
|
|
10437
|
+
empty.classList.add("hidden");
|
|
10438
|
+
content.appendChild(container);
|
|
10439
|
+
mounted = true;
|
|
10440
|
+
applyHistoryVisibility();
|
|
10441
|
+
}
|
|
10442
|
+
function unmount() {
|
|
10443
|
+
if (!mounted)
|
|
10444
|
+
return;
|
|
10445
|
+
container.remove();
|
|
10446
|
+
const diff = document.getElementById("diff");
|
|
10447
|
+
if (diff)
|
|
10448
|
+
diff.hidden = false;
|
|
10449
|
+
mounted = false;
|
|
10450
|
+
grid.clear();
|
|
10451
|
+
schemaView.clear();
|
|
10452
|
+
erDiagram.clear();
|
|
10453
|
+
currentDb = null;
|
|
10454
|
+
schemaCache = null;
|
|
10455
|
+
}
|
|
10456
|
+
function setActiveTab(tab, updateUrl = true) {
|
|
10457
|
+
currentTab = tab;
|
|
10458
|
+
tabData.classList.toggle("active", tab === "data");
|
|
10459
|
+
tabSchema.classList.toggle("active", tab === "schema");
|
|
10460
|
+
queryBtn.classList.toggle("active", tab === "query");
|
|
10461
|
+
erBtn.classList.toggle("active", tab === "er");
|
|
10462
|
+
searchBtn.classList.toggle("active", tab === "search");
|
|
10463
|
+
snapshotBtn.classList.toggle("active", tab === "snapshot");
|
|
10464
|
+
tabBar.hidden = tab === "query" || tab === "er" || tab === "search" || tab === "snapshot";
|
|
10465
|
+
grid.el.hidden = tab !== "data";
|
|
10466
|
+
queryEditor.el.hidden = tab !== "query";
|
|
10467
|
+
schemaView.el.hidden = tab !== "schema";
|
|
10468
|
+
erDiagram.el.hidden = tab !== "er";
|
|
10469
|
+
globalSearchView.el.hidden = tab !== "search";
|
|
10470
|
+
snapshotView.el.hidden = tab !== "snapshot";
|
|
10471
|
+
if (tab === "query")
|
|
10472
|
+
queryEditor.focus();
|
|
10473
|
+
if (updateUrl) {
|
|
10474
|
+
const activeTable = tableList.el.querySelector(".db-table-item.active");
|
|
10475
|
+
deps.setRoute({
|
|
10476
|
+
screen: "database",
|
|
10477
|
+
db: currentDb?.id,
|
|
10478
|
+
table: activeTable?.dataset.table,
|
|
10479
|
+
tab: tab === "data" ? undefined : tab,
|
|
10480
|
+
range: deps.currentRange()
|
|
10481
|
+
}, true);
|
|
10482
|
+
}
|
|
10483
|
+
}
|
|
10484
|
+
tabData.addEventListener("click", () => setActiveTab("data"));
|
|
10485
|
+
tabSchema.addEventListener("click", () => {
|
|
10486
|
+
setActiveTab("schema");
|
|
10487
|
+
const active = tableList.el.querySelector(".db-table-item.active");
|
|
10488
|
+
if (active?.dataset.table)
|
|
10489
|
+
showSchema(active.dataset.table);
|
|
10490
|
+
});
|
|
10491
|
+
async function fetchDbFiles() {
|
|
10492
|
+
const res = await deps.trackLoad(fetch("/_db/files"));
|
|
10493
|
+
if (!res.ok)
|
|
10494
|
+
return [];
|
|
10495
|
+
const data = await res.json();
|
|
10496
|
+
return data.files;
|
|
10497
|
+
}
|
|
10498
|
+
async function fetchSchema(dbId) {
|
|
10499
|
+
const res = await deps.trackLoad(fetch(`/_db/schema?db=${encodeURIComponent(dbId)}&includeColumns=1`));
|
|
10500
|
+
if (!res.ok)
|
|
10501
|
+
return null;
|
|
10502
|
+
return await res.json();
|
|
10503
|
+
}
|
|
10504
|
+
async function fetchTablePage(table2, offset, limit, sort, filters) {
|
|
10505
|
+
if (!currentDb)
|
|
10506
|
+
throw new Error("no database selected");
|
|
10507
|
+
const params = new URLSearchParams({
|
|
10508
|
+
db: currentDb.id,
|
|
10509
|
+
table: table2,
|
|
10510
|
+
offset: String(offset),
|
|
10511
|
+
limit: String(limit)
|
|
10512
|
+
});
|
|
10513
|
+
if (sort) {
|
|
10514
|
+
params.set("sort", sort.column);
|
|
10515
|
+
params.set("dir", sort.direction);
|
|
10516
|
+
}
|
|
10517
|
+
if (filters.length > 0) {
|
|
10518
|
+
params.set("filters", JSON.stringify(filters));
|
|
10519
|
+
}
|
|
10520
|
+
const res = await fetch(`/_db/table?${params}`);
|
|
10521
|
+
if (!res.ok)
|
|
10522
|
+
throw new Error(await res.text());
|
|
10523
|
+
return await res.json();
|
|
10524
|
+
}
|
|
10525
|
+
async function executeQuery(sql) {
|
|
10526
|
+
if (!currentDb)
|
|
10527
|
+
throw new Error("no database selected");
|
|
10528
|
+
const res = await fetch("/_db/query", {
|
|
10529
|
+
method: "POST",
|
|
10530
|
+
headers: {
|
|
10531
|
+
"Content-Type": "application/json",
|
|
10532
|
+
"X-Code-Viewer-Action": "1"
|
|
10533
|
+
},
|
|
10534
|
+
body: JSON.stringify({
|
|
10535
|
+
db: currentDb.id,
|
|
10536
|
+
sql,
|
|
10537
|
+
saveHistory: true,
|
|
10538
|
+
source: "browser",
|
|
10539
|
+
executedBy: "user"
|
|
10540
|
+
})
|
|
10541
|
+
});
|
|
10542
|
+
return await res.json();
|
|
10543
|
+
}
|
|
10544
|
+
async function selectDb(dbId) {
|
|
10545
|
+
const schema = await fetchSchema(dbId);
|
|
10546
|
+
if (!schema)
|
|
10547
|
+
return;
|
|
10548
|
+
schemaCache = schema;
|
|
10549
|
+
tableList.render(schema.tables);
|
|
10550
|
+
grid.clear();
|
|
10551
|
+
schemaView.clear();
|
|
10552
|
+
erDiagram.clear();
|
|
10553
|
+
setActiveTab("data");
|
|
10554
|
+
if (schema.tables.length > 0) {
|
|
10555
|
+
selectTable(schema.tables[0].name);
|
|
10556
|
+
}
|
|
10557
|
+
}
|
|
10558
|
+
async function selectTable(table2) {
|
|
10559
|
+
tableList.setActive(table2);
|
|
10560
|
+
if (!currentDb)
|
|
10561
|
+
return;
|
|
10562
|
+
if (currentTab === "query" || currentTab === "er") {
|
|
10563
|
+
setActiveTab("data", false);
|
|
10564
|
+
}
|
|
10565
|
+
deps.setRoute({
|
|
10566
|
+
screen: "database",
|
|
10567
|
+
db: currentDb.id,
|
|
10568
|
+
table: table2,
|
|
10569
|
+
tab: currentTab === "data" ? undefined : currentTab,
|
|
10570
|
+
range: deps.currentRange()
|
|
10571
|
+
}, true);
|
|
10572
|
+
try {
|
|
10573
|
+
const data = await deps.trackLoad(fetchTablePage(table2, 0, 200, null, []));
|
|
10574
|
+
grid.load(table2, data);
|
|
10575
|
+
} catch {
|
|
10576
|
+
grid.load(table2);
|
|
10577
|
+
}
|
|
10578
|
+
if (currentTab === "schema") {
|
|
10579
|
+
const columns = await fetchColumns(table2);
|
|
10580
|
+
schemaView.render(table2, columns, schemaCache?.indexes || []);
|
|
10581
|
+
}
|
|
10582
|
+
}
|
|
10583
|
+
async function fetchColumns(table2) {
|
|
10584
|
+
if (schemaCache?.columnsMap?.[table2]) {
|
|
10585
|
+
return schemaCache.columnsMap[table2];
|
|
10586
|
+
}
|
|
10587
|
+
if (!currentDb)
|
|
10588
|
+
return [];
|
|
10589
|
+
const res = await fetch(`/_db/columns?db=${encodeURIComponent(currentDb.id)}&table=${encodeURIComponent(table2)}`);
|
|
10590
|
+
if (!res.ok)
|
|
10591
|
+
return [];
|
|
10592
|
+
const data = await res.json();
|
|
10593
|
+
return data.columns;
|
|
10594
|
+
}
|
|
10595
|
+
async function showSchema(table2) {
|
|
10596
|
+
setActiveTab("schema");
|
|
10597
|
+
if (!currentDb)
|
|
10598
|
+
return;
|
|
10599
|
+
const columns = await fetchColumns(table2);
|
|
10600
|
+
schemaView.render(table2, columns, schemaCache?.indexes || []);
|
|
10601
|
+
}
|
|
10602
|
+
async function showDdl(table2) {
|
|
10603
|
+
if (!currentDb)
|
|
10604
|
+
return;
|
|
10605
|
+
setActiveTab("schema");
|
|
10606
|
+
try {
|
|
10607
|
+
const res = await fetch(`/_db/ddl?db=${encodeURIComponent(currentDb.id)}&table=${encodeURIComponent(table2)}`);
|
|
10608
|
+
if (!res.ok)
|
|
10609
|
+
return;
|
|
10610
|
+
const data = await res.json();
|
|
10611
|
+
const columns = await fetchColumns(table2);
|
|
10612
|
+
schemaView.render(table2, columns, schemaCache?.indexes || [], {
|
|
10613
|
+
foreignKeys: schemaCache?.foreignKeys,
|
|
10614
|
+
triggers: data.triggers,
|
|
10615
|
+
ddl: data.sql
|
|
10616
|
+
});
|
|
10617
|
+
} catch {}
|
|
10618
|
+
}
|
|
10619
|
+
async function renderErDiagram() {
|
|
10620
|
+
if (!schemaCache || !currentDb)
|
|
10621
|
+
return;
|
|
10622
|
+
const columnsMap = new Map;
|
|
10623
|
+
if (schemaCache.columnsMap) {
|
|
10624
|
+
for (const [name, cols] of Object.entries(schemaCache.columnsMap)) {
|
|
10625
|
+
columnsMap.set(name, cols);
|
|
10626
|
+
}
|
|
10627
|
+
} else {
|
|
10628
|
+
const tables = schemaCache.tables.filter((t2) => t2.type !== "view");
|
|
10629
|
+
const results = await Promise.all(tables.map((t2) => fetchColumns(t2.name).then((cols) => ({ name: t2.name, cols }))));
|
|
10630
|
+
for (const { name, cols } of results) {
|
|
10631
|
+
if (cols.length > 0)
|
|
10632
|
+
columnsMap.set(name, cols);
|
|
10633
|
+
}
|
|
10634
|
+
}
|
|
10635
|
+
erDiagram.render(schemaCache, columnsMap);
|
|
10636
|
+
}
|
|
10637
|
+
dbSelect.addEventListener("change", () => {
|
|
10638
|
+
const dbId = dbSelect.value;
|
|
10639
|
+
if (!dbId)
|
|
10640
|
+
return;
|
|
10641
|
+
const file = lastFiles.find((f2) => f2.id === dbId);
|
|
10642
|
+
const option = dbSelect.selectedOptions[0];
|
|
10643
|
+
currentDb = {
|
|
10644
|
+
id: dbId,
|
|
10645
|
+
path: file?.path || dbId,
|
|
10646
|
+
name: option?.textContent || dbId,
|
|
10647
|
+
sizeBytes: file?.sizeBytes || 0,
|
|
10648
|
+
kind: file?.kind || "sqlite"
|
|
10649
|
+
};
|
|
10650
|
+
clearDockerNotice();
|
|
10651
|
+
deps.setRoute({ screen: "database", db: dbId, range: deps.currentRange() }, true);
|
|
10652
|
+
selectDb(dbId);
|
|
10653
|
+
});
|
|
10654
|
+
async function enter(db, table2, tab) {
|
|
10655
|
+
mount();
|
|
10656
|
+
document.body.classList.add("gdp-database-page");
|
|
10657
|
+
deps.setPageMode();
|
|
10658
|
+
deps.syncHeaderMenu();
|
|
10659
|
+
const files = await fetchDbFiles();
|
|
10660
|
+
lastFiles = files;
|
|
10661
|
+
dbSelect.innerHTML = "";
|
|
10662
|
+
if (files.length === 0) {
|
|
10663
|
+
const opt = document.createElement("option");
|
|
10664
|
+
opt.value = "";
|
|
10665
|
+
opt.textContent = "No database files found";
|
|
10666
|
+
dbSelect.appendChild(opt);
|
|
10667
|
+
dbSelect.disabled = true;
|
|
10668
|
+
return;
|
|
10669
|
+
}
|
|
10670
|
+
dbSelect.disabled = false;
|
|
10671
|
+
for (const f2 of files) {
|
|
10672
|
+
const opt = document.createElement("option");
|
|
10673
|
+
opt.value = f2.id;
|
|
10674
|
+
const isDocker = f2.id.startsWith("docker:");
|
|
10675
|
+
const label = isDocker ? `${f2.name} (Docker)` : `${f2.path} (${formatSize(f2.sizeBytes)})`;
|
|
10676
|
+
opt.textContent = label;
|
|
10677
|
+
dbSelect.appendChild(opt);
|
|
10678
|
+
}
|
|
10679
|
+
const target = db && files.find((f2) => f2.id === db) ? db : files[0].id;
|
|
10680
|
+
dbSelect.value = target;
|
|
10681
|
+
currentDb = files.find((f2) => f2.id === target) || null;
|
|
10682
|
+
await selectDb(target);
|
|
10683
|
+
if (table2) {
|
|
10684
|
+
await selectTable(table2);
|
|
10685
|
+
}
|
|
10686
|
+
if (tab && tab !== "data") {
|
|
10687
|
+
setActiveTab(tab);
|
|
10688
|
+
if (tab === "schema") {
|
|
10689
|
+
const activeTable = tableList.el.querySelector(".db-table-item.active");
|
|
10690
|
+
if (activeTable?.dataset.table)
|
|
10691
|
+
showSchema(activeTable.dataset.table);
|
|
10692
|
+
} else if (tab === "er") {
|
|
10693
|
+
if (schemaCache)
|
|
10694
|
+
renderErDiagram();
|
|
10695
|
+
} else if (tab === "snapshot") {
|
|
10696
|
+
snapshotView.refresh();
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
}
|
|
10700
|
+
function leave() {
|
|
10701
|
+
document.body.classList.remove("gdp-database-page");
|
|
10702
|
+
unmount();
|
|
10703
|
+
}
|
|
10704
|
+
function handleSse(event, data) {
|
|
10705
|
+
if (!mounted)
|
|
10706
|
+
return;
|
|
10707
|
+
if (historyOpen)
|
|
10708
|
+
historyView.refresh();
|
|
10709
|
+
if (event === "db-snapshot" && data) {
|
|
10710
|
+
snapshotView.handleSse(data);
|
|
10711
|
+
}
|
|
10712
|
+
}
|
|
10713
|
+
return { enter, leave, handleSse };
|
|
10714
|
+
}
|
|
10715
|
+
function formatSize(bytes) {
|
|
10716
|
+
if (bytes >= 1024 * 1024 * 1024)
|
|
10717
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
10718
|
+
if (bytes >= 1024 * 1024)
|
|
10719
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
10720
|
+
if (bytes >= 1024)
|
|
10721
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10722
|
+
return `${bytes} B`;
|
|
10723
|
+
}
|
|
10724
|
+
|
|
10725
|
+
// web-src/views/diff-line-select.ts
|
|
10726
|
+
var SELECTED_CLASS = "gdp-diff-line-selected";
|
|
10727
|
+
function cardPath(el) {
|
|
10728
|
+
return el.closest(".gdp-file-shell[data-path]")?.dataset.path || "";
|
|
10729
|
+
}
|
|
10730
|
+
function afterLineFromCell(cell) {
|
|
10731
|
+
const sideCell = cell.closest("td.d2h-code-side-linenumber");
|
|
10732
|
+
if (sideCell) {
|
|
10733
|
+
const side = sideCell.closest(".d2h-file-side-diff");
|
|
10734
|
+
const wrapper = sideCell.closest(".d2h-file-wrapper");
|
|
10735
|
+
if (!side || !wrapper)
|
|
10736
|
+
return null;
|
|
10737
|
+
const sides = wrapper.querySelectorAll(".d2h-file-side-diff");
|
|
10738
|
+
if (sides.length < 2 || side !== sides[1])
|
|
10739
|
+
return null;
|
|
10740
|
+
const line2 = Number((sideCell.textContent || "").trim());
|
|
10741
|
+
return Number.isInteger(line2) && line2 > 0 ? line2 : null;
|
|
10742
|
+
}
|
|
10743
|
+
const numCell = cell.closest("td.d2h-code-linenumber");
|
|
10744
|
+
if (!numCell)
|
|
10745
|
+
return null;
|
|
10746
|
+
const raw = (numCell.querySelector(".line-num2")?.textContent || "").trim();
|
|
10747
|
+
const line = Number(raw);
|
|
10748
|
+
return Number.isInteger(line) && line > 0 ? line : null;
|
|
10749
|
+
}
|
|
10750
|
+
function rowsWithAfterLines(card) {
|
|
10751
|
+
const out = [];
|
|
10752
|
+
card.querySelectorAll("table.d2h-diff-table tr").forEach((row) => {
|
|
10753
|
+
const cell = row.querySelector("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
|
|
10754
|
+
if (!cell)
|
|
10755
|
+
return;
|
|
10756
|
+
const line = afterLineFromCell(cell);
|
|
10757
|
+
if (line !== null)
|
|
10758
|
+
out.push({ row, line });
|
|
10759
|
+
});
|
|
10760
|
+
return out;
|
|
10761
|
+
}
|
|
10762
|
+
function createDiffLineSelect(deps) {
|
|
10763
|
+
let drag = null;
|
|
10764
|
+
let selection = null;
|
|
10765
|
+
function clearHighlights() {
|
|
10766
|
+
document.querySelectorAll(`.${SELECTED_CLASS}`).forEach((row) => {
|
|
10767
|
+
row.classList.remove(SELECTED_CLASS);
|
|
10768
|
+
});
|
|
10769
|
+
}
|
|
10770
|
+
function applySelection(next) {
|
|
10771
|
+
selection = next;
|
|
10772
|
+
clearHighlights();
|
|
10773
|
+
if (!next) {
|
|
10774
|
+
deps.pill.hide();
|
|
10775
|
+
return;
|
|
10776
|
+
}
|
|
10777
|
+
const start = Math.min(next.start, next.end);
|
|
10778
|
+
const end = Math.max(next.start, next.end);
|
|
10779
|
+
const card = document.querySelector(`.gdp-file-shell[data-path="${CSS.escape(next.path)}"]`);
|
|
10780
|
+
if (card) {
|
|
10781
|
+
for (const item of rowsWithAfterLines(card)) {
|
|
10782
|
+
if (item.line >= start && item.line <= end)
|
|
10783
|
+
item.row.classList.add(SELECTED_CLASS);
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10786
|
+
deps.pill.show(next.path, start, end);
|
|
10787
|
+
}
|
|
10788
|
+
function clear() {
|
|
10789
|
+
drag = null;
|
|
10790
|
+
applySelection(null);
|
|
10791
|
+
}
|
|
10792
|
+
const diff = document.querySelector("#diff");
|
|
10793
|
+
if (!diff)
|
|
10794
|
+
return { clear };
|
|
10795
|
+
diff.addEventListener("mousedown", (e2) => {
|
|
10796
|
+
const target = e2.target;
|
|
10797
|
+
const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
|
|
10798
|
+
if (!cell)
|
|
10799
|
+
return;
|
|
10800
|
+
const line = afterLineFromCell(cell);
|
|
10801
|
+
const path = cardPath(cell);
|
|
10802
|
+
if (line === null || !path) {
|
|
10803
|
+
if (selection)
|
|
10804
|
+
clear();
|
|
10805
|
+
return;
|
|
10806
|
+
}
|
|
10807
|
+
e2.preventDefault();
|
|
10808
|
+
drag = { path, start: line };
|
|
10809
|
+
applySelection({ path, start: line, end: line });
|
|
10810
|
+
});
|
|
10811
|
+
diff.addEventListener("mouseover", (e2) => {
|
|
10812
|
+
if (!drag)
|
|
10813
|
+
return;
|
|
10814
|
+
const target = e2.target;
|
|
10815
|
+
const cell = target.closest("td.d2h-code-linenumber, td.d2h-code-side-linenumber");
|
|
10816
|
+
if (!cell || cardPath(cell) !== drag.path)
|
|
10817
|
+
return;
|
|
10818
|
+
const line = afterLineFromCell(cell);
|
|
10819
|
+
if (line === null)
|
|
10820
|
+
return;
|
|
10821
|
+
applySelection({ path: drag.path, start: drag.start, end: line });
|
|
10822
|
+
});
|
|
10823
|
+
document.addEventListener("mouseup", () => {
|
|
10824
|
+
drag = null;
|
|
10825
|
+
});
|
|
10826
|
+
document.addEventListener("keydown", (e2) => {
|
|
10827
|
+
if (e2.key === "Escape" && selection && !drag)
|
|
10828
|
+
clear();
|
|
10829
|
+
});
|
|
10830
|
+
return { clear };
|
|
10831
|
+
}
|
|
10832
|
+
|
|
10833
|
+
// web-src/core/file-path-copy.ts
|
|
10834
|
+
function filePathClipboardText(path) {
|
|
10835
|
+
return path || "";
|
|
10836
|
+
}
|
|
10837
|
+
function fileNameClipboardText(path) {
|
|
10838
|
+
if (!path)
|
|
10839
|
+
return "";
|
|
10840
|
+
const parts = path.split("/").filter(Boolean);
|
|
10841
|
+
return parts[parts.length - 1] || "";
|
|
10842
|
+
}
|
|
10843
|
+
function fileReferenceClipboardText(path, start, end) {
|
|
10844
|
+
if (!path)
|
|
10845
|
+
return "";
|
|
10846
|
+
const a2 = Math.max(1, Math.floor(Math.min(start, end)));
|
|
10847
|
+
const b2 = Math.max(1, Math.floor(Math.max(start, end)));
|
|
10848
|
+
return a2 === b2 ? `@${path}#${a2}` : `@${path}#${a2}-${b2}`;
|
|
10849
|
+
}
|
|
10850
|
+
|
|
10851
|
+
// web-src/core/ws-highlight.ts
|
|
10852
|
+
function isWhitespaceOnlyInlineHighlight(text2) {
|
|
10853
|
+
return !!text2 && !/\S/.test(text2);
|
|
10854
|
+
}
|
|
10855
|
+
function suppressWhitespaceOnlyInlineHighlights(root) {
|
|
10856
|
+
root.querySelectorAll("ins, del").forEach((el) => {
|
|
10857
|
+
if (!isWhitespaceOnlyInlineHighlight(el.textContent))
|
|
10858
|
+
return;
|
|
10859
|
+
const parent = el.parentNode;
|
|
10860
|
+
if (!parent)
|
|
10861
|
+
return;
|
|
10862
|
+
parent.replaceChild(document.createTextNode(el.textContent || ""), el);
|
|
10863
|
+
});
|
|
10864
|
+
}
|
|
10865
|
+
|
|
10866
|
+
// web-src/views/media-embed.ts
|
|
10867
|
+
var MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
|
|
10868
|
+
var IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
|
|
10869
|
+
var VIDEO_RE = /\.(mp4|webm|mov)$/i;
|
|
10870
|
+
var AUDIO_RE = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
|
|
10871
|
+
function isMedia(p2) {
|
|
10872
|
+
return MEDIA_RE.test(p2);
|
|
10873
|
+
}
|
|
10874
|
+
function isImage(p2) {
|
|
10875
|
+
return IMAGE_RE.test(p2);
|
|
10876
|
+
}
|
|
10877
|
+
function isVideo(p2) {
|
|
10878
|
+
return VIDEO_RE.test(p2);
|
|
10879
|
+
}
|
|
10880
|
+
function isAudio(p2) {
|
|
10881
|
+
return AUDIO_RE.test(p2);
|
|
10882
|
+
}
|
|
10883
|
+
function fileURL(path, ref) {
|
|
10884
|
+
return `/_file?path=${encodeURIComponent(path)}&ref=${ref}`;
|
|
10885
|
+
}
|
|
10886
|
+
function mediaTag(path, ref) {
|
|
10887
|
+
const url = fileURL(path, ref);
|
|
10888
|
+
if (isVideo(path)) {
|
|
10889
|
+
return `<video src="${url}" controls preload="metadata"></video>`;
|
|
10890
|
+
}
|
|
10891
|
+
if (isAudio(path)) {
|
|
10892
|
+
return `<audio src="${url}" controls preload="metadata"></audio>`;
|
|
10893
|
+
}
|
|
10894
|
+
return `<img src="${url}" alt="" loading="lazy">`;
|
|
10895
|
+
}
|
|
7945
10896
|
function enhanceMediaCard(file, card) {
|
|
7946
10897
|
const path = file.path;
|
|
7947
10898
|
if (!file.media_kind && !isMedia(path))
|
|
@@ -7997,6 +10948,7 @@ ${frontmatter.yaml}
|
|
|
7997
10948
|
setProjectName,
|
|
7998
10949
|
getProjectName,
|
|
7999
10950
|
createOpenPathButton,
|
|
10951
|
+
persistViewedFiles,
|
|
8000
10952
|
applyHideTests,
|
|
8001
10953
|
getServerGeneration,
|
|
8002
10954
|
setServerGeneration
|
|
@@ -8023,9 +10975,6 @@ ${frontmatter.yaml}
|
|
|
8023
10975
|
span.title = { M: "modified", A: "added", D: "deleted", R: "renamed" }[ch] || ch;
|
|
8024
10976
|
return span;
|
|
8025
10977
|
}
|
|
8026
|
-
function persistViewedFiles() {
|
|
8027
|
-
localStorage.setItem("gdp:viewed-files", JSON.stringify([...STATE.viewedFiles]));
|
|
8028
|
-
}
|
|
8029
10978
|
function setFileViewed(path, viewed) {
|
|
8030
10979
|
if (viewed)
|
|
8031
10980
|
STATE.viewedFiles.add(path);
|
|
@@ -8171,21 +11120,112 @@ ${frontmatter.yaml}
|
|
|
8171
11120
|
syncViewedCardDisplay(card, viewed);
|
|
8172
11121
|
});
|
|
8173
11122
|
}
|
|
8174
|
-
let CLIENT_REQ_SEQ = 0;
|
|
8175
|
-
const LOAD_QUEUE = [];
|
|
8176
|
-
let ACTIVE_LOADS = 0;
|
|
8177
|
-
const MAX_PARALLEL = 2;
|
|
8178
|
-
let lazyObserver = null;
|
|
8179
|
-
|
|
11123
|
+
let CLIENT_REQ_SEQ = 0;
|
|
11124
|
+
const LOAD_QUEUE = [];
|
|
11125
|
+
let ACTIVE_LOADS = 0;
|
|
11126
|
+
const MAX_PARALLEL = 2;
|
|
11127
|
+
let lazyObserver = null;
|
|
11128
|
+
let scrollSpyInstalled = false;
|
|
11129
|
+
let prevListSignature = "";
|
|
11130
|
+
let prevCardSignatures = new Map;
|
|
11131
|
+
function fileKey(f2) {
|
|
11132
|
+
return f2.key || f2.path;
|
|
11133
|
+
}
|
|
11134
|
+
function computeListSignature(files) {
|
|
11135
|
+
return files.map((f2) => `${fileKey(f2)}\x00${f2.path}\x00${f2.old_path || ""}\x00${f2.status || "M"}`).join(`
|
|
11136
|
+
`);
|
|
11137
|
+
}
|
|
11138
|
+
function computeCardSignature(f2) {
|
|
11139
|
+
return [
|
|
11140
|
+
fileKey(f2),
|
|
11141
|
+
f2.status || "M",
|
|
11142
|
+
f2.additions || 0,
|
|
11143
|
+
f2.deletions || 0,
|
|
11144
|
+
f2.binary ? 1 : 0,
|
|
11145
|
+
f2.media_kind || "",
|
|
11146
|
+
f2.size_class || "small",
|
|
11147
|
+
f2.force_layout || "",
|
|
11148
|
+
f2.highlight ? 1 : 0,
|
|
11149
|
+
f2.load_url,
|
|
11150
|
+
f2.preview_url || "",
|
|
11151
|
+
f2.estimated_height_px || 0,
|
|
11152
|
+
f2.untracked ? 1 : 0
|
|
11153
|
+
].join("\x00");
|
|
11154
|
+
}
|
|
11155
|
+
function ensureLazyObserver() {
|
|
11156
|
+
if (!lazyObserver) {
|
|
11157
|
+
lazyObserver = new IntersectionObserver((entries) => {
|
|
11158
|
+
for (const entry of entries) {
|
|
11159
|
+
if (!entry.isIntersecting)
|
|
11160
|
+
continue;
|
|
11161
|
+
const card = entry.target;
|
|
11162
|
+
if (card.classList.contains("loaded") || card.classList.contains("loading"))
|
|
11163
|
+
continue;
|
|
11164
|
+
const f2 = STATE.files.find((x) => x.path === card.dataset.path);
|
|
11165
|
+
if (f2)
|
|
11166
|
+
enqueueLoad(f2, card, 0);
|
|
11167
|
+
}
|
|
11168
|
+
}, { rootMargin: "1200px 0px 1600px 0px" });
|
|
11169
|
+
}
|
|
11170
|
+
return lazyObserver;
|
|
11171
|
+
}
|
|
11172
|
+
function activatePendingCard(card, file) {
|
|
11173
|
+
ensureLazyObserver().observe(card);
|
|
11174
|
+
const rect = card.getBoundingClientRect();
|
|
11175
|
+
if (rect.top <= window.innerHeight + 1600) {
|
|
11176
|
+
enqueueLoad(file, card, 0);
|
|
11177
|
+
}
|
|
11178
|
+
}
|
|
11179
|
+
function invalidateLoadedCard(card, file, changedPaths) {
|
|
11180
|
+
if (!card.classList.contains("loaded"))
|
|
11181
|
+
return false;
|
|
11182
|
+
if (changedPaths && !changedPaths.has(file.path))
|
|
11183
|
+
return false;
|
|
11184
|
+
card.classList.remove("loaded", "error");
|
|
11185
|
+
card.classList.add("pending");
|
|
11186
|
+
card._diffData = null;
|
|
11187
|
+
const body = card.querySelector(".gdp-shell-body");
|
|
11188
|
+
if (body)
|
|
11189
|
+
body.innerHTML = "";
|
|
11190
|
+
const head = card.querySelector(".gdp-shell-header");
|
|
11191
|
+
if (head)
|
|
11192
|
+
head.style.display = "";
|
|
11193
|
+
const indicator = card.querySelector(".loading-indicator");
|
|
11194
|
+
if (indicator)
|
|
11195
|
+
indicator.hidden = false;
|
|
11196
|
+
activatePendingCard(card, file);
|
|
11197
|
+
return true;
|
|
11198
|
+
}
|
|
11199
|
+
function updateSidebarStats(files) {
|
|
11200
|
+
for (const f2 of files) {
|
|
11201
|
+
const li = document.querySelector(`#filelist li[data-path="${CSS.escape(f2.path)}"]`);
|
|
11202
|
+
if (!li)
|
|
11203
|
+
continue;
|
|
11204
|
+
const badge = li.querySelector(".badge");
|
|
11205
|
+
if (badge) {
|
|
11206
|
+
const ch = (f2.status || "M")[0].toUpperCase();
|
|
11207
|
+
if (badge.textContent !== ch) {
|
|
11208
|
+
badge.textContent = ch;
|
|
11209
|
+
badge.className = `badge ${ch}`;
|
|
11210
|
+
}
|
|
11211
|
+
}
|
|
11212
|
+
}
|
|
11213
|
+
}
|
|
11214
|
+
function renderShell(meta, changedPaths) {
|
|
8180
11215
|
const newFiles = meta.files || [];
|
|
11216
|
+
const newListSig = computeListSignature(newFiles);
|
|
11217
|
+
const listSame = newListSig === prevListSignature && prevListSignature !== "";
|
|
8181
11218
|
STATE.files = newFiles;
|
|
8182
11219
|
setServerGeneration(meta.generation || 0);
|
|
8183
11220
|
window._lastMeta = meta;
|
|
8184
11221
|
renderMeta(meta);
|
|
8185
|
-
renderSidebar(newFiles);
|
|
8186
11222
|
const target = $("#diff");
|
|
8187
11223
|
const empty = $("#empty");
|
|
8188
11224
|
if (!newFiles.length) {
|
|
11225
|
+
prevListSignature = newListSig;
|
|
11226
|
+
prevCardSignatures.clear();
|
|
11227
|
+
if (!listSame)
|
|
11228
|
+
renderSidebar(newFiles);
|
|
8189
11229
|
if (STATE.route.screen === "file") {
|
|
8190
11230
|
empty.classList.add("hidden");
|
|
8191
11231
|
applySourceRouteToShell();
|
|
@@ -8194,17 +11234,91 @@ ${frontmatter.yaml}
|
|
|
8194
11234
|
target.replaceChildren();
|
|
8195
11235
|
}
|
|
8196
11236
|
LOAD_QUEUE.length = 0;
|
|
8197
|
-
return
|
|
11237
|
+
return {
|
|
11238
|
+
structureChanged: !listSame,
|
|
11239
|
+
invalidatedCards: 0,
|
|
11240
|
+
preservedDom: listSame
|
|
11241
|
+
};
|
|
8198
11242
|
}
|
|
8199
11243
|
empty.classList.add("hidden");
|
|
11244
|
+
const newCardSigs = new Map;
|
|
11245
|
+
for (const f2 of newFiles) {
|
|
11246
|
+
newCardSigs.set(fileKey(f2), computeCardSignature(f2));
|
|
11247
|
+
}
|
|
11248
|
+
let invalidatedCards = 0;
|
|
11249
|
+
if (listSame) {
|
|
11250
|
+
const pathsUnknown = !changedPaths;
|
|
11251
|
+
let sidebarNeedsStatsUpdate = false;
|
|
11252
|
+
for (const f2 of newFiles) {
|
|
11253
|
+
const key = fileKey(f2);
|
|
11254
|
+
const oldSig = prevCardSignatures.get(key);
|
|
11255
|
+
const newSig = newCardSigs.get(key);
|
|
11256
|
+
const sigChanged = oldSig !== newSig;
|
|
11257
|
+
const pathHint = pathsUnknown || changedPaths.has(f2.path);
|
|
11258
|
+
if (!sigChanged && !pathHint) {
|
|
11259
|
+
continue;
|
|
11260
|
+
}
|
|
11261
|
+
const card = document.querySelector(`.gdp-file-shell[data-key="${CSS.escape(key)}"]`);
|
|
11262
|
+
if (!card)
|
|
11263
|
+
continue;
|
|
11264
|
+
const sizeChanged = card.dataset.sizeClass !== (f2.size_class || "small");
|
|
11265
|
+
const statusChanged = card.dataset.status !== (f2.status || "M");
|
|
11266
|
+
if (sizeChanged || statusChanged) {
|
|
11267
|
+
card.classList.remove("loaded", "error");
|
|
11268
|
+
card.classList.add("pending");
|
|
11269
|
+
card.replaceChildren();
|
|
11270
|
+
const tmp = createPlaceholder(f2);
|
|
11271
|
+
while (tmp.firstChild)
|
|
11272
|
+
card.appendChild(tmp.firstChild);
|
|
11273
|
+
card.dataset.sizeClass = f2.size_class || "small";
|
|
11274
|
+
card.dataset.status = f2.status || "M";
|
|
11275
|
+
delete card.dataset.manualRendered;
|
|
11276
|
+
delete card.dataset.manualLoad;
|
|
11277
|
+
delete card.dataset.manualMode;
|
|
11278
|
+
card.style.minHeight = `${f2.estimated_height_px || 80}px`;
|
|
11279
|
+
card._diffData = null;
|
|
11280
|
+
card._file = null;
|
|
11281
|
+
activatePendingCard(card, f2);
|
|
11282
|
+
invalidatedCards++;
|
|
11283
|
+
sidebarNeedsStatsUpdate = true;
|
|
11284
|
+
} else {
|
|
11285
|
+
const stats = card.querySelector(".gdp-shell-header .stats");
|
|
11286
|
+
if (stats) {
|
|
11287
|
+
stats.innerHTML = '<span class="a">+' + (f2.additions || 0) + "</span>" + '<span class="d">−' + (f2.deletions || 0) + "</span>";
|
|
11288
|
+
}
|
|
11289
|
+
card._file = f2;
|
|
11290
|
+
const didInvalidate = sigChanged ? invalidateLoadedCard(card, f2, null) : invalidateLoadedCard(card, f2, changedPaths);
|
|
11291
|
+
if (didInvalidate)
|
|
11292
|
+
invalidatedCards++;
|
|
11293
|
+
if (sigChanged)
|
|
11294
|
+
sidebarNeedsStatsUpdate = true;
|
|
11295
|
+
}
|
|
11296
|
+
}
|
|
11297
|
+
if (sidebarNeedsStatsUpdate)
|
|
11298
|
+
updateSidebarStats(newFiles);
|
|
11299
|
+
prevCardSignatures = newCardSigs;
|
|
11300
|
+
prevListSignature = newListSig;
|
|
11301
|
+
applySourceRouteToShell();
|
|
11302
|
+
if (!scrollSpyInstalled) {
|
|
11303
|
+
setupScrollSpy();
|
|
11304
|
+
scrollSpyInstalled = true;
|
|
11305
|
+
}
|
|
11306
|
+
return {
|
|
11307
|
+
structureChanged: false,
|
|
11308
|
+
invalidatedCards,
|
|
11309
|
+
preservedDom: invalidatedCards === 0
|
|
11310
|
+
};
|
|
11311
|
+
}
|
|
11312
|
+
if (!listSame)
|
|
11313
|
+
renderSidebar(newFiles);
|
|
8200
11314
|
const oldByKey = new Map;
|
|
8201
11315
|
document.querySelectorAll(".gdp-file-shell").forEach((c2) => {
|
|
8202
11316
|
if (c2.dataset.key)
|
|
8203
11317
|
oldByKey.set(c2.dataset.key, c2);
|
|
8204
11318
|
});
|
|
8205
11319
|
const ordered = [];
|
|
8206
|
-
|
|
8207
|
-
const key = f2
|
|
11320
|
+
for (const f2 of newFiles) {
|
|
11321
|
+
const key = fileKey(f2);
|
|
8208
11322
|
const old = oldByKey.get(key);
|
|
8209
11323
|
if (old) {
|
|
8210
11324
|
oldByKey.delete(key);
|
|
@@ -8225,6 +11339,7 @@ ${frontmatter.yaml}
|
|
|
8225
11339
|
old.style.minHeight = `${f2.estimated_height_px || 80}px`;
|
|
8226
11340
|
old._diffData = null;
|
|
8227
11341
|
old._file = null;
|
|
11342
|
+
invalidatedCards++;
|
|
8228
11343
|
} else {
|
|
8229
11344
|
const stats = old.querySelector(".gdp-shell-header .stats");
|
|
8230
11345
|
if (stats) {
|
|
@@ -8235,8 +11350,9 @@ ${frontmatter.yaml}
|
|
|
8235
11350
|
ordered.push(old);
|
|
8236
11351
|
} else {
|
|
8237
11352
|
ordered.push(createPlaceholder(f2));
|
|
11353
|
+
invalidatedCards++;
|
|
8238
11354
|
}
|
|
8239
|
-
}
|
|
11355
|
+
}
|
|
8240
11356
|
oldByKey.forEach((c2) => {
|
|
8241
11357
|
c2.remove();
|
|
8242
11358
|
});
|
|
@@ -8245,14 +11361,18 @@ ${frontmatter.yaml}
|
|
|
8245
11361
|
if (!LOAD_QUEUE[i2].card.isConnected)
|
|
8246
11362
|
LOAD_QUEUE.splice(i2, 1);
|
|
8247
11363
|
}
|
|
11364
|
+
prevCardSignatures = newCardSigs;
|
|
11365
|
+
prevListSignature = newListSig;
|
|
8248
11366
|
setupLazyObserver();
|
|
8249
11367
|
enqueueInitialLoads();
|
|
8250
11368
|
applySourceRouteToShell();
|
|
8251
11369
|
setupScrollSpy();
|
|
11370
|
+
scrollSpyInstalled = true;
|
|
8252
11371
|
if (typeof applyHideTests === "function")
|
|
8253
11372
|
applyHideTests();
|
|
8254
11373
|
applyFilter();
|
|
8255
11374
|
applyViewedState();
|
|
11375
|
+
return { structureChanged: true, invalidatedCards, preservedDom: false };
|
|
8256
11376
|
}
|
|
8257
11377
|
function createPlaceholder(f2) {
|
|
8258
11378
|
const card = document.createElement("div");
|
|
@@ -9008,6 +12128,7 @@ ${frontmatter.yaml}
|
|
|
9008
12128
|
var HELP_SECTIONS = [
|
|
9009
12129
|
"overview",
|
|
9010
12130
|
"annotations",
|
|
12131
|
+
"database",
|
|
9011
12132
|
"skills",
|
|
9012
12133
|
"keybindings"
|
|
9013
12134
|
];
|
|
@@ -9140,6 +12261,93 @@ ${frontmatter.yaml}
|
|
|
9140
12261
|
}
|
|
9141
12262
|
]
|
|
9142
12263
|
},
|
|
12264
|
+
database: {
|
|
12265
|
+
nav: "Database",
|
|
12266
|
+
title: "Database Viewer",
|
|
12267
|
+
intro: "Browse SQLite files and Docker-hosted MySQL/PostgreSQL databases. Run queries with syntax highlighting and explore table schemas and ER diagrams.",
|
|
12268
|
+
groups: [
|
|
12269
|
+
{
|
|
12270
|
+
title: "Supported databases",
|
|
12271
|
+
blocks: [
|
|
12272
|
+
{
|
|
12273
|
+
kind: "table",
|
|
12274
|
+
rows: [
|
|
12275
|
+
[
|
|
12276
|
+
"SQLite",
|
|
12277
|
+
"Automatically discovered from .db, .sqlite, .sqlite3 files in the repository."
|
|
12278
|
+
],
|
|
12279
|
+
[
|
|
12280
|
+
"MySQL / MariaDB",
|
|
12281
|
+
"Detected from docker-compose.yml services. Multiple databases per server are listed."
|
|
12282
|
+
],
|
|
12283
|
+
[
|
|
12284
|
+
"PostgreSQL",
|
|
12285
|
+
"Detected from docker-compose.yml services. Multiple databases per server are listed."
|
|
12286
|
+
]
|
|
12287
|
+
]
|
|
12288
|
+
}
|
|
12289
|
+
]
|
|
12290
|
+
},
|
|
12291
|
+
{
|
|
12292
|
+
title: "UI layout",
|
|
12293
|
+
blocks: [
|
|
12294
|
+
{
|
|
12295
|
+
kind: "table",
|
|
12296
|
+
rows: [
|
|
12297
|
+
[
|
|
12298
|
+
"Sidebar",
|
|
12299
|
+
"DB selector, table tree (expand to see columns), filter, Query/ER buttons."
|
|
12300
|
+
],
|
|
12301
|
+
[
|
|
12302
|
+
"Data tab",
|
|
12303
|
+
"Paginated table grid with sort, filter, cell copy, and CSV/JSON export."
|
|
12304
|
+
],
|
|
12305
|
+
[
|
|
12306
|
+
"Schema tab",
|
|
12307
|
+
"Column definitions, indexes, foreign keys, triggers, and DDL."
|
|
12308
|
+
],
|
|
12309
|
+
[
|
|
12310
|
+
"Query editor",
|
|
12311
|
+
"SQL syntax highlighting, Tab indent, auto-resize, Ctrl+Enter to run."
|
|
12312
|
+
],
|
|
12313
|
+
[
|
|
12314
|
+
"ER Diagram",
|
|
12315
|
+
"Mermaid-based entity-relationship diagram with zoom and pan."
|
|
12316
|
+
],
|
|
12317
|
+
[
|
|
12318
|
+
"Query History",
|
|
12319
|
+
"Bottom panel with master-detail layout. Left: history list, Right: result preview."
|
|
12320
|
+
]
|
|
12321
|
+
]
|
|
12322
|
+
}
|
|
12323
|
+
]
|
|
12324
|
+
},
|
|
12325
|
+
{
|
|
12326
|
+
title: "CLI query (for AI agents)",
|
|
12327
|
+
blocks: [
|
|
12328
|
+
{
|
|
12329
|
+
kind: "paragraph",
|
|
12330
|
+
text: "AI agents can execute read-only queries from the CLI. Results are saved to query history and visible in the browser."
|
|
12331
|
+
},
|
|
12332
|
+
{
|
|
12333
|
+
kind: "command",
|
|
12334
|
+
title: "Run a query",
|
|
12335
|
+
command: 'code-viewer query exec --db data.db --sql "SELECT * FROM users LIMIT 10" --title "Sample data"'
|
|
12336
|
+
},
|
|
12337
|
+
{
|
|
12338
|
+
kind: "command",
|
|
12339
|
+
title: "List databases",
|
|
12340
|
+
command: "code-viewer query list"
|
|
12341
|
+
},
|
|
12342
|
+
{
|
|
12343
|
+
kind: "command",
|
|
12344
|
+
title: "Agent reference",
|
|
12345
|
+
command: "code-viewer query agent-help"
|
|
12346
|
+
}
|
|
12347
|
+
]
|
|
12348
|
+
}
|
|
12349
|
+
]
|
|
12350
|
+
},
|
|
9143
12351
|
skills: {
|
|
9144
12352
|
nav: "Agent Skill",
|
|
9145
12353
|
title: "Agent Skill Setup",
|
|
@@ -9396,6 +12604,93 @@ ${frontmatter.yaml}
|
|
|
9396
12604
|
}
|
|
9397
12605
|
]
|
|
9398
12606
|
},
|
|
12607
|
+
database: {
|
|
12608
|
+
nav: "データベース",
|
|
12609
|
+
title: "データベースビューア",
|
|
12610
|
+
intro: "SQLite ファイルや Docker 上の MySQL/PostgreSQL を閲覧できます。シンタックスハイライト付きクエリ実行、スキーマ表示、ER 図を提供します。",
|
|
12611
|
+
groups: [
|
|
12612
|
+
{
|
|
12613
|
+
title: "対応データベース",
|
|
12614
|
+
blocks: [
|
|
12615
|
+
{
|
|
12616
|
+
kind: "table",
|
|
12617
|
+
rows: [
|
|
12618
|
+
[
|
|
12619
|
+
"SQLite",
|
|
12620
|
+
"リポジトリ内の .db, .sqlite, .sqlite3 ファイルを自動検出します。"
|
|
12621
|
+
],
|
|
12622
|
+
[
|
|
12623
|
+
"MySQL / MariaDB",
|
|
12624
|
+
"docker-compose.yml のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
|
|
12625
|
+
],
|
|
12626
|
+
[
|
|
12627
|
+
"PostgreSQL",
|
|
12628
|
+
"docker-compose.yml のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
|
|
12629
|
+
]
|
|
12630
|
+
]
|
|
12631
|
+
}
|
|
12632
|
+
]
|
|
12633
|
+
},
|
|
12634
|
+
{
|
|
12635
|
+
title: "UI構成",
|
|
12636
|
+
blocks: [
|
|
12637
|
+
{
|
|
12638
|
+
kind: "table",
|
|
12639
|
+
rows: [
|
|
12640
|
+
[
|
|
12641
|
+
"サイドバー",
|
|
12642
|
+
"DB選択、テーブルツリー(展開でカラム表示)、フィルター、Query/ERボタン。"
|
|
12643
|
+
],
|
|
12644
|
+
[
|
|
12645
|
+
"Data タブ",
|
|
12646
|
+
"ページネーション付きグリッド。ソート、フィルター、セルコピー、CSV/JSONエクスポート。"
|
|
12647
|
+
],
|
|
12648
|
+
[
|
|
12649
|
+
"Schema タブ",
|
|
12650
|
+
"カラム定義、インデックス、外部キー、トリガー、DDL。"
|
|
12651
|
+
],
|
|
12652
|
+
[
|
|
12653
|
+
"クエリエディター",
|
|
12654
|
+
"SQLシンタックスハイライト、Tabインデント、自動リサイズ、Ctrl+Enterで実行。"
|
|
12655
|
+
],
|
|
12656
|
+
[
|
|
12657
|
+
"ER図",
|
|
12658
|
+
"Mermaidベースのエンティティ関係図。ズーム・パン対応。"
|
|
12659
|
+
],
|
|
12660
|
+
[
|
|
12661
|
+
"クエリ履歴",
|
|
12662
|
+
"下部パネルにマスター/ディテール表示。左に履歴一覧、右に結果プレビュー。"
|
|
12663
|
+
]
|
|
12664
|
+
]
|
|
12665
|
+
}
|
|
12666
|
+
]
|
|
12667
|
+
},
|
|
12668
|
+
{
|
|
12669
|
+
title: "CLI クエリ(AIエージェント用)",
|
|
12670
|
+
blocks: [
|
|
12671
|
+
{
|
|
12672
|
+
kind: "paragraph",
|
|
12673
|
+
text: "AIエージェントはCLIから読み取り専用クエリを実行できます。結果はクエリ履歴に保存され、ブラウザで確認できます。"
|
|
12674
|
+
},
|
|
12675
|
+
{
|
|
12676
|
+
kind: "command",
|
|
12677
|
+
title: "クエリを実行",
|
|
12678
|
+
command: 'code-viewer query exec --db data.db --sql "SELECT * FROM users LIMIT 10" --title "サンプルデータ"'
|
|
12679
|
+
},
|
|
12680
|
+
{
|
|
12681
|
+
kind: "command",
|
|
12682
|
+
title: "データベース一覧",
|
|
12683
|
+
command: "code-viewer query list"
|
|
12684
|
+
},
|
|
12685
|
+
{
|
|
12686
|
+
kind: "command",
|
|
12687
|
+
title: "エージェント向けリファレンス",
|
|
12688
|
+
command: "code-viewer query agent-help"
|
|
12689
|
+
}
|
|
12690
|
+
]
|
|
12691
|
+
}
|
|
12692
|
+
]
|
|
12693
|
+
},
|
|
9399
12694
|
skills: {
|
|
9400
12695
|
nav: "スキル登録",
|
|
9401
12696
|
title: "Agent Skill の登録",
|
|
@@ -12819,6 +16114,7 @@ ${frontmatter.yaml}
|
|
|
12819
16114
|
fileBadge,
|
|
12820
16115
|
fileEntryIcon,
|
|
12821
16116
|
applyViewedState,
|
|
16117
|
+
persistCollapsedDirs,
|
|
12822
16118
|
appendScopeParams,
|
|
12823
16119
|
createOpenPathButton,
|
|
12824
16120
|
normalizeViewerFontSize,
|
|
@@ -13068,7 +16364,7 @@ ${frontmatter.yaml}
|
|
|
13068
16364
|
STATE.collapsedDirs.add(dir.path);
|
|
13069
16365
|
else
|
|
13070
16366
|
STATE.collapsedDirs.delete(dir.path);
|
|
13071
|
-
|
|
16367
|
+
persistCollapsedDirs();
|
|
13072
16368
|
};
|
|
13073
16369
|
if (!dir.children_omitted) {
|
|
13074
16370
|
chev.addEventListener("click", toggleDir);
|
|
@@ -13275,7 +16571,7 @@ ${frontmatter.yaml}
|
|
|
13275
16571
|
STATE.collapsedDirs.add(dir.path);
|
|
13276
16572
|
else
|
|
13277
16573
|
STATE.collapsedDirs.delete(dir.path);
|
|
13278
|
-
|
|
16574
|
+
persistCollapsedDirs();
|
|
13279
16575
|
rerenderVirtualSidebar();
|
|
13280
16576
|
} finally {
|
|
13281
16577
|
delete li.dataset.toggling;
|
|
@@ -13585,7 +16881,7 @@ ${frontmatter.yaml}
|
|
|
13585
16881
|
STATE.collapsedDirs.add(row.path);
|
|
13586
16882
|
}
|
|
13587
16883
|
}
|
|
13588
|
-
|
|
16884
|
+
persistCollapsedDirs();
|
|
13589
16885
|
rerenderVirtualSidebar();
|
|
13590
16886
|
return;
|
|
13591
16887
|
}
|
|
@@ -13600,7 +16896,7 @@ ${frontmatter.yaml}
|
|
|
13600
16896
|
if (collapsed)
|
|
13601
16897
|
STATE.collapsedDirs.add(path);
|
|
13602
16898
|
});
|
|
13603
|
-
|
|
16899
|
+
persistCollapsedDirs();
|
|
13604
16900
|
}
|
|
13605
16901
|
function sidebarAncestorDirs(path) {
|
|
13606
16902
|
const parts = path.split("/").filter(Boolean);
|
|
@@ -13623,7 +16919,7 @@ ${frontmatter.yaml}
|
|
|
13623
16919
|
setFolderIcon(icon, false);
|
|
13624
16920
|
}
|
|
13625
16921
|
if (changed)
|
|
13626
|
-
|
|
16922
|
+
persistCollapsedDirs();
|
|
13627
16923
|
rerenderVirtualSidebar();
|
|
13628
16924
|
}
|
|
13629
16925
|
function markActive(path, options = {}) {
|
|
@@ -13938,7 +17234,7 @@ ${frontmatter.yaml}
|
|
|
13938
17234
|
STATE.collapsedDirs.add(row.path);
|
|
13939
17235
|
else
|
|
13940
17236
|
STATE.collapsedDirs.delete(row.path);
|
|
13941
|
-
|
|
17237
|
+
persistCollapsedDirs();
|
|
13942
17238
|
rerenderVirtualSidebar();
|
|
13943
17239
|
scrollVirtualSidebarPathIntoView(row.path);
|
|
13944
17240
|
return;
|
|
@@ -14427,6 +17723,8 @@ ${frontmatter.yaml}
|
|
|
14427
17723
|
preview.className = "gdp-html-preview";
|
|
14428
17724
|
const frame = document.createElement("iframe");
|
|
14429
17725
|
frame.title = `${target.path} preview`;
|
|
17726
|
+
frame.sandbox.value = "";
|
|
17727
|
+
frame.referrerPolicy = "no-referrer";
|
|
14430
17728
|
frame.srcdoc = html;
|
|
14431
17729
|
preview.appendChild(frame);
|
|
14432
17730
|
return preview;
|
|
@@ -15310,6 +18608,7 @@ ${frontmatter.yaml}
|
|
|
15310
18608
|
const isStandalone = card.classList.contains("gdp-standalone-source");
|
|
15311
18609
|
const view = document.createElement("div");
|
|
15312
18610
|
view.className = "gdp-source-viewer media";
|
|
18611
|
+
view.classList.add(mediaKind);
|
|
15313
18612
|
if (!isStandalone) {
|
|
15314
18613
|
const meta = document.createElement("div");
|
|
15315
18614
|
meta.className = "gdp-source-meta";
|
|
@@ -15651,10 +18950,25 @@ ${frontmatter.yaml}
|
|
|
15651
18950
|
const UNDO_STACK = [];
|
|
15652
18951
|
let PENDING_G_SCOPE = null;
|
|
15653
18952
|
let PENDING_G_UNTIL = 0;
|
|
18953
|
+
let PROJECT_NAME = "";
|
|
15654
18954
|
const SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX = "gdp:scope-omit-dirs:";
|
|
15655
18955
|
const SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX = "gdp:scope-exclude-names:";
|
|
15656
18956
|
const CODE_FONT_SIZE_STORAGE_KEY = "gdp:code-font-size";
|
|
15657
18957
|
const VIEWER_LANGUAGE_STORAGE_KEY = "gdp:language";
|
|
18958
|
+
function scopedKey(base2) {
|
|
18959
|
+
return PROJECT_NAME ? `${base2}:${PROJECT_NAME}` : base2;
|
|
18960
|
+
}
|
|
18961
|
+
function readScopedStorage(base2) {
|
|
18962
|
+
if (PROJECT_NAME) {
|
|
18963
|
+
const v = localStorage.getItem(`${base2}:${PROJECT_NAME}`);
|
|
18964
|
+
if (v !== null)
|
|
18965
|
+
return v;
|
|
18966
|
+
}
|
|
18967
|
+
return localStorage.getItem(base2);
|
|
18968
|
+
}
|
|
18969
|
+
function writeScopedStorage(base2, value) {
|
|
18970
|
+
localStorage.setItem(scopedKey(base2), value);
|
|
18971
|
+
}
|
|
15658
18972
|
const VIEWER_LANGUAGES = ["en", "ja"];
|
|
15659
18973
|
const CLIENT_SCOPE_OMIT_DIRS_DEFAULT = [
|
|
15660
18974
|
"node_modules",
|
|
@@ -15769,6 +19083,29 @@ ${frontmatter.yaml}
|
|
|
15769
19083
|
projectTitle.textContent = project;
|
|
15770
19084
|
projectTitle.title = project;
|
|
15771
19085
|
}
|
|
19086
|
+
reloadScopedState();
|
|
19087
|
+
}
|
|
19088
|
+
function reloadScopedState() {
|
|
19089
|
+
const collapsed = readScopedStorage("gdp:collapsed-dirs");
|
|
19090
|
+
if (collapsed !== null) {
|
|
19091
|
+
STATE.collapsedDirs = new Set(JSON.parse(collapsed));
|
|
19092
|
+
}
|
|
19093
|
+
const viewed = readScopedStorage("gdp:viewed-files");
|
|
19094
|
+
if (viewed !== null) {
|
|
19095
|
+
STATE.viewedFiles = new Set(JSON.parse(viewed));
|
|
19096
|
+
}
|
|
19097
|
+
const igRaw = readScopedStorage("gdp:ignore-ws");
|
|
19098
|
+
if (igRaw !== null)
|
|
19099
|
+
STATE.ignoreWs = igRaw === "1";
|
|
19100
|
+
const from = readScopedStorage("gdp:from");
|
|
19101
|
+
const to = readScopedStorage("gdp:to");
|
|
19102
|
+
if (from !== null)
|
|
19103
|
+
STATE.from = from;
|
|
19104
|
+
if (to !== null)
|
|
19105
|
+
STATE.to = to;
|
|
19106
|
+
const ht = readScopedStorage("gdp:hide-tests");
|
|
19107
|
+
if (ht !== null)
|
|
19108
|
+
STATE.hideTests = ht === "1";
|
|
15772
19109
|
}
|
|
15773
19110
|
function savedScopeOmitDirs() {
|
|
15774
19111
|
const raw = localStorage.getItem(scopeOmitDirsStorageKey());
|
|
@@ -15856,10 +19193,10 @@ ${frontmatter.yaml}
|
|
|
15856
19193
|
}
|
|
15857
19194
|
}
|
|
15858
19195
|
const STATE = (() => {
|
|
15859
|
-
const igRaw =
|
|
19196
|
+
const igRaw = readScopedStorage("gdp:ignore-ws");
|
|
15860
19197
|
const fallbackRange = {
|
|
15861
|
-
from:
|
|
15862
|
-
to:
|
|
19198
|
+
from: readScopedStorage("gdp:from") || DEFAULT_RANGE.from,
|
|
19199
|
+
to: readScopedStorage("gdp:to") || DEFAULT_RANGE.to
|
|
15863
19200
|
};
|
|
15864
19201
|
const savedLanguage = viewerLanguageFromSearch(window.location.search) || savedViewerLanguage();
|
|
15865
19202
|
const parsedRoute = parseRoute(window.location.pathname, window.location.search, fallbackRange);
|
|
@@ -15872,22 +19209,22 @@ ${frontmatter.yaml}
|
|
|
15872
19209
|
sbView: localStorage.getItem("gdp:sbview") || "tree",
|
|
15873
19210
|
sbWidth: parseInt(localStorage.getItem("gdp:sbwidth") ?? "", 10) || 308,
|
|
15874
19211
|
sidebarHidden: localStorage.getItem("gdp:sidebar-hidden") === "1",
|
|
15875
|
-
collapsedDirs: new Set(JSON.parse(
|
|
19212
|
+
collapsedDirs: new Set(JSON.parse(readScopedStorage("gdp:collapsed-dirs") || "[]")),
|
|
15876
19213
|
ignoreWs: igRaw === null ? true : igRaw === "1",
|
|
15877
19214
|
from: route.range.from,
|
|
15878
19215
|
to: route.range.to,
|
|
15879
19216
|
collapsed: false,
|
|
15880
19217
|
files: [],
|
|
15881
19218
|
activeFile: null,
|
|
15882
|
-
hideTests:
|
|
19219
|
+
hideTests: readScopedStorage("gdp:hide-tests") === "1",
|
|
15883
19220
|
syntaxHighlight: localStorage.getItem("gdp:syntax-highlight") !== "0",
|
|
15884
|
-
viewedFiles: new Set(JSON.parse(
|
|
19221
|
+
viewedFiles: new Set(JSON.parse(readScopedStorage("gdp:viewed-files") || "[]")),
|
|
15885
19222
|
route,
|
|
15886
|
-
repoRef: route.screen === "repo" ? route.ref : "worktree"
|
|
19223
|
+
repoRef: route.screen === "repo" ? route.ref : "worktree",
|
|
19224
|
+
autoUpdate: localStorage.getItem("gdp:auto-update") !== "0"
|
|
15887
19225
|
};
|
|
15888
19226
|
})();
|
|
15889
19227
|
let highlightConfigured = false;
|
|
15890
|
-
let PROJECT_NAME = "";
|
|
15891
19228
|
let REPO_SIDEBAR_REF = null;
|
|
15892
19229
|
const LINE_REF_PILL = createLineRefPill();
|
|
15893
19230
|
const DIFF_LINE_SELECT = createDiffLineSelect({ pill: LINE_REF_PILL });
|
|
@@ -15911,6 +19248,7 @@ ${frontmatter.yaml}
|
|
|
15911
19248
|
fileBadge: (status) => DIFF_VIEW.fileBadge(status),
|
|
15912
19249
|
fileEntryIcon: () => REPO_VIEW.fileEntryIcon(),
|
|
15913
19250
|
applyViewedState: () => DIFF_VIEW.applyViewedState(),
|
|
19251
|
+
persistCollapsedDirs: () => writeScopedStorage("gdp:collapsed-dirs", JSON.stringify([...STATE.collapsedDirs])),
|
|
15914
19252
|
appendScopeParams,
|
|
15915
19253
|
createOpenPathButton,
|
|
15916
19254
|
normalizeViewerFontSize,
|
|
@@ -16073,10 +19411,12 @@ ${frontmatter.yaml}
|
|
|
16073
19411
|
repo: "Repository",
|
|
16074
19412
|
diff: "Diff Viewer",
|
|
16075
19413
|
history: "History",
|
|
19414
|
+
database: "Database",
|
|
16076
19415
|
help: "Help"
|
|
16077
19416
|
},
|
|
16078
19417
|
global: {
|
|
16079
19418
|
annotations: "code annotations",
|
|
19419
|
+
queryHistory: "query history",
|
|
16080
19420
|
settings: "viewer settings",
|
|
16081
19421
|
theme: "toggle theme",
|
|
16082
19422
|
product: "code viewer"
|
|
@@ -16095,7 +19435,18 @@ ${frontmatter.yaml}
|
|
|
16095
19435
|
syntaxLoadingTitle: "loading syntax highlighter",
|
|
16096
19436
|
syntaxErrorTitle: "failed to load syntax highlighter",
|
|
16097
19437
|
syntaxOffTitle: "syntax highlighting off",
|
|
16098
|
-
hideTests: "hide test files (test|spec)"
|
|
19438
|
+
hideTests: "hide test files (test|spec)",
|
|
19439
|
+
autoUpdate: "auto",
|
|
19440
|
+
autoUpdateOnTitle: "auto update on file change",
|
|
19441
|
+
autoUpdateOffTitle: "auto update off — manual reload"
|
|
19442
|
+
},
|
|
19443
|
+
changeBanner: {
|
|
19444
|
+
text: "Files changed",
|
|
19445
|
+
reload: "Reload",
|
|
19446
|
+
justNow: "just now",
|
|
19447
|
+
secondsAgo: (seconds) => `${seconds}s ago`,
|
|
19448
|
+
minutesAgo: (minutes) => `${minutes}m ago`,
|
|
19449
|
+
hoursAgo: (hours) => `${hours}h ago`
|
|
16099
19450
|
},
|
|
16100
19451
|
sidebar: {
|
|
16101
19452
|
files: "Files",
|
|
@@ -16119,7 +19470,8 @@ ${frontmatter.yaml}
|
|
|
16119
19470
|
close: "close viewer settings",
|
|
16120
19471
|
display: "Display",
|
|
16121
19472
|
language: "Language",
|
|
16122
|
-
fileListFontSize: "
|
|
19473
|
+
fileListFontSize: "UI font size",
|
|
19474
|
+
fileListFontSizeHelp: "Applies to the file sidebar and database UI.",
|
|
16123
19475
|
codeFontSize: "Code font size",
|
|
16124
19476
|
sizeSmall: "Small",
|
|
16125
19477
|
sizeRegular: "Regular",
|
|
@@ -16149,10 +19501,12 @@ ${frontmatter.yaml}
|
|
|
16149
19501
|
repo: "リポジトリ",
|
|
16150
19502
|
diff: "Diff ビューア",
|
|
16151
19503
|
history: "履歴",
|
|
19504
|
+
database: "データベース",
|
|
16152
19505
|
help: "ヘルプ"
|
|
16153
19506
|
},
|
|
16154
19507
|
global: {
|
|
16155
19508
|
annotations: "コード注釈",
|
|
19509
|
+
queryHistory: "クエリ履歴",
|
|
16156
19510
|
settings: "ビューア設定",
|
|
16157
19511
|
theme: "テーマ切り替え",
|
|
16158
19512
|
product: "code viewer"
|
|
@@ -16171,7 +19525,18 @@ ${frontmatter.yaml}
|
|
|
16171
19525
|
syntaxLoadingTitle: "シンタックスハイライトを読み込み中",
|
|
16172
19526
|
syntaxErrorTitle: "シンタックスハイライトの読み込みに失敗",
|
|
16173
19527
|
syntaxOffTitle: "シンタックスハイライト無効",
|
|
16174
|
-
hideTests: "test/spec ファイルを隠す"
|
|
19528
|
+
hideTests: "test/spec ファイルを隠す",
|
|
19529
|
+
autoUpdate: "自動",
|
|
19530
|
+
autoUpdateOnTitle: "ファイル変更時に自動更新",
|
|
19531
|
+
autoUpdateOffTitle: "自動更新オフ — 手動で再読み込み"
|
|
19532
|
+
},
|
|
19533
|
+
changeBanner: {
|
|
19534
|
+
text: "ファイルに変更がありました",
|
|
19535
|
+
reload: "再読み込みする",
|
|
19536
|
+
justNow: "たった今",
|
|
19537
|
+
secondsAgo: (seconds) => `${seconds}秒前`,
|
|
19538
|
+
minutesAgo: (minutes) => `${minutes}分前`,
|
|
19539
|
+
hoursAgo: (hours) => `${hours}時間前`
|
|
16175
19540
|
},
|
|
16176
19541
|
sidebar: {
|
|
16177
19542
|
files: "ファイル",
|
|
@@ -16195,8 +19560,9 @@ ${frontmatter.yaml}
|
|
|
16195
19560
|
close: "ビューア設定を閉じる",
|
|
16196
19561
|
display: "表示",
|
|
16197
19562
|
language: "言語",
|
|
16198
|
-
fileListFontSize: "
|
|
16199
|
-
|
|
19563
|
+
fileListFontSize: "UIの文字サイズ",
|
|
19564
|
+
fileListFontSizeHelp: "ファイル一覧とデータベース画面に適用されます。",
|
|
19565
|
+
codeFontSize: "コード表示の文字サイズ",
|
|
16200
19566
|
sizeSmall: "小",
|
|
16201
19567
|
sizeRegular: "標準",
|
|
16202
19568
|
sizeLarge: "大",
|
|
@@ -16255,6 +19621,11 @@ ${frontmatter.yaml}
|
|
|
16255
19621
|
annotationsToggle.title = text2.global.annotations;
|
|
16256
19622
|
annotationsToggle.setAttribute("aria-label", text2.global.annotations);
|
|
16257
19623
|
}
|
|
19624
|
+
const queryHistoryToggle = document.querySelector("#query-history-toggle");
|
|
19625
|
+
if (queryHistoryToggle) {
|
|
19626
|
+
queryHistoryToggle.title = text2.global.queryHistory;
|
|
19627
|
+
queryHistoryToggle.setAttribute("aria-label", text2.global.queryHistory);
|
|
19628
|
+
}
|
|
16258
19629
|
const viewerSettings = document.querySelector("#viewer-settings");
|
|
16259
19630
|
if (viewerSettings) {
|
|
16260
19631
|
viewerSettings.title = text2.global.settings;
|
|
@@ -16279,6 +19650,7 @@ ${frontmatter.yaml}
|
|
|
16279
19650
|
const hideTests = document.querySelector("#hide-tests");
|
|
16280
19651
|
if (hideTests)
|
|
16281
19652
|
hideTests.title = text2.topbar.hideTests;
|
|
19653
|
+
applyAutoUpdateButton();
|
|
16282
19654
|
setHighlightButton(STATE.syntaxHighlight && getHljs() ? "loaded" : "idle");
|
|
16283
19655
|
setElementText(".sb-title", text2.sidebar.files);
|
|
16284
19656
|
const sidebarActions = document.querySelector(".sb-actions");
|
|
@@ -16348,6 +19720,7 @@ ${frontmatter.yaml}
|
|
|
16348
19720
|
large: text2.settings.sizeLarge,
|
|
16349
19721
|
xlarge: text2.settings.sizeExtraLarge
|
|
16350
19722
|
});
|
|
19723
|
+
setElementText("#ui-font-size-help", text2.settings.fileListFontSizeHelp);
|
|
16351
19724
|
setElementText("#display-settings-source", text2.settings.displaySource);
|
|
16352
19725
|
setButtonLabel(document.querySelector("#scope-omit-reset"), text2.settings.reset);
|
|
16353
19726
|
setButtonLabel(document.querySelector("#scope-omit-save"), text2.settings.save);
|
|
@@ -16363,6 +19736,8 @@ ${frontmatter.yaml}
|
|
|
16363
19736
|
setButtonLabel(document.querySelector("#annotation-clear"), text2.annotations.clear);
|
|
16364
19737
|
setButtonLabel(document.querySelector("#annotation-panel-close"), text2.annotations.close);
|
|
16365
19738
|
setElementText(".annotation-list-head strong", text2.annotations.sessions);
|
|
19739
|
+
setElementText(".query-history-panel-head strong", text2.global.queryHistory);
|
|
19740
|
+
setButtonLabel(document.querySelector("#query-history-panel-close"), text2.annotations.close);
|
|
16366
19741
|
}
|
|
16367
19742
|
function setViewerLanguage(language, persist = true) {
|
|
16368
19743
|
const next = normalizeViewerLanguage(language);
|
|
@@ -16651,6 +20026,9 @@ ${frontmatter.yaml}
|
|
|
16651
20026
|
function repoFileTargetFromRoute() {
|
|
16652
20027
|
return STATE.route.screen === "file" && STATE.route.view === "blob" ? STATE.route.ref : null;
|
|
16653
20028
|
}
|
|
20029
|
+
function isRepoBlobRoute(route) {
|
|
20030
|
+
return route.screen === "file" && route.view === "blob";
|
|
20031
|
+
}
|
|
16654
20032
|
let ANNOTATIONS_UI = null;
|
|
16655
20033
|
function applyInlineAnnotations() {
|
|
16656
20034
|
ANNOTATIONS_UI?.applyInlineAnnotations();
|
|
@@ -16680,12 +20058,23 @@ ${frontmatter.yaml}
|
|
|
16680
20058
|
syncHeaderMenu();
|
|
16681
20059
|
syncLineRefPill();
|
|
16682
20060
|
}
|
|
20061
|
+
function setQueryHistoryPanelOpen(open) {
|
|
20062
|
+
const panel = document.getElementById("query-history-panel");
|
|
20063
|
+
if (!panel)
|
|
20064
|
+
return;
|
|
20065
|
+
panel.hidden = !open;
|
|
20066
|
+
document.body.classList.toggle("query-history-panel-open", open);
|
|
20067
|
+
if (open && ANNOTATIONS_UI) {
|
|
20068
|
+
ANNOTATIONS_UI.setAnnotationPanelOpen(false);
|
|
20069
|
+
}
|
|
20070
|
+
}
|
|
16683
20071
|
function setPageMode() {
|
|
16684
20072
|
document.body.classList.toggle("gdp-file-detail-page", STATE.route.screen === "file");
|
|
16685
20073
|
document.body.classList.toggle("gdp-repo-blob-page", STATE.route.screen === "file" && STATE.route.view === "blob");
|
|
16686
20074
|
document.body.classList.toggle("gdp-repo-page", STATE.route.screen === "repo");
|
|
16687
20075
|
document.body.classList.toggle("gdp-help-page", STATE.route.screen === "help");
|
|
16688
20076
|
document.body.classList.toggle("gdp-history-page", STATE.route.screen === "history");
|
|
20077
|
+
document.body.classList.toggle("gdp-database-page", STATE.route.screen === "database");
|
|
16689
20078
|
placeSidebarToggle();
|
|
16690
20079
|
syncSidebarHeaderHeight();
|
|
16691
20080
|
const historyPanel = $("#history-panel");
|
|
@@ -16697,6 +20086,19 @@ ${frontmatter.yaml}
|
|
|
16697
20086
|
historyRefInput.value = STATE.route.ref || "HEAD";
|
|
16698
20087
|
}
|
|
16699
20088
|
syncRepoTargetInput(repoFileTargetFromRoute() || "worktree");
|
|
20089
|
+
const isDatabase = STATE.route.screen === "database";
|
|
20090
|
+
const annotationsToggle = document.querySelector("#annotations-toggle");
|
|
20091
|
+
const qhToggle = document.querySelector("#query-history-toggle");
|
|
20092
|
+
if (annotationsToggle)
|
|
20093
|
+
annotationsToggle.hidden = isDatabase;
|
|
20094
|
+
if (qhToggle)
|
|
20095
|
+
qhToggle.hidden = !isDatabase;
|
|
20096
|
+
if (!isDatabase) {
|
|
20097
|
+
setQueryHistoryPanelOpen(false);
|
|
20098
|
+
}
|
|
20099
|
+
if (isDatabase && ANNOTATIONS_UI) {
|
|
20100
|
+
ANNOTATIONS_UI.setAnnotationPanelOpen(false);
|
|
20101
|
+
}
|
|
16700
20102
|
}
|
|
16701
20103
|
function syncHeaderMenu() {
|
|
16702
20104
|
document.querySelectorAll(".app-menu-item, .global-help-link").forEach((link2) => {
|
|
@@ -16725,6 +20127,12 @@ ${frontmatter.yaml}
|
|
|
16725
20127
|
range: currentRange()
|
|
16726
20128
|
});
|
|
16727
20129
|
}
|
|
20130
|
+
if (link2.dataset.route === "database") {
|
|
20131
|
+
link2.href = buildRoute({
|
|
20132
|
+
screen: "database",
|
|
20133
|
+
range: currentRange()
|
|
20134
|
+
});
|
|
20135
|
+
}
|
|
16728
20136
|
if (link2.dataset.route === "help") {
|
|
16729
20137
|
link2.href = buildRoute({
|
|
16730
20138
|
screen: "help",
|
|
@@ -16867,6 +20275,7 @@ ${frontmatter.yaml}
|
|
|
16867
20275
|
setProjectName,
|
|
16868
20276
|
getProjectName: () => PROJECT_NAME,
|
|
16869
20277
|
createOpenPathButton,
|
|
20278
|
+
persistViewedFiles: () => writeScopedStorage("gdp:viewed-files", JSON.stringify([...STATE.viewedFiles])),
|
|
16870
20279
|
applyHideTests: () => applyHideTests(),
|
|
16871
20280
|
getServerGeneration: () => SERVER_GENERATION,
|
|
16872
20281
|
setServerGeneration: (generation) => {
|
|
@@ -17220,10 +20629,24 @@ ${frontmatter.yaml}
|
|
|
17220
20629
|
setStatus("live");
|
|
17221
20630
|
renderHelpPage();
|
|
17222
20631
|
syncHeaderMenu();
|
|
17223
|
-
return Promise.resolve();
|
|
20632
|
+
return Promise.resolve(null);
|
|
20633
|
+
}
|
|
20634
|
+
if (STATE.route.screen === "database") {
|
|
20635
|
+
DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab);
|
|
20636
|
+
setStatus("live");
|
|
20637
|
+
return Promise.resolve(null);
|
|
20638
|
+
}
|
|
20639
|
+
if (isRepoBlobRoute(STATE.route)) {
|
|
20640
|
+
setStatus("live");
|
|
20641
|
+
applySourceRouteToShell();
|
|
20642
|
+
return Promise.resolve({
|
|
20643
|
+
structureChanged: false,
|
|
20644
|
+
invalidatedCards: 0,
|
|
20645
|
+
preservedDom: true
|
|
20646
|
+
});
|
|
17224
20647
|
}
|
|
17225
20648
|
if (STATE.route.screen === "repo")
|
|
17226
|
-
return loadRepo();
|
|
20649
|
+
return loadRepo().then(() => null);
|
|
17227
20650
|
{
|
|
17228
20651
|
const empty = $("#empty");
|
|
17229
20652
|
if (empty) {
|
|
@@ -17248,9 +20671,13 @@ ${frontmatter.yaml}
|
|
|
17248
20671
|
params.set("nocache", "1");
|
|
17249
20672
|
const url = `/diff.json${params.toString() ? `?${params.toString()}` : ""}`;
|
|
17250
20673
|
return trackLoad(fetch(url).then((r2) => r2.json())).then((data) => {
|
|
17251
|
-
renderShell(data);
|
|
20674
|
+
const result = renderShell(data, options.changedPaths);
|
|
17252
20675
|
setStatus("live");
|
|
17253
|
-
|
|
20676
|
+
return result;
|
|
20677
|
+
}).catch(() => {
|
|
20678
|
+
setStatus("error");
|
|
20679
|
+
return null;
|
|
20680
|
+
});
|
|
17254
20681
|
}
|
|
17255
20682
|
loadSettings().finally(() => {
|
|
17256
20683
|
if (STATE.route.screen === "help") {
|
|
@@ -17265,6 +20692,9 @@ ${frontmatter.yaml}
|
|
|
17265
20692
|
parkRangeForHistory();
|
|
17266
20693
|
setStatus("live");
|
|
17267
20694
|
HISTORY_VIEW.enterHistory();
|
|
20695
|
+
} else if (STATE.route.screen === "database") {
|
|
20696
|
+
setStatus("live");
|
|
20697
|
+
DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab);
|
|
17268
20698
|
} else
|
|
17269
20699
|
load();
|
|
17270
20700
|
syncLineRefPill();
|
|
@@ -17280,8 +20710,8 @@ ${frontmatter.yaml}
|
|
|
17280
20710
|
preHistoryRange = null;
|
|
17281
20711
|
STATE.from = from || "";
|
|
17282
20712
|
STATE.to = to || "";
|
|
17283
|
-
|
|
17284
|
-
|
|
20713
|
+
writeScopedStorage("gdp:from", STATE.from);
|
|
20714
|
+
writeScopedStorage("gdp:to", STATE.to);
|
|
17285
20715
|
syncRefInputs();
|
|
17286
20716
|
const range = currentRange();
|
|
17287
20717
|
if (STATE.route.screen === "file") {
|
|
@@ -17311,7 +20741,7 @@ ${frontmatter.yaml}
|
|
|
17311
20741
|
STATE.from = range.from;
|
|
17312
20742
|
STATE.to = range.to;
|
|
17313
20743
|
syncRefInputs();
|
|
17314
|
-
return load();
|
|
20744
|
+
return load().then(() => {});
|
|
17315
20745
|
},
|
|
17316
20746
|
showEmptyDiffPane: () => {
|
|
17317
20747
|
const diff = $("#diff");
|
|
@@ -17331,6 +20761,13 @@ ${frontmatter.yaml}
|
|
|
17331
20761
|
},
|
|
17332
20762
|
trackLoad
|
|
17333
20763
|
});
|
|
20764
|
+
const DATABASE_VIEW = createDatabaseView({
|
|
20765
|
+
setRoute,
|
|
20766
|
+
setPageMode,
|
|
20767
|
+
currentRange,
|
|
20768
|
+
trackLoad,
|
|
20769
|
+
syncHeaderMenu
|
|
20770
|
+
});
|
|
17334
20771
|
const REF_PICKER = createRefPicker({
|
|
17335
20772
|
$,
|
|
17336
20773
|
escapeHtml: escapeHtml3,
|
|
@@ -17355,6 +20792,9 @@ ${frontmatter.yaml}
|
|
|
17355
20792
|
if (STATE.route.screen === "history" && window.location.pathname !== "/history") {
|
|
17356
20793
|
restoreRangeAfterHistory();
|
|
17357
20794
|
}
|
|
20795
|
+
if (STATE.route.screen === "database" && window.location.pathname !== "/database") {
|
|
20796
|
+
DATABASE_VIEW.leave();
|
|
20797
|
+
}
|
|
17358
20798
|
const parsedRoute = parseRoute(window.location.pathname, window.location.search, currentRange());
|
|
17359
20799
|
const routeLanguage = viewerLanguageFromSearch(window.location.search);
|
|
17360
20800
|
if (routeLanguage && routeLanguage !== STATE.language)
|
|
@@ -17391,6 +20831,14 @@ ${frontmatter.yaml}
|
|
|
17391
20831
|
HISTORY_VIEW.enterHistory();
|
|
17392
20832
|
return;
|
|
17393
20833
|
}
|
|
20834
|
+
if (STATE.route.screen === "database") {
|
|
20835
|
+
cancelActiveSourceLoad("navigation");
|
|
20836
|
+
setPageMode();
|
|
20837
|
+
removeStandaloneSource();
|
|
20838
|
+
DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab);
|
|
20839
|
+
setStatus("live");
|
|
20840
|
+
return;
|
|
20841
|
+
}
|
|
17394
20842
|
if (STATE.route.screen !== "file") {
|
|
17395
20843
|
cancelActiveSourceLoad("navigation");
|
|
17396
20844
|
setPageMode();
|
|
@@ -17422,7 +20870,7 @@ ${frontmatter.yaml}
|
|
|
17422
20870
|
applyIgnoreWs();
|
|
17423
20871
|
$("#ignore-ws").addEventListener("click", () => {
|
|
17424
20872
|
STATE.ignoreWs = !STATE.ignoreWs;
|
|
17425
|
-
|
|
20873
|
+
writeScopedStorage("gdp:ignore-ws", STATE.ignoreWs ? "1" : "0");
|
|
17426
20874
|
applyIgnoreWs();
|
|
17427
20875
|
load();
|
|
17428
20876
|
});
|
|
@@ -17479,7 +20927,7 @@ ${frontmatter.yaml}
|
|
|
17479
20927
|
applyHideTests();
|
|
17480
20928
|
$("#hide-tests").addEventListener("click", () => {
|
|
17481
20929
|
STATE.hideTests = !STATE.hideTests;
|
|
17482
|
-
|
|
20930
|
+
writeScopedStorage("gdp:hide-tests", STATE.hideTests ? "1" : "0");
|
|
17483
20931
|
applyHideTests();
|
|
17484
20932
|
});
|
|
17485
20933
|
ANNOTATIONS_UI = createAnnotationsUi({
|
|
@@ -17503,8 +20951,8 @@ ${frontmatter.yaml}
|
|
|
17503
20951
|
setRange: (from, to) => {
|
|
17504
20952
|
STATE.from = from;
|
|
17505
20953
|
STATE.to = to;
|
|
17506
|
-
|
|
17507
|
-
|
|
20954
|
+
writeScopedStorage("gdp:from", from);
|
|
20955
|
+
writeScopedStorage("gdp:to", to);
|
|
17508
20956
|
}
|
|
17509
20957
|
});
|
|
17510
20958
|
createAnnotationsPlayer({
|
|
@@ -17516,35 +20964,240 @@ ${frontmatter.yaml}
|
|
|
17516
20964
|
onAnnotationOpened: (cb) => ANNOTATIONS_UI?.onAnnotationOpened(cb),
|
|
17517
20965
|
getActiveAnnotationId: () => ANNOTATIONS_UI ? ANNOTATIONS_UI.getActiveAnnotationId() : null
|
|
17518
20966
|
});
|
|
20967
|
+
const qhToggleBtn = document.getElementById("query-history-toggle");
|
|
20968
|
+
if (qhToggleBtn) {
|
|
20969
|
+
qhToggleBtn.addEventListener("click", () => {
|
|
20970
|
+
const panel = document.getElementById("query-history-panel");
|
|
20971
|
+
const opening = panel ? panel.hidden : true;
|
|
20972
|
+
setQueryHistoryPanelOpen(opening);
|
|
20973
|
+
if (opening)
|
|
20974
|
+
DATABASE_VIEW.handleSse();
|
|
20975
|
+
});
|
|
20976
|
+
}
|
|
20977
|
+
const qhCloseBtn = document.getElementById("query-history-panel-close");
|
|
20978
|
+
if (qhCloseBtn) {
|
|
20979
|
+
qhCloseBtn.addEventListener("click", () => {
|
|
20980
|
+
setQueryHistoryPanelOpen(false);
|
|
20981
|
+
});
|
|
20982
|
+
}
|
|
20983
|
+
(function setupQueryHistoryResizer() {
|
|
20984
|
+
const panel = document.getElementById("query-history-panel");
|
|
20985
|
+
const handle = document.getElementById("query-history-resizer");
|
|
20986
|
+
if (!panel || !handle)
|
|
20987
|
+
return;
|
|
20988
|
+
const STORAGE_KEY = "gdp:qh-panel-width";
|
|
20989
|
+
const MIN_W = 280;
|
|
20990
|
+
const MAX_W = 800;
|
|
20991
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
20992
|
+
if (saved) {
|
|
20993
|
+
const w = Math.max(MIN_W, Math.min(MAX_W, Number(saved) || 420));
|
|
20994
|
+
panel.style.width = `${w}px`;
|
|
20995
|
+
}
|
|
20996
|
+
let dragging = false;
|
|
20997
|
+
let startX = 0;
|
|
20998
|
+
let startW = 0;
|
|
20999
|
+
handle.addEventListener("mousedown", (e2) => {
|
|
21000
|
+
dragging = true;
|
|
21001
|
+
startX = e2.clientX;
|
|
21002
|
+
startW = panel.offsetWidth;
|
|
21003
|
+
document.body.classList.add("db-resizing");
|
|
21004
|
+
e2.preventDefault();
|
|
21005
|
+
});
|
|
21006
|
+
window.addEventListener("mousemove", (e2) => {
|
|
21007
|
+
if (!dragging)
|
|
21008
|
+
return;
|
|
21009
|
+
const w = Math.max(MIN_W, Math.min(MAX_W, startW - (e2.clientX - startX)));
|
|
21010
|
+
panel.style.width = `${w}px`;
|
|
21011
|
+
});
|
|
21012
|
+
window.addEventListener("mouseup", () => {
|
|
21013
|
+
if (!dragging)
|
|
21014
|
+
return;
|
|
21015
|
+
dragging = false;
|
|
21016
|
+
document.body.classList.remove("db-resizing");
|
|
21017
|
+
localStorage.setItem(STORAGE_KEY, String(panel.offsetWidth));
|
|
21018
|
+
});
|
|
21019
|
+
})();
|
|
21020
|
+
function applyAutoUpdateButton() {
|
|
21021
|
+
const btn = document.querySelector("#auto-update");
|
|
21022
|
+
if (!btn)
|
|
21023
|
+
return;
|
|
21024
|
+
const text2 = uiText();
|
|
21025
|
+
btn.classList.toggle("active", STATE.autoUpdate);
|
|
21026
|
+
btn.textContent = text2.topbar.autoUpdate;
|
|
21027
|
+
btn.title = STATE.autoUpdate ? text2.topbar.autoUpdateOnTitle : text2.topbar.autoUpdateOffTitle;
|
|
21028
|
+
btn.setAttribute("aria-pressed", STATE.autoUpdate ? "true" : "false");
|
|
21029
|
+
}
|
|
21030
|
+
function setAutoUpdate(on) {
|
|
21031
|
+
STATE.autoUpdate = on;
|
|
21032
|
+
localStorage.setItem("gdp:auto-update", on ? "1" : "0");
|
|
21033
|
+
applyAutoUpdateButton();
|
|
21034
|
+
if (on) {
|
|
21035
|
+
if (bannerPendingPaths) {
|
|
21036
|
+
const paths = bannerPendingPaths;
|
|
21037
|
+
hideChangeBanner();
|
|
21038
|
+
doSseLoad(paths);
|
|
21039
|
+
return;
|
|
21040
|
+
}
|
|
21041
|
+
hideChangeBanner();
|
|
21042
|
+
}
|
|
21043
|
+
}
|
|
21044
|
+
let bannerPendingPaths = null;
|
|
21045
|
+
let changeBannerShownAt = 0;
|
|
21046
|
+
let changeBannerAgeTimer = null;
|
|
21047
|
+
function formatChangeBannerAge(now) {
|
|
21048
|
+
const text2 = uiText().changeBanner;
|
|
21049
|
+
const elapsedSeconds = Math.max(0, Math.floor((now - changeBannerShownAt) / 1000));
|
|
21050
|
+
if (elapsedSeconds < 5)
|
|
21051
|
+
return text2.justNow;
|
|
21052
|
+
if (elapsedSeconds < 60)
|
|
21053
|
+
return text2.secondsAgo(elapsedSeconds);
|
|
21054
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
21055
|
+
if (elapsedMinutes < 60)
|
|
21056
|
+
return text2.minutesAgo(elapsedMinutes);
|
|
21057
|
+
return text2.hoursAgo(Math.floor(elapsedMinutes / 60));
|
|
21058
|
+
}
|
|
21059
|
+
function updateChangeBannerAge() {
|
|
21060
|
+
const ageEl = document.getElementById("change-banner-age");
|
|
21061
|
+
if (ageEl)
|
|
21062
|
+
ageEl.textContent = formatChangeBannerAge(Date.now());
|
|
21063
|
+
}
|
|
21064
|
+
function showChangeBanner(paths) {
|
|
21065
|
+
bannerPendingPaths = paths;
|
|
21066
|
+
changeBannerShownAt = Date.now();
|
|
21067
|
+
const banner = document.getElementById("change-banner");
|
|
21068
|
+
if (!banner)
|
|
21069
|
+
return;
|
|
21070
|
+
const text2 = uiText();
|
|
21071
|
+
const textEl = document.getElementById("change-banner-text");
|
|
21072
|
+
if (textEl)
|
|
21073
|
+
textEl.textContent = text2.changeBanner.text;
|
|
21074
|
+
updateChangeBannerAge();
|
|
21075
|
+
if (!changeBannerAgeTimer) {
|
|
21076
|
+
changeBannerAgeTimer = setInterval(updateChangeBannerAge, 1000);
|
|
21077
|
+
}
|
|
21078
|
+
const reloadBtn = document.getElementById("change-banner-reload");
|
|
21079
|
+
if (reloadBtn)
|
|
21080
|
+
reloadBtn.textContent = text2.changeBanner.reload;
|
|
21081
|
+
banner.hidden = false;
|
|
21082
|
+
}
|
|
21083
|
+
function hideChangeBanner() {
|
|
21084
|
+
const banner = document.getElementById("change-banner");
|
|
21085
|
+
if (banner)
|
|
21086
|
+
banner.hidden = true;
|
|
21087
|
+
bannerPendingPaths = null;
|
|
21088
|
+
changeBannerShownAt = 0;
|
|
21089
|
+
if (changeBannerAgeTimer) {
|
|
21090
|
+
clearInterval(changeBannerAgeTimer);
|
|
21091
|
+
changeBannerAgeTimer = null;
|
|
21092
|
+
}
|
|
21093
|
+
}
|
|
21094
|
+
document.getElementById("change-banner-reload")?.addEventListener("click", () => {
|
|
21095
|
+
const paths = bannerPendingPaths;
|
|
21096
|
+
hideChangeBanner();
|
|
21097
|
+
const route = STATE.route;
|
|
21098
|
+
if (isRepoBlobRoute(route)) {
|
|
21099
|
+
renderStandaloneSource({
|
|
21100
|
+
path: route.path,
|
|
21101
|
+
ref: route.ref || "worktree"
|
|
21102
|
+
});
|
|
21103
|
+
return;
|
|
21104
|
+
}
|
|
21105
|
+
doSseLoad(paths);
|
|
21106
|
+
});
|
|
21107
|
+
document.getElementById("change-banner-dismiss")?.addEventListener("click", () => {
|
|
21108
|
+
hideChangeBanner();
|
|
21109
|
+
});
|
|
21110
|
+
document.getElementById("auto-update")?.addEventListener("click", () => {
|
|
21111
|
+
setAutoUpdate(!STATE.autoUpdate);
|
|
21112
|
+
});
|
|
21113
|
+
applyAutoUpdateButton();
|
|
21114
|
+
function doSseLoad(paths) {
|
|
21115
|
+
const route = STATE.route;
|
|
21116
|
+
if (isRepoBlobRoute(route)) {
|
|
21117
|
+
const viewingPath = route.path;
|
|
21118
|
+
if (paths && viewingPath && !paths.has(viewingPath))
|
|
21119
|
+
return;
|
|
21120
|
+
renderStandaloneSource({
|
|
21121
|
+
path: route.path,
|
|
21122
|
+
ref: route.ref || "worktree"
|
|
21123
|
+
});
|
|
21124
|
+
return;
|
|
21125
|
+
}
|
|
21126
|
+
if (route.screen === "repo") {
|
|
21127
|
+
invalidateRepoSidebar();
|
|
21128
|
+
loadRepo();
|
|
21129
|
+
return;
|
|
21130
|
+
}
|
|
21131
|
+
const savedScroll = window.scrollY;
|
|
21132
|
+
const savedActive = STATE.activeFile;
|
|
21133
|
+
load({ changedPaths: paths }).then((result) => {
|
|
21134
|
+
if (result?.preservedDom)
|
|
21135
|
+
return;
|
|
21136
|
+
if (savedActive) {
|
|
21137
|
+
const card = document.querySelector(diffCardSelector(savedActive));
|
|
21138
|
+
if (card) {
|
|
21139
|
+
card.scrollIntoView({ block: "start" });
|
|
21140
|
+
return;
|
|
21141
|
+
}
|
|
21142
|
+
}
|
|
21143
|
+
window.scrollTo(0, savedScroll);
|
|
21144
|
+
});
|
|
21145
|
+
}
|
|
17519
21146
|
let sseTimer = null;
|
|
17520
|
-
|
|
21147
|
+
let pendingSseChangedPaths = new Set;
|
|
21148
|
+
function scheduleSseLoad(changedPaths) {
|
|
21149
|
+
if (STATE.route.screen === "database" || STATE.route.screen === "help")
|
|
21150
|
+
return;
|
|
21151
|
+
if (changedPaths && pendingSseChangedPaths) {
|
|
21152
|
+
for (const p2 of changedPaths)
|
|
21153
|
+
pendingSseChangedPaths.add(p2);
|
|
21154
|
+
} else {
|
|
21155
|
+
pendingSseChangedPaths = null;
|
|
21156
|
+
}
|
|
17521
21157
|
if (sseTimer)
|
|
17522
21158
|
clearTimeout(sseTimer);
|
|
17523
21159
|
sseTimer = setTimeout(() => {
|
|
17524
21160
|
sseTimer = null;
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
const
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
}
|
|
21161
|
+
const paths = pendingSseChangedPaths;
|
|
21162
|
+
pendingSseChangedPaths = new Set;
|
|
21163
|
+
const route = STATE.route;
|
|
21164
|
+
if (isRepoBlobRoute(route)) {
|
|
21165
|
+
const viewingPath = route.path;
|
|
21166
|
+
if (paths && viewingPath && !paths.has(viewingPath))
|
|
21167
|
+
return;
|
|
21168
|
+
}
|
|
21169
|
+
if (STATE.autoUpdate) {
|
|
21170
|
+
doSseLoad(paths);
|
|
21171
|
+
} else {
|
|
21172
|
+
showChangeBanner(paths);
|
|
21173
|
+
}
|
|
17538
21174
|
}, 350);
|
|
17539
21175
|
}
|
|
17540
21176
|
const es = new EventSource("/events");
|
|
17541
21177
|
const catchUpGate = createCatchUpGate(() => Date.now(), 1000);
|
|
17542
21178
|
let openedOnce = false;
|
|
17543
|
-
es.addEventListener("update", () =>
|
|
21179
|
+
es.addEventListener("update", (event) => {
|
|
21180
|
+
const raw = event.data;
|
|
21181
|
+
let paths = null;
|
|
21182
|
+
if (raw && raw !== "tick") {
|
|
21183
|
+
try {
|
|
21184
|
+
const parsed = JSON.parse(raw);
|
|
21185
|
+
if (Array.isArray(parsed.paths))
|
|
21186
|
+
paths = parsed.paths;
|
|
21187
|
+
} catch {}
|
|
21188
|
+
}
|
|
21189
|
+
scheduleSseLoad(paths);
|
|
21190
|
+
});
|
|
17544
21191
|
es.addEventListener("reload", () => location.reload());
|
|
17545
21192
|
es.addEventListener("annotation", (event) => {
|
|
17546
21193
|
ANNOTATIONS_UI?.handleSse(event.data);
|
|
17547
21194
|
});
|
|
21195
|
+
es.addEventListener("db-query", () => {
|
|
21196
|
+
DATABASE_VIEW.handleSse("db-query");
|
|
21197
|
+
});
|
|
21198
|
+
es.addEventListener("db-snapshot", (event) => {
|
|
21199
|
+
DATABASE_VIEW.handleSse("db-snapshot", event.data);
|
|
21200
|
+
});
|
|
17548
21201
|
es.addEventListener("error", () => setStatus("error"));
|
|
17549
21202
|
es.addEventListener("open", () => {
|
|
17550
21203
|
setStatus("live");
|
|
@@ -17559,6 +21212,10 @@ ${frontmatter.yaml}
|
|
|
17559
21212
|
return;
|
|
17560
21213
|
if (!catchUpGate())
|
|
17561
21214
|
return;
|
|
21215
|
+
if (!STATE.autoUpdate) {
|
|
21216
|
+
showChangeBanner(null);
|
|
21217
|
+
return;
|
|
21218
|
+
}
|
|
17562
21219
|
load({ force: true });
|
|
17563
21220
|
}
|
|
17564
21221
|
document.addEventListener("visibilitychange", () => {
|