Enterprise UI redesign — Phases 14–22 (Docusign-branded migration console) #1
|
|
@ -0,0 +1,221 @@
|
||||||
|
// History & Audit view — filterable, exportable migration history
|
||||||
|
|
||||||
|
import { api } from './api.js';
|
||||||
|
import { escHtml, formatDateTime, shortHash, downloadCsv, debounce } from './utils.js';
|
||||||
|
|
||||||
|
let _allRecords = [];
|
||||||
|
let _filter = { search: '', status: 'all', from: '', to: '' };
|
||||||
|
let _sort = { col: 'timestamp', dir: 'desc' };
|
||||||
|
const PAGE_SIZE = 50;
|
||||||
|
let _page = 0;
|
||||||
|
|
||||||
|
export async function renderHistory() {
|
||||||
|
const outlet = document.getElementById('router-outlet');
|
||||||
|
outlet.innerHTML = `<div class="empty-state"><div class="spinner"></div></div>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await api.migrate.history();
|
||||||
|
_allRecords = (data.history || []).reverse(); // newest first
|
||||||
|
} catch (e) {
|
||||||
|
outlet.innerHTML = `<div class="callout error"><span class="callout-icon">❌</span>Failed to load history: ${escHtml(e.message)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_page = 0;
|
||||||
|
_render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _render() {
|
||||||
|
const outlet = document.getElementById('router-outlet');
|
||||||
|
const filtered = _applyFilter(_allRecords);
|
||||||
|
const page = filtered.slice(_page * PAGE_SIZE, (_page + 1) * PAGE_SIZE);
|
||||||
|
const totalPages = Math.ceil(filtered.length / PAGE_SIZE);
|
||||||
|
|
||||||
|
outlet.innerHTML = `
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<div class="page-title">History & Audit</div>
|
||||||
|
<div class="page-subtitle">${_allRecords.length} total migration records</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-actions">
|
||||||
|
<button class="btn btn-secondary btn-sm" id="btn-export-history">⬇ Export CSV</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter bar -->
|
||||||
|
<div class="filter-bar">
|
||||||
|
<input type="search" class="search-input" id="hist-search"
|
||||||
|
placeholder="Search by template name…" value="${escHtml(_filter.search)}" />
|
||||||
|
<div class="filter-tabs">
|
||||||
|
<button class="filter-tab ${_filter.status === 'all' ? 'active' : ''}" data-status="all">All</button>
|
||||||
|
<button class="filter-tab ${_filter.status === 'success' ? 'active' : ''}" data-status="success">Success</button>
|
||||||
|
<button class="filter-tab ${_filter.status === 'error' ? 'active' : ''}" data-status="error">Errors</button>
|
||||||
|
<button class="filter-tab ${_filter.status === 'dry_run' ? 'active' : ''}" data-status="dry_run">Dry Run</button>
|
||||||
|
<button class="filter-tab ${_filter.status === 'skipped' ? 'active' : ''}" data-status="skipped">Skipped</button>
|
||||||
|
</div>
|
||||||
|
<div class="date-filter">
|
||||||
|
<label style="font-size:11px;color:var(--text-muted)">From:</label>
|
||||||
|
<input type="date" class="date-input" id="hist-from" value="${_filter.from}" />
|
||||||
|
<label style="font-size:11px;color:var(--text-muted)">To:</label>
|
||||||
|
<input type="date" class="date-input" id="hist-to" value="${_filter.to}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${filtered.length === 0 ? `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon">📋</div>
|
||||||
|
<div class="empty-state-title">${_allRecords.length ? 'No records match your filter' : 'No migration history yet'}</div>
|
||||||
|
<div class="empty-state-sub">${_allRecords.length ? 'Try clearing the search or filters.' : 'Run a migration to see history here.'}</div>
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
${_th('timestamp', 'Time')}
|
||||||
|
${_th('adobe_template_name', 'Template')}
|
||||||
|
${_th('action', 'Action')}
|
||||||
|
${_th('status', 'Status')}
|
||||||
|
<th>DocuSign ID</th>
|
||||||
|
<th>Checksum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${page.map(r => _historyRow(r)).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${totalPages > 1 ? `
|
||||||
|
<div class="pagination">
|
||||||
|
<span>Showing ${_page * PAGE_SIZE + 1}–${Math.min((_page + 1) * PAGE_SIZE, filtered.length)} of ${filtered.length}</span>
|
||||||
|
<div class="pagination-pages">
|
||||||
|
<button class="page-btn" id="pg-prev" ${_page === 0 ? 'disabled' : ''}>‹ Prev</button>
|
||||||
|
<button class="page-btn" id="pg-next" ${_page >= totalPages - 1 ? 'disabled' : ''}>Next ›</button>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
_bindEvents(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _th(col, label) {
|
||||||
|
const dir = _sort.col === col ? (_sort.dir === 'asc' ? 'sort-asc' : 'sort-desc') : '';
|
||||||
|
return `<th class="sortable ${dir}" data-col="${col}">${label}</th>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _historyRow(r) {
|
||||||
|
const statusBadge = r.status === 'success'
|
||||||
|
? `<span class="badge badge-green">${escHtml(r.action || 'success')}</span>`
|
||||||
|
: `<span class="badge badge-${r.status === 'skipped' ? 'gray' : r.status === 'dry_run' ? 'gray' : 'red'}">${escHtml(r.status || '—')}</span>`;
|
||||||
|
|
||||||
|
const checksum = r.checksum_sha256 || r.checksum || '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="row-expandable" data-expanded="false">
|
||||||
|
<td style="white-space:nowrap;font-size:12px">${(r.timestamp||'').slice(0,19).replace('T',' ')}</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-name">${escHtml(r.adobe_template_name || r.adobe_template_id || '—')}</div>
|
||||||
|
<div class="table-sub">${escHtml(r.adobe_template_id || '')}</div>
|
||||||
|
</td>
|
||||||
|
<td>${escHtml(r.action || '—')}</td>
|
||||||
|
<td>${statusBadge}</td>
|
||||||
|
<td class="mono" style="font-size:11px">${r.docusign_template_id ? escHtml(r.docusign_template_id.slice(0,12)) + '…' : '—'}</td>
|
||||||
|
<td>
|
||||||
|
${checksum
|
||||||
|
? `<span class="checksum" title="${escHtml(checksum)}">${escHtml(shortHash(checksum))}</span>`
|
||||||
|
: '<span style="color:var(--text-muted)">—</span>'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
${(r.blockers || r.warnings || r.error) ? `
|
||||||
|
<tr class="row-expanded-content" style="display:none">
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="row-expand-body">
|
||||||
|
${(r.blockers||[]).map(b => `<div style="color:var(--error);font-size:12px">🚫 ${escHtml(b)}</div>`).join('')}
|
||||||
|
${(r.warnings||[]).map(w => `<div style="color:var(--warning);font-size:12px">⚠ ${escHtml(w)}</div>`).join('')}
|
||||||
|
${r.error ? `<div style="color:var(--error);font-size:12px">❌ ${escHtml(r.error)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _applyFilter(records) {
|
||||||
|
let list = [...records];
|
||||||
|
|
||||||
|
if (_filter.search) {
|
||||||
|
const q = _filter.search.toLowerCase();
|
||||||
|
list = list.filter(r =>
|
||||||
|
(r.adobe_template_name || '').toLowerCase().includes(q) ||
|
||||||
|
(r.adobe_template_id || '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_filter.status !== 'all') {
|
||||||
|
list = list.filter(r => r.status === _filter.status);
|
||||||
|
}
|
||||||
|
if (_filter.from) {
|
||||||
|
list = list.filter(r => r.timestamp >= _filter.from);
|
||||||
|
}
|
||||||
|
if (_filter.to) {
|
||||||
|
list = list.filter(r => r.timestamp <= _filter.to + 'T23:59:59');
|
||||||
|
}
|
||||||
|
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const va = a[_sort.col] || '';
|
||||||
|
const vb = b[_sort.col] || '';
|
||||||
|
return _sort.dir === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindEvents(filtered) {
|
||||||
|
document.getElementById('hist-search')?.addEventListener('input', debounce(e => {
|
||||||
|
_filter.search = e.target.value; _page = 0; _render();
|
||||||
|
}, 250));
|
||||||
|
|
||||||
|
document.querySelectorAll('.filter-tab[data-status]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => { _filter.status = btn.dataset.status; _page = 0; _render(); });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('hist-from')?.addEventListener('change', e => { _filter.from = e.target.value; _page = 0; _render(); });
|
||||||
|
document.getElementById('hist-to')?.addEventListener('change', e => { _filter.to = e.target.value; _page = 0; _render(); });
|
||||||
|
|
||||||
|
document.querySelectorAll('th.sortable').forEach(th => {
|
||||||
|
th.addEventListener('click', () => {
|
||||||
|
const col = th.dataset.col;
|
||||||
|
_sort.dir = _sort.col === col && _sort.dir === 'asc' ? 'desc' : 'asc';
|
||||||
|
_sort.col = col; _page = 0; _render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pg-prev')?.addEventListener('click', () => { if (_page > 0) { _page--; _render(); } });
|
||||||
|
document.getElementById('pg-next')?.addEventListener('click', () => { _page++; _render(); });
|
||||||
|
|
||||||
|
// Expand rows
|
||||||
|
document.querySelectorAll('.row-expandable').forEach(row => {
|
||||||
|
row.addEventListener('click', () => {
|
||||||
|
const next = row.nextElementSibling;
|
||||||
|
if (next && next.classList.contains('row-expanded-content')) {
|
||||||
|
const open = next.style.display !== 'none';
|
||||||
|
next.style.display = open ? 'none' : 'table-row';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-export-history')?.addEventListener('click', () => {
|
||||||
|
downloadCsv('migration-history.csv', filtered.map(r => ({
|
||||||
|
timestamp: r.timestamp || '',
|
||||||
|
template: r.adobe_template_name || r.adobe_template_id || '',
|
||||||
|
adobe_id: r.adobe_template_id || '',
|
||||||
|
docusign_id: r.docusign_template_id || '',
|
||||||
|
action: r.action || '',
|
||||||
|
status: r.status || '',
|
||||||
|
checksum: r.checksum_sha256 || '',
|
||||||
|
warnings: (r.warnings || []).join('; '),
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue