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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m41s
This commit is contained in:
@@ -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="Создать связь"
|
||||
|
||||
Reference in New Issue
Block a user