refactor(ServerModal): simplify country selection and remove unused gateway handling logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s

This commit is contained in:
2026-02-17 12:45:16 +07:00
parent 06799406a3
commit a0b5eb910f
+12 -185
View File
@@ -11,20 +11,8 @@ import {
} from '../../utils/serverUtils.js';
/**
* Списки стран и провайдеров
* Список провайдеров
*/
const COUNTRY_OPTIONS = [
{ value: '', label: 'Выберите страну', disabled: true },
{ value: 'RU', label: 'Россия (RU)' },
{ value: 'SWE', label: 'Швеция (SWE)' },
{ value: 'FI', label: 'Финляндия (FI)' },
{ value: 'US', label: 'США (US)' },
{ value: 'DE', label: 'Германия (DE)' },
{ value: 'NL', label: 'Нидерланды (NL)' },
{ value: 'SE', label: 'Швеция (SE)' },
{ value: 'SG', label: 'Сингапур (SG)' },
];
const PROVIDER_OPTIONS = [
{ value: '', label: 'Выберите провайдера', disabled: true },
{ value: 'Yandex', label: 'Yandex Cloud' },
@@ -154,53 +142,6 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
setLocalServer(prev => ({ ...prev, [field]: value }));
};
/**
* Обработчики для gateways
*/
const handleGatewayChange = (idx, field, value) => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
const updatedGateways = currentGateways.map((gw, i) =>
i === idx ? { ...gw, [field]: value } : gw
);
return { ...prev, gateways: updatedGateways };
});
};
const handlePrimaryChange = (idx, checked) => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
const updatedGateways = currentGateways.map((gw, i) => ({
...gw,
primary: i === idx ? checked : false
}));
if (!updatedGateways.some(g => g.primary) && updatedGateways.length > 0) {
updatedGateways[idx].primary = true;
}
return { ...prev, gateways: updatedGateways };
});
};
const handleAddGateway = () => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
const newGw = makeGateway({ primary: currentGateways.length === 0 });
return { ...prev, gateways: [...currentGateways, newGw] };
});
};
const handleRemoveGateway = (idx) => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
if (currentGateways.length <= 1) return prev;
const updatedGateways = currentGateways.filter((_, i) => i !== idx);
if (!updatedGateways.some(g => g.primary) && updatedGateways.length > 0) {
updatedGateways[0].primary = true;
}
return { ...prev, gateways: updatedGateways };
});
};
/**
* Валидация и сохранение
*/
@@ -236,14 +177,13 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
return;
}
// Валидация gateways для jumphost/exit
// Шлюзы для jumphost/exit задаются в другом месте (network-config); при сохранении передаём существующие или один по умолчанию
const needsGws = NEEDS_GATEWAYS(localServer.type);
let finalGateways = [];
if (needsGws) {
finalGateways = normalizeGateways(localServer.gateways, localServer.gateway || localServer.dns || localServer.ip);
if (finalGateways.length === 0) {
setInternalError('Добавьте хотя бы один шлюз');
return;
finalGateways = [makeGateway({ primary: true, name: localServer.dns?.split('.')[0] || 'main' })];
}
}
@@ -256,9 +196,6 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
onSave && onSave(serverToSave);
};
const currentGateways = Array.isArray(localServer.gateways) ? localServer.gateways : [];
const showGateways = NEEDS_GATEWAYS(localServer.type);
return (
<div className="modal" tabIndex="-1" ref={modalRef}>
<div className="modal-dialog modal-lg">
@@ -367,26 +304,24 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
/>
</div>
{/* Страна */}
{/* Страна — 2-буквенный код как в интерфейсах */}
<div className="col-md-6">
<label className="form-label required">Страна</label>
<div className="input-icon">
<span className="input-icon-addon">
<IconMapPin size={16} />
</span>
<select
className="form-select ps-8"
<input
type="text"
className="form-control ps-8"
value={localServer.country || ''}
onChange={(e) => handleFieldChange('country', e.target.value)}
onChange={(e) => handleFieldChange('country', e.target.value.toUpperCase().slice(0, 2))}
placeholder="RU, SE, DE…"
maxLength={2}
required
>
{COUNTRY_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
{opt.label}
</option>
))}
</select>
/>
</div>
<div className="form-text">2-буквенный код страны (ISO 3166-1 alpha-2)</div>
</div>
{/* Провайдер */}
@@ -477,116 +412,8 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
</div>
)}
{/* Основной шлюз (текстовое поле для совместимости) */}
<div className="col-12">
<label className="form-label">
Основной шлюз <span className="text-muted">(устаревшее поле)</span>
</label>
<input
type="text"
className="form-control"
value={localServer.gateway || ''}
onChange={(e) => handleFieldChange('gateway', e.target.value)}
placeholder="Cloudflare, Hetzner и т.д."
/>
<div className="form-text">
Используйте секцию "Шлюзы" ниже для более детальной настройки
</div>
</div>
</div>
{/* Секция шлюзов */}
{showGateways && (
<div className="mt-4">
<div className="d-flex align-items-center justify-content-between mb-2">
<label className="form-label mb-0">
Шлюзы ({currentGateways.length})
</label>
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={handleAddGateway}
>
<IconPlus size={14} className="me-1" />
Добавить шлюз
</button>
</div>
<div className="list-group list-group-flush border rounded">
{currentGateways.map((gw, idx) => (
<div key={gw.id || `gw-${idx}`} className="list-group-item">
<div className="row g-2 align-items-end">
<div className="col-12 col-md-3">
<label className="form-label small mb-1">Имя *</label>
<input
type="text"
className="form-control form-control-sm"
value={gw.name || ''}
onChange={(e) => handleGatewayChange(idx, 'name', e.target.value)}
placeholder="GW-MSK"
required
/>
</div>
<div className="col-12 col-md-3">
<label className="form-label small mb-1">IP</label>
<input
type="text"
className="form-control form-control-sm"
value={gw.ip || ''}
onChange={(e) => handleGatewayChange(idx, 'ip', e.target.value)}
placeholder="10.0.0.1"
/>
</div>
<div className="col-12 col-md-3">
<label className="form-label small mb-1">Комментарий</label>
<input
type="text"
className="form-control form-control-sm"
value={gw.comment || ''}
onChange={(e) => handleGatewayChange(idx, 'comment', e.target.value)}
placeholder="Описание"
/>
</div>
<div className="col-6 col-md-2">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
checked={!!gw.primary}
onChange={(e) => handlePrimaryChange(idx, e.target.checked)}
id={`primary-${gw.id || idx}`}
/>
<label className="form-check-label small" htmlFor={`primary-${gw.id || idx}`}>
Основной
</label>
</div>
</div>
<div className="col-6 col-md-1 text-end">
{currentGateways.length > 1 && (
<button
type="button"
className="btn btn-ghost-danger btn-icon btn-sm"
onClick={() => handleRemoveGateway(idx)}
title="Удалить шлюз"
>
<IconTrash size={14} />
</button>
)}
</div>
</div>
<div className="text-muted small mt-1">
{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}
</div>
</div>
))}
</div>
<div className="form-text mt-2">
Для типов jumphost/exit должен быть минимум один шлюз с признаком "Основной"
</div>
</div>
)}
{/* MikroTik RouterOS API — для jumphost и домашних (входных) роутеров */}
{(localServer.type === 'jumphost' || localServer.type === 'home') && (
<div className="mt-4 pt-3 border-top">