feat: Replace ForceGraph with react-vis-network-graph for improved visualization and enhance ServerManager with connection validation and deletion functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m50s

This commit is contained in:
2025-07-15 19:37:31 +07:00
parent 2590b08262
commit c5da9e357e
4 changed files with 379 additions and 265 deletions
+91 -14
View File
@@ -398,9 +398,22 @@ function ServerManager() {
setError('Все поля для связи должны быть заполнены.');
return;
}
if (newConnection.from === newConnection.to) {
setError('Сервер A и сервер B не могут быть одинаковыми.');
return;
}
setConnections([...connections, newConnection]);
setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' });
setError('');
setSuccess('Связь успешно добавлена!');
setTimeout(() => setSuccess(''), 3000);
};
// Удаление связи
const handleDeleteConnection = (index) => {
setConnections(connections.filter((_, i) => i !== index));
setSuccess('Связь удалена!');
setTimeout(() => setSuccess(''), 3000);
};
return (
@@ -668,22 +681,22 @@ function ServerManager() {
<h3 className="card-title mb-0">Добавить связь между серверами</h3>
</div>
<div className="card-body">
<div className="row g-2 align-items-end">
<div className="col-md-3">
<label className="form-label">Сервер A (IP)</label>
<div className="row g-3">
<div className="col-md-6">
<label className="form-label">Сервер A (IP/DNS)</label>
<select className="form-select" value={newConnection.from} onChange={e => setNewConnection({ ...newConnection, from: e.target.value })}>
<option value="">Выберите</option>
<option value="">Выберите сервер A</option>
{servers.map(s => <option key={s.ip} value={s.ip}>{s.ip} ({s.dns})</option>)}
</select>
</div>
<div className="col-md-3">
<label className="form-label">Сервер B (IP)</label>
<div className="col-md-6">
<label className="form-label">Сервер B (IP/DNS)</label>
<select className="form-select" value={newConnection.to} onChange={e => setNewConnection({ ...newConnection, to: e.target.value })}>
<option value="">Выберите</option>
<option value="">Выберите сервер B</option>
{servers.map(s => <option key={s.ip} value={s.ip}>{s.ip} ({s.dns})</option>)}
</select>
</div>
<div className="col-md-2">
<div className="col-md-4">
<label className="form-label">Тип туннеля</label>
<select className="form-select" value={newConnection.tunnelType} onChange={e => setNewConnection({ ...newConnection, tunnelType: e.target.value })}>
<option value="GRE">GRE</option>
@@ -692,20 +705,84 @@ function ServerManager() {
<option value="OpenVPN">OpenVPN</option>
</select>
</div>
<div className="col-md-2">
<label className="form-label">IP сервера A</label>
<div className="col-md-4">
<label className="form-label">IP адрес сервера A</label>
<input className="form-control" value={newConnection.ipA} onChange={e => setNewConnection({ ...newConnection, ipA: e.target.value })} placeholder="10.10.100.1" />
</div>
<div className="col-md-2">
<label className="form-label">IP сервера B</label>
<div className="col-md-4">
<label className="form-label">IP адрес сервера B</label>
<input className="form-control" value={newConnection.ipB} onChange={e => setNewConnection({ ...newConnection, ipB: e.target.value })} placeholder="10.10.100.2" />
</div>
<div className="col-md-12 mt-2">
<button className="btn btn-primary" type="button" onClick={handleAddConnection}>Добавить связь</button>
<div className="col-12">
<button className="btn btn-primary d-flex align-items-center" type="button" onClick={handleAddConnection}>
<span className="me-2 d-flex align-items-center"><IconPlus className="icon" /></span>
<span className="fw-bold">Добавить связь</span>
</button>
</div>
</div>
</div>
</div>
{/* Список существующих связей */}
{connections.length > 0 && (
<div className="card w-100 mb-4">
<div className="card-header">
<h3 className="card-title mb-0">Существующие связи ({connections.length})</h3>
</div>
<div className="card-body">
<div className="table-responsive">
<table className="table table-vcenter">
<thead>
<tr>
<th>Сервер A</th>
<th>Сервер B</th>
<th>Тип туннеля</th>
<th>IP адреса туннеля</th>
<th className="text-end">Действия</th>
</tr>
</thead>
<tbody>
{connections.map((connection, index) => {
const serverA = servers.find(s => s.ip === connection.from);
const serverB = servers.find(s => s.ip === connection.to);
return (
<tr key={index}>
<td>
<div>
<div className="fw-bold">{connection.from}</div>
<div className="text-muted">{serverA?.dns || 'Неизвестный сервер'}</div>
</div>
</td>
<td>
<div>
<div className="fw-bold">{connection.to}</div>
<div className="text-muted">{serverB?.dns || 'Неизвестный сервер'}</div>
</div>
</td>
<td>
<span className="badge bg-green-lt text-green">{connection.tunnelType}</span>
</td>
<td>
<code>{connection.ipA} {connection.ipB}</code>
</td>
<td className="text-end">
<button
className="btn btn-icon btn-sm btn-outline-danger rounded"
title="Удалить связь"
onClick={() => handleDeleteConnection(index)}
>
<IconTrash size={18} />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
)}
</>
);
}