reorg
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Domain list view
|
||||
*/
|
||||
|
||||
class DomainListView {
|
||||
constructor() {
|
||||
this.domains = [];
|
||||
this.filteredDomains = [];
|
||||
this.sortColumn = null;
|
||||
this.sortDirection = 'asc';
|
||||
this.filterStatus = 'all';
|
||||
this.searchQuery = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the domain list view
|
||||
*/
|
||||
async render() {
|
||||
const container = document.getElementById('domainListContent');
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
<div class="spinner"></div>
|
||||
<p class="mt-4 text-tertiary">Loading domains...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fetch data
|
||||
const data = await window.apiClient.getDomains();
|
||||
this.domains = data.domains || [];
|
||||
this.filteredDomains = [...this.domains];
|
||||
|
||||
// Update sidebar stats
|
||||
await window.utils.updateSidebarStats();
|
||||
|
||||
// Render the view
|
||||
this.renderContent(container);
|
||||
} catch (error) {
|
||||
console.error('Error loading domains:', error);
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12">
|
||||
<p class="text-red-400">Error loading domains: ${error.message}</p>
|
||||
<button onclick="location.reload()" class="btn btn-primary mt-4">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the content
|
||||
*/
|
||||
renderContent(container) {
|
||||
container.innerHTML = `
|
||||
<!-- Search and Filter Bar -->
|
||||
<div class="mb-4 flex gap-4" style="width: 100%;">
|
||||
<div class="flex-1" style="min-width: 0;">
|
||||
<input
|
||||
type="text"
|
||||
id="domainSearchInput"
|
||||
placeholder="Search domains..."
|
||||
class="input"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: 12rem; flex-shrink: 0;">
|
||||
<select
|
||||
id="domainStatusFilter"
|
||||
class="select"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="insufficient_quorum">Insufficient Quorum</option>
|
||||
<option value="tie">Tie</option>
|
||||
<option value="no_claims">No Claims</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Domain Table -->
|
||||
<div class="table-container">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-column="domain">
|
||||
Domain
|
||||
<span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th data-column="status">
|
||||
Status
|
||||
<span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th data-column="claimant">
|
||||
Resolved Claimant
|
||||
<span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th data-column="votes">
|
||||
Votes
|
||||
<span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th data-column="quorum">
|
||||
Quorum
|
||||
<span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th data-column="peers">
|
||||
Active Peers
|
||||
<span class="sort-indicator"></span>
|
||||
</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="domainTableBody">
|
||||
${this.renderTableRows()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${this.filteredDomains.length === 0 ? `
|
||||
<div class="p-6 text-center text-tertiary">
|
||||
No domains found
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render table rows
|
||||
*/
|
||||
renderTableRows() {
|
||||
if (this.filteredDomains.length === 0) {
|
||||
return '<tr><td colspan="7" class="px-4 py-8 text-center text-tertiary">No domains found</td></tr>';
|
||||
}
|
||||
|
||||
return this.filteredDomains.map(domain => {
|
||||
const consensus = domain.consensus || {};
|
||||
const status = consensus.status || 'unknown';
|
||||
const statusText = window.utils.getStatusText(status);
|
||||
const statusClass = window.utils.getStatusBadgeClass(status);
|
||||
const totalVotes = consensus.totalVotes || 0;
|
||||
const minVotes = consensus.minVotes || 0;
|
||||
const quorumMet = consensus.quorumMet || false;
|
||||
const quorumPercentage = window.utils.calculateQuorumPercentage(totalVotes, minVotes);
|
||||
const claimant = domain.consensus?.resolvedClaimant ? window.utils.formatPeerId(domain.consensus.resolvedClaimant) : 'N/A';
|
||||
const activePeers = domain.consensus?.activePeers || 0;
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td class="font-medium">
|
||||
<span class="font-mono">${window.utils.escapeHtml(domain.domain)}</span>
|
||||
${domain.isLocal ? '<span class="ml-2 text-xs text-blue-400">(local)</span>' : ''}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge ${statusClass}">${statusText}</span>
|
||||
</td>
|
||||
<td class="font-mono text-sm">${claimant}</td>
|
||||
<td>
|
||||
${totalVotes} / ${minVotes}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 progress-bar-container">
|
||||
<div class="progress-bar ${quorumMet ? 'success' : 'warning'}" style="width: ${Math.min(100, quorumPercentage)}%"></div>
|
||||
</div>
|
||||
<span class="text-xs text-tertiary">${quorumPercentage}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>${activePeers}</td>
|
||||
<td class="text-right">
|
||||
<button
|
||||
onclick="window.domainListView.viewDomain('${window.utils.escapeHtml(domain.domain)}')"
|
||||
class="btn btn-primary text-sm"
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Search input
|
||||
const searchInput = document.getElementById('domainSearchInput');
|
||||
if (searchInput) {
|
||||
const debouncedSearch = window.utils.debounce(() => {
|
||||
this.searchQuery = searchInput.value.toLowerCase();
|
||||
this.applyFilters();
|
||||
}, 300);
|
||||
searchInput.addEventListener('input', debouncedSearch);
|
||||
}
|
||||
|
||||
// Status filter
|
||||
const statusFilter = document.getElementById('domainStatusFilter');
|
||||
if (statusFilter) {
|
||||
statusFilter.addEventListener('change', (e) => {
|
||||
this.filterStatus = e.target.value;
|
||||
this.applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// Sortable columns
|
||||
document.querySelectorAll('[data-column]').forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const column = header.dataset.column;
|
||||
if (this.sortColumn === column) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortColumn = column;
|
||||
this.sortDirection = 'asc';
|
||||
}
|
||||
this.applySort();
|
||||
this.updateSortIndicators();
|
||||
const tbody = document.getElementById('domainTableBody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = this.renderTableRows();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters
|
||||
*/
|
||||
applyFilters() {
|
||||
this.filteredDomains = this.domains.filter(domain => {
|
||||
// Search filter
|
||||
if (this.searchQuery && !domain.domain.toLowerCase().includes(this.searchQuery)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (this.filterStatus !== 'all') {
|
||||
const status = domain.consensus?.status || 'unknown';
|
||||
if (status !== this.filterStatus) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Apply sort
|
||||
this.applySort();
|
||||
|
||||
// Re-render table
|
||||
const tbody = document.getElementById('domainTableBody');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = this.renderTableRows();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sort
|
||||
*/
|
||||
applySort() {
|
||||
if (!this.sortColumn) return;
|
||||
|
||||
this.filteredDomains.sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
|
||||
switch (this.sortColumn) {
|
||||
case 'domain':
|
||||
aVal = a.domain.toLowerCase();
|
||||
bVal = b.domain.toLowerCase();
|
||||
break;
|
||||
case 'status':
|
||||
aVal = a.consensus?.status || 'unknown';
|
||||
bVal = b.consensus?.status || 'unknown';
|
||||
break;
|
||||
case 'claimant':
|
||||
aVal = a.consensus?.resolvedClaimant || '';
|
||||
bVal = b.consensus?.resolvedClaimant || '';
|
||||
break;
|
||||
case 'votes':
|
||||
aVal = a.consensus?.totalVotes || 0;
|
||||
bVal = b.consensus?.totalVotes || 0;
|
||||
break;
|
||||
case 'quorum':
|
||||
const aQuorum = window.utils.calculateQuorumPercentage(a.consensus?.totalVotes || 0, a.consensus?.minVotes || 1);
|
||||
const bQuorum = window.utils.calculateQuorumPercentage(b.consensus?.totalVotes || 0, b.consensus?.minVotes || 1);
|
||||
aVal = aQuorum;
|
||||
bVal = bQuorum;
|
||||
break;
|
||||
case 'peers':
|
||||
aVal = a.consensus?.activePeers || 0;
|
||||
bVal = b.consensus?.activePeers || 0;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (aVal < bVal) return this.sortDirection === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return this.sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sort indicators
|
||||
*/
|
||||
updateSortIndicators() {
|
||||
document.querySelectorAll('[data-column] .sort-indicator').forEach(indicator => {
|
||||
indicator.textContent = '';
|
||||
});
|
||||
|
||||
if (this.sortColumn) {
|
||||
const header = document.querySelector(`[data-column="${this.sortColumn}"]`);
|
||||
if (header) {
|
||||
const indicator = header.querySelector('.sort-indicator');
|
||||
if (indicator) {
|
||||
indicator.textContent = this.sortDirection === 'asc' ? ' ▲' : ' ▼';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View domain details
|
||||
*/
|
||||
viewDomain(domain) {
|
||||
// Switch to domain detail view
|
||||
window.app.showDomainDetail(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle updates from WebSocket
|
||||
*/
|
||||
async handleUpdate(data) {
|
||||
// Always update on any consensus-related change
|
||||
if (data.domains || data.consensus || data.changedDomains) {
|
||||
try {
|
||||
// Fetch fresh data
|
||||
window.apiClient.invalidateCache('domains');
|
||||
const response = await window.apiClient.getDomains();
|
||||
this.domains = response.domains || [];
|
||||
|
||||
// Update sidebar stats
|
||||
await window.utils.updateSidebarStats();
|
||||
|
||||
// Apply current filters and sort (this already updates the table body)
|
||||
this.applyFilters();
|
||||
} catch (error) {
|
||||
console.error('Error updating domain list:', error);
|
||||
// Fall back to full render on error
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export
|
||||
window.domainListView = new DomainListView();
|
||||
|
||||
Reference in New Issue
Block a user