feat(admin/tables): show source, schedule, folder, registered, and sync-error in row
This commit is contained in:
parent
b230d44687
commit
6bc8739010
2 changed files with 155 additions and 10 deletions
|
|
@ -10,6 +10,9 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Admin / Tables**: registry table now shows Source (bucket/table), Schedule, Folder, Registered by/at, and a sync-error warning icon per row. The page widens to ~1600px to accommodate.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Admin / Tables**: long table descriptions no longer push the row's Edit / Manage access / Delete buttons off-screen. The Description column is now clamped to 2 lines with the full text available on hover and in the Edit modal.
|
- **Admin / Tables**: long table descriptions no longer push the row's Edit / Manage access / Delete buttons off-screen. The Description column is now clamped to 2 lines with the full text available on hover and in the Edit modal.
|
||||||
- **Admin / Tables**: descriptions stored with shell-quoting backslash-escapes (`Don\'t`, `\n`) now render correctly. The same normalization also runs at register/update time so newly-saved descriptions are never corrupted.
|
- **Admin / Tables**: descriptions stored with shell-quoting backslash-escapes (`Don\'t`, `\n`) now render correctly. The same normalization also runs at register/update time so newly-saved descriptions are never corrupted.
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
|
|
||||||
/* ── Page Title ── */
|
/* ── Page Title ── */
|
||||||
.page-title {
|
.page-title {
|
||||||
max-width: 1000px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 32px 24px 24px;
|
padding: 32px 24px 24px;
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
|
|
||||||
/* ── Content Layout ── */
|
/* ── Content Layout ── */
|
||||||
.content {
|
.content {
|
||||||
max-width: 1000px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 24px 32px;
|
padding: 0 24px 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -477,7 +477,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.registry-table .col-description {
|
.registry-table .col-description {
|
||||||
max-width: 0; /* lets line-clamp / overflow take effect inside fixed layout */
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
|
@ -517,7 +516,7 @@
|
||||||
.registry-table .col-id {
|
.registry-table .col-id {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
max-width: 280px;
|
max-width: 220px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -530,6 +529,94 @@
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Registry table — wide layout ── */
|
||||||
|
.registry-table .col-mode {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-source {
|
||||||
|
width: 200px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-pk {
|
||||||
|
width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-schedule {
|
||||||
|
width: 100px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-folder {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-registered {
|
||||||
|
width: 160px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-registered .registered-by {
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-registered .registered-at {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.registry-table .col-status {
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.mode-local {
|
||||||
|
background: var(--success-light);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.mode-remote {
|
||||||
|
background: var(--primary-light);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge.mode-materialized {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
color: #8B5CF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Modal overlay ── */
|
/* ── Modal overlay ── */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -2619,6 +2706,12 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderModeBadge(mode) {
|
||||||
|
var m = (mode || 'local').toLowerCase();
|
||||||
|
var cls = 'mode-badge mode-' + (['local','remote','materialized'].indexOf(m) >= 0 ? m : 'local');
|
||||||
|
return '<span class="' + cls + '">' + escapeHtml(m) + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
function renderRegistryListing(target, tables) {
|
function renderRegistryListing(target, tables) {
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
if (tables.length === 0) {
|
if (tables.length === 0) {
|
||||||
|
|
@ -2627,19 +2720,68 @@
|
||||||
}
|
}
|
||||||
var html = '<table class="registry-table">';
|
var html = '<table class="registry-table">';
|
||||||
html += '<thead><tr>';
|
html += '<thead><tr>';
|
||||||
html += '<th>Table ID</th>';
|
html += '<th class="col-id">Table ID</th>';
|
||||||
html += '<th>Mode</th>';
|
html += '<th class="col-mode">Mode</th>';
|
||||||
html += '<th>Primary Key</th>';
|
html += '<th class="col-source">Source</th>';
|
||||||
|
html += '<th class="col-pk">Primary Key</th>';
|
||||||
|
html += '<th class="col-schedule">Schedule</th>';
|
||||||
|
html += '<th class="col-folder">Folder</th>';
|
||||||
html += '<th>Description</th>';
|
html += '<th>Description</th>';
|
||||||
|
html += '<th class="col-registered">Registered</th>';
|
||||||
|
html += '<th class="col-status"></th>';
|
||||||
html += '<th class="col-actions">Actions</th>';
|
html += '<th class="col-actions">Actions</th>';
|
||||||
html += '</tr></thead><tbody>';
|
html += '</tr></thead><tbody>';
|
||||||
tables.forEach(function(table) {
|
tables.forEach(function(table) {
|
||||||
html += '<tr>';
|
html += '<tr>';
|
||||||
html += '<td class="col-id" title="' + escapeHtml(table.id) + '">' + escapeHtml(table.id) + '</td>';
|
html += '<td class="col-id" title="' + escapeHtml(table.id) + '">' + escapeHtml(table.id) + '</td>';
|
||||||
html += '<td>' + escapeHtml(table.query_mode || 'local') + '</td>';
|
html += '<td class="col-mode">' + renderModeBadge(table.query_mode) + '</td>';
|
||||||
html += '<td>' + escapeHtml((table.primary_key || []).join(', ') || '-') + '</td>';
|
|
||||||
|
// Source: bucket / source_table; em-dash when both empty (custom-SQL row).
|
||||||
|
var bucket = table.bucket || '';
|
||||||
|
var srcTable = table.source_table || '';
|
||||||
|
var sourceText = '';
|
||||||
|
if (bucket && srcTable) {
|
||||||
|
sourceText = bucket + ' / ' + srcTable;
|
||||||
|
} else if (bucket || srcTable) {
|
||||||
|
sourceText = bucket || srcTable;
|
||||||
|
}
|
||||||
|
var sourceCell = sourceText ? escapeHtml(sourceText) : '—';
|
||||||
|
html += '<td class="col-source" title="' + escapeHtml(sourceText) + '">' + sourceCell + '</td>';
|
||||||
|
|
||||||
|
html += '<td class="col-pk">' + escapeHtml((table.primary_key || []).join(', ') || '—') + '</td>';
|
||||||
|
html += '<td class="col-schedule">' + escapeHtml(table.sync_schedule || '—') + '</td>';
|
||||||
|
|
||||||
|
// Folder badge — '—' when null/empty.
|
||||||
|
if (table.folder) {
|
||||||
|
html += '<td class="col-folder"><span class="folder-badge">' + escapeHtml(table.folder) + '</span></td>';
|
||||||
|
} else {
|
||||||
|
html += '<td class="col-folder">—</td>';
|
||||||
|
}
|
||||||
|
|
||||||
var desc = unescapeShellQuoting(table.description || '');
|
var desc = unescapeShellQuoting(table.description || '');
|
||||||
html += '<td class="col-description" title="' + escapeHtml(desc) + '">' + escapeHtml(desc || '-') + '</td>';
|
html += '<td class="col-description" title="' + escapeHtml(desc) + '">' + escapeHtml(desc || '—') + '</td>';
|
||||||
|
|
||||||
|
// Registered: stacked email + date (first 10 chars of ISO timestamp).
|
||||||
|
var regBy = table.registered_by || '';
|
||||||
|
var regByDisplay = regBy;
|
||||||
|
if (regBy.length > 24 && regBy.indexOf('@') > 0) {
|
||||||
|
regByDisplay = regBy.split('@')[0];
|
||||||
|
}
|
||||||
|
var regAt = table.registered_at ? String(table.registered_at).slice(0, 10) : '';
|
||||||
|
html += '<td class="col-registered">';
|
||||||
|
html += '<div class="registered-by" title="' + escapeHtml(regBy) + '">' + (regByDisplay ? escapeHtml(regByDisplay) : '—') + '</div>';
|
||||||
|
html += '<div class="registered-at">' + escapeHtml(regAt || '') + '</div>';
|
||||||
|
html += '</td>';
|
||||||
|
|
||||||
|
// Status: warning icon when the last sync errored.
|
||||||
|
html += '<td class="col-status">';
|
||||||
|
if (table.last_sync_error) {
|
||||||
|
html += '<span title="' + escapeHtml(table.last_sync_error) + '">';
|
||||||
|
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--error);"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
|
||||||
|
html += '</span>';
|
||||||
|
}
|
||||||
|
html += '</td>';
|
||||||
|
|
||||||
html += '<td class="col-actions"><div style="display:flex; gap:4px; justify-content:flex-end;">';
|
html += '<td class="col-actions"><div style="display:flex; gap:4px; justify-content:flex-end;">';
|
||||||
html += '<button class="btn-icon" title="Edit" onclick=\'openEditModal(' + JSON.stringify(table).replace(/'/g, "\\'") + ')\'>';
|
html += '<button class="btn-icon" title="Edit" onclick=\'openEditModal(' + JSON.stringify(table).replace(/'/g, "\\'") + ')\'>';
|
||||||
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
html += '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue