158 lines
6.9 KiB
JavaScript
158 lines
6.9 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { getDiagLog, clearDiagLog } from '../api/dns';
|
|
import ConfirmDialog from './ConfirmDialog';
|
|
import { exportCsv } from '../utils/exportCsv';
|
|
|
|
const PAGE_SIZE = 50;
|
|
|
|
function formatDate(iso) {
|
|
if (!iso) return '—';
|
|
const d = new Date(iso);
|
|
return `${d.toLocaleDateString('sv-SE')} ${d.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`;
|
|
}
|
|
|
|
export default function DiagnosticsPage() {
|
|
const [entries, setEntries] = useState([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [filterProvider, setFilterProvider] = useState('');
|
|
const [filterOk, setFilterOk] = useState('');
|
|
const [confirmClear, setConfirmClear] = useState(false);
|
|
|
|
const load = useCallback((pg = 0) => {
|
|
setLoading(true); setError('');
|
|
getDiagLog({
|
|
provider: filterProvider || undefined,
|
|
ok: filterOk !== '' ? filterOk : undefined,
|
|
limit: PAGE_SIZE,
|
|
offset: pg * PAGE_SIZE,
|
|
})
|
|
.then(({ entries, total }) => { setEntries(entries); setTotal(total); })
|
|
.catch(e => setError(e.message))
|
|
.finally(() => setLoading(false));
|
|
}, [filterProvider, filterOk]);
|
|
|
|
useEffect(() => { setPage(0); load(0); }, [load]);
|
|
|
|
function goToPage(pg) { setPage(pg); load(pg); }
|
|
|
|
async function handleClear() {
|
|
await clearDiagLog();
|
|
setConfirmClear(false);
|
|
setEntries([]); setTotal(0);
|
|
}
|
|
|
|
const totalPages = Math.ceil(total / PAGE_SIZE);
|
|
const errorCount = entries.filter(e => !e.ok).length;
|
|
|
|
return (
|
|
<div className="dashboard">
|
|
<div className="dashboard-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
<div>
|
|
<h2>Provider Diagnostics</h2>
|
|
<p className="dashboard-hint">
|
|
Every API call made to DNS providers — last 200 entries. Use this to troubleshoot connectivity issues.
|
|
</p>
|
|
</div>
|
|
<button className="btn-danger" onClick={() => setConfirmClear(true)}>Clear Log</button>
|
|
</div>
|
|
|
|
{error && <div className="error-banner" style={{ marginBottom: 16 }}><strong>Error:</strong> {error}</div>}
|
|
|
|
<div className="records-actions" style={{ marginBottom: 16 }}>
|
|
<select className="filter-input" value={filterProvider} onChange={e => setFilterProvider(e.target.value)}>
|
|
<option value="">All providers</option>
|
|
<option value="cloudflare">Cloudflare</option>
|
|
<option value="loopia">Loopia</option>
|
|
<option value="pihole">Pi-hole</option>
|
|
<option value="azure">Azure DNS</option>
|
|
<option value="cpanel">cPanel</option>
|
|
</select>
|
|
<select className="filter-input" value={filterOk} onChange={e => setFilterOk(e.target.value)}>
|
|
<option value="">All results</option>
|
|
<option value="false">Errors only</option>
|
|
<option value="true">Success only</option>
|
|
</select>
|
|
<button className="btn-secondary" onClick={() => load(page)}>↺ Refresh</button>
|
|
<button className="btn-export" onClick={() =>
|
|
exportCsv(
|
|
entries.map(e => ({ timestamp: e.timestamp, provider: e.provider, operation: e.operation, method: e.method, url: e.url, status: e.status ?? '', latency: e.latency, ok: e.ok ? 'yes' : 'no', error: e.error ?? '' })),
|
|
['timestamp','provider','operation','method','url','status','latency','ok','error'],
|
|
{ timestamp:'Time', provider:'Provider', operation:'Operation', method:'Method', url:'URL', status:'Status', latency:'Latency (ms)', ok:'OK', error:'Error' },
|
|
'diagnostics.csv'
|
|
)
|
|
}>⬇ Export CSV</button>
|
|
<span className="record-count">{total} entries{errorCount > 0 && <span style={{ color: '#ef4444', marginLeft: 8 }}>· {errorCount} errors on this page</span>}</span>
|
|
</div>
|
|
|
|
{loading ? <p className="hint">Loading…</p> : entries.length === 0 ? (
|
|
<div className="empty-state">
|
|
<p>No diagnostic entries yet — make a DNS operation (sync, add, delete) to populate the log.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="table-wrapper">
|
|
<table className="records-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Provider</th>
|
|
<th>Operation</th>
|
|
<th>Status</th>
|
|
<th>Latency</th>
|
|
<th>URL</th>
|
|
<th>Error</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{entries.map(e => (
|
|
<tr key={e.id} className={!e.ok ? 'diag-row-error' : ''}>
|
|
<td style={{ fontSize: 11, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{formatDate(e.timestamp)}</td>
|
|
<td style={{ textTransform: 'capitalize', fontWeight: 500 }}>{e.provider}</td>
|
|
<td style={{ fontFamily: 'monospace', fontSize: 12 }}>{e.operation}</td>
|
|
<td>
|
|
<span className={`health-badge ${e.ok ? 'health-ok' : 'health-error'}`}>
|
|
{e.status ?? 'ERR'}
|
|
</span>
|
|
</td>
|
|
<td style={{ fontFamily: 'monospace', fontSize: 12, color: e.latency > 2000 ? '#f59e0b' : 'var(--text-muted)' }}>
|
|
{e.latency}ms
|
|
</td>
|
|
<td style={{ fontFamily: 'monospace', fontSize: 11, color: 'var(--text-muted)', maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={e.url}>
|
|
{e.url}
|
|
</td>
|
|
<td style={{ fontSize: 12, color: '#fca5a5', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={e.error ?? ''}>
|
|
{e.error ?? ''}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{totalPages > 1 && (
|
|
<div className="audit-pagination">
|
|
<button className="btn-secondary" onClick={() => goToPage(page - 1)} disabled={page === 0}>← Prev</button>
|
|
<span className="record-count">Page {page + 1} of {totalPages}</span>
|
|
<button className="btn-secondary" onClick={() => goToPage(page + 1)} disabled={page >= totalPages - 1}>Next →</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{confirmClear && (
|
|
<ConfirmDialog
|
|
title="Clear Diagnostic Log"
|
|
message="This will delete all diagnostic entries. This cannot be undone."
|
|
confirmLabel="Clear Log"
|
|
danger
|
|
onConfirm={handleClear}
|
|
onCancel={() => setConfirmClear(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|