Files
alterdb/templates/database_detail.html
2026-10-02 13:12:49 +08:00

253 lines
8.7 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "_shell.html" %}
{% block title %}連接詳情:{{ database.name }} — alterdb{% endblock %}
{% block style %}
<style>
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.toolbar .title { display: flex; align-items: baseline; gap: .7rem; margin: 0; }
.toolbar h2 { margin: 0; }
.toolbar .type-badge {
font-size: .75rem;
font-weight: 600;
letter-spacing: .05em;
text-transform: uppercase;
padding: .1rem .5rem;
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
border-radius: 999px;
}
.toolbar .actions { display: flex; gap: .6rem; }
.toolbar .actions a {
font: inherit;
padding: .35rem .9rem;
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
border-radius: 6px;
background: transparent;
color: inherit;
cursor: pointer;
text-decoration: none;
}
.toolbar .actions a:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
.meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
gap: .6rem 1.4rem;
margin: 0 0 1.8rem;
padding: 1rem 1.2rem;
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
border-radius: 10px;
}
.meta div { min-width: 0; }
.meta dt {
font-size: .8rem;
opacity: .65;
margin: 0;
}
.meta dd { margin: 0; overflow-wrap: anywhere; }
section h3 { margin: 0 0 .7rem; }
.status-line { display: flex; align-items: center; gap: .8rem; margin-bottom: 1.6rem; }
.status {
display: inline-flex;
align-items: center;
gap: .45rem;
font-weight: 600;
}
.status::before {
content: "";
width: .55rem;
height: .55rem;
border-radius: 50%;
background: currentColor;
}
.status.pending { opacity: .65; }
.status.ok { color: light-dark(#146c2e, #6ddb8c); }
.status.bad { color: light-dark(#b3261e, #ffb4ab); }
#retest {
font: inherit;
padding: .3rem .9rem;
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
border-radius: 6px;
background: transparent;
color: inherit;
cursor: pointer;
}
#retest:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
#retest:disabled { opacity: .5; cursor: wait; }
.status-detail { opacity: .8; font-size: .9rem; overflow-wrap: anywhere; }
.error {
margin: 0 0 1rem;
padding: .5rem .75rem;
border-radius: 6px;
color: light-dark(#b3261e, #ffb4ab);
background: color-mix(in srgb, currentColor 8%, transparent);
}
.db-table {
width: 100%;
border-collapse: collapse;
font-size: .95rem;
}
.db-table th, .db-table td {
padding: .5rem .6rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
text-align: left;
}
.db-table th { font-weight: 600; }
.db-table td.empty { text-align: center; opacity: .7; padding: 1.5rem 0; }
</style>
{% endblock %}
{% block page %}
<div id="db-detail" data-database-id="{{ database.id }}">
<div class="toolbar">
<div class="title">
<h2>{{ database.name }}</h2>
<span class="type-badge">{{ database.db_type.as_str() }}</span>
</div>
<div class="actions">
<a href="/databases/{{ database.id }}/edit">編輯</a>
<a href="/databases">返回列表</a>
</div>
</div>
<dl class="meta">
<div><dt>主機</dt><dd>{{ database.host }}:{{ database.port }}</dd></div>
<div><dt>資料庫</dt><dd>{{ database.database_name }}</dd></div>
<div><dt>用戶</dt><dd>{{ database.username }}</dd></div>
<div><dt>建立於</dt><dd>{{ created_at_display }}</dd></div>
<div><dt>更新於</dt><dd>{{ updated_at_display }}</dd></div>
</dl>
<section aria-label="連接狀態">
<h3>連接狀態</h3>
<div class="status-line">
<span id="status" class="status pending">測試中…</span>
<span id="status-detail" class="status-detail"></span>
<button type="button" id="retest">重新測試</button>
</div>
</section>
<section aria-label="數據表">
<h3>數據表</h3>
<p id="tables-error" class="error" role="alert" hidden></p>
<table class="db-table">
<thead>
<tr><th>名稱</th><th>類型</th></tr>
</thead>
<tbody id="table-rows"></tbody>
</table>
</section>
</div>
{% endblock %}
{% block script %}
<script>
(() => {
'use strict';
const root = document.getElementById('db-detail');
const databaseId = root.dataset.databaseId;
const statusBadge = document.getElementById('status');
const statusDetail = document.getElementById('status-detail');
const retestButton = document.getElementById('retest');
const tablesError = document.getElementById('tables-error');
const tbody = document.getElementById('table-rows');
// information_schema 的 TABLE_TYPE → 中文標籤;未知值原樣展示
const KIND_LABELS = { 'BASE TABLE': '表', 'VIEW': '視圖' };
async function api(path, options) {
const response = await fetch(path, options);
if (!response.ok) {
let message = '請求失敗(HTTP ' + response.status + ')';
try {
const body = await response.json();
if (body.error) { message = body.error; }
} catch (_) { /* 非 JSON 錯誤體,保留默認訊息 */ }
throw new Error(message);
}
return response.status === 204 ? null : response.json();
}
function messageRow(text) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 2;
td.className = 'empty';
td.textContent = text;
tr.append(td);
return tr;
}
async function runTest() {
retestButton.disabled = true;
statusBadge.textContent = '測試中…';
statusBadge.className = 'status pending';
statusDetail.textContent = '';
try {
const outcome = await api('/api/databases/' + databaseId + '/test', { method: 'POST' });
if (outcome.ok) {
statusBadge.textContent = '已連接';
statusBadge.className = 'status ok';
statusDetail.textContent =
(outcome.server_version || '') + ' · ' + outcome.elapsed_ms + ' ms';
} else {
statusBadge.textContent = '無法連接';
statusBadge.className = 'status bad';
statusDetail.textContent = outcome.error || '未知錯誤';
}
} catch (err) {
statusBadge.textContent = '測試失敗';
statusBadge.className = 'status bad';
statusDetail.textContent = err.message;
} finally {
retestButton.disabled = false;
}
}
// 所有動態值一律經 textContent 寫入,避免以字串拼接 HTML 引入注入風險
function renderTables(tables) {
if (tables.length === 0) {
tbody.replaceChildren(messageRow('此資料庫目前 schema 下沒有數據表。'));
return;
}
const rows = tables.map((table) => {
const tr = document.createElement('tr');
const name = document.createElement('td');
// 表名連到表管理頁(欄位結構管理,資料瀏覽在其「數據管理」頁);路徑段編碼後拼接
const link = document.createElement('a');
link.href = '/databases/' + databaseId + '/tables/' + encodeURIComponent(table.name);
link.textContent = table.name;
name.append(link);
const kind = document.createElement('td');
kind.textContent = KIND_LABELS[table.kind] || table.kind;
tr.append(name, kind);
return tr;
});
tbody.replaceChildren(...rows);
}
async function loadTables() {
tablesError.hidden = true;
tbody.replaceChildren(messageRow('載入中…'));
try {
renderTables(await api('/api/databases/' + databaseId + '/tables'));
} catch (err) {
tbody.replaceChildren();
tablesError.textContent = err.message;
tablesError.hidden = false;
}
}
function refresh() {
runTest();
loadTables();
}
retestButton.addEventListener('click', refresh);
refresh();
})();
</script>
{% endblock %}