feat(MikrotikTools): add traceroute tab state management for improved user navigation between results and analysis
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m35s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m35s
This commit is contained in:
+171
-157
@@ -36,6 +36,7 @@ function MikrotikTools() {
|
|||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [hops, setHops] = useState([]);
|
const [hops, setHops] = useState([]);
|
||||||
const [analysis, setAnalysis] = useState([]);
|
const [analysis, setAnalysis] = useState([]);
|
||||||
|
const [tracerouteTab, setTracerouteTab] = useState('table'); // 'table' | 'analysis'
|
||||||
const [pingResult, setPingResult] = useState(null);
|
const [pingResult, setPingResult] = useState(null);
|
||||||
const [useDns, setUseDns] = useState(true);
|
const [useDns, setUseDns] = useState(true);
|
||||||
|
|
||||||
@@ -508,144 +509,6 @@ function MikrotikTools() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-8">
|
<div className="col-md-8">
|
||||||
{mode === 'traceroute' && (
|
|
||||||
<div className="card mb-3">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="card-title mb-0">Результаты трассировки</h3>
|
|
||||||
</div>
|
|
||||||
<div className="card-body p-0">
|
|
||||||
{hops.length === 0 ? (
|
|
||||||
<div className="p-3 text-muted small">
|
|
||||||
Результаты трассировки появятся здесь после запуска проверки.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="table-responsive">
|
|
||||||
<table className="table table-vcenter card-table table-hover mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style={{ width: 70 }}>Хоп</th>
|
|
||||||
<th>Адрес / узел</th>
|
|
||||||
<th style={{ width: 130 }}>Среднее, мс</th>
|
|
||||||
<th style={{ width: 140 }}>Мин / Макс, мс</th>
|
|
||||||
<th style={{ width: 110 }}>Потери</th>
|
|
||||||
<th style={{ width: 200 }}>Статус</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{hops.map((h) => {
|
|
||||||
const ip = h.ip || '';
|
|
||||||
const dns = h.dns || '';
|
|
||||||
|
|
||||||
let mainLabel;
|
|
||||||
if (useDns) {
|
|
||||||
if (dns && ip) mainLabel = `${dns} (${ip})`;
|
|
||||||
else mainLabel = dns || ip || h.host || '—';
|
|
||||||
} else {
|
|
||||||
mainLabel = ip || dns || h.host || '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
const avg = typeof h.avgMs === 'number' ? h.avgMs : null;
|
|
||||||
const loss = typeof h.loss === 'number' ? h.loss : null;
|
|
||||||
const statusRaw = (h.status || '').toString();
|
|
||||||
const statusLower = statusRaw.toLowerCase();
|
|
||||||
const hasTimeout =
|
|
||||||
statusLower.includes('timeout') ||
|
|
||||||
statusLower.includes('no reply') ||
|
|
||||||
statusLower.includes('unreachable');
|
|
||||||
|
|
||||||
let severity = 'ok';
|
|
||||||
if (hasTimeout || (loss != null && loss >= 50)) {
|
|
||||||
severity = 'danger';
|
|
||||||
} else if ((loss != null && loss > 0) || (avg != null && avg > 200)) {
|
|
||||||
severity = 'warn';
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowClass =
|
|
||||||
severity === 'danger'
|
|
||||||
? 'table-danger'
|
|
||||||
: severity === 'warn'
|
|
||||||
? 'table-warning'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={h.hop || `${ip || h.host}-${Math.random()}`} className={rowClass}>
|
|
||||||
<td>
|
|
||||||
<span className="badge bg-blue-lt text-blue">
|
|
||||||
{h.hop}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<span className="fw-semibold text-truncate" title={mainLabel}>
|
|
||||||
{mainLabel}
|
|
||||||
</span>
|
|
||||||
{ip && dns && (
|
|
||||||
<span className="text-muted small text-truncate" title={ip}>
|
|
||||||
IP: {ip}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{avg != null ? (
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
avg > 200
|
|
||||||
? 'text-danger fw-semibold'
|
|
||||||
: avg > 120
|
|
||||||
? 'text-warning fw-semibold'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{avg.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'—'
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{h.bestMs != null ? h.bestMs.toFixed(1) : '—'} /{' '}
|
|
||||||
{h.worstMs != null ? h.worstMs.toFixed(1) : '—'}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{loss != null ? (
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
loss >= 50
|
|
||||||
? 'badge bg-red-lt text-red'
|
|
||||||
: loss > 0
|
|
||||||
? 'badge bg-yellow-lt text-yellow'
|
|
||||||
: 'badge bg-green-lt text-green'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{loss}%
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'—'
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{severity === 'danger' && (
|
|
||||||
<span className="badge bg-red-lt text-red me-1">Проблемный хоп</span>
|
|
||||||
)}
|
|
||||||
{severity === 'warn' && (
|
|
||||||
<span className="badge bg-yellow-lt text-yellow me-1">Возможная проблема</span>
|
|
||||||
)}
|
|
||||||
<span className="text-muted small">
|
|
||||||
{statusRaw || (loss != null || avg != null ? 'OK' : '—')}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === 'ping' && (
|
{mode === 'ping' && (
|
||||||
<div className="card mb-3">
|
<div className="card mb-3">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@@ -767,26 +630,177 @@ function MikrotikTools() {
|
|||||||
{mode === 'traceroute' && (
|
{mode === 'traceroute' && (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3 className="card-title d-flex align-items-center">
|
<ul className="nav nav-tabs card-header-tabs" role="tablist">
|
||||||
<IconAlertTriangle size={18} className="me-2 text-yellow" />
|
<li className="nav-item" role="presentation">
|
||||||
Расшифровка трассировки
|
<button
|
||||||
</h3>
|
type="button"
|
||||||
</div>
|
className={`nav-link ${tracerouteTab === 'table' ? 'active' : ''}`}
|
||||||
<div className="card-body">
|
onClick={() => setTracerouteTab('table')}
|
||||||
{analysis.length === 0 ? (
|
role="tab"
|
||||||
<div className="text-muted small">
|
>
|
||||||
После выполнения трассировки здесь появится человекочитаемое описание пути и возможных проблем.
|
Результаты
|
||||||
</div>
|
</button>
|
||||||
) : (
|
</li>
|
||||||
<ul className="list-unstyled mb-0">
|
<li className="nav-item" role="presentation">
|
||||||
{analysis.map((line, idx) => (
|
<button
|
||||||
<li key={idx} className="mb-1">
|
type="button"
|
||||||
{line}
|
className={`nav-link ${tracerouteTab === 'analysis' ? 'active' : ''}`}
|
||||||
</li>
|
onClick={() => setTracerouteTab('analysis')}
|
||||||
))}
|
role="tab"
|
||||||
</ul>
|
>
|
||||||
)}
|
Расшифровка
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{tracerouteTab === 'table' ? (
|
||||||
|
<div className="card-body p-0">
|
||||||
|
{hops.length === 0 ? (
|
||||||
|
<div className="p-3 text-muted small">
|
||||||
|
Результаты трассировки появятся здесь после запуска проверки.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-vcenter card-table table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: 70 }}>Хоп</th>
|
||||||
|
<th>Адрес / узел</th>
|
||||||
|
<th style={{ width: 130 }}>Среднее, мс</th>
|
||||||
|
<th style={{ width: 140 }}>Мин / Макс, мс</th>
|
||||||
|
<th style={{ width: 110 }}>Потери</th>
|
||||||
|
<th style={{ width: 200 }}>Статус</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{hops.map((h) => {
|
||||||
|
const ip = h.ip || '';
|
||||||
|
const dns = h.dns || '';
|
||||||
|
|
||||||
|
let mainLabel;
|
||||||
|
if (useDns) {
|
||||||
|
if (dns && ip) mainLabel = `${dns} (${ip})`;
|
||||||
|
else mainLabel = dns || ip || h.host || '—';
|
||||||
|
} else {
|
||||||
|
mainLabel = ip || dns || h.host || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
const avg = typeof h.avgMs === 'number' ? h.avgMs : null;
|
||||||
|
const loss = typeof h.loss === 'number' ? h.loss : null;
|
||||||
|
const statusRaw = (h.status || '').toString();
|
||||||
|
const statusLower = statusRaw.toLowerCase();
|
||||||
|
const hasTimeout =
|
||||||
|
statusLower.includes('timeout') ||
|
||||||
|
statusLower.includes('no reply') ||
|
||||||
|
statusLower.includes('unreachable');
|
||||||
|
|
||||||
|
let severity = 'ok';
|
||||||
|
if (hasTimeout || (loss != null && loss >= 50)) {
|
||||||
|
severity = 'danger';
|
||||||
|
} else if ((loss != null && loss > 0) || (avg != null && avg > 200)) {
|
||||||
|
severity = 'warn';
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowClass =
|
||||||
|
severity === 'danger'
|
||||||
|
? 'table-danger'
|
||||||
|
: severity === 'warn'
|
||||||
|
? 'table-warning'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={h.hop || `${ip || h.host}-${Math.random()}`} className={rowClass}>
|
||||||
|
<td>
|
||||||
|
<span className="badge bg-blue-lt text-blue">
|
||||||
|
{h.hop}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="d-flex flex-column">
|
||||||
|
<span className="fw-semibold text-truncate" title={mainLabel}>
|
||||||
|
{mainLabel}
|
||||||
|
</span>
|
||||||
|
{ip && dns && (
|
||||||
|
<span className="text-muted small text-truncate" title={ip}>
|
||||||
|
IP: {ip}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{avg != null ? (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
avg > 200
|
||||||
|
? 'text-danger fw-semibold'
|
||||||
|
: avg > 120
|
||||||
|
? 'text-warning fw-semibold'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{avg.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{h.bestMs != null ? h.bestMs.toFixed(1) : '—'} /{' '}
|
||||||
|
{h.worstMs != null ? h.worstMs.toFixed(1) : '—'}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{loss != null ? (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
loss >= 50
|
||||||
|
? 'badge bg-red-lt text-red'
|
||||||
|
: loss > 0
|
||||||
|
? 'badge bg-yellow-lt text-yellow'
|
||||||
|
: 'badge bg-green-lt text-green'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loss}%
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{severity === 'danger' && (
|
||||||
|
<span className="badge bg-red-lt text-red me-1">Проблемный хоп</span>
|
||||||
|
)}
|
||||||
|
{severity === 'warn' && (
|
||||||
|
<span className="badge bg-yellow-lt text-yellow me-1">Возможная проблема</span>
|
||||||
|
)}
|
||||||
|
<span className="text-muted small">
|
||||||
|
{statusRaw || (loss != null || avg != null ? 'OK' : '—')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="card-body">
|
||||||
|
{analysis.length === 0 ? (
|
||||||
|
<div className="text-muted small">
|
||||||
|
После выполнения трассировки здесь появится человекочитаемое описание пути и возможных проблем.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="list-unstyled mb-0">
|
||||||
|
{analysis.map((line, idx) => (
|
||||||
|
<li key={idx} className="mb-1">
|
||||||
|
{line}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user