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:
@@ -744,6 +744,9 @@ function FilterManager() {
|
||||
const [selectedFilterKeys, setSelectedFilterKeys] = useState(new Set());
|
||||
const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null });
|
||||
|
||||
// Состояние для копирования правил с сервера
|
||||
const [copyFromServerModalOpen, setCopyFromServerModalOpen] = useState(false);
|
||||
|
||||
// Communities directory for autocomplete
|
||||
const [communities, setCommunities] = useState([]);
|
||||
// Базовая AS из настроек
|
||||
@@ -937,9 +940,9 @@ function FilterManager() {
|
||||
}
|
||||
});
|
||||
|
||||
// Фильтруем только jumphost'ы (не exit-ноды)
|
||||
// Фильтруем только jumphost'ы (исключаем exit-ноды, bgp и dns серверы)
|
||||
const jumphosts = allServers
|
||||
.filter(s => s.type !== 'exit')
|
||||
.filter(s => s.type === 'jumphost')
|
||||
.map(s => {
|
||||
const shortName = s.dns?.split('.')[0] || s.ip;
|
||||
// Ищем legacyId по имени сервера
|
||||
@@ -1252,6 +1255,47 @@ function FilterManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// Копирование правил с другого сервера
|
||||
const handleCopyRulesFromServer = async (sourceServerId) => {
|
||||
if (!selectedServer) {
|
||||
setError('Сначала выберите целевой сервер.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceServer = servers.find(s => s.id === sourceServerId);
|
||||
if (!sourceServer) {
|
||||
setError('Исходный сервер не найден.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Загружаем фильтры исходного сервера
|
||||
const response = await api.get(`/server-filters/${sourceServerId}`);
|
||||
const sourceFilters = Array.isArray(response.data) ? response.data : [];
|
||||
|
||||
if (sourceFilters.length === 0) {
|
||||
setError(`У сервера "${sourceServer.name}" нет фильтров для копирования.`);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Копируем фильтры на текущий сервер
|
||||
const updatedFilters = [...sourceFilters];
|
||||
await api.post(`/server-filters/${selectedServer.id}`, { filters: updatedFilters });
|
||||
|
||||
setServerFilters(updatedFilters);
|
||||
setCopyFromServerModalOpen(false);
|
||||
setSuccess(`Скопировано ${sourceFilters.length} фильтров с сервера "${sourceServer.name}"`);
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error copying rules from server:', error);
|
||||
setError('Не удалось скопировать фильтры.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFilter = async () => {
|
||||
if (!selectedServer) {
|
||||
setError('Сначала выберите сервер.');
|
||||
@@ -1968,6 +2012,15 @@ function FilterManager() {
|
||||
<IconSearch size={18} />
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={() => setCopyFromServerModalOpen(true)}
|
||||
disabled={servers.filter(s => s.id !== selectedServer?.id).length === 0}
|
||||
title="Скопировать правила с другого сервера"
|
||||
>
|
||||
<IconCopy className="icon me-1" />
|
||||
Скопировать с сервера
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setAddFilterModalOpen(true)}
|
||||
@@ -2516,6 +2569,67 @@ function FilterManager() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Модальное окно выбора сервера для копирования правил */}
|
||||
<Modal
|
||||
show={copyFromServerModalOpen}
|
||||
onClose={() => setCopyFromServerModalOpen(false)}
|
||||
title="Скопировать правила с сервера"
|
||||
size="md"
|
||||
>
|
||||
<div className="alert alert-info mb-3">
|
||||
<IconCopy className="me-2" />
|
||||
Выберите сервер, с которого хотите скопировать фильтры на <strong>{selectedServer?.name}</strong>
|
||||
</div>
|
||||
|
||||
<div className="alert alert-warning mb-3">
|
||||
<IconAlertTriangle className="me-2" />
|
||||
<strong>Внимание:</strong> Текущие фильтры сервера будут заменены!
|
||||
</div>
|
||||
|
||||
<div className="list-group">
|
||||
{servers
|
||||
.filter(s => s.id !== selectedServer?.id)
|
||||
.map(server => {
|
||||
const meta = inventoryServers.find(
|
||||
(inv) =>
|
||||
String(inv.dns || '').trim() === server.name ||
|
||||
String(inv.hostName || '').trim() === server.name ||
|
||||
String(inv.ip || '').trim() === server.name
|
||||
);
|
||||
const flag = meta?.country ? countryToFlag(meta.country) : '';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={server.id}
|
||||
className="list-group-item list-group-item-action d-flex align-items-center gap-3"
|
||||
onClick={() => handleCopyRulesFromServer(server.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
<span className="avatar bg-primary-lt" title={meta?.country || ''}>
|
||||
{flag || <IconServer size={16} />}
|
||||
</span>
|
||||
<div className="flex-grow-1">
|
||||
<div className="fw-bold">{server.name}</div>
|
||||
<div className="text-muted small">
|
||||
{meta?.dns || server.description || server.ip || ''}
|
||||
</div>
|
||||
</div>
|
||||
<IconChevronDown size={16} className="text-muted" style={{ transform: 'rotate(-90deg)' }} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{servers.filter(s => s.id !== selectedServer?.id).length === 0 && (
|
||||
<div className="empty py-4">
|
||||
<div className="empty-icon">
|
||||
<IconServer size={32} className="text-muted" />
|
||||
</div>
|
||||
<p className="empty-subtitle text-muted">Нет других серверов для копирования</p>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmState.open}
|
||||
title="Подтверждение"
|
||||
|
||||
@@ -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