refactor(ServerModal): simplify country selection and remove unused gateway handling logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s
This commit is contained in:
@@ -11,20 +11,8 @@ import {
|
|||||||
} from '../../utils/serverUtils.js';
|
} 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 = [
|
const PROVIDER_OPTIONS = [
|
||||||
{ value: '', label: 'Выберите провайдера', disabled: true },
|
{ value: '', label: 'Выберите провайдера', disabled: true },
|
||||||
{ value: 'Yandex', label: 'Yandex Cloud' },
|
{ value: 'Yandex', label: 'Yandex Cloud' },
|
||||||
@@ -154,53 +142,6 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
|
|||||||
setLocalServer(prev => ({ ...prev, [field]: value }));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Валидация gateways для jumphost/exit
|
// Шлюзы для jumphost/exit задаются в другом месте (network-config); при сохранении передаём существующие или один по умолчанию
|
||||||
const needsGws = NEEDS_GATEWAYS(localServer.type);
|
const needsGws = NEEDS_GATEWAYS(localServer.type);
|
||||||
let finalGateways = [];
|
let finalGateways = [];
|
||||||
if (needsGws) {
|
if (needsGws) {
|
||||||
finalGateways = normalizeGateways(localServer.gateways, localServer.gateway || localServer.dns || localServer.ip);
|
finalGateways = normalizeGateways(localServer.gateways, localServer.gateway || localServer.dns || localServer.ip);
|
||||||
if (finalGateways.length === 0) {
|
if (finalGateways.length === 0) {
|
||||||
setInternalError('Добавьте хотя бы один шлюз');
|
finalGateways = [makeGateway({ primary: true, name: localServer.dns?.split('.')[0] || 'main' })];
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,9 +196,6 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
|
|||||||
onSave && onSave(serverToSave);
|
onSave && onSave(serverToSave);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentGateways = Array.isArray(localServer.gateways) ? localServer.gateways : [];
|
|
||||||
const showGateways = NEEDS_GATEWAYS(localServer.type);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal" tabIndex="-1" ref={modalRef}>
|
<div className="modal" tabIndex="-1" ref={modalRef}>
|
||||||
<div className="modal-dialog modal-lg">
|
<div className="modal-dialog modal-lg">
|
||||||
@@ -367,26 +304,24 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Страна */}
|
{/* Страна — 2-буквенный код как в интерфейсах */}
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label required">Страна</label>
|
<label className="form-label required">Страна</label>
|
||||||
<div className="input-icon">
|
<div className="input-icon">
|
||||||
<span className="input-icon-addon">
|
<span className="input-icon-addon">
|
||||||
<IconMapPin size={16} />
|
<IconMapPin size={16} />
|
||||||
</span>
|
</span>
|
||||||
<select
|
<input
|
||||||
className="form-select ps-8"
|
type="text"
|
||||||
|
className="form-control ps-8"
|
||||||
value={localServer.country || ''}
|
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
|
required
|
||||||
>
|
/>
|
||||||
{COUNTRY_OPTIONS.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-text">2-буквенный код страны (ISO 3166-1 alpha-2)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Провайдер */}
|
{/* Провайдер */}
|
||||||
@@ -477,116 +412,8 @@ function ServerModal({ show, mode = 'add', server, onSave, onClose, error: exter
|
|||||||
</div>
|
</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>
|
</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 и домашних (входных) роутеров */}
|
{/* MikroTik RouterOS API — для jumphost и домашних (входных) роутеров */}
|
||||||
{(localServer.type === 'jumphost' || localServer.type === 'home') && (
|
{(localServer.type === 'jumphost' || localServer.type === 'home') && (
|
||||||
<div className="mt-4 pt-3 border-top">
|
<div className="mt-4 pt-3 border-top">
|
||||||
|
|||||||
Reference in New Issue
Block a user