feat(MikrotikTools): add ping functionality alongside traceroute with mode selection and result handling
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m54s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m54s
This commit is contained in:
+214
-45
@@ -31,9 +31,12 @@ function MikrotikTools() {
|
||||
const [target, setTarget] = useState('');
|
||||
const [maxHops, setMaxHops] = useState(30);
|
||||
|
||||
const [mode, setMode] = useState('traceroute'); // 'traceroute' | 'ping'
|
||||
|
||||
const [running, setRunning] = useState(false);
|
||||
const [hops, setHops] = useState([]);
|
||||
const [analysis, setAnalysis] = useState([]);
|
||||
const [pingResult, setPingResult] = useState(null);
|
||||
const [useDns, setUseDns] = useState(true);
|
||||
|
||||
const [serverSearch, setServerSearch] = useState('');
|
||||
@@ -77,50 +80,73 @@ function MikrotikTools() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunTraceroute = async () => {
|
||||
const handleRunCheck = async () => {
|
||||
if (!serverId) {
|
||||
notify.error('Выберите сервер (jumphost)');
|
||||
return;
|
||||
}
|
||||
if (!target || String(target).trim() === '') {
|
||||
notify.error('Укажите домен или IP для трассировки');
|
||||
|
||||
const trimmedTarget = String(target || '').trim();
|
||||
if (!trimmedTarget) {
|
||||
notify.error('Укажите домен или IP назначения');
|
||||
return;
|
||||
}
|
||||
|
||||
setRunning(true);
|
||||
setHops([]);
|
||||
setAnalysis([]);
|
||||
setPingResult(null);
|
||||
|
||||
try {
|
||||
const body = {
|
||||
serverId,
|
||||
target: String(target).trim(),
|
||||
useDns,
|
||||
};
|
||||
// gatewayRef может быть как ID gateway, так и IP/строка —
|
||||
// для простоты, если в meta есть ip — используем его как gatewayIp.
|
||||
if (gatewayMeta?.ip) {
|
||||
body.gatewayIp = gatewayMeta.ip;
|
||||
}
|
||||
// Max hops
|
||||
const hopsNum = Number(maxHops);
|
||||
if (Number.isFinite(hopsNum) && hopsNum > 0) {
|
||||
body.maxHops = hopsNum;
|
||||
}
|
||||
if (mode === 'traceroute') {
|
||||
const body = {
|
||||
serverId,
|
||||
target: trimmedTarget,
|
||||
useDns,
|
||||
};
|
||||
if (gatewayMeta?.ip) {
|
||||
body.gatewayIp = gatewayMeta.ip;
|
||||
}
|
||||
const hopsNum = Number(maxHops);
|
||||
if (Number.isFinite(hopsNum) && hopsNum > 0) {
|
||||
body.maxHops = hopsNum;
|
||||
}
|
||||
|
||||
const res = await api.post('/mikrotik/traceroute', body);
|
||||
const data = res?.data || {};
|
||||
if (!data.ok) {
|
||||
notify.error(data.error || 'Трассировка завершилась с ошибкой');
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/mikrotik/traceroute', body);
|
||||
const data = res?.data || {};
|
||||
if (!data.ok) {
|
||||
notify.error(data.error || 'Трассировка завершилась с ошибкой');
|
||||
return;
|
||||
}
|
||||
|
||||
const hopsList = Array.isArray(data.hops) ? data.hops : [];
|
||||
setHops(hopsList);
|
||||
setAnalysis(buildTracerouteAnalysis(hopsList, networkConfig, servers, body.target));
|
||||
const hopsList = Array.isArray(data.hops) ? data.hops : [];
|
||||
setHops(hopsList);
|
||||
setAnalysis(buildTracerouteAnalysis(hopsList, networkConfig, servers, body.target));
|
||||
} else {
|
||||
// mode === 'ping'
|
||||
const body = {
|
||||
serverId,
|
||||
target: trimmedTarget,
|
||||
count: 4,
|
||||
};
|
||||
if (gatewayMeta?.ip) {
|
||||
body.gatewayIp = gatewayMeta.ip;
|
||||
}
|
||||
|
||||
const res = await api.post('/mikrotik/ping', body);
|
||||
const data = res?.data || {};
|
||||
if (data.ok === false) {
|
||||
notify.error(data.error || 'Ping завершился с ошибкой');
|
||||
return;
|
||||
}
|
||||
setPingResult({
|
||||
target: trimmedTarget,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MikrotikTools] traceroute failed', error);
|
||||
notify.error(error?.message || 'Ошибка при выполнении трассировки');
|
||||
console.error('[MikrotikTools] check failed', error);
|
||||
notify.error(error?.message || 'Ошибка при выполнении проверки');
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
@@ -132,6 +158,7 @@ function MikrotikTools() {
|
||||
setTarget('');
|
||||
setHops([]);
|
||||
setAnalysis([]);
|
||||
setPingResult(null);
|
||||
};
|
||||
|
||||
const jumphostServers = useMemo(
|
||||
@@ -180,7 +207,7 @@ function MikrotikTools() {
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
title="MikroTik Tools"
|
||||
pretitle="Traceroute через выбранный jumphost/gateway"
|
||||
pretitle="Проверка доступности через выбранный jumphost/gateway"
|
||||
meta={`Текущий сервер: ${currentServerLabel}`}
|
||||
/>
|
||||
|
||||
@@ -277,7 +304,25 @@ function MikrotikTools() {
|
||||
<div className="col-md-4">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Параметры проверки</h3>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<h3 className="card-title mb-0">Параметры проверки</h3>
|
||||
<div className="btn-group btn-group-sm" role="group" aria-label="Тип проверки">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${mode === 'traceroute' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => { setMode('traceroute'); setHops([]); setAnalysis([]); setPingResult(null); }}
|
||||
>
|
||||
Traceroute
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${mode === 'ping' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => { setMode('ping'); setHops([]); setAnalysis([]); setPingResult(null); }}
|
||||
>
|
||||
Ping
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{loading && !networkConfig ? (
|
||||
@@ -397,17 +442,19 @@ function MikrotikTools() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Максимальное число хопов</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
min={1}
|
||||
max={64}
|
||||
value={maxHops}
|
||||
onChange={(e) => setMaxHops(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{mode === 'traceroute' && (
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Максимальное число хопов</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
min={1}
|
||||
max={64}
|
||||
value={maxHops}
|
||||
onChange={(e) => setMaxHops(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Формат адресов в результатах</label>
|
||||
@@ -424,7 +471,7 @@ function MikrotikTools() {
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-text">
|
||||
Этот параметр также передаётся на MikroTik как <code>use-dns</code>.
|
||||
Для traceroute параметр также передаётся на MikroTik как <code>use-dns</code>.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -443,16 +490,17 @@ function MikrotikTools() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleRunTraceroute}
|
||||
onClick={handleRunCheck}
|
||||
disabled={running || loading}
|
||||
>
|
||||
{running ? 'Трассировка…' : 'Запустить трассировку'}
|
||||
{running ? 'Выполнение…' : (mode === 'traceroute' ? 'Запустить трассировку' : 'Запустить ping')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-8">
|
||||
{mode === 'traceroute' && (
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title mb-0">Результаты трассировки</h3>
|
||||
@@ -588,7 +636,127 @@ function MikrotikTools() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'ping' && (
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title mb-0">Результаты ping</h3>
|
||||
</div>
|
||||
<div className="card-body p-0">
|
||||
{!pingResult ? (
|
||||
<div className="p-3 text-muted small">
|
||||
Результаты ping появятся здесь после запуска проверки.
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter card-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
{(() => {
|
||||
const avg = typeof pingResult.avgMs === 'number' ? pingResult.avgMs : null;
|
||||
const loss = typeof pingResult.loss === 'number' ? pingResult.loss : null;
|
||||
|
||||
let severity = 'ok';
|
||||
if (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 className={rowClass}>
|
||||
<td>
|
||||
<div className="d-flex flex-column">
|
||||
<span className="fw-semibold">
|
||||
{pingResult.target || target || '—'}
|
||||
</span>
|
||||
{gatewayMeta?.ip && (
|
||||
<span className="text-muted small">
|
||||
через gateway {gatewayMeta.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>
|
||||
{pingResult.minMs != null ? pingResult.minMs.toFixed(1) : '—'} /{' '}
|
||||
{pingResult.maxMs != null ? pingResult.maxMs.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">
|
||||
{loss === 0
|
||||
? 'Все пакеты доставлены'
|
||||
: loss != null
|
||||
? `Потеряно пакетов: ${loss}%`
|
||||
: 'Нет данных о потере пакетов'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'traceroute' && (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title d-flex align-items-center">
|
||||
@@ -612,6 +780,7 @@ function MikrotikTools() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user