feat(MikrotikTools): add popular targets for ping/traceroute with icons for enhanced user selection
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m17s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m17s
This commit is contained in:
+109
-80
@@ -12,8 +12,18 @@ import {
|
|||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
|
IconBrandGoogle,
|
||||||
|
IconBrandYandex,
|
||||||
|
IconBrandCloudflare,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
|
/** Популярные цели для ping/traceroute — карточки с логотипами */
|
||||||
|
const POPULAR_TARGETS = [
|
||||||
|
{ id: 'yandex', name: 'Yandex', target: 'yandex.ru', Icon: IconBrandYandex, color: 'red' },
|
||||||
|
{ id: 'google', name: 'Google', target: '8.8.8.8', Icon: IconBrandGoogle, color: 'blue' },
|
||||||
|
{ id: 'cloudflare', name: 'Cloudflare DNS', target: '1.1.1.1', Icon: IconBrandCloudflare, color: 'orange' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Раздел "Инструменты → MikroTik": проверка маршрута (traceroute)
|
* Раздел "Инструменты → MikroTik": проверка маршрута (traceroute)
|
||||||
* с выбором сервера, gateway и домена/IP.
|
* с выбором сервера, gateway и домена/IP.
|
||||||
@@ -222,35 +232,31 @@ function MikrotikTools() {
|
|||||||
meta={`Текущий сервер: ${currentServerLabel}`}
|
meta={`Текущий сервер: ${currentServerLabel}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Выбор сервера для трассировки (в стиле MikrotikBackups / filters) */}
|
{/* Объединённый блок: сервер + шлюз + популярные цели */}
|
||||||
<div className="card mb-3">
|
<div className="card mb-3">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="row align-items-center g-2">
|
<div className="row align-items-center g-2">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<h3 className="card-title mb-1">Сервер для проверки</h3>
|
<h3 className="card-title mb-1">Сервер и шлюз для проверки</h3>
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
Доступно Jumphost-серверов: {jumphostServers.length}.{' '}
|
Доступно Jumphost-серверов: {jumphostServers.length}.{' '}
|
||||||
{serverId
|
{serverId
|
||||||
? 'Кликните по другой карточке, чтобы сменить сервер.'
|
? 'Кликните по другой карточке, чтобы сменить сервер. Ниже — выбор шлюза и быстрые цели.'
|
||||||
: 'Выберите сервер, чтобы выполнять проверки через него.'}
|
: 'Выберите сервер, чтобы выполнять проверки через него.'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto ms-auto d-print-none" style={{ minWidth: 260 }}>
|
<div className="col-auto ms-auto d-print-none" style={{ minWidth: 260 }}>
|
||||||
<div className="d-flex">
|
<div className="input-icon">
|
||||||
<div className="me-0 flex-grow-1">
|
<input
|
||||||
<div className="input-icon">
|
type="text"
|
||||||
<input
|
className="form-control form-control-sm"
|
||||||
type="text"
|
placeholder="Поиск серверов..."
|
||||||
className="form-control form-control-sm"
|
value={serverSearch}
|
||||||
placeholder="Поиск серверов..."
|
onChange={(e) => setServerSearch(e.target.value)}
|
||||||
value={serverSearch}
|
/>
|
||||||
onChange={(e) => setServerSearch(e.target.value)}
|
<span className="input-icon-addon">
|
||||||
/>
|
<IconSearch size={16} className="text-muted" />
|
||||||
<span className="input-icon-addon">
|
</span>
|
||||||
<IconSearch size={16} className="text-muted" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,10 +292,8 @@ function MikrotikTools() {
|
|||||||
{flag || s.ip?.slice(0, 2) || '?'}
|
{flag || s.ip?.slice(0, 2) || '?'}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-grow-1 min-w-0">
|
<div className="flex-grow-1 min-w-0">
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="fw-bold text-truncate" title={s.dns || s.ip || id}>
|
||||||
<div className="fw-bold text-truncate" title={s.dns || s.ip || id}>
|
{s.dns || s.ip || id}
|
||||||
{s.dns || s.ip || id}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{line && (
|
{line && (
|
||||||
<div className="text-muted small text-truncate" title={line}>
|
<div className="text-muted small text-truncate" title={line}>
|
||||||
@@ -311,72 +315,63 @@ function MikrotikTools() {
|
|||||||
Текущий сервер:
|
Текущий сервер:
|
||||||
<span className="fw-semibold">{currentServerLabel}</span>
|
<span className="fw-semibold">{currentServerLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Шлюз и параметры проверки — только когда выбран сервер, в том же стиле что и блок серверов */}
|
{/* Шлюз (gateway) — только при выбранном сервере */}
|
||||||
{serverId && (
|
{serverId && (
|
||||||
<>
|
<>
|
||||||
{/* Выбор gateway в стиле карточек серверов */}
|
<hr className="my-3" />
|
||||||
<div className="card mb-3">
|
<h5 className="mb-2">Шлюз (gateway)</h5>
|
||||||
<div className="card-header">
|
<div className="text-muted small mb-2">
|
||||||
<h3 className="card-title mb-1">Шлюз (gateway)</h3>
|
|
||||||
<div className="text-muted small">
|
|
||||||
{gatewaysForServer.length > 0
|
{gatewaysForServer.length > 0
|
||||||
? 'Выберите шлюз для привязки проверки (опционально). Можно искать по IP или интерфейсу ниже.'
|
? 'Выберите шлюз для привязки проверки (опционально). Можно искать по IP или интерфейсу ниже.'
|
||||||
: 'Нет настроенных шлюзов для этого сервера. Проверка пойдёт без привязки к gateway.'}
|
: 'Нет настроенных шлюзов для этого сервера. Проверка пойдёт без привязки к gateway.'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
|
||||||
{loading && !networkConfig ? (
|
{loading && !networkConfig ? (
|
||||||
<div className="text-muted small">Загрузка сетевых настроек…</div>
|
<div className="text-muted small">Загрузка сетевых настроек…</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{gatewaysForServer.length > 0 && (
|
{gatewaysForServer.length > 0 && (
|
||||||
<div className="row row-cards g-2 mb-3">
|
<div className="row row-cards g-2 mb-2">
|
||||||
{gatewaysForServer .map((g) => {
|
{gatewaysForServer.map((g) => {
|
||||||
const isSelected = gatewayRef === g.ip || gatewayRef === g.id;
|
const isSelected = gatewayRef === g.ip || gatewayRef === g.id;
|
||||||
const flag = g.country ? countryToFlag(g.country) : '';
|
const flag = g.country ? countryToFlag(g.country) : '';
|
||||||
const subtitle = [g.country, g.provider].filter(Boolean).join(' · ');
|
const subtitle = [g.country, g.provider].filter(Boolean).join(' · ');
|
||||||
return (
|
return (
|
||||||
<div className="col-12 col-sm-6 col-md-4 col-lg-3" key={g.id || g.ip}>
|
<div className="col-12 col-sm-6 col-md-4 col-lg-3" key={g.id || g.ip}>
|
||||||
<div
|
<div
|
||||||
className={`card card-sm h-100 cursor-pointer ${
|
className={`card card-sm h-100 cursor-pointer ${
|
||||||
isSelected ? 'border-primary card-hover' : 'card-hover'
|
isSelected ? 'border-primary card-hover' : 'card-hover'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const next = isSelected ? '' : (g.ip || g.id);
|
const next = isSelected ? '' : (g.ip || g.id);
|
||||||
setGatewayRef(next);
|
setGatewayRef(next);
|
||||||
if (!next) {
|
if (!next) setGatewayMeta(null);
|
||||||
setGatewayMeta(null);
|
else {
|
||||||
} else {
|
setGatewayMeta({ ip: g.ip, gateway: g });
|
||||||
setGatewayMeta({ ip: g.ip, gateway: g });
|
if (!target && g.ip) setTarget(g.ip);
|
||||||
if (!target && g.ip) setTarget(g.ip);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<div className="card-body d-flex align-items-start gap-2">
|
||||||
<div className="card-body d-flex align-items-start gap-2">
|
<span className="avatar bg-primary-lt" title={g.country || ''}>
|
||||||
<span className="avatar bg-primary-lt" title={g.country || ''}>
|
{flag || (g.ip ? g.ip.slice(0, 2) : '?')}
|
||||||
{flag || (g.ip ? g.ip.slice(0, 2) : '?')}
|
</span>
|
||||||
</span>
|
<div className="flex-grow-1 min-w-0">
|
||||||
<div className="flex-grow-1 min-w-0">
|
<div className="fw-bold text-truncate" title={g.ip || ''}>
|
||||||
<div className="fw-bold text-truncate" title={g.ip || ''}>
|
{g.ip || '—'}
|
||||||
{g.ip || '—'}
|
|
||||||
</div>
|
|
||||||
{(g.description || g.ip) && (
|
|
||||||
<div className="text-muted small text-truncate" title={g.description || g.ip}>
|
|
||||||
{g.description || g.ip}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="text-muted small mt-1">
|
|
||||||
{subtitle || '—'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{(g.description || g.ip) && (
|
||||||
|
<div className="text-muted small text-truncate" title={g.description || g.ip}>
|
||||||
|
{g.description || g.ip}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-muted small mt-1">{subtitle || '—'}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<GatewayAutocompleteInput
|
<GatewayAutocompleteInput
|
||||||
@@ -388,17 +383,52 @@ function MikrotikTools() {
|
|||||||
placeholder="IP gateway или remote IP туннеля…"
|
placeholder="IP gateway или remote IP туннеля…"
|
||||||
onSelectMeta={handleSelectGatewayMeta}
|
onSelectMeta={handleSelectGatewayMeta}
|
||||||
/>
|
/>
|
||||||
<div className="text-muted small mt-2">
|
<div className="text-muted small mt-2 mb-0">
|
||||||
Текущий gateway:
|
Текущий gateway:
|
||||||
<span className="fw-semibold">{gatewayRef || gatewayMeta?.ip || 'не выбран'}</span>
|
<span className="fw-semibold">{gatewayRef || gatewayMeta?.ip || 'не выбран'}</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Режим и кнопки запуска */}
|
{/* Популярные цели — быстрый выбор таргета по логотипу */}
|
||||||
<div className="card mb-3">
|
<hr className="my-3" />
|
||||||
|
<h5 className="mb-2">Популярные цели</h5>
|
||||||
|
<div className="text-muted small mb-2">
|
||||||
|
Клик по карточке подставит цель в поле «Домен или IP назначения».
|
||||||
|
</div>
|
||||||
|
<div className="row row-cards g-2">
|
||||||
|
{POPULAR_TARGETS.map(({ id: tid, name, target: tgt, Icon, color }) => {
|
||||||
|
const isSelected = target === tgt;
|
||||||
|
const bgClass = color === 'red' ? 'bg-red-lt' : color === 'blue' ? 'bg-blue-lt' : 'bg-orange-lt';
|
||||||
|
const textClass = color === 'red' ? 'text-red' : color === 'blue' ? 'text-blue' : 'text-orange';
|
||||||
|
return (
|
||||||
|
<div className="col-12 col-sm-6 col-md-4" key={tid}>
|
||||||
|
<div
|
||||||
|
className={`card card-sm h-100 cursor-pointer ${isSelected ? 'border-primary card-hover' : 'card-hover'}`}
|
||||||
|
onClick={() => setTarget(tgt)}
|
||||||
|
>
|
||||||
|
<div className="card-body d-flex align-items-center gap-2">
|
||||||
|
<span className={`avatar ${bgClass} ${textClass}`}>
|
||||||
|
<Icon size={24} stroke={1.5} />
|
||||||
|
</span>
|
||||||
|
<div className="flex-grow-1 min-w-0">
|
||||||
|
<div className="fw-bold text-truncate">{name}</div>
|
||||||
|
<div className="text-muted small text-truncate">{tgt}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Режим и кнопки запуска — только при выбранном сервере */}
|
||||||
|
{serverId && (
|
||||||
|
<div className="card mb-3">
|
||||||
<div className="card-body py-2">
|
<div className="card-body py-2">
|
||||||
<div className="row align-items-center g-2">
|
<div className="row align-items-center g-2">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
@@ -449,7 +479,6 @@ function MikrotikTools() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Параметры проверки + результаты */}
|
{/* Параметры проверки + результаты */}
|
||||||
|
|||||||
Reference in New Issue
Block a user