feat: Add gateway management UI in EditServerModal, allowing users to add, edit, and remove gateways with validation for primary gateway selection, enhancing server configuration experience.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m59s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m59s
This commit is contained in:
@@ -1443,9 +1443,16 @@ function EditServerModal({ show, server, onChange, onSave, onClose }) {
|
||||
// Локальное состояние для формы
|
||||
const [localServer, setLocalServer] = useState(server || {});
|
||||
|
||||
const ensureGateways = (srv) => {
|
||||
if (!srv) return {};
|
||||
const needs = NEEDS_GATEWAYS(srv.type);
|
||||
const gateways = needs ? normalizeGateways(srv.gateways, srv.gateway || srv.dns || srv.ip) : [];
|
||||
return { ...srv, gateways };
|
||||
};
|
||||
|
||||
// Синхронизируем локальное состояние с пропсами
|
||||
useEffect(() => {
|
||||
setLocalServer(server || {});
|
||||
setLocalServer(ensureGateways(server || {}));
|
||||
}, [server]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1523,6 +1530,114 @@ function EditServerModal({ show, server, onChange, onSave, onClose }) {
|
||||
<label className="form-label">Тип туннеля</label>
|
||||
<input type="text" className="form-control" value={localServer.tunnel || ''} onChange={e => handleChange('tunnel', e.target.value)} />
|
||||
</div>
|
||||
{NEEDS_GATEWAYS(localServer.type) && (
|
||||
<>
|
||||
<div className="form-label">Шлюзы</div>
|
||||
<div className="list-group list-group-flush border rounded mb-2">
|
||||
{(localServer.gateways || []).map((gw, idx) => (
|
||||
<div key={gw.id || idx} className="list-group-item">
|
||||
<div className="row g-2 align-items-end">
|
||||
<div className="col-12 col-md-4">
|
||||
<label className="form-label small mb-1">Имя *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={gw.name || ''}
|
||||
onChange={(e) => {
|
||||
const list = (localServer.gateways || []).map((g, i) => i === idx ? { ...g, name: e.target.value } : g);
|
||||
const next = { ...localServer, gateways: list };
|
||||
setLocalServer(next);
|
||||
onChange && onChange(next);
|
||||
}}
|
||||
placeholder="GW-MSK"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-4">
|
||||
<label className="form-label small mb-1">IP</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={gw.ip || ''}
|
||||
onChange={(e) => {
|
||||
const list = (localServer.gateways || []).map((g, i) => i === idx ? { ...g, ip: e.target.value } : g);
|
||||
const next = { ...localServer, gateways: list };
|
||||
setLocalServer(next);
|
||||
onChange && onChange(next);
|
||||
}}
|
||||
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"
|
||||
value={gw.comment || ''}
|
||||
onChange={(e) => {
|
||||
const list = (localServer.gateways || []).map((g, i) => i === idx ? { ...g, comment: e.target.value } : g);
|
||||
const next = { ...localServer, gateways: list };
|
||||
setLocalServer(next);
|
||||
onChange && onChange(next);
|
||||
}}
|
||||
placeholder="Основной шлюз"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-1 d-flex align-items-center justify-content-center">
|
||||
<div className="form-check form-switch m-0">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={gw.primary}
|
||||
onChange={(e) => {
|
||||
const list = (localServer.gateways || []).map((g, i) => ({ ...g, primary: i === idx ? e.target.checked : false }));
|
||||
if (!list.some(g => g.primary)) list[idx].primary = true;
|
||||
const next = { ...localServer, gateways: list };
|
||||
setLocalServer(next);
|
||||
onChange && onChange(next);
|
||||
}}
|
||||
title="Основной шлюз"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
||||
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
|
||||
{(localServer.gateways || []).length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link text-danger px-0"
|
||||
onClick={() => {
|
||||
const list = (localServer.gateways || []).filter((_, i) => i !== idx);
|
||||
if (!list.some(g => g.primary) && list.length > 0) list[0].primary = true;
|
||||
const next = { ...localServer, gateways: list };
|
||||
setLocalServer(next);
|
||||
onChange && onChange(next);
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => {
|
||||
const list = [...(localServer.gateways || []), makeGateway({ primary: !(localServer.gateways || []).length })];
|
||||
const next = { ...localServer, gateways: list };
|
||||
setLocalServer(next);
|
||||
onChange && onChange(next);
|
||||
}}
|
||||
>
|
||||
<IconPlus className="icon" />
|
||||
Добавить шлюз
|
||||
</button>
|
||||
<div className="form-text">Для jumphost/exit должен быть один основной gateway.</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
|
||||
Reference in New Issue
Block a user