179 lines
5.8 KiB
HTML
179 lines
5.8 KiB
HTML
{% extends "_shell.html" %}
|
||
{% block title %}數據庫連接 — alterdb{% endblock %}
|
||
{% block style %}
|
||
<style>
|
||
.toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.toolbar h2 { margin: 0; }
|
||
.toolbar a, .db-table .actions a, .db-table .actions button {
|
||
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 a:hover, .db-table .actions a:hover, .db-table .actions button:hover {
|
||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||
}
|
||
.db-table .actions { white-space: nowrap; }
|
||
.db-table .actions a, .db-table .actions button {
|
||
padding: .2rem .6rem;
|
||
font-size: .85rem;
|
||
margin-left: .3rem;
|
||
}
|
||
.db-table .actions .danger { color: light-dark(#b3261e, #ffb4ab); }
|
||
.db-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-bottom: 2rem;
|
||
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;
|
||
vertical-align: top;
|
||
}
|
||
.db-table th { font-weight: 600; }
|
||
.db-table td.type { text-transform: uppercase; font-size: .8rem; letter-spacing: .04em; }
|
||
.db-table td.empty { text-align: center; opacity: .7; padding: 1.5rem 0; }
|
||
.error {
|
||
margin: 0 0 1rem;
|
||
padding: .5rem .75rem;
|
||
border-radius: 6px;
|
||
color: light-dark(#b3261e, #ffb4ab);
|
||
background: color-mix(in srgb, currentColor 8%, transparent);
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
{% block page %}
|
||
<div class="toolbar">
|
||
<h2>數據庫連接</h2>
|
||
<a href="/databases/new">新增連接</a>
|
||
</div>
|
||
<p id="list-error" class="error" role="alert" hidden></p>
|
||
<table class="db-table">
|
||
<thead>
|
||
<tr>
|
||
<th>名稱</th><th>類型</th><th>主機:埠</th><th>資料庫</th><th>用戶</th><th>更新時間</th><th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="db-rows"></tbody>
|
||
</table>
|
||
{% endblock %}
|
||
{% block script %}
|
||
<script>
|
||
(() => {
|
||
'use strict';
|
||
|
||
const listError = document.getElementById('list-error');
|
||
const tbody = document.getElementById('db-rows');
|
||
|
||
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 formatTime(iso) {
|
||
return iso.replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
|
||
}
|
||
|
||
function renderRows(databases) {
|
||
const rows = [];
|
||
if (databases.length === 0) {
|
||
const tr = document.createElement('tr');
|
||
const td = document.createElement('td');
|
||
td.colSpan = 7;
|
||
td.className = 'empty';
|
||
td.textContent = '尚無連接,點擊「新增連接」建立第一個。';
|
||
tr.append(td);
|
||
rows.push(tr);
|
||
} else {
|
||
for (const item of databases) { rows.push(renderRow(item)); }
|
||
}
|
||
tbody.replaceChildren(...rows);
|
||
}
|
||
|
||
// 所有動態值一律經 textContent 寫入,避免以字串拼接 HTML 引入注入風險
|
||
function renderRow(item) {
|
||
const tr = document.createElement('tr');
|
||
|
||
// 名稱連到詳情頁(連接狀態與數據表瀏覽)
|
||
const nameCell = document.createElement('td');
|
||
const nameLink = document.createElement('a');
|
||
nameLink.href = '/databases/' + item.id;
|
||
nameLink.textContent = item.name;
|
||
nameCell.append(nameLink);
|
||
tr.append(nameCell);
|
||
|
||
const cells = [
|
||
{ text: item.type, className: 'type' },
|
||
{ text: item.host + ':' + item.port },
|
||
{ text: item.database_name },
|
||
{ text: item.username },
|
||
{ text: formatTime(item.updated_at) },
|
||
];
|
||
for (const cell of cells) {
|
||
const td = document.createElement('td');
|
||
td.textContent = cell.text;
|
||
if (cell.className) { td.className = cell.className; }
|
||
tr.append(td);
|
||
}
|
||
|
||
const actions = document.createElement('td');
|
||
actions.className = 'actions';
|
||
const editLink = document.createElement('a');
|
||
editLink.href = '/databases/' + item.id + '/edit';
|
||
editLink.textContent = '編輯';
|
||
const deleteButton = document.createElement('button');
|
||
deleteButton.type = 'button';
|
||
deleteButton.textContent = '刪除';
|
||
deleteButton.className = 'danger';
|
||
deleteButton.addEventListener('click', () => removeDatabase(item));
|
||
actions.append(editLink, deleteButton);
|
||
tr.append(actions);
|
||
return tr;
|
||
}
|
||
|
||
async function loadList() {
|
||
listError.hidden = true;
|
||
try {
|
||
renderRows(await api('/api/databases'));
|
||
} catch (err) {
|
||
listError.textContent = err.message;
|
||
listError.hidden = false;
|
||
}
|
||
}
|
||
|
||
async function removeDatabase(item) {
|
||
if (!window.confirm('確定刪除連接「' + item.name + '」?')) { return; }
|
||
try {
|
||
await api('/api/databases/' + item.id, { method: 'DELETE' });
|
||
await loadList();
|
||
} catch (err) {
|
||
listError.textContent = err.message;
|
||
listError.hidden = false;
|
||
}
|
||
}
|
||
|
||
loadList();
|
||
})();
|
||
</script>
|
||
{% endblock %}
|