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

This commit is contained in:
2026-02-12 22:33:43 +07:00
parent dd089453bc
commit 73bc623fdf
+214 -45
View File
@@ -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>