@youtyan/code-viewer 0.2.4 → 0.2.5
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/README.md +14 -5
- package/dist/code-viewer.js +2744 -1424
- package/package.json +1 -1
- package/web/app.js +1182 -213
- package/web/index.html +2 -2
- package/web/style.css +229 -4
package/web/app.js
CHANGED
|
@@ -6700,7 +6700,7 @@
|
|
|
6700
6700
|
}
|
|
6701
6701
|
return resolved.join("/");
|
|
6702
6702
|
}
|
|
6703
|
-
function createMarkdownIt(target, highlighter, signal) {
|
|
6703
|
+
function createMarkdownIt(target, highlighter, signal, resolveAssetUrl) {
|
|
6704
6704
|
const md = new lib_default({
|
|
6705
6705
|
html: false,
|
|
6706
6706
|
linkify: true,
|
|
@@ -6765,8 +6765,11 @@
|
|
|
6765
6765
|
const token = tokens[idx];
|
|
6766
6766
|
const src = token.attrGet("src") || "";
|
|
6767
6767
|
const resolved = resolveMarkdownAssetPath(target.path, src);
|
|
6768
|
-
if (resolved)
|
|
6769
|
-
|
|
6768
|
+
if (resolved) {
|
|
6769
|
+
const assetUrl = resolveAssetUrl ? resolveAssetUrl(resolved, src) : buildRawFileUrl({ path: resolved, ref: target.ref || "worktree" });
|
|
6770
|
+
if (assetUrl)
|
|
6771
|
+
token.attrSet("src", assetUrl);
|
|
6772
|
+
}
|
|
6770
6773
|
token.attrSet("loading", "lazy");
|
|
6771
6774
|
return image2(tokens, idx, options, env, self);
|
|
6772
6775
|
};
|
|
@@ -6796,7 +6799,7 @@
|
|
|
6796
6799
|
markdown.className = "gdp-markdown-preview markdown-body";
|
|
6797
6800
|
if (options.signal?.aborted)
|
|
6798
6801
|
return markdown;
|
|
6799
|
-
markdown.innerHTML = renderMarkdownHtml(textValue, target, highlighter, options.signal);
|
|
6802
|
+
markdown.innerHTML = renderMarkdownHtml(textValue, target, highlighter, options.signal, options.resolveAssetUrl);
|
|
6800
6803
|
if (options.signal?.aborted)
|
|
6801
6804
|
return markdown;
|
|
6802
6805
|
enhanceTaskLists(markdown);
|
|
@@ -6812,8 +6815,8 @@
|
|
|
6812
6815
|
wireMarkdownInteractions(markdown, target, options);
|
|
6813
6816
|
return markdown;
|
|
6814
6817
|
}
|
|
6815
|
-
function renderMarkdownHtml(textValue, target, highlighter, signal) {
|
|
6816
|
-
const md = createMarkdownIt(target, highlighter, signal);
|
|
6818
|
+
function renderMarkdownHtml(textValue, target, highlighter, signal, resolveAssetUrl) {
|
|
6819
|
+
const md = createMarkdownIt(target, highlighter, signal, resolveAssetUrl);
|
|
6817
6820
|
const frontmatter = splitYamlFrontmatter(textValue);
|
|
6818
6821
|
if (!frontmatter)
|
|
6819
6822
|
return md.render(textValue);
|
|
@@ -7304,7 +7307,7 @@ ${frontmatter.yaml}
|
|
|
7304
7307
|
}
|
|
7305
7308
|
function annotationLocationLabel(entry) {
|
|
7306
7309
|
if (entry.target?.kind === "database") {
|
|
7307
|
-
const parts = ["
|
|
7310
|
+
const parts = ["Datastores"];
|
|
7308
7311
|
if (entry.target.db)
|
|
7309
7312
|
parts.push(entry.target.db);
|
|
7310
7313
|
if (entry.target.schema)
|
|
@@ -7412,7 +7415,7 @@ ${frontmatter.yaml}
|
|
|
7412
7415
|
return;
|
|
7413
7416
|
const strip = document.createElement("section");
|
|
7414
7417
|
strip.className = "gdp-db-annotation-strip";
|
|
7415
|
-
strip.setAttribute("aria-label", "
|
|
7418
|
+
strip.setAttribute("aria-label", "Datastore annotations");
|
|
7416
7419
|
for (const entry of matches) {
|
|
7417
7420
|
strip.appendChild(buildDatabaseAnnotationBlock(entry));
|
|
7418
7421
|
}
|
|
@@ -7655,7 +7658,7 @@ ${frontmatter.yaml}
|
|
|
7655
7658
|
return firstLine.length > 90 ? `${firstLine.slice(0, 90)}…` : firstLine;
|
|
7656
7659
|
}
|
|
7657
7660
|
function databaseAnnotationTitle(target) {
|
|
7658
|
-
const parts = [target.table || target.schema || target.db || "
|
|
7661
|
+
const parts = [target.table || target.schema || target.db || "Datastores"];
|
|
7659
7662
|
if (target.schema && target.table)
|
|
7660
7663
|
parts.unshift(target.schema);
|
|
7661
7664
|
if (target.tab === "data" && target.data?.search)
|
|
@@ -7669,7 +7672,7 @@ ${frontmatter.yaml}
|
|
|
7669
7672
|
return parts.join(" / ");
|
|
7670
7673
|
}
|
|
7671
7674
|
function openDatabaseCaptureForm(target) {
|
|
7672
|
-
$("#annotation-detail-session").textContent = activeSessionId || "
|
|
7675
|
+
$("#annotation-detail-session").textContent = activeSessionId || "Datastore annotations";
|
|
7673
7676
|
$("#annotation-detail-step").textContent = "new";
|
|
7674
7677
|
const location2 = $("#annotation-detail-location");
|
|
7675
7678
|
location2.textContent = databaseAnnotationTitle(target);
|
|
@@ -7710,7 +7713,7 @@ ${frontmatter.yaml}
|
|
|
7710
7713
|
body: JSON.stringify({
|
|
7711
7714
|
action: "add",
|
|
7712
7715
|
session_id: activeSessionId || undefined,
|
|
7713
|
-
session_title: activeSessionId ? undefined : "
|
|
7716
|
+
session_title: activeSessionId ? undefined : "Datastore annotations",
|
|
7714
7717
|
target,
|
|
7715
7718
|
title: titleInput.value,
|
|
7716
7719
|
body: bodyInput.value
|
|
@@ -8148,6 +8151,35 @@ ${frontmatter.yaml}
|
|
|
8148
8151
|
return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
8149
8152
|
}
|
|
8150
8153
|
|
|
8154
|
+
// web-src/views/database/abort-guard.ts
|
|
8155
|
+
function createAbortGuard() {
|
|
8156
|
+
let active = null;
|
|
8157
|
+
let runId = 0;
|
|
8158
|
+
return {
|
|
8159
|
+
start() {
|
|
8160
|
+
active?.abort();
|
|
8161
|
+
const abort = new AbortController;
|
|
8162
|
+
active = abort;
|
|
8163
|
+
const requestRunId = ++runId;
|
|
8164
|
+
return {
|
|
8165
|
+
signal: abort.signal,
|
|
8166
|
+
isStale() {
|
|
8167
|
+
return abort.signal.aborted || requestRunId !== runId;
|
|
8168
|
+
},
|
|
8169
|
+
finish() {
|
|
8170
|
+
if (active === abort)
|
|
8171
|
+
active = null;
|
|
8172
|
+
}
|
|
8173
|
+
};
|
|
8174
|
+
},
|
|
8175
|
+
dispose() {
|
|
8176
|
+
runId++;
|
|
8177
|
+
active?.abort();
|
|
8178
|
+
active = null;
|
|
8179
|
+
}
|
|
8180
|
+
};
|
|
8181
|
+
}
|
|
8182
|
+
|
|
8151
8183
|
// web-src/views/media-embed.ts
|
|
8152
8184
|
var MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
|
|
8153
8185
|
var IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
|
|
@@ -8591,35 +8623,6 @@ ${frontmatter.yaml}
|
|
|
8591
8623
|
return fallback.charAt(0).toUpperCase() + fallback.slice(1);
|
|
8592
8624
|
}
|
|
8593
8625
|
|
|
8594
|
-
// web-src/views/database/abort-guard.ts
|
|
8595
|
-
function createAbortGuard() {
|
|
8596
|
-
let active = null;
|
|
8597
|
-
let runId = 0;
|
|
8598
|
-
return {
|
|
8599
|
-
start() {
|
|
8600
|
-
active?.abort("abort-guard:start");
|
|
8601
|
-
const abort = new AbortController;
|
|
8602
|
-
active = abort;
|
|
8603
|
-
const requestRunId = ++runId;
|
|
8604
|
-
return {
|
|
8605
|
-
signal: abort.signal,
|
|
8606
|
-
isStale() {
|
|
8607
|
-
return abort.signal.aborted || requestRunId !== runId;
|
|
8608
|
-
},
|
|
8609
|
-
finish() {
|
|
8610
|
-
if (active === abort)
|
|
8611
|
-
active = null;
|
|
8612
|
-
}
|
|
8613
|
-
};
|
|
8614
|
-
},
|
|
8615
|
-
dispose() {
|
|
8616
|
-
runId++;
|
|
8617
|
-
active?.abort("abort-guard:dispose");
|
|
8618
|
-
active = null;
|
|
8619
|
-
}
|
|
8620
|
-
};
|
|
8621
|
-
}
|
|
8622
|
-
|
|
8623
8626
|
// web-src/views/database/pane-status.ts
|
|
8624
8627
|
function setPaneStatus(el, message, options = {}) {
|
|
8625
8628
|
el.innerHTML = "";
|
|
@@ -8703,6 +8706,10 @@ ${frontmatter.yaml}
|
|
|
8703
8706
|
let suppressNotify = false;
|
|
8704
8707
|
let queryNotifyTimer = null;
|
|
8705
8708
|
let disposed = false;
|
|
8709
|
+
let activeIndexRow = null;
|
|
8710
|
+
let activeDocRow = null;
|
|
8711
|
+
const indexRowsByName = new Map;
|
|
8712
|
+
const docRowsById = new Map;
|
|
8706
8713
|
const indexGuard = createAbortGuard();
|
|
8707
8714
|
const mappingGuard = createAbortGuard();
|
|
8708
8715
|
const docsGuard = createAbortGuard();
|
|
@@ -8728,14 +8735,17 @@ ${frontmatter.yaml}
|
|
|
8728
8735
|
}
|
|
8729
8736
|
function renderIndices(indices) {
|
|
8730
8737
|
indexList.innerHTML = "";
|
|
8738
|
+
indexRowsByName.clear();
|
|
8731
8739
|
if (indices.length === 0) {
|
|
8732
8740
|
setIndexStatus("(no indices)");
|
|
8733
8741
|
return;
|
|
8734
8742
|
}
|
|
8743
|
+
const fragment = document.createDocumentFragment();
|
|
8735
8744
|
for (const ix of indices) {
|
|
8736
8745
|
const item = document.createElement("div");
|
|
8737
8746
|
item.className = "es-index-item";
|
|
8738
8747
|
item.dataset.indexName = ix.name;
|
|
8748
|
+
indexRowsByName.set(ix.name, item);
|
|
8739
8749
|
const name = document.createElement("span");
|
|
8740
8750
|
name.className = "es-index-name";
|
|
8741
8751
|
name.textContent = ix.name;
|
|
@@ -8744,20 +8754,24 @@ ${frontmatter.yaml}
|
|
|
8744
8754
|
meta.className = "es-index-meta";
|
|
8745
8755
|
meta.textContent = `${ix.docCount.toLocaleString()} docs / ${formatBytes(ix.sizeBytes)}`;
|
|
8746
8756
|
item.append(name, meta);
|
|
8747
|
-
|
|
8748
|
-
indexList.appendChild(item);
|
|
8757
|
+
fragment.appendChild(item);
|
|
8749
8758
|
}
|
|
8759
|
+
indexList.appendChild(fragment);
|
|
8750
8760
|
}
|
|
8751
8761
|
function highlightActiveIndex(name) {
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8762
|
+
if (activeIndexRow?.dataset.indexName === name)
|
|
8763
|
+
return;
|
|
8764
|
+
activeIndexRow?.classList.remove("active");
|
|
8765
|
+
activeIndexRow = indexRowsByName.get(name) ?? null;
|
|
8766
|
+
activeIndexRow?.classList.add("active");
|
|
8755
8767
|
}
|
|
8756
8768
|
function appendDocs(hits) {
|
|
8769
|
+
const fragment = document.createDocumentFragment();
|
|
8757
8770
|
for (const hit of hits) {
|
|
8758
8771
|
const row = document.createElement("div");
|
|
8759
8772
|
row.className = "es-doc-item";
|
|
8760
8773
|
row.dataset.docId = hit._id;
|
|
8774
|
+
docRowsById.set(hit._id, row);
|
|
8761
8775
|
const id = document.createElement("span");
|
|
8762
8776
|
id.className = "es-doc-id";
|
|
8763
8777
|
id.textContent = hit._id;
|
|
@@ -8766,24 +8780,38 @@ ${frontmatter.yaml}
|
|
|
8766
8780
|
preview.className = "es-doc-preview";
|
|
8767
8781
|
preview.textContent = previewSource(hit._source);
|
|
8768
8782
|
row.append(id, preview);
|
|
8769
|
-
|
|
8770
|
-
docList.appendChild(row);
|
|
8783
|
+
fragment.appendChild(row);
|
|
8771
8784
|
}
|
|
8785
|
+
docList.appendChild(fragment);
|
|
8772
8786
|
}
|
|
8773
8787
|
function previewSource(src) {
|
|
8774
8788
|
if (src === null || src === undefined)
|
|
8775
8789
|
return "";
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8790
|
+
if (Array.isArray(src))
|
|
8791
|
+
return `array(${src.length})`;
|
|
8792
|
+
if (typeof src === "object") {
|
|
8793
|
+
const entries = Object.entries(src).slice(0, 6);
|
|
8794
|
+
const parts = entries.map(([key, value]) => {
|
|
8795
|
+
if (value === null)
|
|
8796
|
+
return `${key}=null`;
|
|
8797
|
+
if (Array.isArray(value))
|
|
8798
|
+
return `${key}=array(${value.length})`;
|
|
8799
|
+
if (typeof value === "object")
|
|
8800
|
+
return `${key}=object`;
|
|
8801
|
+
const text3 = String(value);
|
|
8802
|
+
return `${key}=${text3.length > 40 ? `${text3.slice(0, 40)}...` : text3}`;
|
|
8803
|
+
});
|
|
8804
|
+
return parts.join(" / ");
|
|
8781
8805
|
}
|
|
8806
|
+
const text2 = String(src);
|
|
8807
|
+
return text2.length > 200 ? `${text2.slice(0, 200)}...` : text2;
|
|
8782
8808
|
}
|
|
8783
8809
|
function highlightActiveDoc(id) {
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8810
|
+
if (activeDocRow?.dataset.docId === id)
|
|
8811
|
+
return;
|
|
8812
|
+
activeDocRow?.classList.remove("active");
|
|
8813
|
+
activeDocRow = docRowsById.get(id) ?? null;
|
|
8814
|
+
activeDocRow?.classList.add("active");
|
|
8787
8815
|
}
|
|
8788
8816
|
function setDetailTab(tab) {
|
|
8789
8817
|
detailTab = tab;
|
|
@@ -8913,6 +8941,8 @@ ${frontmatter.yaml}
|
|
|
8913
8941
|
if (!append) {
|
|
8914
8942
|
lastSort = undefined;
|
|
8915
8943
|
setDocStatus("Loading docs...");
|
|
8944
|
+
docRowsById.clear();
|
|
8945
|
+
activeDocRow = null;
|
|
8916
8946
|
}
|
|
8917
8947
|
try {
|
|
8918
8948
|
const params = new URLSearchParams({
|
|
@@ -8938,8 +8968,10 @@ ${frontmatter.yaml}
|
|
|
8938
8968
|
if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestIndex !== currentIndex || data.index !== requestIndex || requestQuery !== currentQuery) {
|
|
8939
8969
|
return;
|
|
8940
8970
|
}
|
|
8941
|
-
if (!append)
|
|
8971
|
+
if (!append) {
|
|
8942
8972
|
docList.innerHTML = "";
|
|
8973
|
+
docRowsById.clear();
|
|
8974
|
+
}
|
|
8943
8975
|
if (data.hits.length === 0 && !append) {
|
|
8944
8976
|
setDocStatus("(no docs)");
|
|
8945
8977
|
} else {
|
|
@@ -9039,6 +9071,22 @@ ${frontmatter.yaml}
|
|
|
9039
9071
|
}, 300);
|
|
9040
9072
|
});
|
|
9041
9073
|
docMoreBtn.addEventListener("click", () => loadDocs(true));
|
|
9074
|
+
indexList.addEventListener("click", (e2) => {
|
|
9075
|
+
const row = e2.target?.closest(".es-index-item");
|
|
9076
|
+
if (!row || !indexList.contains(row))
|
|
9077
|
+
return;
|
|
9078
|
+
const name = row.dataset.indexName;
|
|
9079
|
+
if (name)
|
|
9080
|
+
selectIndex(name);
|
|
9081
|
+
});
|
|
9082
|
+
docList.addEventListener("click", (e2) => {
|
|
9083
|
+
const row = e2.target?.closest(".es-doc-item");
|
|
9084
|
+
if (!row || !docList.contains(row))
|
|
9085
|
+
return;
|
|
9086
|
+
const id = row.dataset.docId;
|
|
9087
|
+
if (id)
|
|
9088
|
+
selectDoc(id);
|
|
9089
|
+
});
|
|
9042
9090
|
async function load(dbId, initial) {
|
|
9043
9091
|
if (disposed)
|
|
9044
9092
|
return;
|
|
@@ -9061,6 +9109,9 @@ ${frontmatter.yaml}
|
|
|
9061
9109
|
searchInput.value = initial?.query ?? "";
|
|
9062
9110
|
currentQuery = searchInput.value.trim();
|
|
9063
9111
|
docList.innerHTML = "";
|
|
9112
|
+
docRowsById.clear();
|
|
9113
|
+
activeIndexRow = null;
|
|
9114
|
+
activeDocRow = null;
|
|
9064
9115
|
docMoreBtn.hidden = true;
|
|
9065
9116
|
mappingBody.innerHTML = "";
|
|
9066
9117
|
mappingBody.textContent = "Select an index to view its mapping.";
|
|
@@ -9117,6 +9168,10 @@ ${frontmatter.yaml}
|
|
|
9117
9168
|
searchInput.value = "";
|
|
9118
9169
|
indexList.innerHTML = "";
|
|
9119
9170
|
docList.innerHTML = "";
|
|
9171
|
+
indexRowsByName.clear();
|
|
9172
|
+
docRowsById.clear();
|
|
9173
|
+
activeIndexRow = null;
|
|
9174
|
+
activeDocRow = null;
|
|
9120
9175
|
docMoreBtn.hidden = true;
|
|
9121
9176
|
mappingBody.innerHTML = "";
|
|
9122
9177
|
mappingBody.textContent = "Select an index to view its mapping.";
|
|
@@ -9964,6 +10019,22 @@ ${frontmatter.yaml}
|
|
|
9964
10019
|
let entries = [];
|
|
9965
10020
|
const expandedIds = new Set;
|
|
9966
10021
|
let clearConfirmTimer = null;
|
|
10022
|
+
let lastRefreshKey = null;
|
|
10023
|
+
let lastRefreshAt = 0;
|
|
10024
|
+
let inFlightRefresh = null;
|
|
10025
|
+
const entryRowsById = new Map;
|
|
10026
|
+
let selectedEntryRow = null;
|
|
10027
|
+
function currentRefreshParams() {
|
|
10028
|
+
const dbId = callbacks.getDbId();
|
|
10029
|
+
const schema = callbacks.getSchema();
|
|
10030
|
+
const searchParams = new URLSearchParams;
|
|
10031
|
+
if (dbId)
|
|
10032
|
+
searchParams.set("db", dbId);
|
|
10033
|
+
if (schema)
|
|
10034
|
+
searchParams.set("schema", schema);
|
|
10035
|
+
const params = searchParams.toString() ? `?${searchParams.toString()}` : "";
|
|
10036
|
+
return { key: params, params };
|
|
10037
|
+
}
|
|
9967
10038
|
function armButtonConfirm(button, confirmText, defaultText) {
|
|
9968
10039
|
if (button.dataset.confirm === "1") {
|
|
9969
10040
|
button.dataset.confirm = "";
|
|
@@ -9980,29 +10051,42 @@ ${frontmatter.yaml}
|
|
|
9980
10051
|
}, 3000);
|
|
9981
10052
|
return false;
|
|
9982
10053
|
}
|
|
9983
|
-
async function refresh() {
|
|
9984
|
-
const
|
|
9985
|
-
const
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
if (
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
10054
|
+
async function refresh(options = {}) {
|
|
10055
|
+
const { key: refreshKey, params } = currentRefreshParams();
|
|
10056
|
+
const now = Date.now();
|
|
10057
|
+
if (inFlightRefresh?.key === refreshKey) {
|
|
10058
|
+
return inFlightRefresh.promise;
|
|
10059
|
+
}
|
|
10060
|
+
if (!options.force && refreshKey === lastRefreshKey && now - lastRefreshAt < 1000) {
|
|
10061
|
+
return;
|
|
10062
|
+
}
|
|
10063
|
+
const promise = (async () => {
|
|
9993
10064
|
const res = await fetch(`/_db/history${params}`);
|
|
9994
10065
|
if (!res.ok)
|
|
9995
10066
|
return;
|
|
9996
10067
|
const state = await res.json();
|
|
10068
|
+
if (currentRefreshParams().key !== refreshKey)
|
|
10069
|
+
return;
|
|
9997
10070
|
entries = state.entries;
|
|
10071
|
+
lastRefreshKey = refreshKey;
|
|
10072
|
+
lastRefreshAt = Date.now();
|
|
9998
10073
|
if (selectedEntryId && !entries.some((entry) => entry.id === selectedEntryId)) {
|
|
9999
10074
|
clearDetail();
|
|
10000
10075
|
}
|
|
10001
10076
|
render();
|
|
10002
|
-
}
|
|
10077
|
+
})().catch(() => {});
|
|
10078
|
+
inFlightRefresh = { key: refreshKey, promise };
|
|
10079
|
+
try {
|
|
10080
|
+
await promise;
|
|
10081
|
+
} finally {
|
|
10082
|
+
if (inFlightRefresh?.promise === promise)
|
|
10083
|
+
inFlightRefresh = null;
|
|
10084
|
+
}
|
|
10003
10085
|
}
|
|
10004
10086
|
function render() {
|
|
10005
10087
|
listEl.innerHTML = "";
|
|
10088
|
+
entryRowsById.clear();
|
|
10089
|
+
selectedEntryRow = null;
|
|
10006
10090
|
if (entries.length === 0) {
|
|
10007
10091
|
const empty = document.createElement("div");
|
|
10008
10092
|
empty.className = "db-query-history-empty";
|
|
@@ -10010,21 +10094,25 @@ ${frontmatter.yaml}
|
|
|
10010
10094
|
listEl.appendChild(empty);
|
|
10011
10095
|
return;
|
|
10012
10096
|
}
|
|
10097
|
+
const fragment = document.createDocumentFragment();
|
|
10013
10098
|
for (const entry of entries) {
|
|
10014
|
-
|
|
10099
|
+
fragment.appendChild(renderEntry(entry));
|
|
10015
10100
|
}
|
|
10101
|
+
listEl.appendChild(fragment);
|
|
10016
10102
|
}
|
|
10017
10103
|
let selectedEntryId = null;
|
|
10018
10104
|
function clearDetail() {
|
|
10019
10105
|
selectedEntryId = null;
|
|
10106
|
+
selectedEntryRow?.classList.remove("selected");
|
|
10107
|
+
selectedEntryRow = null;
|
|
10020
10108
|
detailCol.innerHTML = "";
|
|
10021
10109
|
detailCol.appendChild(detailPlaceholder);
|
|
10022
10110
|
}
|
|
10023
10111
|
function selectEntry(entry) {
|
|
10024
10112
|
selectedEntryId = entry.id;
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10113
|
+
selectedEntryRow?.classList.remove("selected");
|
|
10114
|
+
selectedEntryRow = entryRowsById.get(entry.id) ?? null;
|
|
10115
|
+
selectedEntryRow?.classList.add("selected");
|
|
10028
10116
|
renderDetail(entry);
|
|
10029
10117
|
}
|
|
10030
10118
|
function renderDetail(entry) {
|
|
@@ -10081,9 +10169,12 @@ ${frontmatter.yaml}
|
|
|
10081
10169
|
function renderEntry(entry) {
|
|
10082
10170
|
const item = document.createElement("div");
|
|
10083
10171
|
item.className = "db-query-history-entry";
|
|
10084
|
-
if (entry.id === selectedEntryId)
|
|
10172
|
+
if (entry.id === selectedEntryId) {
|
|
10085
10173
|
item.classList.add("selected");
|
|
10174
|
+
selectedEntryRow = item;
|
|
10175
|
+
}
|
|
10086
10176
|
item.dataset.id = entry.id;
|
|
10177
|
+
entryRowsById.set(entry.id, item);
|
|
10087
10178
|
const meta = document.createElement("div");
|
|
10088
10179
|
meta.className = "db-query-history-entry-meta";
|
|
10089
10180
|
const byIcon = document.createElement("span");
|
|
@@ -10170,8 +10261,9 @@ ${frontmatter.yaml}
|
|
|
10170
10261
|
expandedIds.delete(id);
|
|
10171
10262
|
if (selectedEntryId === id)
|
|
10172
10263
|
resetDetailCol();
|
|
10173
|
-
const itemEl =
|
|
10264
|
+
const itemEl = entryRowsById.get(id);
|
|
10174
10265
|
if (itemEl) {
|
|
10266
|
+
entryRowsById.delete(id);
|
|
10175
10267
|
itemEl.remove();
|
|
10176
10268
|
if (entries.length === 0)
|
|
10177
10269
|
render();
|
|
@@ -10215,11 +10307,13 @@ ${frontmatter.yaml}
|
|
|
10215
10307
|
} catch {}
|
|
10216
10308
|
});
|
|
10217
10309
|
refreshBtn.addEventListener("click", () => {
|
|
10218
|
-
refresh();
|
|
10310
|
+
refresh({ force: true });
|
|
10219
10311
|
});
|
|
10220
10312
|
function clear() {
|
|
10221
10313
|
entries = [];
|
|
10222
10314
|
expandedIds.clear();
|
|
10315
|
+
lastRefreshKey = null;
|
|
10316
|
+
lastRefreshAt = 0;
|
|
10223
10317
|
if (clearConfirmTimer) {
|
|
10224
10318
|
clearTimeout(clearConfirmTimer);
|
|
10225
10319
|
clearConfirmTimer = null;
|
|
@@ -10321,6 +10415,10 @@ ${frontmatter.yaml}
|
|
|
10321
10415
|
let keyRunId = 0;
|
|
10322
10416
|
let suppressNotify = false;
|
|
10323
10417
|
let disposed = false;
|
|
10418
|
+
let activeDbRow = null;
|
|
10419
|
+
let activeKeyRow = null;
|
|
10420
|
+
const dbRowsByIndex = new Map;
|
|
10421
|
+
const keyRowsByName = new Map;
|
|
10324
10422
|
const dbGuard = createAbortGuard();
|
|
10325
10423
|
const keysGuard = createAbortGuard();
|
|
10326
10424
|
const valueGuard = createAbortGuard();
|
|
@@ -10346,10 +10444,13 @@ ${frontmatter.yaml}
|
|
|
10346
10444
|
}
|
|
10347
10445
|
function renderDatabases(databases) {
|
|
10348
10446
|
dbList.innerHTML = "";
|
|
10447
|
+
dbRowsByIndex.clear();
|
|
10448
|
+
const fragment = document.createDocumentFragment();
|
|
10349
10449
|
for (const db of databases) {
|
|
10350
10450
|
const item = document.createElement("div");
|
|
10351
10451
|
item.className = "redis-db-item";
|
|
10352
10452
|
item.dataset.dbIndex = String(db.index);
|
|
10453
|
+
dbRowsByIndex.set(db.index, item);
|
|
10353
10454
|
const name = document.createElement("span");
|
|
10354
10455
|
name.className = "redis-db-name";
|
|
10355
10456
|
name.textContent = `db${db.index}`;
|
|
@@ -10357,20 +10458,24 @@ ${frontmatter.yaml}
|
|
|
10357
10458
|
count.className = "redis-db-count";
|
|
10358
10459
|
count.textContent = `${db.keyCount.toLocaleString()} keys`;
|
|
10359
10460
|
item.append(name, count);
|
|
10360
|
-
|
|
10361
|
-
dbList.appendChild(item);
|
|
10461
|
+
fragment.appendChild(item);
|
|
10362
10462
|
}
|
|
10463
|
+
dbList.appendChild(fragment);
|
|
10363
10464
|
}
|
|
10364
10465
|
function highlightActiveDb(index) {
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10466
|
+
if (activeDbRow?.dataset.dbIndex === String(index))
|
|
10467
|
+
return;
|
|
10468
|
+
activeDbRow?.classList.remove("active");
|
|
10469
|
+
activeDbRow = dbRowsByIndex.get(index) ?? null;
|
|
10470
|
+
activeDbRow?.classList.add("active");
|
|
10368
10471
|
}
|
|
10369
10472
|
function appendKeys(keys) {
|
|
10473
|
+
const fragment = document.createDocumentFragment();
|
|
10370
10474
|
for (const k of keys) {
|
|
10371
10475
|
const row = document.createElement("div");
|
|
10372
10476
|
row.className = "redis-key-item";
|
|
10373
10477
|
row.dataset.keyName = k.name;
|
|
10478
|
+
keyRowsByName.set(k.name, row);
|
|
10374
10479
|
const typeBadge = document.createElement("span");
|
|
10375
10480
|
typeBadge.className = `redis-type-badge redis-type-${k.type}`;
|
|
10376
10481
|
typeBadge.textContent = k.type;
|
|
@@ -10379,18 +10484,20 @@ ${frontmatter.yaml}
|
|
|
10379
10484
|
nameEl.textContent = k.name;
|
|
10380
10485
|
nameEl.title = k.name;
|
|
10381
10486
|
row.append(typeBadge, nameEl);
|
|
10382
|
-
|
|
10383
|
-
keyList.appendChild(row);
|
|
10487
|
+
fragment.appendChild(row);
|
|
10384
10488
|
}
|
|
10489
|
+
keyList.appendChild(fragment);
|
|
10385
10490
|
}
|
|
10386
10491
|
function highlightActiveKey(name) {
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10492
|
+
if (activeKeyRow?.dataset.keyName === name)
|
|
10493
|
+
return;
|
|
10494
|
+
activeKeyRow?.classList.remove("active");
|
|
10495
|
+
activeKeyRow = keyRowsByName.get(name) ?? null;
|
|
10496
|
+
activeKeyRow?.classList.add("active");
|
|
10390
10497
|
}
|
|
10391
10498
|
function makeNotice(message, kind = "info") {
|
|
10392
10499
|
const div = document.createElement("div");
|
|
10393
|
-
div.className = kind === "warn" ? "
|
|
10500
|
+
div.className = kind === "warn" ? "datastore-value-truncation" : "datastore-value-info";
|
|
10394
10501
|
div.textContent = message;
|
|
10395
10502
|
return div;
|
|
10396
10503
|
}
|
|
@@ -10548,6 +10655,8 @@ ${frontmatter.yaml}
|
|
|
10548
10655
|
notifySelectionChange();
|
|
10549
10656
|
highlightActiveDb(dbIndex);
|
|
10550
10657
|
keyList.innerHTML = "";
|
|
10658
|
+
keyRowsByName.clear();
|
|
10659
|
+
activeKeyRow = null;
|
|
10551
10660
|
mainPane.textContent = "Select a key to view its value.";
|
|
10552
10661
|
await loadKeys(false);
|
|
10553
10662
|
}
|
|
@@ -10583,8 +10692,11 @@ ${frontmatter.yaml}
|
|
|
10583
10692
|
if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestDbIndex !== currentDbIndex || data.dbIndex !== requestDbIndex) {
|
|
10584
10693
|
return;
|
|
10585
10694
|
}
|
|
10586
|
-
if (!append)
|
|
10695
|
+
if (!append) {
|
|
10587
10696
|
keyList.innerHTML = "";
|
|
10697
|
+
keyRowsByName.clear();
|
|
10698
|
+
activeKeyRow = null;
|
|
10699
|
+
}
|
|
10588
10700
|
if (data.keys.length === 0 && !append) {
|
|
10589
10701
|
setKeyStatus("(no keys)");
|
|
10590
10702
|
} else {
|
|
@@ -10605,6 +10717,23 @@ ${frontmatter.yaml}
|
|
|
10605
10717
|
}
|
|
10606
10718
|
}
|
|
10607
10719
|
keyMoreBtn.addEventListener("click", () => loadKeys(true));
|
|
10720
|
+
dbList.addEventListener("click", (e2) => {
|
|
10721
|
+
const row = e2.target?.closest(".redis-db-item");
|
|
10722
|
+
if (!row || !dbList.contains(row))
|
|
10723
|
+
return;
|
|
10724
|
+
const raw = row.dataset.dbIndex;
|
|
10725
|
+
const index = raw === undefined ? NaN : Number(raw);
|
|
10726
|
+
if (Number.isInteger(index))
|
|
10727
|
+
selectDatabase(index);
|
|
10728
|
+
});
|
|
10729
|
+
keyList.addEventListener("click", (e2) => {
|
|
10730
|
+
const row = e2.target?.closest(".redis-key-item");
|
|
10731
|
+
if (!row || !keyList.contains(row))
|
|
10732
|
+
return;
|
|
10733
|
+
const key = row.dataset.keyName;
|
|
10734
|
+
if (key)
|
|
10735
|
+
selectKey(key);
|
|
10736
|
+
});
|
|
10608
10737
|
keyFilterForm.addEventListener("submit", (e2) => {
|
|
10609
10738
|
e2.preventDefault();
|
|
10610
10739
|
const nextFilter = keyFilterInput.value.trim() || "*";
|
|
@@ -10634,6 +10763,9 @@ ${frontmatter.yaml}
|
|
|
10634
10763
|
keyFilterInput.value = currentKeyFilter === "*" ? "" : currentKeyFilter;
|
|
10635
10764
|
currentCursor = "0";
|
|
10636
10765
|
keyList.innerHTML = "";
|
|
10766
|
+
keyRowsByName.clear();
|
|
10767
|
+
activeDbRow = null;
|
|
10768
|
+
activeKeyRow = null;
|
|
10637
10769
|
keyMoreBtn.hidden = true;
|
|
10638
10770
|
mainPane.textContent = "Select a key to view its value.";
|
|
10639
10771
|
setDbStatus("Loading databases...");
|
|
@@ -10653,11 +10785,21 @@ ${frontmatter.yaml}
|
|
|
10653
10785
|
if (initial?.dbIndex !== undefined && data.databases.some((d2) => d2.index === initial.dbIndex)) {
|
|
10654
10786
|
suppressNotify = true;
|
|
10655
10787
|
try {
|
|
10656
|
-
|
|
10788
|
+
currentDbIndex = initial.dbIndex;
|
|
10789
|
+
currentKey = null;
|
|
10790
|
+
currentCursor = "0";
|
|
10791
|
+
highlightActiveDb(initial.dbIndex);
|
|
10792
|
+
keyList.innerHTML = "";
|
|
10793
|
+
keyRowsByName.clear();
|
|
10794
|
+
mainPane.textContent = "Select a key to view its value.";
|
|
10795
|
+
const valuePromise = initial.key ? selectKey(initial.key).catch(() => {}) : null;
|
|
10796
|
+
await loadKeys(false);
|
|
10657
10797
|
if (currentDbId !== dbId)
|
|
10658
10798
|
return;
|
|
10659
|
-
if (
|
|
10660
|
-
await
|
|
10799
|
+
if (valuePromise) {
|
|
10800
|
+
await valuePromise;
|
|
10801
|
+
if (initial.key)
|
|
10802
|
+
highlightActiveKey(initial.key);
|
|
10661
10803
|
}
|
|
10662
10804
|
} finally {
|
|
10663
10805
|
suppressNotify = false;
|
|
@@ -10687,6 +10829,10 @@ ${frontmatter.yaml}
|
|
|
10687
10829
|
currentCursor = "0";
|
|
10688
10830
|
dbList.innerHTML = "";
|
|
10689
10831
|
keyList.innerHTML = "";
|
|
10832
|
+
dbRowsByIndex.clear();
|
|
10833
|
+
keyRowsByName.clear();
|
|
10834
|
+
activeDbRow = null;
|
|
10835
|
+
activeKeyRow = null;
|
|
10690
10836
|
keyMoreBtn.hidden = true;
|
|
10691
10837
|
mainPane.textContent = "Select a database to view keys.";
|
|
10692
10838
|
}
|
|
@@ -10704,6 +10850,654 @@ ${frontmatter.yaml}
|
|
|
10704
10850
|
return { el: container, load, clear, dispose, getSelection };
|
|
10705
10851
|
}
|
|
10706
10852
|
|
|
10853
|
+
// web-src/core/database/s3-keys.ts
|
|
10854
|
+
function s3ObjectName(key) {
|
|
10855
|
+
const trimmed = key.replace(/\/+$/, "");
|
|
10856
|
+
const idx = trimmed.lastIndexOf("/");
|
|
10857
|
+
return idx >= 0 ? trimmed.slice(idx + 1) : trimmed;
|
|
10858
|
+
}
|
|
10859
|
+
|
|
10860
|
+
// web-src/views/source-preview-elements.ts
|
|
10861
|
+
function renderHtmlPreviewFrame(title, html, extraClass = "") {
|
|
10862
|
+
const preview = document.createElement("div");
|
|
10863
|
+
preview.className = ["gdp-html-preview", extraClass].filter(Boolean).join(" ");
|
|
10864
|
+
const frame = document.createElement("iframe");
|
|
10865
|
+
frame.title = title;
|
|
10866
|
+
frame.sandbox.value = "";
|
|
10867
|
+
frame.referrerPolicy = "no-referrer";
|
|
10868
|
+
frame.srcdoc = html;
|
|
10869
|
+
preview.appendChild(frame);
|
|
10870
|
+
return preview;
|
|
10871
|
+
}
|
|
10872
|
+
function appendMediaEmbed(view, opts) {
|
|
10873
|
+
if (opts.kind === "video") {
|
|
10874
|
+
const video = document.createElement("video");
|
|
10875
|
+
video.src = opts.url;
|
|
10876
|
+
video.controls = true;
|
|
10877
|
+
video.preload = "metadata";
|
|
10878
|
+
view.appendChild(video);
|
|
10879
|
+
} else if (opts.kind === "audio") {
|
|
10880
|
+
const audio = document.createElement("audio");
|
|
10881
|
+
audio.src = opts.url;
|
|
10882
|
+
audio.controls = true;
|
|
10883
|
+
audio.preload = "metadata";
|
|
10884
|
+
view.appendChild(audio);
|
|
10885
|
+
} else if (opts.kind === "pdf") {
|
|
10886
|
+
const frame = document.createElement("iframe");
|
|
10887
|
+
frame.src = opts.url;
|
|
10888
|
+
frame.title = opts.title;
|
|
10889
|
+
frame.loading = "lazy";
|
|
10890
|
+
view.appendChild(frame);
|
|
10891
|
+
} else {
|
|
10892
|
+
const img = document.createElement("img");
|
|
10893
|
+
img.src = opts.url;
|
|
10894
|
+
img.alt = "";
|
|
10895
|
+
if (opts.onImageLoad) {
|
|
10896
|
+
img.addEventListener("load", () => opts.onImageLoad?.(img), {
|
|
10897
|
+
once: true
|
|
10898
|
+
});
|
|
10899
|
+
}
|
|
10900
|
+
view.appendChild(img);
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10903
|
+
function renderUnsupportedPreview(opts) {
|
|
10904
|
+
const view = document.createElement("div");
|
|
10905
|
+
view.className = ["gdp-source-viewer unsupported", opts.className || ""].filter(Boolean).join(" ");
|
|
10906
|
+
const content = document.createElement("div");
|
|
10907
|
+
content.className = "gdp-source-unsupported-content";
|
|
10908
|
+
const title = document.createElement("strong");
|
|
10909
|
+
title.className = "gdp-source-unsupported-title";
|
|
10910
|
+
title.textContent = "Preview unavailable";
|
|
10911
|
+
const message = document.createElement("div");
|
|
10912
|
+
message.className = "gdp-source-unsupported-message";
|
|
10913
|
+
message.textContent = opts.message;
|
|
10914
|
+
content.append(title, message, ...opts.extraChildren || []);
|
|
10915
|
+
view.appendChild(content);
|
|
10916
|
+
return view;
|
|
10917
|
+
}
|
|
10918
|
+
|
|
10919
|
+
// web-src/views/database/s3-explorer.ts
|
|
10920
|
+
function buildS3RawUrl(dbId, bucket, key) {
|
|
10921
|
+
const params = new URLSearchParams({ db: dbId, bucket, key });
|
|
10922
|
+
return `/_db/s3/raw?${params}`;
|
|
10923
|
+
}
|
|
10924
|
+
function s3Uri(bucket, key) {
|
|
10925
|
+
return `s3://${bucket}/${key}`;
|
|
10926
|
+
}
|
|
10927
|
+
function objectTypeLabel(key, contentType) {
|
|
10928
|
+
const kind = sourceDisplayKind(key);
|
|
10929
|
+
return humanFileKind(key, contentType, kind === "unsupported" ? "unsupported file" : kind);
|
|
10930
|
+
}
|
|
10931
|
+
function createS3Explorer(callbacks = {}) {
|
|
10932
|
+
const container = document.createElement("div");
|
|
10933
|
+
container.className = "s3-explorer";
|
|
10934
|
+
const objectPane = document.createElement("div");
|
|
10935
|
+
objectPane.className = "s3-object-list-pane";
|
|
10936
|
+
const toolbar = document.createElement("div");
|
|
10937
|
+
toolbar.className = "s3-toolbar";
|
|
10938
|
+
const bucketRow = document.createElement("div");
|
|
10939
|
+
bucketRow.className = "s3-bucket-row";
|
|
10940
|
+
const bucketLabel = document.createElement("label");
|
|
10941
|
+
bucketLabel.className = "s3-field-label";
|
|
10942
|
+
bucketLabel.textContent = "Bucket";
|
|
10943
|
+
const bucketSelect = document.createElement("select");
|
|
10944
|
+
bucketSelect.className = "s3-bucket-select";
|
|
10945
|
+
bucketLabel.appendChild(bucketSelect);
|
|
10946
|
+
bucketRow.appendChild(bucketLabel);
|
|
10947
|
+
const searchRow = document.createElement("form");
|
|
10948
|
+
searchRow.className = "s3-search-row";
|
|
10949
|
+
const searchInput = document.createElement("input");
|
|
10950
|
+
searchInput.type = "search";
|
|
10951
|
+
searchInput.className = "s3-search-input";
|
|
10952
|
+
searchInput.placeholder = "Search objects";
|
|
10953
|
+
searchInput.autocomplete = "off";
|
|
10954
|
+
const searchBtn = document.createElement("button");
|
|
10955
|
+
searchBtn.type = "submit";
|
|
10956
|
+
searchBtn.className = "db-btn db-btn-primary s3-search-btn";
|
|
10957
|
+
searchBtn.textContent = "Search";
|
|
10958
|
+
searchRow.append(searchInput, searchBtn);
|
|
10959
|
+
const optionRow = document.createElement("div");
|
|
10960
|
+
optionRow.className = "s3-options-row";
|
|
10961
|
+
const modeSeg = document.createElement("div");
|
|
10962
|
+
modeSeg.className = "seg s3-mode-seg";
|
|
10963
|
+
const prefixModeBtn = document.createElement("button");
|
|
10964
|
+
prefixModeBtn.type = "button";
|
|
10965
|
+
prefixModeBtn.textContent = "Prefix";
|
|
10966
|
+
const containsModeBtn = document.createElement("button");
|
|
10967
|
+
containsModeBtn.type = "button";
|
|
10968
|
+
containsModeBtn.textContent = "Contains";
|
|
10969
|
+
modeSeg.append(prefixModeBtn, containsModeBtn);
|
|
10970
|
+
const sortSelect = document.createElement("select");
|
|
10971
|
+
sortSelect.className = "s3-sort-select";
|
|
10972
|
+
const sortUpdated = document.createElement("option");
|
|
10973
|
+
sortUpdated.value = "updated-desc";
|
|
10974
|
+
sortUpdated.textContent = "Updated newest";
|
|
10975
|
+
const sortKey = document.createElement("option");
|
|
10976
|
+
sortKey.value = "key-asc";
|
|
10977
|
+
sortKey.textContent = "Key A-Z";
|
|
10978
|
+
sortSelect.append(sortUpdated, sortKey);
|
|
10979
|
+
optionRow.append(modeSeg, sortSelect);
|
|
10980
|
+
toolbar.append(bucketRow, searchRow, optionRow);
|
|
10981
|
+
objectPane.appendChild(toolbar);
|
|
10982
|
+
const objectStatus = document.createElement("div");
|
|
10983
|
+
objectStatus.className = "s3-object-status";
|
|
10984
|
+
objectPane.appendChild(objectStatus);
|
|
10985
|
+
const objectList = document.createElement("div");
|
|
10986
|
+
objectList.className = "s3-object-list";
|
|
10987
|
+
objectPane.appendChild(objectList);
|
|
10988
|
+
const moreBtn = document.createElement("button");
|
|
10989
|
+
moreBtn.type = "button";
|
|
10990
|
+
moreBtn.className = "s3-object-more-btn";
|
|
10991
|
+
moreBtn.textContent = "Load more";
|
|
10992
|
+
moreBtn.hidden = true;
|
|
10993
|
+
objectPane.appendChild(moreBtn);
|
|
10994
|
+
const previewPane = document.createElement("div");
|
|
10995
|
+
previewPane.className = "s3-preview-pane";
|
|
10996
|
+
previewPane.textContent = "Select an object to preview.";
|
|
10997
|
+
container.append(objectPane, previewPane);
|
|
10998
|
+
let currentDbId = null;
|
|
10999
|
+
let currentBucket = null;
|
|
11000
|
+
let currentKey = null;
|
|
11001
|
+
let currentMode = "prefix";
|
|
11002
|
+
let currentSort = "updated-desc";
|
|
11003
|
+
let currentSearch = "";
|
|
11004
|
+
let currentNextToken;
|
|
11005
|
+
const objectsByKey = new Map;
|
|
11006
|
+
const objectRowsByKey = new Map;
|
|
11007
|
+
let disposed = false;
|
|
11008
|
+
let loadRunId = 0;
|
|
11009
|
+
let objectRunId = 0;
|
|
11010
|
+
let suppressNotify = false;
|
|
11011
|
+
let activeObjectRow = null;
|
|
11012
|
+
const bucketGuard = createAbortGuard();
|
|
11013
|
+
const objectGuard = createAbortGuard();
|
|
11014
|
+
const previewGuard = createAbortGuard();
|
|
11015
|
+
function notifySelectionChange() {
|
|
11016
|
+
if (suppressNotify)
|
|
11017
|
+
return;
|
|
11018
|
+
callbacks.onSelectionChange?.(getSelection());
|
|
11019
|
+
}
|
|
11020
|
+
function setMode(mode) {
|
|
11021
|
+
currentMode = mode;
|
|
11022
|
+
prefixModeBtn.classList.toggle("active", mode === "prefix");
|
|
11023
|
+
containsModeBtn.classList.toggle("active", mode === "contains");
|
|
11024
|
+
searchInput.placeholder = mode === "prefix" ? "Prefix, e.g. photos/2026/" : "Filename contains";
|
|
11025
|
+
}
|
|
11026
|
+
function renderBuckets(buckets) {
|
|
11027
|
+
bucketSelect.innerHTML = "";
|
|
11028
|
+
for (const bucket of buckets) {
|
|
11029
|
+
const opt = document.createElement("option");
|
|
11030
|
+
opt.value = bucket.name;
|
|
11031
|
+
opt.textContent = bucket.name;
|
|
11032
|
+
bucketSelect.appendChild(opt);
|
|
11033
|
+
}
|
|
11034
|
+
bucketSelect.disabled = buckets.length === 0;
|
|
11035
|
+
}
|
|
11036
|
+
function setObjectStatusFromResponse(resp) {
|
|
11037
|
+
objectStatus.textContent = "";
|
|
11038
|
+
const parts = [
|
|
11039
|
+
`${resp.objects.length.toLocaleString()} shown`,
|
|
11040
|
+
`${resp.scannedObjects.toLocaleString()} scanned`,
|
|
11041
|
+
resp.sort === "updated-desc" ? "newest first in scanned objects" : "sorted by key"
|
|
11042
|
+
];
|
|
11043
|
+
if (resp.scanLimitReached) {
|
|
11044
|
+
parts.push("scan cap reached; narrow the prefix to search more precisely");
|
|
11045
|
+
}
|
|
11046
|
+
objectStatus.textContent = parts.join(" / ");
|
|
11047
|
+
objectStatus.classList.toggle("warn", !!resp.scanLimitReached);
|
|
11048
|
+
}
|
|
11049
|
+
function highlightActiveObject(key) {
|
|
11050
|
+
if (activeObjectRow?.dataset.key === key)
|
|
11051
|
+
return;
|
|
11052
|
+
activeObjectRow?.classList.remove("active");
|
|
11053
|
+
activeObjectRow = key ? objectRowsByKey.get(key) ?? null : null;
|
|
11054
|
+
activeObjectRow?.classList.add("active");
|
|
11055
|
+
}
|
|
11056
|
+
async function fetchObjectHeadForSelection(key, signal) {
|
|
11057
|
+
if (!currentDbId || !currentBucket)
|
|
11058
|
+
return { key, sizeBytes: 0 };
|
|
11059
|
+
const params = new URLSearchParams({
|
|
11060
|
+
db: currentDbId,
|
|
11061
|
+
bucket: currentBucket,
|
|
11062
|
+
key
|
|
11063
|
+
});
|
|
11064
|
+
const res = await fetch(`/_db/s3/head?${params}`, { signal });
|
|
11065
|
+
if (!res.ok)
|
|
11066
|
+
return { key, sizeBytes: 0 };
|
|
11067
|
+
const data = await res.json();
|
|
11068
|
+
return {
|
|
11069
|
+
key,
|
|
11070
|
+
sizeBytes: data.sizeBytes ?? 0,
|
|
11071
|
+
...data.updatedAt ? { updatedAt: data.updatedAt } : {},
|
|
11072
|
+
...data.contentType ? { contentType: data.contentType } : {},
|
|
11073
|
+
...data.etag ? { etag: data.etag } : {}
|
|
11074
|
+
};
|
|
11075
|
+
}
|
|
11076
|
+
function appendObjects(objects) {
|
|
11077
|
+
const fragment = document.createDocumentFragment();
|
|
11078
|
+
for (const object of objects) {
|
|
11079
|
+
objectsByKey.set(object.key, object);
|
|
11080
|
+
const row = document.createElement("div");
|
|
11081
|
+
row.className = "s3-object-item";
|
|
11082
|
+
row.dataset.key = object.key;
|
|
11083
|
+
objectRowsByKey.set(object.key, row);
|
|
11084
|
+
const name = document.createElement("span");
|
|
11085
|
+
name.className = "s3-object-name";
|
|
11086
|
+
name.textContent = object.key;
|
|
11087
|
+
name.title = object.key;
|
|
11088
|
+
const meta = document.createElement("span");
|
|
11089
|
+
meta.className = "s3-object-meta";
|
|
11090
|
+
const kind = objectTypeLabel(object.key, object.contentType);
|
|
11091
|
+
const size = formatBytes(object.sizeBytes);
|
|
11092
|
+
const updated = formatFileDate(object.updatedAt);
|
|
11093
|
+
meta.textContent = [kind, size, updated].filter(Boolean).join(" / ");
|
|
11094
|
+
row.append(name, meta);
|
|
11095
|
+
fragment.appendChild(row);
|
|
11096
|
+
}
|
|
11097
|
+
objectList.appendChild(fragment);
|
|
11098
|
+
}
|
|
11099
|
+
async function loadObjects(append) {
|
|
11100
|
+
if (!currentDbId || !currentBucket || disposed)
|
|
11101
|
+
return;
|
|
11102
|
+
const slot = objectGuard.start();
|
|
11103
|
+
const requestRunId = loadRunId;
|
|
11104
|
+
const requestDbId = currentDbId;
|
|
11105
|
+
const requestBucket = currentBucket;
|
|
11106
|
+
const requestSearch = currentSearch;
|
|
11107
|
+
const requestMode = currentMode;
|
|
11108
|
+
const requestSort = currentSort;
|
|
11109
|
+
moreBtn.disabled = true;
|
|
11110
|
+
if (!append) {
|
|
11111
|
+
objectList.innerHTML = "";
|
|
11112
|
+
objectsByKey.clear();
|
|
11113
|
+
objectRowsByKey.clear();
|
|
11114
|
+
activeObjectRow = null;
|
|
11115
|
+
currentKey = null;
|
|
11116
|
+
currentNextToken = undefined;
|
|
11117
|
+
setPaneStatus(objectList, "Loading objects...");
|
|
11118
|
+
previewPane.textContent = "Select an object to preview.";
|
|
11119
|
+
}
|
|
11120
|
+
try {
|
|
11121
|
+
const params = new URLSearchParams({
|
|
11122
|
+
db: requestDbId,
|
|
11123
|
+
bucket: requestBucket,
|
|
11124
|
+
mode: requestMode,
|
|
11125
|
+
sort: requestSort,
|
|
11126
|
+
limit: requestSort === "updated-desc" ? "1000" : "200"
|
|
11127
|
+
});
|
|
11128
|
+
if (requestMode === "prefix") {
|
|
11129
|
+
if (requestSearch)
|
|
11130
|
+
params.set("q", requestSearch);
|
|
11131
|
+
} else if (requestSearch) {
|
|
11132
|
+
params.set("q", requestSearch);
|
|
11133
|
+
}
|
|
11134
|
+
if (append && currentNextToken)
|
|
11135
|
+
params.set("token", currentNextToken);
|
|
11136
|
+
const res = await fetch(`/_db/s3/objects?${params}`, {
|
|
11137
|
+
signal: slot.signal
|
|
11138
|
+
});
|
|
11139
|
+
if (disposed || slot.isStale())
|
|
11140
|
+
return;
|
|
11141
|
+
if (!res.ok) {
|
|
11142
|
+
const text2 = await res.text();
|
|
11143
|
+
setPaneStatus(objectList, `Error: ${text2 || res.statusText}`, {
|
|
11144
|
+
error: true
|
|
11145
|
+
});
|
|
11146
|
+
objectStatus.textContent = "";
|
|
11147
|
+
return;
|
|
11148
|
+
}
|
|
11149
|
+
const data = await res.json();
|
|
11150
|
+
if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestBucket !== currentBucket || requestSearch !== currentSearch || requestMode !== currentMode || requestSort !== currentSort) {
|
|
11151
|
+
return;
|
|
11152
|
+
}
|
|
11153
|
+
if (!append)
|
|
11154
|
+
objectList.innerHTML = "";
|
|
11155
|
+
if (data.objects.length === 0 && !append) {
|
|
11156
|
+
setPaneStatus(objectList, data.scanLimitReached ? `(no matches in the first ${data.scannedObjects.toLocaleString()} scanned objects; narrow the prefix and search again)` : "(no objects)");
|
|
11157
|
+
} else {
|
|
11158
|
+
appendObjects(data.objects);
|
|
11159
|
+
}
|
|
11160
|
+
currentNextToken = data.nextToken;
|
|
11161
|
+
moreBtn.hidden = !data.nextToken;
|
|
11162
|
+
setObjectStatusFromResponse(data);
|
|
11163
|
+
highlightActiveObject(currentKey);
|
|
11164
|
+
} catch (err) {
|
|
11165
|
+
if (slot.isStale())
|
|
11166
|
+
return;
|
|
11167
|
+
setPaneStatus(objectList, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
|
|
11168
|
+
objectStatus.textContent = "";
|
|
11169
|
+
} finally {
|
|
11170
|
+
slot.finish();
|
|
11171
|
+
if (!slot.isStale())
|
|
11172
|
+
moreBtn.disabled = false;
|
|
11173
|
+
}
|
|
11174
|
+
}
|
|
11175
|
+
function renderPreviewHeader(object) {
|
|
11176
|
+
const header = document.createElement("div");
|
|
11177
|
+
header.className = "s3-preview-header";
|
|
11178
|
+
const title = document.createElement("div");
|
|
11179
|
+
title.className = "s3-preview-title";
|
|
11180
|
+
title.textContent = object.key;
|
|
11181
|
+
title.title = object.key;
|
|
11182
|
+
const meta = document.createElement("div");
|
|
11183
|
+
meta.className = "s3-preview-meta";
|
|
11184
|
+
meta.textContent = [
|
|
11185
|
+
objectTypeLabel(object.key, object.contentType),
|
|
11186
|
+
formatBytes(object.sizeBytes),
|
|
11187
|
+
formatFileDate(object.updatedAt)
|
|
11188
|
+
].filter(Boolean).join(" / ");
|
|
11189
|
+
const actions = document.createElement("div");
|
|
11190
|
+
actions.className = "s3-preview-actions";
|
|
11191
|
+
if (currentDbId && currentBucket) {
|
|
11192
|
+
const rawUrl = buildS3RawUrl(currentDbId, currentBucket, object.key);
|
|
11193
|
+
const open = document.createElement("a");
|
|
11194
|
+
open.className = "db-btn db-btn-sm";
|
|
11195
|
+
open.href = rawUrl;
|
|
11196
|
+
open.target = "_blank";
|
|
11197
|
+
open.rel = "noreferrer";
|
|
11198
|
+
open.textContent = "Open raw";
|
|
11199
|
+
const download = document.createElement("a");
|
|
11200
|
+
download.className = "db-btn db-btn-sm";
|
|
11201
|
+
download.href = rawUrl;
|
|
11202
|
+
download.download = s3ObjectName(object.key);
|
|
11203
|
+
download.textContent = "Download";
|
|
11204
|
+
const copy = document.createElement("button");
|
|
11205
|
+
copy.type = "button";
|
|
11206
|
+
copy.className = "db-btn db-btn-sm";
|
|
11207
|
+
copy.textContent = "Copy S3 URI";
|
|
11208
|
+
copy.addEventListener("click", async () => {
|
|
11209
|
+
try {
|
|
11210
|
+
await navigator.clipboard.writeText(s3Uri(currentBucket || "", object.key));
|
|
11211
|
+
copy.textContent = "Copied";
|
|
11212
|
+
window.setTimeout(() => {
|
|
11213
|
+
copy.textContent = "Copy S3 URI";
|
|
11214
|
+
}, 1200);
|
|
11215
|
+
} catch {
|
|
11216
|
+
copy.textContent = "Copy failed";
|
|
11217
|
+
}
|
|
11218
|
+
});
|
|
11219
|
+
actions.append(open, download, copy);
|
|
11220
|
+
}
|
|
11221
|
+
header.append(title, meta, actions);
|
|
11222
|
+
return header;
|
|
11223
|
+
}
|
|
11224
|
+
function renderMediaPreview(object, kind) {
|
|
11225
|
+
const view = document.createElement("div");
|
|
11226
|
+
view.className = `gdp-source-viewer media ${kind} s3-source-preview`;
|
|
11227
|
+
if (!currentDbId || !currentBucket)
|
|
11228
|
+
return view;
|
|
11229
|
+
const url = buildS3RawUrl(currentDbId, currentBucket, object.key);
|
|
11230
|
+
appendMediaEmbed(view, { url, kind, title: object.key });
|
|
11231
|
+
return view;
|
|
11232
|
+
}
|
|
11233
|
+
function renderUnsupported() {
|
|
11234
|
+
return renderUnsupportedPreview({
|
|
11235
|
+
className: "s3-source-preview",
|
|
11236
|
+
message: "This object type cannot be previewed safely in the browser."
|
|
11237
|
+
});
|
|
11238
|
+
}
|
|
11239
|
+
async function renderTextPreview(object, slot) {
|
|
11240
|
+
if (!currentDbId || !currentBucket)
|
|
11241
|
+
return null;
|
|
11242
|
+
const params = new URLSearchParams({
|
|
11243
|
+
db: currentDbId,
|
|
11244
|
+
bucket: currentBucket,
|
|
11245
|
+
key: object.key
|
|
11246
|
+
});
|
|
11247
|
+
const res = await fetch(`/_db/s3/text?${params}`, { signal: slot.signal });
|
|
11248
|
+
if (disposed || slot.isStale())
|
|
11249
|
+
return null;
|
|
11250
|
+
if (!res.ok) {
|
|
11251
|
+
const text2 = await res.text();
|
|
11252
|
+
const error2 = document.createElement("div");
|
|
11253
|
+
error2.className = "db-pane-error";
|
|
11254
|
+
error2.textContent = text2 || res.statusText;
|
|
11255
|
+
return error2;
|
|
11256
|
+
}
|
|
11257
|
+
const data = await res.json();
|
|
11258
|
+
if (disposed || slot.isStale())
|
|
11259
|
+
return null;
|
|
11260
|
+
const previewKind = sourcePreviewKind(object.key);
|
|
11261
|
+
let body;
|
|
11262
|
+
if (previewKind === "html") {
|
|
11263
|
+
body = renderHtmlPreviewFrame(`${object.key} preview`, data.text, "s3-html-preview");
|
|
11264
|
+
} else if (previewKind === "markdown") {
|
|
11265
|
+
body = await renderMarkdownPreview(data.text, { path: object.key, ref: "s3" }, {
|
|
11266
|
+
syntaxHighlight: true,
|
|
11267
|
+
signal: slot.signal,
|
|
11268
|
+
resolveAssetUrl: (path) => currentDbId && currentBucket ? buildS3RawUrl(currentDbId, currentBucket, path) : null
|
|
11269
|
+
});
|
|
11270
|
+
} else {
|
|
11271
|
+
const pre = document.createElement("pre");
|
|
11272
|
+
pre.className = "s3-text-preview";
|
|
11273
|
+
pre.textContent = data.text;
|
|
11274
|
+
body = pre;
|
|
11275
|
+
}
|
|
11276
|
+
if (data.truncated) {
|
|
11277
|
+
const wrap = document.createElement("div");
|
|
11278
|
+
wrap.className = "s3-text-preview-wrap";
|
|
11279
|
+
const note = document.createElement("div");
|
|
11280
|
+
note.className = "datastore-value-truncation";
|
|
11281
|
+
note.textContent = `Showing first ${formatBytes(512 * 1024)}.`;
|
|
11282
|
+
wrap.append(note, body);
|
|
11283
|
+
return wrap;
|
|
11284
|
+
}
|
|
11285
|
+
return body;
|
|
11286
|
+
}
|
|
11287
|
+
async function selectObject(object) {
|
|
11288
|
+
if (disposed || !currentDbId || !currentBucket)
|
|
11289
|
+
return;
|
|
11290
|
+
const slot = previewGuard.start();
|
|
11291
|
+
const requestRunId = ++objectRunId;
|
|
11292
|
+
const requestDbId = currentDbId;
|
|
11293
|
+
const requestBucket = currentBucket;
|
|
11294
|
+
currentKey = object.key;
|
|
11295
|
+
notifySelectionChange();
|
|
11296
|
+
highlightActiveObject(object.key);
|
|
11297
|
+
previewPane.innerHTML = "";
|
|
11298
|
+
previewPane.appendChild(renderPreviewHeader(object));
|
|
11299
|
+
const body = document.createElement("div");
|
|
11300
|
+
body.className = "s3-preview-body";
|
|
11301
|
+
setPaneStatus(body, "Loading preview...");
|
|
11302
|
+
previewPane.appendChild(body);
|
|
11303
|
+
try {
|
|
11304
|
+
const displayKind = sourceDisplayKind(object.key);
|
|
11305
|
+
let preview;
|
|
11306
|
+
if (displayKind === "image" || displayKind === "video" || displayKind === "audio" || displayKind === "pdf") {
|
|
11307
|
+
preview = renderMediaPreview(object, displayKind);
|
|
11308
|
+
} else if (displayKind === "text") {
|
|
11309
|
+
preview = await renderTextPreview(object, slot);
|
|
11310
|
+
} else {
|
|
11311
|
+
preview = renderUnsupported();
|
|
11312
|
+
}
|
|
11313
|
+
if (disposed || slot.isStale() || requestRunId !== objectRunId || requestDbId !== currentDbId || requestBucket !== currentBucket || object.key !== currentKey) {
|
|
11314
|
+
return;
|
|
11315
|
+
}
|
|
11316
|
+
body.innerHTML = "";
|
|
11317
|
+
if (preview)
|
|
11318
|
+
body.appendChild(preview);
|
|
11319
|
+
} catch (err) {
|
|
11320
|
+
if (slot.isStale())
|
|
11321
|
+
return;
|
|
11322
|
+
setPaneStatus(body, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
|
|
11323
|
+
} finally {
|
|
11324
|
+
slot.finish();
|
|
11325
|
+
}
|
|
11326
|
+
}
|
|
11327
|
+
async function selectBucket(bucket) {
|
|
11328
|
+
if (disposed || !currentDbId)
|
|
11329
|
+
return;
|
|
11330
|
+
currentBucket = bucket;
|
|
11331
|
+
currentKey = null;
|
|
11332
|
+
currentNextToken = undefined;
|
|
11333
|
+
notifySelectionChange();
|
|
11334
|
+
await loadObjects(false);
|
|
11335
|
+
}
|
|
11336
|
+
bucketSelect.addEventListener("change", () => {
|
|
11337
|
+
if (bucketSelect.value)
|
|
11338
|
+
selectBucket(bucketSelect.value);
|
|
11339
|
+
});
|
|
11340
|
+
prefixModeBtn.addEventListener("click", () => {
|
|
11341
|
+
setMode("prefix");
|
|
11342
|
+
notifySelectionChange();
|
|
11343
|
+
loadObjects(false);
|
|
11344
|
+
});
|
|
11345
|
+
containsModeBtn.addEventListener("click", () => {
|
|
11346
|
+
setMode("contains");
|
|
11347
|
+
notifySelectionChange();
|
|
11348
|
+
loadObjects(false);
|
|
11349
|
+
});
|
|
11350
|
+
sortSelect.addEventListener("change", () => {
|
|
11351
|
+
currentSort = sortSelect.value === "key-asc" ? "key-asc" : "updated-desc";
|
|
11352
|
+
notifySelectionChange();
|
|
11353
|
+
loadObjects(false);
|
|
11354
|
+
});
|
|
11355
|
+
searchRow.addEventListener("submit", (e2) => {
|
|
11356
|
+
e2.preventDefault();
|
|
11357
|
+
currentSearch = searchInput.value.trim();
|
|
11358
|
+
notifySelectionChange();
|
|
11359
|
+
loadObjects(false);
|
|
11360
|
+
});
|
|
11361
|
+
searchInput.addEventListener("keydown", (e2) => {
|
|
11362
|
+
if (isImeComposing(e2))
|
|
11363
|
+
return;
|
|
11364
|
+
if (e2.key === "Escape") {
|
|
11365
|
+
searchInput.value = "";
|
|
11366
|
+
currentSearch = "";
|
|
11367
|
+
notifySelectionChange();
|
|
11368
|
+
loadObjects(false);
|
|
11369
|
+
}
|
|
11370
|
+
});
|
|
11371
|
+
moreBtn.addEventListener("click", () => loadObjects(true));
|
|
11372
|
+
objectList.addEventListener("click", (e2) => {
|
|
11373
|
+
const row = e2.target?.closest(".s3-object-item");
|
|
11374
|
+
if (!row || !objectList.contains(row))
|
|
11375
|
+
return;
|
|
11376
|
+
const key = row.dataset.key;
|
|
11377
|
+
if (!key)
|
|
11378
|
+
return;
|
|
11379
|
+
const object = objectsByKey.get(key);
|
|
11380
|
+
if (object)
|
|
11381
|
+
selectObject(object);
|
|
11382
|
+
});
|
|
11383
|
+
async function load(dbId, initial) {
|
|
11384
|
+
if (disposed)
|
|
11385
|
+
return;
|
|
11386
|
+
if (currentDbId === dbId && !initial)
|
|
11387
|
+
return;
|
|
11388
|
+
bucketGuard.dispose();
|
|
11389
|
+
objectGuard.dispose();
|
|
11390
|
+
previewGuard.dispose();
|
|
11391
|
+
const slot = bucketGuard.start();
|
|
11392
|
+
const requestRunId = ++loadRunId;
|
|
11393
|
+
const initialMode = initial?.mode === "contains" ? "contains" : "prefix";
|
|
11394
|
+
currentDbId = dbId;
|
|
11395
|
+
currentBucket = null;
|
|
11396
|
+
currentKey = null;
|
|
11397
|
+
currentSearch = initialMode === "contains" ? initial?.query ?? "" : initial?.prefix ?? "";
|
|
11398
|
+
currentSort = initial?.sort === "key-asc" ? "key-asc" : "updated-desc";
|
|
11399
|
+
setMode(initialMode);
|
|
11400
|
+
sortSelect.value = currentSort;
|
|
11401
|
+
searchInput.value = currentSearch;
|
|
11402
|
+
objectStatus.textContent = "";
|
|
11403
|
+
objectList.innerHTML = "";
|
|
11404
|
+
objectsByKey.clear();
|
|
11405
|
+
objectRowsByKey.clear();
|
|
11406
|
+
activeObjectRow = null;
|
|
11407
|
+
moreBtn.hidden = true;
|
|
11408
|
+
previewPane.textContent = "Select an object to preview.";
|
|
11409
|
+
setPaneStatus(objectList, "Loading buckets...");
|
|
11410
|
+
try {
|
|
11411
|
+
const res = await fetch(`/_db/s3/buckets?db=${encodeURIComponent(dbId)}`, { signal: slot.signal });
|
|
11412
|
+
if (disposed || slot.isStale())
|
|
11413
|
+
return;
|
|
11414
|
+
if (!res.ok) {
|
|
11415
|
+
const text2 = await res.text();
|
|
11416
|
+
setPaneStatus(objectList, `Error: ${text2 || res.statusText}`, {
|
|
11417
|
+
error: true
|
|
11418
|
+
});
|
|
11419
|
+
return;
|
|
11420
|
+
}
|
|
11421
|
+
const data = await res.json();
|
|
11422
|
+
if (disposed || slot.isStale() || requestRunId !== loadRunId || currentDbId !== dbId) {
|
|
11423
|
+
return;
|
|
11424
|
+
}
|
|
11425
|
+
renderBuckets(data.buckets);
|
|
11426
|
+
const selected = initial?.bucket && data.buckets.some((bucket) => bucket.name === initial.bucket) && initial.bucket || data.buckets[0]?.name || null;
|
|
11427
|
+
if (!selected) {
|
|
11428
|
+
setPaneStatus(objectList, "(no buckets)");
|
|
11429
|
+
return;
|
|
11430
|
+
}
|
|
11431
|
+
bucketSelect.value = selected;
|
|
11432
|
+
suppressNotify = true;
|
|
11433
|
+
try {
|
|
11434
|
+
currentBucket = selected;
|
|
11435
|
+
currentKey = null;
|
|
11436
|
+
currentNextToken = undefined;
|
|
11437
|
+
notifySelectionChange();
|
|
11438
|
+
highlightActiveObject(null);
|
|
11439
|
+
const headPromise = initial?.key ? fetchObjectHeadForSelection(initial.key, slot.signal).catch(() => null) : null;
|
|
11440
|
+
await loadObjects(false);
|
|
11441
|
+
if (currentDbId !== dbId)
|
|
11442
|
+
return;
|
|
11443
|
+
if (initial?.key) {
|
|
11444
|
+
const object = objectsByKey.get(initial.key) || (headPromise ? await headPromise : null) || { key: initial.key, sizeBytes: 0 };
|
|
11445
|
+
await selectObject(object);
|
|
11446
|
+
}
|
|
11447
|
+
} finally {
|
|
11448
|
+
suppressNotify = false;
|
|
11449
|
+
}
|
|
11450
|
+
notifySelectionChange();
|
|
11451
|
+
} catch (err) {
|
|
11452
|
+
if (slot.isStale())
|
|
11453
|
+
return;
|
|
11454
|
+
setPaneStatus(objectList, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
|
|
11455
|
+
} finally {
|
|
11456
|
+
slot.finish();
|
|
11457
|
+
}
|
|
11458
|
+
}
|
|
11459
|
+
function clear() {
|
|
11460
|
+
bucketGuard.dispose();
|
|
11461
|
+
objectGuard.dispose();
|
|
11462
|
+
previewGuard.dispose();
|
|
11463
|
+
loadRunId++;
|
|
11464
|
+
objectRunId++;
|
|
11465
|
+
suppressNotify = false;
|
|
11466
|
+
currentDbId = null;
|
|
11467
|
+
currentBucket = null;
|
|
11468
|
+
currentKey = null;
|
|
11469
|
+
currentSearch = "";
|
|
11470
|
+
currentNextToken = undefined;
|
|
11471
|
+
currentSort = "updated-desc";
|
|
11472
|
+
setMode("prefix");
|
|
11473
|
+
sortSelect.value = "updated-desc";
|
|
11474
|
+
searchInput.value = "";
|
|
11475
|
+
bucketSelect.innerHTML = "";
|
|
11476
|
+
objectStatus.textContent = "";
|
|
11477
|
+
objectList.innerHTML = "";
|
|
11478
|
+
objectsByKey.clear();
|
|
11479
|
+
objectRowsByKey.clear();
|
|
11480
|
+
activeObjectRow = null;
|
|
11481
|
+
moreBtn.hidden = true;
|
|
11482
|
+
previewPane.textContent = "Select an object to preview.";
|
|
11483
|
+
}
|
|
11484
|
+
function getSelection() {
|
|
11485
|
+
return {
|
|
11486
|
+
bucket: currentBucket ?? undefined,
|
|
11487
|
+
...currentMode === "prefix" ? { prefix: currentSearch || undefined } : { query: currentSearch || undefined },
|
|
11488
|
+
mode: currentMode,
|
|
11489
|
+
sort: currentSort,
|
|
11490
|
+
key: currentKey ?? undefined
|
|
11491
|
+
};
|
|
11492
|
+
}
|
|
11493
|
+
function dispose() {
|
|
11494
|
+
disposed = true;
|
|
11495
|
+
clear();
|
|
11496
|
+
}
|
|
11497
|
+
setMode("prefix");
|
|
11498
|
+
return { el: container, load, clear, dispose, getSelection };
|
|
11499
|
+
}
|
|
11500
|
+
|
|
10707
11501
|
// web-src/views/database/schema-view.ts
|
|
10708
11502
|
function createSchemaView() {
|
|
10709
11503
|
const el = document.createElement("div");
|
|
@@ -11546,6 +12340,7 @@ ${frontmatter.yaml}
|
|
|
11546
12340
|
let pageCache = new Map;
|
|
11547
12341
|
let pendingPages = new Map;
|
|
11548
12342
|
let loadGeneration = 0;
|
|
12343
|
+
let loadController = new AbortController;
|
|
11549
12344
|
let rafId = 0;
|
|
11550
12345
|
let statusEl = null;
|
|
11551
12346
|
let filterTimer = null;
|
|
@@ -11646,10 +12441,18 @@ ${frontmatter.yaml}
|
|
|
11646
12441
|
detailPanel.hidden = true;
|
|
11647
12442
|
detailPanel.innerHTML = "";
|
|
11648
12443
|
}
|
|
12444
|
+
function startNewLoadGeneration() {
|
|
12445
|
+
loadController.abort("db-grid:generation");
|
|
12446
|
+
loadController = new AbortController;
|
|
12447
|
+
loadGeneration++;
|
|
12448
|
+
}
|
|
12449
|
+
function isAbortError(err) {
|
|
12450
|
+
return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && err.name === "AbortError";
|
|
12451
|
+
}
|
|
11649
12452
|
function invalidateData() {
|
|
11650
12453
|
pageCache = new Map;
|
|
11651
12454
|
pendingPages = new Map;
|
|
11652
|
-
|
|
12455
|
+
startNewLoadGeneration();
|
|
11653
12456
|
viewport.scrollTop = 0;
|
|
11654
12457
|
resetSelectionAndDetail();
|
|
11655
12458
|
return ensurePage(0);
|
|
@@ -11668,7 +12471,7 @@ ${frontmatter.yaml}
|
|
|
11668
12471
|
filterRow.innerHTML = "";
|
|
11669
12472
|
pageCache = new Map;
|
|
11670
12473
|
pendingPages = new Map;
|
|
11671
|
-
|
|
12474
|
+
startNewLoadGeneration();
|
|
11672
12475
|
cancelAnimationFrame(rafId);
|
|
11673
12476
|
if (filterTimer)
|
|
11674
12477
|
clearTimeout(filterTimer);
|
|
@@ -11881,6 +12684,7 @@ ${frontmatter.yaml}
|
|
|
11881
12684
|
}
|
|
11882
12685
|
pageCache = new Map;
|
|
11883
12686
|
pendingPages = new Map;
|
|
12687
|
+
startNewLoadGeneration();
|
|
11884
12688
|
resetSelectionAndDetail();
|
|
11885
12689
|
renderHeader();
|
|
11886
12690
|
renderViewport();
|
|
@@ -11892,8 +12696,9 @@ ${frontmatter.yaml}
|
|
|
11892
12696
|
if (pending)
|
|
11893
12697
|
return pending;
|
|
11894
12698
|
const gen = loadGeneration;
|
|
12699
|
+
const signal = loadController.signal;
|
|
11895
12700
|
const filters = collectFilters();
|
|
11896
|
-
const promise = callbacks.fetchPage(currentTable, pageStart, PAGE_SIZE, sort, filters).then((data) => {
|
|
12701
|
+
const promise = callbacks.fetchPage(currentTable, pageStart, PAGE_SIZE, sort, filters, signal).then((data) => {
|
|
11897
12702
|
if (gen !== loadGeneration)
|
|
11898
12703
|
return;
|
|
11899
12704
|
pageCache.set(pageStart, data.rows);
|
|
@@ -11902,6 +12707,8 @@ ${frontmatter.yaml}
|
|
|
11902
12707
|
updateStatus();
|
|
11903
12708
|
renderViewport();
|
|
11904
12709
|
}).catch((err) => {
|
|
12710
|
+
if (gen !== loadGeneration || isAbortError(err))
|
|
12711
|
+
return;
|
|
11905
12712
|
if (gen === loadGeneration) {
|
|
11906
12713
|
showError(err instanceof Error ? err.message : String(err));
|
|
11907
12714
|
}
|
|
@@ -12429,6 +13236,9 @@ ${frontmatter.yaml}
|
|
|
12429
13236
|
function errorMessage(err) {
|
|
12430
13237
|
return err instanceof Error ? err.message : String(err);
|
|
12431
13238
|
}
|
|
13239
|
+
function isAbortError(err) {
|
|
13240
|
+
return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && err.name === "AbortError";
|
|
13241
|
+
}
|
|
12432
13242
|
function isSqlKind(kind) {
|
|
12433
13243
|
return kind === "sqlite" || kind === "postgresql" || kind === "mysql";
|
|
12434
13244
|
}
|
|
@@ -12438,6 +13248,8 @@ ${frontmatter.yaml}
|
|
|
12438
13248
|
function isSqlView(view) {
|
|
12439
13249
|
return view === "data" || view === "query" || view === "schema" || view === "er" || view === "search" || view === "snapshot";
|
|
12440
13250
|
}
|
|
13251
|
+
var HISTORY_SSE_REFRESH_DELAY_MS = 250;
|
|
13252
|
+
var TABLE_SELECT_FETCH_DELAY_MS = 50;
|
|
12441
13253
|
function computeVisibility(kind, tab, userPrefersHistoryOpen) {
|
|
12442
13254
|
const sqlMode = isSqlKind(kind);
|
|
12443
13255
|
const tableScopedTab = tab === "data" || tab === "schema";
|
|
@@ -12456,7 +13268,8 @@ ${frontmatter.yaml}
|
|
|
12456
13268
|
searchHidden: !sqlMode || tab !== "search",
|
|
12457
13269
|
snapshotHidden: !sqlMode || tab !== "snapshot",
|
|
12458
13270
|
redisHidden: kind !== "redis",
|
|
12459
|
-
esHidden: kind !== "elasticsearch"
|
|
13271
|
+
esHidden: kind !== "elasticsearch",
|
|
13272
|
+
s3Hidden: kind !== "s3"
|
|
12460
13273
|
};
|
|
12461
13274
|
}
|
|
12462
13275
|
function normalizeViewForDb(view, db) {
|
|
@@ -12491,9 +13304,11 @@ ${frontmatter.yaml}
|
|
|
12491
13304
|
let currentSchema = initial.schema ?? null;
|
|
12492
13305
|
let currentTable = initial.table ?? null;
|
|
12493
13306
|
let loadGeneration = 0;
|
|
13307
|
+
const tableSelectGuard = createAbortGuard();
|
|
13308
|
+
let historyRefreshPending = null;
|
|
12494
13309
|
const dbSelect = document.createElement("select");
|
|
12495
13310
|
dbSelect.className = "db-file-select";
|
|
12496
|
-
dbSelect.title = "Select
|
|
13311
|
+
dbSelect.title = "Select datastore";
|
|
12497
13312
|
const schemaSelect = document.createElement("select");
|
|
12498
13313
|
schemaSelect.className = "db-file-select db-schema-select";
|
|
12499
13314
|
schemaSelect.title = "Select PostgreSQL schema";
|
|
@@ -12506,7 +13321,7 @@ ${frontmatter.yaml}
|
|
|
12506
13321
|
const tabData = createInnerTab("Data", true);
|
|
12507
13322
|
const tabSchema = createInnerTab("Schema", false);
|
|
12508
13323
|
tabBar.append(tabData, tabSchema);
|
|
12509
|
-
let currentTab = "data";
|
|
13324
|
+
let currentTab = initial.view ?? "data";
|
|
12510
13325
|
const tableList = createTableList({
|
|
12511
13326
|
onSelectTable: (table2) => selectTable(table2),
|
|
12512
13327
|
onViewCreateTable: (table2) => showDdl(table2),
|
|
@@ -12520,7 +13335,7 @@ ${frontmatter.yaml}
|
|
|
12520
13335
|
const toolsSection = document.createElement("div");
|
|
12521
13336
|
toolsSection.className = "db-icon-toolbar";
|
|
12522
13337
|
toolsSection.setAttribute("role", "toolbar");
|
|
12523
|
-
toolsSection.setAttribute("aria-label", "
|
|
13338
|
+
toolsSection.setAttribute("aria-label", "Datastore tools");
|
|
12524
13339
|
const queryBtn = makeIconButton({
|
|
12525
13340
|
label: "Query",
|
|
12526
13341
|
title: "Query Editor",
|
|
@@ -12579,7 +13394,7 @@ ${frontmatter.yaml}
|
|
|
12579
13394
|
window.addEventListener("mouseup", onUp);
|
|
12580
13395
|
});
|
|
12581
13396
|
const grid = createTableGrid({
|
|
12582
|
-
fetchPage: (table2, offset, limit, sort, filters) => fetchTablePage(table2, offset, limit, sort, filters),
|
|
13397
|
+
fetchPage: (table2, offset, limit, sort, filters, signal) => fetchTablePage(table2, offset, limit, sort, filters, signal),
|
|
12583
13398
|
getDbId: () => currentDbInfo?.id || null
|
|
12584
13399
|
});
|
|
12585
13400
|
const queryEditor = createQueryEditor({
|
|
@@ -12615,9 +13430,13 @@ ${frontmatter.yaml}
|
|
|
12615
13430
|
onSelectionChange: () => cb.onStateChange()
|
|
12616
13431
|
});
|
|
12617
13432
|
esExplorer.el.hidden = true;
|
|
13433
|
+
const s3Explorer = createS3Explorer({
|
|
13434
|
+
onSelectionChange: () => cb.onStateChange()
|
|
13435
|
+
});
|
|
13436
|
+
s3Explorer.el.hidden = true;
|
|
12618
13437
|
const mainContent = document.createElement("div");
|
|
12619
13438
|
mainContent.className = "db-main-content";
|
|
12620
|
-
mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el, redisExplorer.el, esExplorer.el);
|
|
13439
|
+
mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el, redisExplorer.el, esExplorer.el, s3Explorer.el);
|
|
12621
13440
|
queryEditor.el.hidden = true;
|
|
12622
13441
|
globalSearchView.el.hidden = true;
|
|
12623
13442
|
snapshotView.el.hidden = true;
|
|
@@ -12679,6 +13498,7 @@ ${frontmatter.yaml}
|
|
|
12679
13498
|
snapshotView.el.hidden = visibility.snapshotHidden;
|
|
12680
13499
|
redisExplorer.el.hidden = visibility.redisHidden;
|
|
12681
13500
|
esExplorer.el.hidden = visibility.esHidden;
|
|
13501
|
+
s3Explorer.el.hidden = visibility.s3Hidden;
|
|
12682
13502
|
if (!sqlMode) {
|
|
12683
13503
|
queryBtn.classList.remove("active");
|
|
12684
13504
|
erBtn.classList.remove("active");
|
|
@@ -12686,7 +13506,7 @@ ${frontmatter.yaml}
|
|
|
12686
13506
|
snapshotBtn.classList.remove("active");
|
|
12687
13507
|
}
|
|
12688
13508
|
historyToggle.classList.toggle("active", userPrefersHistoryOpen);
|
|
12689
|
-
if (!visibility.historyPaneHidden)
|
|
13509
|
+
if (!visibility.historyPaneHidden && cb.isActive())
|
|
12690
13510
|
historyView.refresh();
|
|
12691
13511
|
}
|
|
12692
13512
|
applyVisibility();
|
|
@@ -12723,6 +13543,33 @@ ${frontmatter.yaml}
|
|
|
12723
13543
|
if (list2)
|
|
12724
13544
|
setPaneStatus(list2, message, options);
|
|
12725
13545
|
}
|
|
13546
|
+
function waitForTableSelectFetch(signal) {
|
|
13547
|
+
if (signal.aborted) {
|
|
13548
|
+
return Promise.reject(new DOMException("aborted", "AbortError"));
|
|
13549
|
+
}
|
|
13550
|
+
return new Promise((resolve, reject) => {
|
|
13551
|
+
let done = false;
|
|
13552
|
+
const cleanup = () => {
|
|
13553
|
+
signal.removeEventListener("abort", onAbort);
|
|
13554
|
+
};
|
|
13555
|
+
const timer = setTimeout(() => {
|
|
13556
|
+
if (done)
|
|
13557
|
+
return;
|
|
13558
|
+
done = true;
|
|
13559
|
+
cleanup();
|
|
13560
|
+
resolve();
|
|
13561
|
+
}, TABLE_SELECT_FETCH_DELAY_MS);
|
|
13562
|
+
const onAbort = () => {
|
|
13563
|
+
if (done)
|
|
13564
|
+
return;
|
|
13565
|
+
done = true;
|
|
13566
|
+
clearTimeout(timer);
|
|
13567
|
+
cleanup();
|
|
13568
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
13569
|
+
};
|
|
13570
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
13571
|
+
});
|
|
13572
|
+
}
|
|
12726
13573
|
function setActiveTab(tab, updateUrl = true) {
|
|
12727
13574
|
currentTab = normalizeViewForDb(tab, currentDbInfo);
|
|
12728
13575
|
const tableScopedTab = currentTab === "data" || currentTab === "schema";
|
|
@@ -12758,6 +13605,8 @@ ${frontmatter.yaml}
|
|
|
12758
13605
|
showSchema(active.dataset.table);
|
|
12759
13606
|
});
|
|
12760
13607
|
async function fetchDbFiles() {
|
|
13608
|
+
if (deps.fetchDbFiles)
|
|
13609
|
+
return deps.fetchDbFiles();
|
|
12761
13610
|
const res = await deps.trackLoad(fetch("/_db/files"));
|
|
12762
13611
|
if (!res.ok)
|
|
12763
13612
|
return { files: [] };
|
|
@@ -12803,7 +13652,7 @@ ${frontmatter.yaml}
|
|
|
12803
13652
|
}
|
|
12804
13653
|
return await res.json();
|
|
12805
13654
|
}
|
|
12806
|
-
async function fetchTablePage(table2, offset, limit, sort, filters) {
|
|
13655
|
+
async function fetchTablePage(table2, offset, limit, sort, filters, signal) {
|
|
12807
13656
|
if (!currentDbInfo)
|
|
12808
13657
|
throw new Error("no database selected");
|
|
12809
13658
|
const params = new URLSearchParams({
|
|
@@ -12820,7 +13669,7 @@ ${frontmatter.yaml}
|
|
|
12820
13669
|
if (filters.length > 0) {
|
|
12821
13670
|
params.set("filters", JSON.stringify(filters));
|
|
12822
13671
|
}
|
|
12823
|
-
const res = await fetch(`/_db/table?${params}
|
|
13672
|
+
const res = await fetch(`/_db/table?${params}`, signal ? { signal } : undefined);
|
|
12824
13673
|
if (!res.ok) {
|
|
12825
13674
|
throw new Error(await responseErrorMessage(res, "failed to fetch table"));
|
|
12826
13675
|
}
|
|
@@ -12848,15 +13697,14 @@ ${frontmatter.yaml}
|
|
|
12848
13697
|
throw new Error(await responseErrorMessage(res, "failed to execute query"));
|
|
12849
13698
|
}
|
|
12850
13699
|
const result = await res.json();
|
|
12851
|
-
if (userPrefersHistoryOpen)
|
|
12852
|
-
historyView.refresh();
|
|
12853
13700
|
return result;
|
|
12854
13701
|
}
|
|
12855
|
-
async function selectDb(dbId, explorerInitial, generation = loadGeneration, preferredSchema, preferredTable) {
|
|
13702
|
+
async function selectDb(dbId, explorerInitial, generation = loadGeneration, preferredSchema, preferredTable, targetView) {
|
|
12856
13703
|
if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
|
|
12857
13704
|
return;
|
|
13705
|
+
tableSelectGuard.dispose();
|
|
12858
13706
|
currentTable = null;
|
|
12859
|
-
if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch") {
|
|
13707
|
+
if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch" || currentDbInfo?.kind === "s3") {
|
|
12860
13708
|
currentSchema = null;
|
|
12861
13709
|
renderSchemaOptions([], null);
|
|
12862
13710
|
currentTab = "data";
|
|
@@ -12868,10 +13716,16 @@ ${frontmatter.yaml}
|
|
|
12868
13716
|
applyVisibility();
|
|
12869
13717
|
if (currentDbInfo.kind === "redis") {
|
|
12870
13718
|
esExplorer.clear();
|
|
13719
|
+
s3Explorer.clear();
|
|
12871
13720
|
await redisExplorer.load(dbId, explorerInitial?.redis);
|
|
12872
|
-
} else {
|
|
13721
|
+
} else if (currentDbInfo.kind === "elasticsearch") {
|
|
12873
13722
|
redisExplorer.clear();
|
|
13723
|
+
s3Explorer.clear();
|
|
12874
13724
|
await esExplorer.load(dbId, explorerInitial?.es);
|
|
13725
|
+
} else {
|
|
13726
|
+
redisExplorer.clear();
|
|
13727
|
+
esExplorer.clear();
|
|
13728
|
+
await s3Explorer.load(dbId, explorerInitial?.s3);
|
|
12875
13729
|
}
|
|
12876
13730
|
if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
|
|
12877
13731
|
return;
|
|
@@ -12880,6 +13734,7 @@ ${frontmatter.yaml}
|
|
|
12880
13734
|
}
|
|
12881
13735
|
redisExplorer.clear();
|
|
12882
13736
|
esExplorer.clear();
|
|
13737
|
+
s3Explorer.clear();
|
|
12883
13738
|
applyVisibility();
|
|
12884
13739
|
tableList.render([]);
|
|
12885
13740
|
setTableListStatus("Loading schema...");
|
|
@@ -12936,10 +13791,13 @@ ${frontmatter.yaml}
|
|
|
12936
13791
|
grid.clear();
|
|
12937
13792
|
schemaView.clear();
|
|
12938
13793
|
erDiagram.clear();
|
|
12939
|
-
|
|
13794
|
+
const normalizedTargetView = normalizeViewForDb(targetView, currentDbInfo);
|
|
13795
|
+
setActiveTab(normalizedTargetView === "schema" ? "schema" : "data", false);
|
|
12940
13796
|
const initialTable = preferredTable || schema.tables[0]?.name;
|
|
12941
|
-
if (initialTable) {
|
|
13797
|
+
if (initialTable && normalizedTargetView === "data") {
|
|
12942
13798
|
await selectTable(initialTable, generation);
|
|
13799
|
+
} else if (initialTable && normalizedTargetView === "schema") {
|
|
13800
|
+
await selectTableSchemaOnly(initialTable, generation);
|
|
12943
13801
|
}
|
|
12944
13802
|
applyVisibility();
|
|
12945
13803
|
cb.onStateChange();
|
|
@@ -12947,6 +13805,7 @@ ${frontmatter.yaml}
|
|
|
12947
13805
|
async function selectTable(table2, generation = loadGeneration) {
|
|
12948
13806
|
if (generation !== loadGeneration)
|
|
12949
13807
|
return;
|
|
13808
|
+
const slot = tableSelectGuard.start();
|
|
12950
13809
|
currentTable = table2;
|
|
12951
13810
|
tableList.setActive(table2);
|
|
12952
13811
|
if (!currentDbInfo)
|
|
@@ -12964,16 +13823,22 @@ ${frontmatter.yaml}
|
|
|
12964
13823
|
range: deps.currentRange()
|
|
12965
13824
|
}, true);
|
|
12966
13825
|
try {
|
|
12967
|
-
|
|
12968
|
-
if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
|
|
13826
|
+
await waitForTableSelectFetch(slot.signal);
|
|
13827
|
+
if (slot.isStale() || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
|
|
13828
|
+
return;
|
|
13829
|
+
}
|
|
13830
|
+
const data = await deps.trackLoad(fetchTablePage(table2, 0, 200, null, [], slot.signal));
|
|
13831
|
+
if (slot.isStale() || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
|
|
12969
13832
|
return;
|
|
12970
13833
|
}
|
|
12971
13834
|
grid.load(table2, data);
|
|
12972
13835
|
} catch (err) {
|
|
12973
|
-
if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
|
|
13836
|
+
if (slot.isStale() || isAbortError(err) || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
|
|
12974
13837
|
return;
|
|
12975
13838
|
}
|
|
12976
13839
|
grid.showError(errorMessage(err));
|
|
13840
|
+
} finally {
|
|
13841
|
+
slot.finish();
|
|
12977
13842
|
}
|
|
12978
13843
|
if (currentTab === "schema") {
|
|
12979
13844
|
const columns = await fetchColumns(table2);
|
|
@@ -12983,6 +13848,21 @@ ${frontmatter.yaml}
|
|
|
12983
13848
|
schemaView.render(table2, columns, schemaCache?.indexes || []);
|
|
12984
13849
|
}
|
|
12985
13850
|
}
|
|
13851
|
+
async function selectTableSchemaOnly(table2, generation = loadGeneration) {
|
|
13852
|
+
if (generation !== loadGeneration)
|
|
13853
|
+
return;
|
|
13854
|
+
currentTable = table2;
|
|
13855
|
+
tableList.setActive(table2);
|
|
13856
|
+
if (!currentDbInfo)
|
|
13857
|
+
return;
|
|
13858
|
+
const requestDbId = currentDbInfo.id;
|
|
13859
|
+
setActiveTab("schema", false);
|
|
13860
|
+
const columns = await fetchColumns(table2);
|
|
13861
|
+
if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
|
|
13862
|
+
return;
|
|
13863
|
+
}
|
|
13864
|
+
schemaView.render(table2, columns, schemaCache?.indexes || []);
|
|
13865
|
+
}
|
|
12986
13866
|
async function fetchColumns(table2) {
|
|
12987
13867
|
if (schemaCache?.columnsMap?.[table2]) {
|
|
12988
13868
|
return schemaCache.columnsMap[table2];
|
|
@@ -13098,6 +13978,7 @@ ${frontmatter.yaml}
|
|
|
13098
13978
|
}
|
|
13099
13979
|
let pendingRedisInitial = initial.redis;
|
|
13100
13980
|
let pendingEsInitial = initial.es;
|
|
13981
|
+
let pendingS3Initial = initial.s3;
|
|
13101
13982
|
async function enter(db, schema, table2, view, options = {}) {
|
|
13102
13983
|
const generation = ++loadGeneration;
|
|
13103
13984
|
const filesResponse = await fetchDbFiles();
|
|
@@ -13114,21 +13995,23 @@ ${frontmatter.yaml}
|
|
|
13114
13995
|
if (files.length === 0) {
|
|
13115
13996
|
const opt = document.createElement("option");
|
|
13116
13997
|
opt.value = "";
|
|
13117
|
-
opt.textContent = "No
|
|
13998
|
+
opt.textContent = "No datastores found";
|
|
13118
13999
|
dbSelect.appendChild(opt);
|
|
13119
14000
|
dbSelect.disabled = true;
|
|
13120
14001
|
cb.onStateChange();
|
|
13121
14002
|
return;
|
|
13122
14003
|
}
|
|
13123
14004
|
dbSelect.disabled = false;
|
|
14005
|
+
const optionsFragment = document.createDocumentFragment();
|
|
13124
14006
|
for (const f2 of files) {
|
|
13125
14007
|
const opt = document.createElement("option");
|
|
13126
14008
|
opt.value = f2.id;
|
|
13127
14009
|
const isDocker = f2.id.startsWith("docker:");
|
|
13128
14010
|
const label = isDocker ? `${f2.name} (Docker)` : `${f2.path} (${formatSize(f2.sizeBytes)})`;
|
|
13129
14011
|
opt.textContent = label;
|
|
13130
|
-
|
|
14012
|
+
optionsFragment.appendChild(opt);
|
|
13131
14013
|
}
|
|
14014
|
+
dbSelect.appendChild(optionsFragment);
|
|
13132
14015
|
const autoSelectFirst = options.autoSelectFirst ?? true;
|
|
13133
14016
|
if (db && !files.find((f2) => f2.id === db)) {
|
|
13134
14017
|
db = autoSelectFirst ? files[0].id : null;
|
|
@@ -13146,6 +14029,7 @@ ${frontmatter.yaml}
|
|
|
13146
14029
|
erDiagram.clear();
|
|
13147
14030
|
redisExplorer.clear();
|
|
13148
14031
|
esExplorer.clear();
|
|
14032
|
+
s3Explorer.clear();
|
|
13149
14033
|
setActiveTab("data", false);
|
|
13150
14034
|
cb.onStateChange();
|
|
13151
14035
|
return;
|
|
@@ -13155,14 +14039,16 @@ ${frontmatter.yaml}
|
|
|
13155
14039
|
currentDbInfo = files.find((f2) => f2.id === target) || null;
|
|
13156
14040
|
const explorerInitial = {
|
|
13157
14041
|
redis: pendingRedisInitial,
|
|
13158
|
-
es: pendingEsInitial
|
|
14042
|
+
es: pendingEsInitial,
|
|
14043
|
+
s3: pendingS3Initial
|
|
13159
14044
|
};
|
|
13160
14045
|
pendingRedisInitial = undefined;
|
|
13161
14046
|
pendingEsInitial = undefined;
|
|
13162
|
-
|
|
14047
|
+
pendingS3Initial = undefined;
|
|
14048
|
+
await selectDb(target, explorerInitial, generation, schema !== undefined ? schema : currentSchema, table2, view);
|
|
13163
14049
|
if (generation !== loadGeneration || currentDbInfo?.id !== target)
|
|
13164
14050
|
return;
|
|
13165
|
-
if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch") {
|
|
14051
|
+
if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch" || currentDbInfo?.kind === "s3") {
|
|
13166
14052
|
cb.onStateChange();
|
|
13167
14053
|
return;
|
|
13168
14054
|
}
|
|
@@ -13219,20 +14105,28 @@ ${frontmatter.yaml}
|
|
|
13219
14105
|
}
|
|
13220
14106
|
}
|
|
13221
14107
|
function handleSse(event, data) {
|
|
13222
|
-
if (userPrefersHistoryOpen)
|
|
13223
|
-
|
|
14108
|
+
if (userPrefersHistoryOpen && cb.isActive()) {
|
|
14109
|
+
if (historyRefreshPending !== null)
|
|
14110
|
+
clearTimeout(historyRefreshPending);
|
|
14111
|
+
historyRefreshPending = setTimeout(() => {
|
|
14112
|
+
historyRefreshPending = null;
|
|
14113
|
+
if (userPrefersHistoryOpen && cb.isActive()) {
|
|
14114
|
+
historyView.refresh({ force: true });
|
|
14115
|
+
}
|
|
14116
|
+
}, HISTORY_SSE_REFRESH_DELAY_MS);
|
|
14117
|
+
}
|
|
13224
14118
|
if (event === "db-snapshot" && data) {
|
|
13225
14119
|
snapshotView.handleSse(data);
|
|
13226
14120
|
}
|
|
13227
14121
|
}
|
|
13228
14122
|
function getState() {
|
|
13229
|
-
const
|
|
14123
|
+
const loaded = !!currentDbInfo;
|
|
13230
14124
|
const state = {
|
|
13231
14125
|
id: cb.tabId,
|
|
13232
|
-
dbId: currentDbInfo?.id ?? null,
|
|
13233
|
-
schema: currentSchema,
|
|
13234
|
-
table: currentTable
|
|
13235
|
-
view: currentTab
|
|
14126
|
+
dbId: currentDbInfo?.id ?? initial.dbId ?? null,
|
|
14127
|
+
schema: loaded ? currentSchema : initial.schema ?? currentSchema,
|
|
14128
|
+
table: loaded ? currentTable : initial.table ?? currentTable ?? null,
|
|
14129
|
+
view: loaded ? currentTab : initial.view ?? currentTab
|
|
13236
14130
|
};
|
|
13237
14131
|
const sqlDraft = queryEditor.getSql();
|
|
13238
14132
|
if (sqlDraft)
|
|
@@ -13243,7 +14137,14 @@ ${frontmatter.yaml}
|
|
|
13243
14137
|
state.historyHeight = historyPane.style.height;
|
|
13244
14138
|
if (sidebar.style.width)
|
|
13245
14139
|
state.sidebarWidth = sidebar.style.width;
|
|
13246
|
-
if (
|
|
14140
|
+
if (!loaded) {
|
|
14141
|
+
if (initial.redis)
|
|
14142
|
+
state.redis = initial.redis;
|
|
14143
|
+
if (initial.es)
|
|
14144
|
+
state.es = initial.es;
|
|
14145
|
+
if (initial.s3)
|
|
14146
|
+
state.s3 = initial.s3;
|
|
14147
|
+
} else if (currentDbInfo?.kind === "redis") {
|
|
13247
14148
|
const sel = redisExplorer.getSelection();
|
|
13248
14149
|
if (sel.dbIndex !== undefined || sel.key !== undefined || sel.keyFilter !== undefined) {
|
|
13249
14150
|
state.redis = sel;
|
|
@@ -13253,6 +14154,11 @@ ${frontmatter.yaml}
|
|
|
13253
14154
|
if (sel.index !== undefined || sel.query !== undefined) {
|
|
13254
14155
|
state.es = sel;
|
|
13255
14156
|
}
|
|
14157
|
+
} else if (currentDbInfo?.kind === "s3") {
|
|
14158
|
+
const sel = s3Explorer.getSelection();
|
|
14159
|
+
if (sel.bucket !== undefined || sel.prefix !== undefined || sel.query !== undefined || sel.key !== undefined || sel.mode !== "prefix" || sel.sort !== "updated-desc") {
|
|
14160
|
+
state.s3 = sel;
|
|
14161
|
+
}
|
|
13256
14162
|
}
|
|
13257
14163
|
return state;
|
|
13258
14164
|
}
|
|
@@ -13290,6 +14196,11 @@ ${frontmatter.yaml}
|
|
|
13290
14196
|
}
|
|
13291
14197
|
function dispose() {
|
|
13292
14198
|
loadGeneration++;
|
|
14199
|
+
tableSelectGuard.dispose();
|
|
14200
|
+
if (historyRefreshPending !== null) {
|
|
14201
|
+
clearTimeout(historyRefreshPending);
|
|
14202
|
+
historyRefreshPending = null;
|
|
14203
|
+
}
|
|
13293
14204
|
grid.destroy();
|
|
13294
14205
|
schemaView.clear();
|
|
13295
14206
|
erDiagram.dispose();
|
|
@@ -13300,6 +14211,7 @@ ${frontmatter.yaml}
|
|
|
13300
14211
|
historyView.clear();
|
|
13301
14212
|
redisExplorer.dispose();
|
|
13302
14213
|
esExplorer.dispose();
|
|
14214
|
+
s3Explorer.dispose();
|
|
13303
14215
|
currentDbInfo = null;
|
|
13304
14216
|
currentSchema = null;
|
|
13305
14217
|
currentTable = null;
|
|
@@ -13343,6 +14255,7 @@ ${frontmatter.yaml}
|
|
|
13343
14255
|
let mounted = false;
|
|
13344
14256
|
const tabsById = new Map;
|
|
13345
14257
|
const paneReadyById = new Map;
|
|
14258
|
+
const lazyInitialById = new Map;
|
|
13346
14259
|
let activeTabId = null;
|
|
13347
14260
|
let draggingTabId = null;
|
|
13348
14261
|
let dropTargetId = null;
|
|
@@ -13350,9 +14263,40 @@ ${frontmatter.yaml}
|
|
|
13350
14263
|
let restoring = false;
|
|
13351
14264
|
let savePending = null;
|
|
13352
14265
|
let saveChain = Promise.resolve();
|
|
14266
|
+
let lastSavedTabsRaw = null;
|
|
14267
|
+
let pendingSavedTabsRaw = null;
|
|
14268
|
+
let saveController = null;
|
|
13353
14269
|
let lifecycleSeq = 0;
|
|
13354
14270
|
let enterQueue = Promise.resolve();
|
|
13355
14271
|
let unloadListenerInstalled = false;
|
|
14272
|
+
let dbFilesCache = null;
|
|
14273
|
+
function fetchDbFilesCached() {
|
|
14274
|
+
const now = Date.now();
|
|
14275
|
+
if (dbFilesCache?.value && dbFilesCache.expiresAt > now) {
|
|
14276
|
+
return Promise.resolve(dbFilesCache.value);
|
|
14277
|
+
}
|
|
14278
|
+
if (dbFilesCache?.promise)
|
|
14279
|
+
return dbFilesCache.promise;
|
|
14280
|
+
const promise = deps.trackLoad(fetch("/_db/files")).then(async (res) => {
|
|
14281
|
+
if (!res.ok) {
|
|
14282
|
+
const value2 = { files: [] };
|
|
14283
|
+
dbFilesCache = { value: value2, expiresAt: Date.now() + 1e4 };
|
|
14284
|
+
return value2;
|
|
14285
|
+
}
|
|
14286
|
+
const value = await res.json();
|
|
14287
|
+
dbFilesCache = { value, expiresAt: Date.now() + 1e4 };
|
|
14288
|
+
return value;
|
|
14289
|
+
}).catch(() => {
|
|
14290
|
+
const value = { files: [] };
|
|
14291
|
+
dbFilesCache = { value, expiresAt: Date.now() + 1e4 };
|
|
14292
|
+
return value;
|
|
14293
|
+
}).finally(() => {
|
|
14294
|
+
if (dbFilesCache?.promise === promise)
|
|
14295
|
+
dbFilesCache = null;
|
|
14296
|
+
});
|
|
14297
|
+
dbFilesCache = { promise };
|
|
14298
|
+
return promise;
|
|
14299
|
+
}
|
|
13356
14300
|
function scheduleSave() {
|
|
13357
14301
|
if (!mounted || restoring)
|
|
13358
14302
|
return;
|
|
@@ -13360,6 +14304,10 @@ ${frontmatter.yaml}
|
|
|
13360
14304
|
clearTimeout(savePending);
|
|
13361
14305
|
savePending = setTimeout(saveNow, 500);
|
|
13362
14306
|
}
|
|
14307
|
+
function abortActiveSave() {
|
|
14308
|
+
saveController?.abort();
|
|
14309
|
+
saveController = null;
|
|
14310
|
+
}
|
|
13363
14311
|
async function saveNow(options = {}) {
|
|
13364
14312
|
savePending = null;
|
|
13365
14313
|
if (!mounted || restoring)
|
|
@@ -13372,7 +14320,33 @@ ${frontmatter.yaml}
|
|
|
13372
14320
|
return;
|
|
13373
14321
|
const body = { version: 1, tabs, activeTabId };
|
|
13374
14322
|
const raw = JSON.stringify(body);
|
|
14323
|
+
if (raw === lastSavedTabsRaw)
|
|
14324
|
+
return;
|
|
14325
|
+
if (!options.keepalive && raw === pendingSavedTabsRaw)
|
|
14326
|
+
return;
|
|
14327
|
+
pendingSavedTabsRaw = raw;
|
|
14328
|
+
abortActiveSave();
|
|
14329
|
+
if (options.keepalive) {
|
|
14330
|
+
try {
|
|
14331
|
+
await fetch("/_db/tabs", {
|
|
14332
|
+
method: "PUT",
|
|
14333
|
+
headers: {
|
|
14334
|
+
"Content-Type": "application/json",
|
|
14335
|
+
"X-Code-Viewer-Action": "1"
|
|
14336
|
+
},
|
|
14337
|
+
body: raw,
|
|
14338
|
+
keepalive: true
|
|
14339
|
+
});
|
|
14340
|
+
lastSavedTabsRaw = raw;
|
|
14341
|
+
} catch {} finally {
|
|
14342
|
+
if (pendingSavedTabsRaw === raw)
|
|
14343
|
+
pendingSavedTabsRaw = null;
|
|
14344
|
+
}
|
|
14345
|
+
return;
|
|
14346
|
+
}
|
|
13375
14347
|
saveChain = saveChain.catch(() => {}).then(async () => {
|
|
14348
|
+
const controller = new AbortController;
|
|
14349
|
+
saveController = controller;
|
|
13376
14350
|
try {
|
|
13377
14351
|
await fetch("/_db/tabs", {
|
|
13378
14352
|
method: "PUT",
|
|
@@ -13381,9 +14355,17 @@ ${frontmatter.yaml}
|
|
|
13381
14355
|
"X-Code-Viewer-Action": "1"
|
|
13382
14356
|
},
|
|
13383
14357
|
body: raw,
|
|
13384
|
-
|
|
14358
|
+
signal: controller.signal
|
|
13385
14359
|
});
|
|
13386
|
-
|
|
14360
|
+
lastSavedTabsRaw = raw;
|
|
14361
|
+
} catch (err) {
|
|
14362
|
+
if (!isAbortError(err)) {}
|
|
14363
|
+
} finally {
|
|
14364
|
+
if (saveController === controller)
|
|
14365
|
+
saveController = null;
|
|
14366
|
+
if (pendingSavedTabsRaw === raw)
|
|
14367
|
+
pendingSavedTabsRaw = null;
|
|
14368
|
+
}
|
|
13387
14369
|
});
|
|
13388
14370
|
await saveChain;
|
|
13389
14371
|
}
|
|
@@ -13436,6 +14418,9 @@ ${frontmatter.yaml}
|
|
|
13436
14418
|
if (!restoring)
|
|
13437
14419
|
syncActiveRoute();
|
|
13438
14420
|
scheduleSave();
|
|
14421
|
+
if (mounted && !restoring) {
|
|
14422
|
+
ensureInitialEnter(id)?.catch(() => {});
|
|
14423
|
+
}
|
|
13439
14424
|
}
|
|
13440
14425
|
function syncActiveRoute() {
|
|
13441
14426
|
if (!activeTabId)
|
|
@@ -13472,7 +14457,8 @@ ${frontmatter.yaml}
|
|
|
13472
14457
|
table: tab.table,
|
|
13473
14458
|
view: tab.view,
|
|
13474
14459
|
redis: tab.redis ?? null,
|
|
13475
|
-
es: tab.es ?? null
|
|
14460
|
+
es: tab.es ?? null,
|
|
14461
|
+
s3: tab.s3 ?? null
|
|
13476
14462
|
});
|
|
13477
14463
|
if (seen.has(key))
|
|
13478
14464
|
continue;
|
|
@@ -13687,7 +14673,7 @@ ${frontmatter.yaml}
|
|
|
13687
14673
|
closeTab(id);
|
|
13688
14674
|
}
|
|
13689
14675
|
});
|
|
13690
|
-
const pane = createTabPane(deps, {
|
|
14676
|
+
const pane = createTabPane({ ...deps, fetchDbFiles: fetchDbFilesCached }, {
|
|
13691
14677
|
tabId: id,
|
|
13692
14678
|
isActive: () => activeTabId === id,
|
|
13693
14679
|
canSyncRoute: () => !restoring,
|
|
@@ -13707,13 +14693,15 @@ ${frontmatter.yaml}
|
|
|
13707
14693
|
historyHeight: initial?.historyHeight,
|
|
13708
14694
|
sidebarWidth: initial?.sidebarWidth,
|
|
13709
14695
|
redis: initial?.redis,
|
|
13710
|
-
es: initial?.es
|
|
14696
|
+
es: initial?.es,
|
|
14697
|
+
s3: initial?.s3
|
|
13711
14698
|
});
|
|
13712
14699
|
pane.el.hidden = true;
|
|
13713
14700
|
tabsList.appendChild(chip);
|
|
13714
14701
|
tabHost.appendChild(pane.el);
|
|
13715
14702
|
tabsById.set(id, { pane, chip, label: labelEl, closeBtn });
|
|
13716
|
-
|
|
14703
|
+
if (options.activate !== false)
|
|
14704
|
+
setActive(id);
|
|
13717
14705
|
if (!options.deferInitialEnter) {
|
|
13718
14706
|
startInitialEnter(id, initial, options);
|
|
13719
14707
|
}
|
|
@@ -13739,6 +14727,16 @@ ${frontmatter.yaml}
|
|
|
13739
14727
|
paneReadyById.set(id, ready);
|
|
13740
14728
|
return ready;
|
|
13741
14729
|
}
|
|
14730
|
+
function ensureInitialEnter(id) {
|
|
14731
|
+
const ready = paneReadyById.get(id);
|
|
14732
|
+
if (ready)
|
|
14733
|
+
return ready;
|
|
14734
|
+
if (!lazyInitialById.has(id))
|
|
14735
|
+
return null;
|
|
14736
|
+
const initial = lazyInitialById.get(id);
|
|
14737
|
+
lazyInitialById.delete(id);
|
|
14738
|
+
return startInitialEnter(id, initial, { autoSelectFirst: false });
|
|
14739
|
+
}
|
|
13742
14740
|
function closeTab(id) {
|
|
13743
14741
|
const entry = tabsById.get(id);
|
|
13744
14742
|
if (!entry)
|
|
@@ -13749,6 +14747,7 @@ ${frontmatter.yaml}
|
|
|
13749
14747
|
entry.chip.remove();
|
|
13750
14748
|
tabsById.delete(id);
|
|
13751
14749
|
paneReadyById.delete(id);
|
|
14750
|
+
lazyInitialById.delete(id);
|
|
13752
14751
|
closeDbIfUnused(closedDbId);
|
|
13753
14752
|
if (activeTabId !== id) {
|
|
13754
14753
|
scheduleSave();
|
|
@@ -13798,6 +14797,11 @@ ${frontmatter.yaml}
|
|
|
13798
14797
|
for (const [id2, entry] of tabsById) {
|
|
13799
14798
|
if (routeMatchesState(entry.pane.getState(), db, schema, table2, view)) {
|
|
13800
14799
|
setActive(id2);
|
|
14800
|
+
const ready2 = ensureInitialEnter(id2);
|
|
14801
|
+
if (ready2)
|
|
14802
|
+
await ready2;
|
|
14803
|
+
if (!mounted)
|
|
14804
|
+
return;
|
|
13801
14805
|
if (options.annotationTarget) {
|
|
13802
14806
|
await enterPane(entry.pane, db, schema, table2, view, options);
|
|
13803
14807
|
if (!mounted)
|
|
@@ -13878,8 +14882,6 @@ ${frontmatter.yaml}
|
|
|
13878
14882
|
return;
|
|
13879
14883
|
if (restored && restored.tabs.length > 0) {
|
|
13880
14884
|
restoring = true;
|
|
13881
|
-
const restoredIds = [];
|
|
13882
|
-
const restoredById = new Map;
|
|
13883
14885
|
try {
|
|
13884
14886
|
const restoredTabs = dedupeTabs(restored.tabs);
|
|
13885
14887
|
for (const t2 of restoredTabs) {
|
|
@@ -13887,28 +14889,21 @@ ${frontmatter.yaml}
|
|
|
13887
14889
|
return;
|
|
13888
14890
|
const id = openTab(t2, {
|
|
13889
14891
|
autoSelectFirst: false,
|
|
13890
|
-
deferInitialEnter: true
|
|
14892
|
+
deferInitialEnter: true,
|
|
14893
|
+
activate: false
|
|
13891
14894
|
});
|
|
13892
|
-
|
|
13893
|
-
restoredById.set(id, t2);
|
|
14895
|
+
lazyInitialById.set(id, t2);
|
|
13894
14896
|
}
|
|
13895
14897
|
const targetId = restored.activeTabId && tabsById.has(restored.activeTabId) ? restored.activeTabId : tabsById.keys().next().value;
|
|
13896
14898
|
if (targetId)
|
|
13897
14899
|
setActive(targetId);
|
|
13898
14900
|
if (targetId) {
|
|
13899
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
14901
|
+
const ready = ensureInitialEnter(targetId);
|
|
14902
|
+
if (ready)
|
|
14903
|
+
await ready;
|
|
13902
14904
|
}
|
|
13903
14905
|
if (!mounted || seq !== lifecycleSeq)
|
|
13904
14906
|
return;
|
|
13905
|
-
for (const id of restoredIds) {
|
|
13906
|
-
if (id === targetId)
|
|
13907
|
-
continue;
|
|
13908
|
-
startInitialEnter(id, restoredById.get(id), {
|
|
13909
|
-
autoSelectFirst: false
|
|
13910
|
-
}).catch(() => {});
|
|
13911
|
-
}
|
|
13912
14907
|
} finally {
|
|
13913
14908
|
restoring = false;
|
|
13914
14909
|
}
|
|
@@ -13977,6 +14972,7 @@ ${frontmatter.yaml}
|
|
|
13977
14972
|
entry.pane.dispose();
|
|
13978
14973
|
}
|
|
13979
14974
|
tabsById.clear();
|
|
14975
|
+
dbFilesCache = null;
|
|
13980
14976
|
tabsList.innerHTML = "";
|
|
13981
14977
|
tabHost.innerHTML = "";
|
|
13982
14978
|
activeTabId = null;
|
|
@@ -15590,12 +16586,12 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
15590
16586
|
]
|
|
15591
16587
|
},
|
|
15592
16588
|
database: {
|
|
15593
|
-
nav: "
|
|
15594
|
-
title: "
|
|
15595
|
-
intro: "Browse SQLite files
|
|
16589
|
+
nav: "Datastores",
|
|
16590
|
+
title: "Datastore Viewer",
|
|
16591
|
+
intro: "Browse SQLite files, Docker-hosted databases, Redis, Elasticsearch, and S3-compatible object stores from one local viewer.",
|
|
15596
16592
|
groups: [
|
|
15597
16593
|
{
|
|
15598
|
-
title: "Supported
|
|
16594
|
+
title: "Supported datastores",
|
|
15599
16595
|
blocks: [
|
|
15600
16596
|
{
|
|
15601
16597
|
kind: "table",
|
|
@@ -15611,6 +16607,10 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
15611
16607
|
[
|
|
15612
16608
|
"PostgreSQL",
|
|
15613
16609
|
"Detected from docker-compose.yml services. Multiple databases per server are listed."
|
|
16610
|
+
],
|
|
16611
|
+
[
|
|
16612
|
+
"S3 / MinIO / LocalStack",
|
|
16613
|
+
"Detected from docker-compose.yml services. Browse buckets, search objects, sort by update time, and preview common file types."
|
|
15614
16614
|
]
|
|
15615
16615
|
]
|
|
15616
16616
|
}
|
|
@@ -15953,12 +16953,12 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
15953
16953
|
]
|
|
15954
16954
|
},
|
|
15955
16955
|
database: {
|
|
15956
|
-
nav: "
|
|
15957
|
-
title: "
|
|
15958
|
-
intro: "SQLite
|
|
16956
|
+
nav: "データストア",
|
|
16957
|
+
title: "データストアビューア",
|
|
16958
|
+
intro: "SQLite ファイル、Docker 上のデータベース、Redis、Elasticsearch、S3 互換オブジェクトストアをローカルビューアで閲覧できます。",
|
|
15959
16959
|
groups: [
|
|
15960
16960
|
{
|
|
15961
|
-
title: "
|
|
16961
|
+
title: "対応データストア",
|
|
15962
16962
|
blocks: [
|
|
15963
16963
|
{
|
|
15964
16964
|
kind: "table",
|
|
@@ -15974,6 +16974,10 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
15974
16974
|
[
|
|
15975
16975
|
"PostgreSQL",
|
|
15976
16976
|
"docker-compose.yml のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
|
|
16977
|
+
],
|
|
16978
|
+
[
|
|
16979
|
+
"S3 / MinIO / LocalStack",
|
|
16980
|
+
"docker-compose.yml のサービスから検出。バケット閲覧、オブジェクト検索、更新日時順表示、主要ファイル形式のプレビューに対応します。"
|
|
15977
16981
|
]
|
|
15978
16982
|
]
|
|
15979
16983
|
}
|
|
@@ -16027,7 +17031,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
16027
17031
|
},
|
|
16028
17032
|
{
|
|
16029
17033
|
kind: "command",
|
|
16030
|
-
title: "
|
|
17034
|
+
title: "データストア一覧",
|
|
16031
17035
|
command: "code-viewer query list"
|
|
16032
17036
|
},
|
|
16033
17037
|
{
|
|
@@ -18838,7 +19842,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
18838
19842
|
setRoute,
|
|
18839
19843
|
currentRange,
|
|
18840
19844
|
appendScopeParams,
|
|
18841
|
-
isAbortError,
|
|
19845
|
+
isAbortError: isAbortError2,
|
|
18842
19846
|
scrollToFile,
|
|
18843
19847
|
applySourceRouteToShell,
|
|
18844
19848
|
fileSourceTarget,
|
|
@@ -19185,7 +20189,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
19185
20189
|
state.status.textContent = response.engine + (state.grepRegex ? " regex" : " plain") + (response.truncated ? " truncated" : "") + " - " + state.items.length + " results";
|
|
19186
20190
|
renderPalette(state);
|
|
19187
20191
|
}).catch((err) => {
|
|
19188
|
-
if (
|
|
20192
|
+
if (isAbortError2(err))
|
|
19189
20193
|
return;
|
|
19190
20194
|
state.status.textContent = "Search failed";
|
|
19191
20195
|
});
|
|
@@ -20646,7 +21650,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
20646
21650
|
setPageMode,
|
|
20647
21651
|
currentRange,
|
|
20648
21652
|
trackLoad,
|
|
20649
|
-
isAbortError,
|
|
21653
|
+
isAbortError: isAbortError2,
|
|
20650
21654
|
loadRepo,
|
|
20651
21655
|
repoRoute,
|
|
20652
21656
|
repoFileTargetFromRoute,
|
|
@@ -20966,16 +21970,6 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
20966
21970
|
}
|
|
20967
21971
|
function renderSourceUnsupported(card, target) {
|
|
20968
21972
|
const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
|
|
20969
|
-
const view = document.createElement("div");
|
|
20970
|
-
view.className = "gdp-source-viewer unsupported";
|
|
20971
|
-
const content = document.createElement("div");
|
|
20972
|
-
content.className = "gdp-source-unsupported-content";
|
|
20973
|
-
const title = document.createElement("strong");
|
|
20974
|
-
title.className = "gdp-source-unsupported-title";
|
|
20975
|
-
title.textContent = "Preview unavailable";
|
|
20976
|
-
const message = document.createElement("div");
|
|
20977
|
-
message.className = "gdp-source-unsupported-message";
|
|
20978
|
-
message.textContent = "This file type cannot be previewed safely in the browser.";
|
|
20979
21973
|
const info = createSourceFileInfo(target, "unsupported file");
|
|
20980
21974
|
const link2 = document.createElement("a");
|
|
20981
21975
|
link2.className = "gdp-btn gdp-btn-sm gdp-source-download";
|
|
@@ -20983,23 +21977,17 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
20983
21977
|
link2.textContent = "Download raw";
|
|
20984
21978
|
link2.target = "_blank";
|
|
20985
21979
|
link2.rel = "noreferrer";
|
|
20986
|
-
|
|
20987
|
-
|
|
21980
|
+
const view = renderUnsupportedPreview({
|
|
21981
|
+
message: "This file type cannot be previewed safely in the browser.",
|
|
21982
|
+
extraChildren: [info, link2]
|
|
21983
|
+
});
|
|
20988
21984
|
if (body)
|
|
20989
21985
|
body.replaceWith(view);
|
|
20990
21986
|
else
|
|
20991
21987
|
card.appendChild(view);
|
|
20992
21988
|
}
|
|
20993
21989
|
function renderHtmlPreview(target, html) {
|
|
20994
|
-
|
|
20995
|
-
preview.className = "gdp-html-preview";
|
|
20996
|
-
const frame = document.createElement("iframe");
|
|
20997
|
-
frame.title = `${target.path} preview`;
|
|
20998
|
-
frame.sandbox.value = "";
|
|
20999
|
-
frame.referrerPolicy = "no-referrer";
|
|
21000
|
-
frame.srcdoc = html;
|
|
21001
|
-
preview.appendChild(frame);
|
|
21002
|
-
return preview;
|
|
21990
|
+
return renderHtmlPreviewFrame(`${target.path} preview`, html);
|
|
21003
21991
|
}
|
|
21004
21992
|
function createSourceFileInfo(target, kind) {
|
|
21005
21993
|
const info = document.createElement("div");
|
|
@@ -21696,7 +22684,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
21696
22684
|
renderedEnd = -1;
|
|
21697
22685
|
render();
|
|
21698
22686
|
}).catch((err) => {
|
|
21699
|
-
if (!
|
|
22687
|
+
if (!isAbortError2(err)) {
|
|
21700
22688
|
failedPages.add(page);
|
|
21701
22689
|
renderedStart = -1;
|
|
21702
22690
|
renderedEnd = -1;
|
|
@@ -21888,35 +22876,16 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
21888
22876
|
const url = buildRawFileUrl(target);
|
|
21889
22877
|
const info = createSourceFileInfo(target, mediaKind);
|
|
21890
22878
|
view.appendChild(info);
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
|
|
21895
|
-
|
|
21896
|
-
view.appendChild(video);
|
|
21897
|
-
} else if (mediaKind === "audio") {
|
|
21898
|
-
const audio = document.createElement("audio");
|
|
21899
|
-
audio.src = url;
|
|
21900
|
-
audio.controls = true;
|
|
21901
|
-
audio.preload = "metadata";
|
|
21902
|
-
view.appendChild(audio);
|
|
21903
|
-
} else if (mediaKind === "pdf") {
|
|
21904
|
-
const frame = document.createElement("iframe");
|
|
21905
|
-
frame.src = url;
|
|
21906
|
-
frame.title = target.path;
|
|
21907
|
-
frame.loading = "lazy";
|
|
21908
|
-
view.appendChild(frame);
|
|
21909
|
-
} else {
|
|
21910
|
-
const img = document.createElement("img");
|
|
21911
|
-
img.src = url;
|
|
21912
|
-
img.alt = "";
|
|
21913
|
-
img.addEventListener("load", () => {
|
|
22879
|
+
appendMediaEmbed(view, {
|
|
22880
|
+
url,
|
|
22881
|
+
kind: mediaKind,
|
|
22882
|
+
title: target.path,
|
|
22883
|
+
onImageLoad: (img) => {
|
|
21914
22884
|
const resolution = document.createElement("span");
|
|
21915
22885
|
resolution.textContent = `${img.naturalWidth} x ${img.naturalHeight}`;
|
|
21916
22886
|
info.appendChild(resolution);
|
|
21917
|
-
}
|
|
21918
|
-
|
|
21919
|
-
}
|
|
22887
|
+
}
|
|
22888
|
+
});
|
|
21920
22889
|
if (body)
|
|
21921
22890
|
body.replaceWith(view);
|
|
21922
22891
|
else
|
|
@@ -22089,7 +23058,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
22089
23058
|
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
22090
23059
|
return;
|
|
22091
23060
|
finishSourceLoad(req);
|
|
22092
|
-
if (
|
|
23061
|
+
if (isAbortError2(err)) {
|
|
22093
23062
|
renderSourceCancelled(card, target);
|
|
22094
23063
|
return;
|
|
22095
23064
|
}
|
|
@@ -22612,7 +23581,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
22612
23581
|
setPageMode,
|
|
22613
23582
|
currentRange,
|
|
22614
23583
|
trackLoad,
|
|
22615
|
-
isAbortError,
|
|
23584
|
+
isAbortError: isAbortError2,
|
|
22616
23585
|
loadRepo: () => REPO_VIEW.loadRepo(),
|
|
22617
23586
|
repoRoute: (ref, path) => REPO_VIEW.repoRoute(ref, path),
|
|
22618
23587
|
repoFileTargetFromRoute,
|
|
@@ -22696,7 +23665,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
22696
23665
|
setRoute,
|
|
22697
23666
|
currentRange,
|
|
22698
23667
|
appendScopeParams,
|
|
22699
|
-
isAbortError,
|
|
23668
|
+
isAbortError: isAbortError2,
|
|
22700
23669
|
scrollToFile: (path, line) => DIFF_VIEW.scrollToFile(path, line),
|
|
22701
23670
|
applySourceRouteToShell,
|
|
22702
23671
|
fileSourceTarget,
|
|
@@ -22712,7 +23681,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
22712
23681
|
repo: "Repository",
|
|
22713
23682
|
diff: "Diff Viewer",
|
|
22714
23683
|
history: "History",
|
|
22715
|
-
database: "
|
|
23684
|
+
database: "Datastores",
|
|
22716
23685
|
help: "Help"
|
|
22717
23686
|
},
|
|
22718
23687
|
global: {
|
|
@@ -22772,7 +23741,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
22772
23741
|
display: "Display",
|
|
22773
23742
|
language: "Language",
|
|
22774
23743
|
fileListFontSize: "UI font size",
|
|
22775
|
-
fileListFontSizeHelp: "Applies to the file sidebar and
|
|
23744
|
+
fileListFontSizeHelp: "Applies to the file sidebar and datastore UI.",
|
|
22776
23745
|
codeFontSize: "Code font size",
|
|
22777
23746
|
sizeSmall: "Small",
|
|
22778
23747
|
sizeRegular: "Regular",
|
|
@@ -22802,7 +23771,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
22802
23771
|
repo: "リポジトリ",
|
|
22803
23772
|
diff: "Diff ビューア",
|
|
22804
23773
|
history: "履歴",
|
|
22805
|
-
database: "
|
|
23774
|
+
database: "データストア",
|
|
22806
23775
|
help: "ヘルプ"
|
|
22807
23776
|
},
|
|
22808
23777
|
global: {
|
|
@@ -22862,7 +23831,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
22862
23831
|
display: "表示",
|
|
22863
23832
|
language: "言語",
|
|
22864
23833
|
fileListFontSize: "UIの文字サイズ",
|
|
22865
|
-
fileListFontSizeHelp: "
|
|
23834
|
+
fileListFontSizeHelp: "ファイル一覧とデータストア画面に適用されます。",
|
|
22866
23835
|
codeFontSize: "コード表示の文字サイズ",
|
|
22867
23836
|
sizeSmall: "小",
|
|
22868
23837
|
sizeRegular: "標準",
|
|
@@ -23279,7 +24248,7 @@ code-viewer annotate add-db --db app.db --tab query \\
|
|
|
23279
24248
|
"'": "'"
|
|
23280
24249
|
})[c2]);
|
|
23281
24250
|
}
|
|
23282
|
-
function
|
|
24251
|
+
function isAbortError2(err) {
|
|
23283
24252
|
return err instanceof DOMException ? err.name === "AbortError" : !!err && typeof err === "object" && ("name" in err) && err.name === "AbortError";
|
|
23284
24253
|
}
|
|
23285
24254
|
function currentRange() {
|