Files
p2ns/plugin-sites/domain.consensus/www/js/views/domain-list.js
T
2025-12-17 20:05:50 -05:00

367 lines
11 KiB
JavaScript

/**
* 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();