feat: Implement gateway management in FilterManager, adding functionality for creating, updating, and validating gateways, enhancing server configuration capabilities and user experience.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m41s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m41s
This commit is contained in:
@@ -14,6 +14,24 @@ const serverConfigsListRoutes = createJsonDataRoutes('server-configs.json', (ser
|
||||
if (!server.id || !server.name) {
|
||||
return `Server at index ${i} is missing required fields`;
|
||||
}
|
||||
if (server.gateways !== undefined) {
|
||||
if (!Array.isArray(server.gateways)) {
|
||||
return `Server at index ${i} has invalid gateways (must be array)`;
|
||||
}
|
||||
if (server.gateways.length === 0) {
|
||||
return `Server at index ${i} must have at least one gateway`;
|
||||
}
|
||||
const defaults = server.gateways.filter(g => g && g.primary);
|
||||
if (defaults.length !== 1) {
|
||||
return `Server at index ${i} must have exactly one primary gateway`;
|
||||
}
|
||||
for (let j = 0; j < server.gateways.length; j++) {
|
||||
const gw = server.gateways[j] || {};
|
||||
if (!gw.name || String(gw.name).trim().length === 0) {
|
||||
return `Server at index ${i} gateway at index ${j} is missing name`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -43,6 +43,39 @@ const countryToFlag = (code) => {
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
|
||||
const normalizeGateways = (gateways = [], fallbackName = '') => {
|
||||
if (!Array.isArray(gateways)) return [];
|
||||
const cleaned = gateways
|
||||
.map((g, idx) => {
|
||||
const name = String(g?.name || '').trim();
|
||||
const ip = String(g?.ip || '').trim();
|
||||
const comment = String(g?.comment || '').trim();
|
||||
const primary = Boolean(g?.primary);
|
||||
const id = g?.id || `${Date.now()}-${idx}-${Math.random().toString(16).slice(2, 6)}`;
|
||||
return { id, name, ip, comment, primary };
|
||||
})
|
||||
.filter(g => g.name || g.ip || g.comment);
|
||||
if (cleaned.length === 0) {
|
||||
return [{ id: `${Date.now()}-primary`, name: fallbackName || 'gateway', ip: '', comment: '', primary: true }];
|
||||
}
|
||||
if (!cleaned.some(g => g.primary)) {
|
||||
cleaned[0].primary = true;
|
||||
} else {
|
||||
// если несколько primary, оставляем только первый
|
||||
let found = false;
|
||||
cleaned.forEach(g => {
|
||||
if (g.primary) {
|
||||
if (!found) {
|
||||
found = true;
|
||||
} else {
|
||||
g.primary = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
function FilterManager() {
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
@@ -57,7 +90,15 @@ function FilterManager() {
|
||||
const [servers, setServers] = useState([]);
|
||||
const [selectedServer, setSelectedServer] = useState(null);
|
||||
const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false);
|
||||
const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true });
|
||||
const makeGateway = (over = {}) => ({
|
||||
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
name: '',
|
||||
ip: '',
|
||||
comment: '',
|
||||
primary: false,
|
||||
...over,
|
||||
});
|
||||
const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] });
|
||||
|
||||
// Состояния для управления фильтрами сервера
|
||||
const [serverFilters, setServerFilters] = useState([]);
|
||||
@@ -71,6 +112,63 @@ function FilterManager() {
|
||||
// Новое состояние для редактирования сервера
|
||||
const [editServerModalOpen, setEditServerModalOpen] = useState(false);
|
||||
const [editingServer, setEditingServer] = useState(null);
|
||||
|
||||
const updateNewGateway = (idx, field, value) => {
|
||||
setNewServer((prev) => {
|
||||
const list = prev.gateways.map((g, i) => {
|
||||
if (i !== idx) return g;
|
||||
const next = { ...g, [field]: value };
|
||||
return next;
|
||||
});
|
||||
if (field === 'primary' && value) {
|
||||
list.forEach((g, i) => { g.primary = i === idx; });
|
||||
}
|
||||
return { ...prev, gateways: list };
|
||||
});
|
||||
};
|
||||
const addNewGateway = () => {
|
||||
setNewServer((prev) => ({ ...prev, gateways: [...prev.gateways, makeGateway({ primary: prev.gateways.length === 0 })] }));
|
||||
};
|
||||
const removeNewGateway = (idx) => {
|
||||
setNewServer((prev) => {
|
||||
if (prev.gateways.length === 1) return prev;
|
||||
const list = prev.gateways.filter((_, i) => i !== idx);
|
||||
if (!list.some(g => g.primary) && list.length > 0) list[0].primary = true;
|
||||
return { ...prev, gateways: list };
|
||||
});
|
||||
};
|
||||
|
||||
const updateEditGateway = (idx, field, value) => {
|
||||
setEditingServer((prev) => {
|
||||
if (!prev) return prev;
|
||||
const list = (prev.gateways || []).map((g, i) => {
|
||||
if (i !== idx) return g;
|
||||
const next = { ...g, [field]: value };
|
||||
return next;
|
||||
});
|
||||
if (field === 'primary' && value) {
|
||||
list.forEach((g, i) => { g.primary = i === idx; });
|
||||
}
|
||||
return { ...prev, gateways: list };
|
||||
});
|
||||
};
|
||||
const addEditGateway = () => {
|
||||
setEditingServer((prev) => {
|
||||
if (!prev) return prev;
|
||||
const list = [...(prev.gateways || []), makeGateway({ primary: !(prev.gateways || []).length })];
|
||||
return { ...prev, gateways: list };
|
||||
});
|
||||
};
|
||||
const removeEditGateway = (idx) => {
|
||||
setEditingServer((prev) => {
|
||||
if (!prev) return prev;
|
||||
const list = (prev.gateways || []);
|
||||
if (list.length === 1) return prev;
|
||||
const next = list.filter((_, i) => i !== idx);
|
||||
if (!next.some(g => g.primary) && next.length > 0) next[0].primary = true;
|
||||
return { ...prev, gateways: next };
|
||||
});
|
||||
};
|
||||
const [editServerError, setEditServerError] = useState('');
|
||||
|
||||
// Состояние поиска/сортировки
|
||||
@@ -181,7 +279,8 @@ function FilterManager() {
|
||||
const fetchServers = async () => {
|
||||
try {
|
||||
const response = await api.get(`/server-configs`);
|
||||
setServers(response.data);
|
||||
const payload = Array.isArray(response.data) ? response.data : [];
|
||||
setServers(payload.map(s => ({ ...s, gateways: normalizeGateways(s.gateways, s.name) })));
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
// Если файл не существует, создаем пустой массив
|
||||
@@ -200,6 +299,7 @@ function FilterManager() {
|
||||
const serverToAdd = {
|
||||
id: Date.now().toString(),
|
||||
...newServer,
|
||||
gateways: normalizeGateways(newServer.gateways, newServer.name),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
@@ -207,7 +307,7 @@ function FilterManager() {
|
||||
await api.post(`/server-configs`, { servers: updatedServers });
|
||||
|
||||
setServers(updatedServers);
|
||||
setNewServer({ name: '', description: '', enabled: true });
|
||||
setNewServer({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] });
|
||||
setServerConfigModalOpen(false);
|
||||
setSuccess('Сервер успешно добавлен!');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
@@ -522,7 +622,7 @@ function FilterManager() {
|
||||
|
||||
// Открыть модалку редактирования
|
||||
const handleEditServer = (server) => {
|
||||
setEditingServer({ ...server });
|
||||
setEditingServer({ ...server, gateways: normalizeGateways(server.gateways, server.name) });
|
||||
setEditServerError('');
|
||||
setEditServerModalOpen(true);
|
||||
};
|
||||
@@ -534,14 +634,15 @@ function FilterManager() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const edited = { ...editingServer, gateways: normalizeGateways(editingServer.gateways, editingServer.name) };
|
||||
const updatedServers = servers.map(s =>
|
||||
s.id === editingServer.id ? { ...s, name: editingServer.name, description: editingServer.description } : s
|
||||
s.id === editingServer.id ? { ...s, name: edited.name, description: edited.description, gateways: edited.gateways } : s
|
||||
);
|
||||
await api.post(`/server-configs`, { servers: updatedServers });
|
||||
setServers(updatedServers);
|
||||
// Если редактируемый сервер выбран — обновить его
|
||||
if (selectedServer?.id === editingServer.id) {
|
||||
setSelectedServer({ ...selectedServer, name: editingServer.name, description: editingServer.description });
|
||||
setSelectedServer({ ...selectedServer, name: edited.name, description: edited.description, gateways: edited.gateways });
|
||||
}
|
||||
setEditServerModalOpen(false);
|
||||
setSuccess('Сервер успешно обновлён!');
|
||||
@@ -1530,7 +1631,7 @@ function FilterManager() {
|
||||
show={serverConfigModalOpen}
|
||||
onClose={() => {
|
||||
setServerConfigModalOpen(false);
|
||||
setNewServer({ name: '', description: '', enabled: true });
|
||||
setNewServer({ name: '', description: '', enabled: true, gateways: [makeGateway({ primary: true })] });
|
||||
}}
|
||||
onSubmit={handleAddServer}
|
||||
title="Добавить новый сервер"
|
||||
@@ -1565,6 +1666,79 @@ function FilterManager() {
|
||||
helpText="Краткое описание сервера (необязательно)"
|
||||
/>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Шлюзы</label>
|
||||
<div className="list-group list-group-flush border rounded">
|
||||
{newServer.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) => updateNewGateway(idx, 'name', e.target.value)}
|
||||
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) => updateNewGateway(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"
|
||||
value={gw.comment}
|
||||
onChange={(e) => updateNewGateway(idx, 'comment', e.target.value)}
|
||||
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) => updateNewGateway(idx, 'primary', e.target.checked)}
|
||||
title="Основной шлюз"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
||||
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
|
||||
{newServer.gateways.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link text-danger px-0"
|
||||
onClick={() => removeNewGateway(idx)}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<button type="button" className="btn btn-outline-primary btn-sm" onClick={addNewGateway}>
|
||||
<IconPlus className="icon" />
|
||||
Добавить шлюз
|
||||
</button>
|
||||
<div className="form-text">Один шлюз должен быть помечен как основной.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-check form-switch mb-3">
|
||||
<input
|
||||
className="form-check-input"
|
||||
@@ -1621,6 +1795,79 @@ function FilterManager() {
|
||||
maxLength={256}
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Шлюзы</label>
|
||||
<div className="list-group list-group-flush border rounded">
|
||||
{(editingServer.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) => updateEditGateway(idx, 'name', e.target.value)}
|
||||
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) => updateEditGateway(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"
|
||||
value={gw.comment}
|
||||
onChange={(e) => updateEditGateway(idx, 'comment', e.target.value)}
|
||||
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) => updateEditGateway(idx, 'primary', e.target.checked)}
|
||||
title="Основной шлюз"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mt-2">
|
||||
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
|
||||
{(editingServer.gateways || []).length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-link text-danger px-0"
|
||||
onClick={() => removeEditGateway(idx)}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<button type="button" className="btn btn-outline-primary btn-sm" onClick={addEditGateway}>
|
||||
<IconPlus className="icon" />
|
||||
Добавить шлюз
|
||||
</button>
|
||||
<div className="form-text">Один шлюз должен быть помечен как основной.</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FormModal>
|
||||
@@ -1661,6 +1908,7 @@ function FilterManager() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Gateway по умолчанию</th>
|
||||
<th>Описание</th>
|
||||
<th>Статус</th>
|
||||
<th className="w-1"></th>
|
||||
@@ -1675,6 +1923,14 @@ function FilterManager() {
|
||||
<strong>{server.name}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{(() => {
|
||||
const primary = normalizeGateways(server.gateways, server.name)[0];
|
||||
if (!primary) return <span className="text-muted">—</span>;
|
||||
const line = [primary.name, primary.ip].filter(Boolean).join(' · ');
|
||||
return <span title={line}>{line}</span>;
|
||||
})()}
|
||||
</td>
|
||||
<td className="text-muted">{server.description || '—'}</td>
|
||||
<td>
|
||||
<span className={`badge ${server.enabled ? 'bg-green-lt text-green' : 'bg-red-lt text-red'}`}>
|
||||
|
||||
Reference in New Issue
Block a user