fixup
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user