feat(FilterManager): add functionality to copy filter rules from another server; implement modal for server selection and enhance jumphost filtering logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m41s

This commit is contained in:
2026-01-21 18:50:55 +07:00
parent fe665ebc1f
commit dace11a407
2 changed files with 135 additions and 58 deletions
+19 -56
View File
@@ -4,12 +4,11 @@ import ConfirmDialog from './components/ConfirmDialog.jsx';
import ConfirmModal from './components/ConfirmModal.jsx';
import FormModal from './components/FormModal.jsx';
import FormField from './components/FormField.jsx';
import { useNotify } from './components/NotifyProvider.jsx';
import {
IconPlus,
IconEdit,
IconTrash,
IconCheck,
IconX,
IconDatabase,
IconDownload,
IconServer,
@@ -48,12 +47,12 @@ function countryToFlag(isoCode) {
}
function ServerManager() {
const notify = useNotify();
// === Основные данные ===
const [servers, setServers] = useState([]);
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
// === UI State ===
const [activeTab, setActiveTab] = useState('servers'); // 'servers' | 'connections'
@@ -152,10 +151,9 @@ function ServerManager() {
gateways: NEEDS_GATEWAYS(srv.type) ? normalizeGateways(srv.gateways, srv.gateway || srv.dns || srv.ip) : (Array.isArray(srv.gateways) ? srv.gateways : []),
}));
setServers(normalized);
setError('');
} catch (error) {
console.error('Error fetching servers:', error);
setError('Не удалось загрузить серверы. Проверьте, запущен ли бэкенд.');
notify.error('Не удалось загрузить серверы. Проверьте, запущен ли бэкенд.');
} finally {
setLoading(false);
}
@@ -317,8 +315,7 @@ function ServerManager() {
}
})();
setSuccess('Сервер успешно добавлен!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Сервер успешно добавлен!');
};
const handleEdit = (server) => {
@@ -339,8 +336,7 @@ function ServerManager() {
}));
setEditModalOpen(false);
setEditModalServer(null);
setSuccess('Сервер успешно обновлён!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Сервер успешно обновлён!');
};
const handleEditModalClose = () => {
@@ -359,8 +355,7 @@ function ServerManager() {
setServers(servers.filter(s => s.ip !== serverToDelete.ip));
setShowDeleteModal(false);
setServerToDelete(null);
setSuccess('Сервер удалён!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Сервер удалён!');
}
};
@@ -374,8 +369,7 @@ function ServerManager() {
setServers(servers.filter(s => !selectedServers.has(s.ip)));
setSelectedServers(new Set());
setConfirmState({ open: false, text: '', onConfirm: null });
setSuccess(`Удалено ${selectedServers.size} серверов. Не забудьте сохранить!`);
setTimeout(() => setSuccess(''), 3000);
notify.success(`Удалено ${selectedServers.size} серверов. Не забудьте сохранить!`);
},
});
};
@@ -385,8 +379,7 @@ function ServerManager() {
setServers(servers.map(s =>
selectedServers.has(s.ip) ? { ...s, tunnel: newTunnel } : s
));
setSuccess(`Туннель изменён на ${newTunnel} для ${selectedServers.size} серверов. Не забудьте сохранить!`);
setTimeout(() => setSuccess(''), 3000);
notify.success(`Туннель изменён на ${newTunnel} для ${selectedServers.size} серверов. Не забудьте сохранить!`);
};
const handleBulkExport = () => {
@@ -427,8 +420,7 @@ function ServerManager() {
if (!p) return;
setUrlSettings(prev => ({ ...prev, ...p }));
localStorage.setItem('urlSettings', JSON.stringify({ ...urlSettings, ...p }));
setSuccess('Профиль применён');
setTimeout(() => setSuccess(''), 2000);
notify.success('Профиль применён');
};
// === Clipboard ===
@@ -440,10 +432,9 @@ function ServerManager() {
textArea.select();
try {
document.execCommand('copy');
setSuccess('Скопировано в буфер обмена!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Скопировано в буфер обмена!');
} catch (err) {
setError('Не удалось скопировать');
notify.error('Не удалось скопировать');
}
document.body.removeChild(textArea);
};
@@ -452,8 +443,7 @@ function ServerManager() {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(text);
setSuccess('Скопировано в буфер обмена!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Скопировано в буфер обмена!');
return;
} catch (err) {
// fallback
@@ -551,11 +541,10 @@ function ServerManager() {
// Не прерываем основную операцию
}
setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Изменения успешно сохранены!');
} catch (error) {
console.error('Error saving changes:', error);
setError('Не удалось сохранить изменения.');
notify.error('Не удалось сохранить изменения.');
} finally {
setLoading(false);
}
@@ -564,17 +553,16 @@ function ServerManager() {
// === Connections ===
const addConnectionInternal = (draft) => {
if (!draft.from || !draft.to || !draft.tunnelType || !draft.ipA || !draft.ipB) {
setError('Все поля для связи должны быть заполнены.');
notify.error('Все поля для связи должны быть заполнены.');
return false;
}
if (draft.from === draft.to) {
setError('Сервер A и сервер B не могут быть одинаковыми.');
notify.error('Сервер A и сервер B не могут быть одинаковыми.');
return false;
}
const next = { ...draft };
setConnections((prev) => [...prev, next]);
setSuccess('Связь успешно добавлена!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Связь успешно добавлена!');
return true;
};
@@ -582,7 +570,6 @@ function ServerManager() {
const ok = addConnectionInternal(newConnection);
if (!ok) return;
setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' });
setError('');
};
const handleConfirmGraphConnection = () => {
@@ -590,38 +577,15 @@ function ServerManager() {
if (!ok) return;
setGraphConnectionModalOpen(false);
setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' });
setError('');
};
const handleDeleteConnection = (index) => {
setConnections(connections.filter((_, i) => i !== index));
setSuccess('Связь удалена!');
setTimeout(() => setSuccess(''), 3000);
notify.success('Связь удалена!');
};
return (
<div className="server-manager-layout">
{/* Уведомления */}
{error && (
<div className="alert alert-danger alert-dismissible" role="alert" style={{ position: 'fixed', top: 80, left: '50%', transform: 'translateX(-50%)', zIndex: 9999, minWidth: 300 }}>
<div className="d-flex">
<IconX className="icon alert-icon" />
<div>{error}</div>
</div>
<button type="button" className="btn-close" onClick={() => setError('')}></button>
</div>
)}
{success && (
<div className="alert alert-success alert-dismissible" role="alert" style={{ position: 'fixed', top: 80, left: '50%', transform: 'translateX(-50%)', zIndex: 9999, minWidth: 300 }}>
<div className="d-flex">
<IconCheck className="icon alert-icon" />
<div>{success}</div>
</div>
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
</div>
)}
{/* Header */}
<div className="page-header d-print-none mb-3">
<div className="row align-items-center">
@@ -1113,7 +1077,6 @@ function ServerManager() {
onClose={() => {
setGraphConnectionModalOpen(false);
setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' });
setError('');
}}
onSubmit={handleConfirmGraphConnection}
title="Создать связь"