forked from snxraven/p2ns
367 lines
11 KiB
JavaScript
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();
|
|
|