feat: improve db status table browsing

This commit is contained in:
hyunho
2026-04-01 17:06:39 +09:00
parent c3afc0c772
commit 19c8c6ade1
3 changed files with 357 additions and 6 deletions

View File

@@ -110,6 +110,14 @@
.panel-body { .panel-body {
padding: 16px 18px 20px; padding: 16px 18px 20px;
} }
.panel-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.panel-body.tight { .panel-body.tight {
padding-top: 0; padding-top: 0;
} }
@@ -211,6 +219,44 @@
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
} }
.toolbar-input {
width: min(360px, 100%);
padding: 11px 14px;
border-radius: 14px;
border: 1px solid rgba(132, 102, 54, 0.16);
background: rgba(255, 251, 244, 0.96);
color: #2f2419;
font: inherit;
}
.toolbar-input:focus {
outline: none;
border-color: rgba(129, 88, 31, 0.34);
box-shadow: 0 0 0 4px rgba(208, 176, 116, 0.16);
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.action-button {
border: 1px solid rgba(128, 98, 48, 0.14);
background: rgba(250, 240, 213, 0.62);
color: #6c4a1a;
border-radius: 999px;
padding: 9px 14px;
font: inherit;
font-size: 12px;
font-weight: 800;
cursor: pointer;
}
.action-button:hover {
background: rgba(244, 228, 186, 0.8);
}
.action-button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.notes { .notes {
margin: 0; margin: 0;
padding-left: 18px; padding-left: 18px;
@@ -389,6 +435,12 @@
<span id="generated-at" class="meta-chip">로딩 중</span> <span id="generated-at" class="meta-chip">로딩 중</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="panel-toolbar">
<input id="table-search" class="toolbar-input" type="search" placeholder="테이블명, 설명, 화면명으로 검색" />
<div class="toolbar-actions">
<span id="table-count" class="meta-chip">0 / 0</span>
</div>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -499,7 +551,10 @@
<h2 id="preview-title">테이블 내용 미리보기</h2> <h2 id="preview-title">테이블 내용 미리보기</h2>
<p id="preview-subtitle" class="muted">선택한 테이블의 컬럼과 최대 50개 row를 표시합니다.</p> <p id="preview-subtitle" class="muted">선택한 테이블의 컬럼과 최대 50개 row를 표시합니다.</p>
</div> </div>
<button id="preview-close" class="modal-close" type="button" aria-label="닫기">×</button> <div class="toolbar-actions">
<button id="preview-download" class="action-button" type="button" disabled>CSV 다운로드</button>
<button id="preview-close" class="modal-close" type="button" aria-label="닫기">×</button>
</div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="preview-meta" class="preview-meta"></div> <div id="preview-meta" class="preview-meta"></div>
@@ -518,6 +573,9 @@
</div> </div>
<script> <script>
let allTables = [];
let currentPreview = null;
function escapeHtml(value) { function escapeHtml(value) {
return String(value ?? "") return String(value ?? "")
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
@@ -572,6 +630,10 @@
function renderTables(items) { function renderTables(items) {
const target = document.getElementById("table-body"); const target = document.getElementById("table-body");
const countTarget = document.getElementById("table-count");
if (countTarget) {
countTarget.textContent = `${formatNumber(items.length)} / ${formatNumber(allTables.length)}`;
}
if (!items.length) { if (!items.length) {
target.innerHTML = '<tr><td colspan="5" class="empty">표시할 테이블이 없습니다.</td></tr>'; target.innerHTML = '<tr><td colspan="5" class="empty">표시할 테이블이 없습니다.</td></tr>';
return; return;
@@ -699,7 +761,9 @@
} }
function renderTablePreview(payload) { function renderTablePreview(payload) {
currentPreview = payload;
const previewModal = document.getElementById("preview-modal"); const previewModal = document.getElementById("preview-modal");
const downloadButton = document.getElementById("preview-download");
const previewMeta = document.getElementById("preview-meta"); const previewMeta = document.getElementById("preview-meta");
const previewTitle = document.getElementById("preview-title"); const previewTitle = document.getElementById("preview-title");
const previewSubtitle = document.getElementById("preview-subtitle"); const previewSubtitle = document.getElementById("preview-subtitle");
@@ -708,6 +772,9 @@
previewTitle.textContent = `${payload.label} · ${payload.table_ref}`; previewTitle.textContent = `${payload.label} · ${payload.table_ref}`;
previewSubtitle.textContent = `${formatNumber(payload.row_count)} rows / 최대 ${formatNumber(payload.limit)}개 표시`; previewSubtitle.textContent = `${formatNumber(payload.row_count)} rows / 최대 ${formatNumber(payload.limit)}개 표시`;
if (downloadButton) {
downloadButton.disabled = !(payload.rows && payload.rows.length);
}
previewMeta.innerHTML = ` previewMeta.innerHTML = `
<div> <div>
<div class="table-title">${escapeHtml(payload.label)}</div> <div class="table-title">${escapeHtml(payload.label)}</div>
@@ -765,11 +832,12 @@
throw new Error(`DB 상태를 불러오지 못했습니다. (${response.status})`); throw new Error(`DB 상태를 불러오지 못했습니다. (${response.status})`);
} }
const payload = await response.json(); const payload = await response.json();
allTables = payload.tables || [];
document.getElementById("generated-at").textContent = payload.generated_at document.getElementById("generated-at").textContent = payload.generated_at
? `갱신 ${formatDateTime(payload.generated_at)}` ? `갱신 ${formatDateTime(payload.generated_at)}`
: "갱신 시각 없음"; : "갱신 시각 없음";
renderOverview(payload.overview || {}); renderOverview(payload.overview || {});
renderTables(payload.tables || []); renderTables(allTables);
renderBatches(payload.import_batches || []); renderBatches(payload.import_batches || []);
renderBinarySources(payload.binary_sources || []); renderBinarySources(payload.binary_sources || []);
renderNotes(payload.notes || []); renderNotes(payload.notes || []);
@@ -778,12 +846,61 @@
renderScreenMap(payload.screen_map || []); renderScreenMap(payload.screen_map || []);
} }
function toCsvValue(value) {
const text = String(value ?? "");
if (!/[",\n]/.test(text)) return text;
return `"${text.replaceAll('"', '""')}"`;
}
function downloadPreviewCsv() {
if (!currentPreview || !currentPreview.columns || !currentPreview.rows || !currentPreview.rows.length) return;
const headers = currentPreview.columns.map((column) => column.name);
const lines = [
headers.map(toCsvValue).join(","),
...currentPreview.rows.map((row) => headers.map((header) => toCsvValue(row[header] ?? "")).join(",")),
];
const blob = new Blob(["\ufeff" + lines.join("\n")], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${currentPreview.table_name || "table-preview"}.csv`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
function applyTableSearch(query) {
const normalized = String(query || "").trim().toLowerCase();
if (!normalized) {
renderTables(allTables);
return;
}
const filtered = allTables.filter((item) => {
const haystack = [
item.label,
item.table_ref,
item.description,
...(item.related_views || []),
item.domain,
item.group,
].join(" ").toLowerCase();
return haystack.includes(normalized);
});
renderTables(filtered);
}
document.getElementById("preview-close").addEventListener("click", () => { document.getElementById("preview-close").addEventListener("click", () => {
const modal = document.getElementById("preview-modal"); const modal = document.getElementById("preview-modal");
modal.classList.remove("open"); modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true"); modal.setAttribute("aria-hidden", "true");
}); });
document.getElementById("preview-download").addEventListener("click", downloadPreviewCsv);
document.getElementById("table-search").addEventListener("input", (event) => {
applyTableSearch(event.target.value);
});
document.getElementById("preview-modal").addEventListener("click", (event) => { document.getElementById("preview-modal").addEventListener("click", (event) => {
if (event.target.id !== "preview-modal") return; if (event.target.id !== "preview-modal") return;
const modal = document.getElementById("preview-modal"); const modal = document.getElementById("preview-modal");

View File

@@ -110,6 +110,14 @@
.panel-body { .panel-body {
padding: 16px 18px 20px; padding: 16px 18px 20px;
} }
.panel-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.panel-body.tight { .panel-body.tight {
padding-top: 0; padding-top: 0;
} }
@@ -211,6 +219,44 @@
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
} }
.toolbar-input {
width: min(360px, 100%);
padding: 11px 14px;
border-radius: 14px;
border: 1px solid rgba(132, 102, 54, 0.16);
background: rgba(255, 251, 244, 0.96);
color: #2f2419;
font: inherit;
}
.toolbar-input:focus {
outline: none;
border-color: rgba(129, 88, 31, 0.34);
box-shadow: 0 0 0 4px rgba(208, 176, 116, 0.16);
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.action-button {
border: 1px solid rgba(128, 98, 48, 0.14);
background: rgba(250, 240, 213, 0.62);
color: #6c4a1a;
border-radius: 999px;
padding: 9px 14px;
font: inherit;
font-size: 12px;
font-weight: 800;
cursor: pointer;
}
.action-button:hover {
background: rgba(244, 228, 186, 0.8);
}
.action-button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.notes { .notes {
margin: 0; margin: 0;
padding-left: 18px; padding-left: 18px;
@@ -389,6 +435,12 @@
<span id="generated-at" class="meta-chip">로딩 중</span> <span id="generated-at" class="meta-chip">로딩 중</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="panel-toolbar">
<input id="table-search" class="toolbar-input" type="search" placeholder="테이블명, 설명, 화면명으로 검색" />
<div class="toolbar-actions">
<span id="table-count" class="meta-chip">0 / 0</span>
</div>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -499,7 +551,10 @@
<h2 id="preview-title">테이블 내용 미리보기</h2> <h2 id="preview-title">테이블 내용 미리보기</h2>
<p id="preview-subtitle" class="muted">선택한 테이블의 컬럼과 최대 50개 row를 표시합니다.</p> <p id="preview-subtitle" class="muted">선택한 테이블의 컬럼과 최대 50개 row를 표시합니다.</p>
</div> </div>
<button id="preview-close" class="modal-close" type="button" aria-label="닫기">×</button> <div class="toolbar-actions">
<button id="preview-download" class="action-button" type="button" disabled>CSV 다운로드</button>
<button id="preview-close" class="modal-close" type="button" aria-label="닫기">×</button>
</div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="preview-meta" class="preview-meta"></div> <div id="preview-meta" class="preview-meta"></div>
@@ -518,6 +573,9 @@
</div> </div>
<script> <script>
let allTables = [];
let currentPreview = null;
function escapeHtml(value) { function escapeHtml(value) {
return String(value ?? "") return String(value ?? "")
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
@@ -572,6 +630,10 @@
function renderTables(items) { function renderTables(items) {
const target = document.getElementById("table-body"); const target = document.getElementById("table-body");
const countTarget = document.getElementById("table-count");
if (countTarget) {
countTarget.textContent = `${formatNumber(items.length)} / ${formatNumber(allTables.length)}`;
}
if (!items.length) { if (!items.length) {
target.innerHTML = '<tr><td colspan="5" class="empty">표시할 테이블이 없습니다.</td></tr>'; target.innerHTML = '<tr><td colspan="5" class="empty">표시할 테이블이 없습니다.</td></tr>';
return; return;
@@ -699,7 +761,9 @@
} }
function renderTablePreview(payload) { function renderTablePreview(payload) {
currentPreview = payload;
const previewModal = document.getElementById("preview-modal"); const previewModal = document.getElementById("preview-modal");
const downloadButton = document.getElementById("preview-download");
const previewMeta = document.getElementById("preview-meta"); const previewMeta = document.getElementById("preview-meta");
const previewTitle = document.getElementById("preview-title"); const previewTitle = document.getElementById("preview-title");
const previewSubtitle = document.getElementById("preview-subtitle"); const previewSubtitle = document.getElementById("preview-subtitle");
@@ -708,6 +772,9 @@
previewTitle.textContent = `${payload.label} · ${payload.table_ref}`; previewTitle.textContent = `${payload.label} · ${payload.table_ref}`;
previewSubtitle.textContent = `${formatNumber(payload.row_count)} rows / 최대 ${formatNumber(payload.limit)}개 표시`; previewSubtitle.textContent = `${formatNumber(payload.row_count)} rows / 최대 ${formatNumber(payload.limit)}개 표시`;
if (downloadButton) {
downloadButton.disabled = !(payload.rows && payload.rows.length);
}
previewMeta.innerHTML = ` previewMeta.innerHTML = `
<div> <div>
<div class="table-title">${escapeHtml(payload.label)}</div> <div class="table-title">${escapeHtml(payload.label)}</div>
@@ -765,11 +832,12 @@
throw new Error(`DB 상태를 불러오지 못했습니다. (${response.status})`); throw new Error(`DB 상태를 불러오지 못했습니다. (${response.status})`);
} }
const payload = await response.json(); const payload = await response.json();
allTables = payload.tables || [];
document.getElementById("generated-at").textContent = payload.generated_at document.getElementById("generated-at").textContent = payload.generated_at
? `갱신 ${formatDateTime(payload.generated_at)}` ? `갱신 ${formatDateTime(payload.generated_at)}`
: "갱신 시각 없음"; : "갱신 시각 없음";
renderOverview(payload.overview || {}); renderOverview(payload.overview || {});
renderTables(payload.tables || []); renderTables(allTables);
renderBatches(payload.import_batches || []); renderBatches(payload.import_batches || []);
renderBinarySources(payload.binary_sources || []); renderBinarySources(payload.binary_sources || []);
renderNotes(payload.notes || []); renderNotes(payload.notes || []);
@@ -778,12 +846,61 @@
renderScreenMap(payload.screen_map || []); renderScreenMap(payload.screen_map || []);
} }
function toCsvValue(value) {
const text = String(value ?? "");
if (!/[",\n]/.test(text)) return text;
return `"${text.replaceAll('"', '""')}"`;
}
function downloadPreviewCsv() {
if (!currentPreview || !currentPreview.columns || !currentPreview.rows || !currentPreview.rows.length) return;
const headers = currentPreview.columns.map((column) => column.name);
const lines = [
headers.map(toCsvValue).join(","),
...currentPreview.rows.map((row) => headers.map((header) => toCsvValue(row[header] ?? "")).join(",")),
];
const blob = new Blob(["\ufeff" + lines.join("\n")], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${currentPreview.table_name || "table-preview"}.csv`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
function applyTableSearch(query) {
const normalized = String(query || "").trim().toLowerCase();
if (!normalized) {
renderTables(allTables);
return;
}
const filtered = allTables.filter((item) => {
const haystack = [
item.label,
item.table_ref,
item.description,
...(item.related_views || []),
item.domain,
item.group,
].join(" ").toLowerCase();
return haystack.includes(normalized);
});
renderTables(filtered);
}
document.getElementById("preview-close").addEventListener("click", () => { document.getElementById("preview-close").addEventListener("click", () => {
const modal = document.getElementById("preview-modal"); const modal = document.getElementById("preview-modal");
modal.classList.remove("open"); modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true"); modal.setAttribute("aria-hidden", "true");
}); });
document.getElementById("preview-download").addEventListener("click", downloadPreviewCsv);
document.getElementById("table-search").addEventListener("input", (event) => {
applyTableSearch(event.target.value);
});
document.getElementById("preview-modal").addEventListener("click", (event) => { document.getElementById("preview-modal").addEventListener("click", (event) => {
if (event.target.id !== "preview-modal") return; if (event.target.id !== "preview-modal") return;
const modal = document.getElementById("preview-modal"); const modal = document.getElementById("preview-modal");

View File

@@ -110,6 +110,14 @@
.panel-body { .panel-body {
padding: 16px 18px 20px; padding: 16px 18px 20px;
} }
.panel-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.panel-body.tight { .panel-body.tight {
padding-top: 0; padding-top: 0;
} }
@@ -211,6 +219,44 @@
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
} }
.toolbar-input {
width: min(360px, 100%);
padding: 11px 14px;
border-radius: 14px;
border: 1px solid rgba(132, 102, 54, 0.16);
background: rgba(255, 251, 244, 0.96);
color: #2f2419;
font: inherit;
}
.toolbar-input:focus {
outline: none;
border-color: rgba(129, 88, 31, 0.34);
box-shadow: 0 0 0 4px rgba(208, 176, 116, 0.16);
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.action-button {
border: 1px solid rgba(128, 98, 48, 0.14);
background: rgba(250, 240, 213, 0.62);
color: #6c4a1a;
border-radius: 999px;
padding: 9px 14px;
font: inherit;
font-size: 12px;
font-weight: 800;
cursor: pointer;
}
.action-button:hover {
background: rgba(244, 228, 186, 0.8);
}
.action-button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.notes { .notes {
margin: 0; margin: 0;
padding-left: 18px; padding-left: 18px;
@@ -389,6 +435,12 @@
<span id="generated-at" class="meta-chip">로딩 중</span> <span id="generated-at" class="meta-chip">로딩 중</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="panel-toolbar">
<input id="table-search" class="toolbar-input" type="search" placeholder="테이블명, 설명, 화면명으로 검색" />
<div class="toolbar-actions">
<span id="table-count" class="meta-chip">0 / 0</span>
</div>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -499,7 +551,10 @@
<h2 id="preview-title">테이블 내용 미리보기</h2> <h2 id="preview-title">테이블 내용 미리보기</h2>
<p id="preview-subtitle" class="muted">선택한 테이블의 컬럼과 최대 50개 row를 표시합니다.</p> <p id="preview-subtitle" class="muted">선택한 테이블의 컬럼과 최대 50개 row를 표시합니다.</p>
</div> </div>
<button id="preview-close" class="modal-close" type="button" aria-label="닫기">×</button> <div class="toolbar-actions">
<button id="preview-download" class="action-button" type="button" disabled>CSV 다운로드</button>
<button id="preview-close" class="modal-close" type="button" aria-label="닫기">×</button>
</div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="preview-meta" class="preview-meta"></div> <div id="preview-meta" class="preview-meta"></div>
@@ -518,6 +573,9 @@
</div> </div>
<script> <script>
let allTables = [];
let currentPreview = null;
function escapeHtml(value) { function escapeHtml(value) {
return String(value ?? "") return String(value ?? "")
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
@@ -572,6 +630,10 @@
function renderTables(items) { function renderTables(items) {
const target = document.getElementById("table-body"); const target = document.getElementById("table-body");
const countTarget = document.getElementById("table-count");
if (countTarget) {
countTarget.textContent = `${formatNumber(items.length)} / ${formatNumber(allTables.length)}`;
}
if (!items.length) { if (!items.length) {
target.innerHTML = '<tr><td colspan="5" class="empty">표시할 테이블이 없습니다.</td></tr>'; target.innerHTML = '<tr><td colspan="5" class="empty">표시할 테이블이 없습니다.</td></tr>';
return; return;
@@ -699,7 +761,9 @@
} }
function renderTablePreview(payload) { function renderTablePreview(payload) {
currentPreview = payload;
const previewModal = document.getElementById("preview-modal"); const previewModal = document.getElementById("preview-modal");
const downloadButton = document.getElementById("preview-download");
const previewMeta = document.getElementById("preview-meta"); const previewMeta = document.getElementById("preview-meta");
const previewTitle = document.getElementById("preview-title"); const previewTitle = document.getElementById("preview-title");
const previewSubtitle = document.getElementById("preview-subtitle"); const previewSubtitle = document.getElementById("preview-subtitle");
@@ -708,6 +772,9 @@
previewTitle.textContent = `${payload.label} · ${payload.table_ref}`; previewTitle.textContent = `${payload.label} · ${payload.table_ref}`;
previewSubtitle.textContent = `${formatNumber(payload.row_count)} rows / 최대 ${formatNumber(payload.limit)}개 표시`; previewSubtitle.textContent = `${formatNumber(payload.row_count)} rows / 최대 ${formatNumber(payload.limit)}개 표시`;
if (downloadButton) {
downloadButton.disabled = !(payload.rows && payload.rows.length);
}
previewMeta.innerHTML = ` previewMeta.innerHTML = `
<div> <div>
<div class="table-title">${escapeHtml(payload.label)}</div> <div class="table-title">${escapeHtml(payload.label)}</div>
@@ -765,11 +832,12 @@
throw new Error(`DB 상태를 불러오지 못했습니다. (${response.status})`); throw new Error(`DB 상태를 불러오지 못했습니다. (${response.status})`);
} }
const payload = await response.json(); const payload = await response.json();
allTables = payload.tables || [];
document.getElementById("generated-at").textContent = payload.generated_at document.getElementById("generated-at").textContent = payload.generated_at
? `갱신 ${formatDateTime(payload.generated_at)}` ? `갱신 ${formatDateTime(payload.generated_at)}`
: "갱신 시각 없음"; : "갱신 시각 없음";
renderOverview(payload.overview || {}); renderOverview(payload.overview || {});
renderTables(payload.tables || []); renderTables(allTables);
renderBatches(payload.import_batches || []); renderBatches(payload.import_batches || []);
renderBinarySources(payload.binary_sources || []); renderBinarySources(payload.binary_sources || []);
renderNotes(payload.notes || []); renderNotes(payload.notes || []);
@@ -778,12 +846,61 @@
renderScreenMap(payload.screen_map || []); renderScreenMap(payload.screen_map || []);
} }
function toCsvValue(value) {
const text = String(value ?? "");
if (!/[",\n]/.test(text)) return text;
return `"${text.replaceAll('"', '""')}"`;
}
function downloadPreviewCsv() {
if (!currentPreview || !currentPreview.columns || !currentPreview.rows || !currentPreview.rows.length) return;
const headers = currentPreview.columns.map((column) => column.name);
const lines = [
headers.map(toCsvValue).join(","),
...currentPreview.rows.map((row) => headers.map((header) => toCsvValue(row[header] ?? "")).join(",")),
];
const blob = new Blob(["\ufeff" + lines.join("\n")], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${currentPreview.table_name || "table-preview"}.csv`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
function applyTableSearch(query) {
const normalized = String(query || "").trim().toLowerCase();
if (!normalized) {
renderTables(allTables);
return;
}
const filtered = allTables.filter((item) => {
const haystack = [
item.label,
item.table_ref,
item.description,
...(item.related_views || []),
item.domain,
item.group,
].join(" ").toLowerCase();
return haystack.includes(normalized);
});
renderTables(filtered);
}
document.getElementById("preview-close").addEventListener("click", () => { document.getElementById("preview-close").addEventListener("click", () => {
const modal = document.getElementById("preview-modal"); const modal = document.getElementById("preview-modal");
modal.classList.remove("open"); modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true"); modal.setAttribute("aria-hidden", "true");
}); });
document.getElementById("preview-download").addEventListener("click", downloadPreviewCsv);
document.getElementById("table-search").addEventListener("input", (event) => {
applyTableSearch(event.target.value);
});
document.getElementById("preview-modal").addEventListener("click", (event) => { document.getElementById("preview-modal").addEventListener("click", (event) => {
if (event.target.id !== "preview-modal") return; if (event.target.id !== "preview-modal") return;
const modal = document.getElementById("preview-modal"); const modal = document.getElementById("preview-modal");