refactor(MikrotikTools): enhance UI layout and styling for server and gateway selection, improving readability and user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m38s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 3m38s
This commit is contained in:
+91
-163
@@ -232,21 +232,18 @@ function MikrotikTools() {
|
||||
meta={`Текущий сервер: ${currentServerLabel}`}
|
||||
/>
|
||||
|
||||
{/* Объединённый блок: сервер + шлюз + популярные цели */}
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
{/* Сервер, шлюз, быстрые цели — один компактный блок */}
|
||||
<div className="card mb-2">
|
||||
<div className="card-header py-2">
|
||||
<div className="row align-items-center g-2">
|
||||
<div className="col">
|
||||
<h3 className="card-title mb-1">Сервер и шлюз для проверки</h3>
|
||||
<div className="text-muted small">
|
||||
Доступно Jumphost-серверов: {jumphostServers.length}.{' '}
|
||||
{serverId
|
||||
? 'Кликните по другой карточке, чтобы сменить сервер. Ниже — выбор шлюза и быстрые цели.'
|
||||
: 'Выберите сервер, чтобы выполнять проверки через него.'}
|
||||
</div>
|
||||
<h3 className="card-title mb-0">Сервер и шлюз</h3>
|
||||
<p className="text-muted small mb-0 mt-0">
|
||||
{jumphostServers.length} серверов · {serverId ? 'шлюз опционален' : 'выберите сервер'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-auto ms-auto d-print-none" style={{ minWidth: 260 }}>
|
||||
<div className="input-icon">
|
||||
<div className="col-auto">
|
||||
<div className="input-icon input-icon-sm">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
@@ -255,13 +252,13 @@ function MikrotikTools() {
|
||||
onChange={(e) => setServerSearch(e.target.value)}
|
||||
/>
|
||||
<span className="input-icon-addon">
|
||||
<IconSearch size={16} className="text-muted" />
|
||||
<IconSearch size={14} className="text-muted" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-body py-2">
|
||||
{isInitialLoading ? (
|
||||
<TableSkeleton rows={2} columns={2} />
|
||||
) : jumphostServers.length === 0 ? (
|
||||
@@ -273,7 +270,7 @@ function MikrotikTools() {
|
||||
По вашему запросу серверы не найдены. Попробуйте изменить фильтр поиска.
|
||||
</div>
|
||||
) : (
|
||||
<div className="row row-cards g-2">
|
||||
<div className="row row-cards g-1">
|
||||
{visibleJumphostServers.map((s) => {
|
||||
const id = s.id;
|
||||
const isActive = id === serverId;
|
||||
@@ -287,21 +284,16 @@ function MikrotikTools() {
|
||||
}`}
|
||||
onClick={() => setServerId(id)}
|
||||
>
|
||||
<div className="card-body d-flex align-items-start gap-2">
|
||||
<span className="avatar bg-primary-lt" title={s.country || ''}>
|
||||
<div className="card-body py-2 px-2 d-flex align-items-center gap-2">
|
||||
<span className="avatar avatar-sm bg-primary-lt" title={s.country || ''}>
|
||||
{flag || s.ip?.slice(0, 2) || '?'}
|
||||
</span>
|
||||
<div className="flex-grow-1 min-w-0">
|
||||
<div className="fw-bold text-truncate" title={s.dns || s.ip || id}>
|
||||
<div className="fw-semibold text-truncate small" title={s.dns || s.ip || id}>
|
||||
{s.dns || s.ip || id}
|
||||
</div>
|
||||
{line && (
|
||||
<div className="text-muted small text-truncate" title={line}>
|
||||
{line}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-muted small mt-1">
|
||||
{[s.country, s.provider].filter(Boolean).join(' · ') || '—'}
|
||||
<div className="text-muted small text-truncate" title={line}>
|
||||
{line || [s.country, s.provider].filter(Boolean).join(' · ') || '—'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,31 +303,34 @@ function MikrotikTools() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-muted small mt-2">
|
||||
Текущий сервер:
|
||||
<span className="fw-semibold">{currentServerLabel}</span>
|
||||
<div className="d-flex flex-wrap align-items-center gap-2 mt-2">
|
||||
<span className="badge bg-primary-lt text-primary">Сервер: {currentServerLabel}</span>
|
||||
{serverId && (
|
||||
<span className="badge bg-azure-lt text-azure">
|
||||
Шлюз: {gatewayRef || gatewayMeta?.ip || '—'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Шлюз (gateway) — только при выбранном сервере */}
|
||||
{/* Шлюз — только при выбранном сервере */}
|
||||
{serverId && (
|
||||
<>
|
||||
<hr className="my-3" />
|
||||
<h5 className="mb-2">Шлюз (gateway)</h5>
|
||||
<div className="text-muted small mb-2">
|
||||
{gatewaysForServer.length > 0
|
||||
? 'Выберите шлюз для привязки проверки (опционально). Можно искать по IP или интерфейсу ниже.'
|
||||
: 'Нет настроенных шлюзов для этого сервера. Проверка пойдёт без привязки к gateway.'}
|
||||
<hr className="my-2" />
|
||||
<div className="d-flex align-items-center gap-2 mb-1">
|
||||
<span className="fw-semibold small">Шлюз</span>
|
||||
{gatewaysForServer.length === 0 && (
|
||||
<span className="text-muted small">нет настроенных</span>
|
||||
)}
|
||||
</div>
|
||||
{loading && !networkConfig ? (
|
||||
<div className="text-muted small">Загрузка сетевых настроек…</div>
|
||||
) : (
|
||||
<>
|
||||
{gatewaysForServer.length > 0 && (
|
||||
<div className="row row-cards g-2 mb-2">
|
||||
<div className="row row-cards g-1 mb-2">
|
||||
{gatewaysForServer.map((g) => {
|
||||
const isSelected = gatewayRef === g.ip || gatewayRef === g.id;
|
||||
const flag = g.country ? countryToFlag(g.country) : '';
|
||||
const subtitle = [g.country, g.provider].filter(Boolean).join(' · ');
|
||||
return (
|
||||
<div className="col-12 col-sm-6 col-md-4 col-lg-3" key={g.id || g.ip}>
|
||||
<div
|
||||
@@ -352,20 +347,17 @@ function MikrotikTools() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="card-body d-flex align-items-start gap-2">
|
||||
<span className="avatar bg-primary-lt" title={g.country || ''}>
|
||||
<div className="card-body py-2 px-2 d-flex align-items-center gap-2">
|
||||
<span className="avatar avatar-sm bg-primary-lt" title={g.country || ''}>
|
||||
{flag || (g.ip ? g.ip.slice(0, 2) : '?')}
|
||||
</span>
|
||||
<div className="flex-grow-1 min-w-0">
|
||||
<div className="fw-bold text-truncate" title={g.ip || ''}>
|
||||
<div className="fw-semibold text-truncate small" title={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 className="text-muted small text-truncate">
|
||||
{g.description || [g.country, g.provider].filter(Boolean).join(' · ') || '—'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -380,44 +372,30 @@ function MikrotikTools() {
|
||||
gateways={gateways}
|
||||
interfaces={interfaces}
|
||||
serverId={serverId || null}
|
||||
placeholder="IP gateway или remote IP туннеля…"
|
||||
placeholder="IP gateway или интерфейс…"
|
||||
onSelectMeta={handleSelectGatewayMeta}
|
||||
/>
|
||||
<div className="text-muted small mt-2 mb-0">
|
||||
Текущий gateway:
|
||||
<span className="fw-semibold">{gatewayRef || gatewayMeta?.ip || 'не выбран'}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Популярные цели — быстрый выбор таргета по логотипу */}
|
||||
<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">
|
||||
{/* Быстрые цели — чипы */}
|
||||
<div className="d-flex flex-wrap align-items-center gap-1 mt-2">
|
||||
<span className="text-muted small me-1">Цель:</span>
|
||||
{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>
|
||||
<button
|
||||
type="button"
|
||||
key={tid}
|
||||
className={`btn btn-sm ${isSelected ? `${bgClass} ${textClass}` : 'btn-outline-secondary'}`}
|
||||
onClick={() => setTarget(tgt)}
|
||||
title={tgt}
|
||||
>
|
||||
<Icon size={14} stroke={1.5} className="me-1" />
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -426,14 +404,16 @@ function MikrotikTools() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Режим и кнопки запуска — только при выбранном сервере */}
|
||||
{serverId && (
|
||||
<div className="card mb-3">
|
||||
<div className="card-body py-2">
|
||||
<div className="row align-items-center g-2">
|
||||
<div className="col">
|
||||
<ul className="nav nav-pills" role="tablist">
|
||||
<li className="nav-item" role="presentation">
|
||||
{/* Параметры + результаты */}
|
||||
<div className="row g-2">
|
||||
<div className="col-md-4">
|
||||
<div className="card mb-2">
|
||||
<div className="card-header py-2 d-flex flex-wrap align-items-center gap-2">
|
||||
<h3 className="card-title mb-0">Параметры</h3>
|
||||
{serverId && (
|
||||
<>
|
||||
<ul className="nav nav-pills nav-pills-sm mb-0" role="tablist">
|
||||
<li className="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link ${mode === 'traceroute' ? 'active' : ''}`}
|
||||
@@ -443,7 +423,7 @@ function MikrotikTools() {
|
||||
Traceroute
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item" role="presentation">
|
||||
<li className="nav-item">
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-link ${mode === 'ping' ? 'active' : ''}`}
|
||||
@@ -454,53 +434,29 @@ function MikrotikTools() {
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-auto ms-auto">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={handleReset}
|
||||
disabled={running}
|
||||
>
|
||||
<IconRefresh size={16} className="me-1" />
|
||||
<div className="btn-list ms-auto">
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={handleReset} disabled={running}>
|
||||
<IconRefresh size={14} className="me-1" />
|
||||
Сбросить
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={handleRunCheck}
|
||||
disabled={running || loading}
|
||||
>
|
||||
{running ? 'Выполнение…' : (mode === 'traceroute' ? 'Запустить трассировку' : 'Запустить ping')}
|
||||
<button type="button" className="btn btn-sm btn-primary" onClick={handleRunCheck} disabled={running || loading}>
|
||||
{running ? '…' : (mode === 'traceroute' ? 'Трассировка' : 'Ping')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Параметры проверки + результаты */}
|
||||
<div className="row g-3">
|
||||
<div className="col-md-4">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title mb-0">Параметры проверки</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-body py-2">
|
||||
{loading && !networkConfig ? (
|
||||
<div className="text-muted small">Загрузка сетевых настроек…</div>
|
||||
<div className="text-muted small">Загрузка…</div>
|
||||
) : !serverId ? (
|
||||
<div className="text-muted small">Выберите сервер выше — шлюз и параметры появятся там.</div>
|
||||
<div className="text-muted small">Выберите сервер выше.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Домен или IP назначения</label>
|
||||
<div className="input-group input-group-flat">
|
||||
<span className="input-group-text">
|
||||
<IconWorld size={16} className="text-muted" />
|
||||
</span>
|
||||
<div className="row g-2">
|
||||
<div className="col-12">
|
||||
<label className="form-label small mb-0">Домен или IP</label>
|
||||
<div className="input-group input-group-flat input-group-sm">
|
||||
<span className="input-group-text"><IconWorld size={14} className="text-muted" /></span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
@@ -509,53 +465,25 @@ function MikrotikTools() {
|
||||
onChange={(e) => setTarget(e.target.value)}
|
||||
/>
|
||||
{gatewayMeta?.ip && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => setTarget(gatewayMeta.ip)}
|
||||
>
|
||||
<button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => setTarget(gatewayMeta.ip)}>
|
||||
IP gateway
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-text">
|
||||
Можно трассировать как до внешнего домена, так и до IP gateway/туннеля.
|
||||
</div>
|
||||
</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 className="col-6 col-md-5">
|
||||
<label className="form-label small mb-0">Макс. хопов</label>
|
||||
<input type="number" className="form-control form-control-sm" min={1} max={64} value={maxHops} onChange={(e) => setMaxHops(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Формат адресов в результатах</label>
|
||||
<div className="form-check form-switch">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
id="useDnsToggle"
|
||||
checked={useDns}
|
||||
onChange={(e) => setUseDns(e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="useDnsToggle">
|
||||
{useDns ? 'DNS + IP (если доступны)' : 'Только IP адреса'}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-text">
|
||||
Для traceroute параметр также передаётся на MikroTik как <code>use-dns</code>.
|
||||
<div className="col-12">
|
||||
<div className="form-check form-switch form-check-inline">
|
||||
<input className="form-check-input" type="checkbox" id="useDnsToggle" checked={useDns} onChange={(e) => setUseDns(e.target.checked)} />
|
||||
<label className="form-check-label small" htmlFor="useDnsToggle">{useDns ? 'DNS+IP' : 'Только IP'}</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -563,8 +491,8 @@ function MikrotikTools() {
|
||||
|
||||
<div className="col-md-8">
|
||||
{mode === 'ping' && (
|
||||
<div className="card mb-3">
|
||||
<div className="card-header">
|
||||
<div className="card mb-2">
|
||||
<div className="card-header py-2">
|
||||
<h3 className="card-title mb-0">Результаты ping</h3>
|
||||
</div>
|
||||
<div className="card-body p-0">
|
||||
@@ -681,8 +609,8 @@ function MikrotikTools() {
|
||||
)}
|
||||
|
||||
{mode === 'traceroute' && (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card mb-2">
|
||||
<div className="card-header py-2">
|
||||
<ul className="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li className="nav-item" role="presentation">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user