feat: Implement server filters management API and integrate filter handling in FilterManager for enhanced server configuration capabilities
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m25s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m25s
This commit is contained in:
@@ -648,6 +648,137 @@ app.delete('/api/server-configs/:serverId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Server Filters Routes ---
|
||||||
|
|
||||||
|
// Get server filters
|
||||||
|
app.get('/api/server-filters/:serverId', async (req, res) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
const params = {
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: `server-filters-${serverId}.json`,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await s3.getObject(params).promise();
|
||||||
|
const fileContent = data.Body.toString('utf-8');
|
||||||
|
let filters = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
filters = JSON.parse(fileContent);
|
||||||
|
if (!Array.isArray(filters)) {
|
||||||
|
filters = [];
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Error parsing server filters:', parseError);
|
||||||
|
filters = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(filters);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'NoSuchKey') {
|
||||||
|
res.json([]); // Return empty array if file does not exist
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Error reading server filters from S3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update server filters
|
||||||
|
app.post('/api/server-filters/:serverId', async (req, res) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
const { filters } = req.body;
|
||||||
|
|
||||||
|
// Validate filters structure
|
||||||
|
if (!Array.isArray(filters)) {
|
||||||
|
return res.status(400).send('Filters must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each filter has required fields
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
const filter = filters[i];
|
||||||
|
if (!filter.community || !filter.gateway) {
|
||||||
|
return res.status(400).send(`Filter at index ${i} is missing required fields`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: `server-filters-${serverId}.json`,
|
||||||
|
Body: JSON.stringify(filters, null, 2), // Pretty print JSON
|
||||||
|
ContentType: 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await s3.putObject(params).promise();
|
||||||
|
res.send('Server filters updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Error writing server filters to S3');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate MikroTik configuration from server filters
|
||||||
|
app.post('/api/server-filters/generate-config', async (req, res) => {
|
||||||
|
const { filters } = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(filters) || filters.length === 0) {
|
||||||
|
return res.json({ config: '// No filters to generate configuration' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group filters by gateway
|
||||||
|
const gatewayGroups = {};
|
||||||
|
filters.forEach(filter => {
|
||||||
|
if (!gatewayGroups[filter.gateway]) {
|
||||||
|
gatewayGroups[filter.gateway] = [];
|
||||||
|
}
|
||||||
|
gatewayGroups[filter.gateway].push(filter.community);
|
||||||
|
});
|
||||||
|
|
||||||
|
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
|
||||||
|
config += '// Generated automatically\n';
|
||||||
|
config += `// Date: ${new Date().toISOString()}\n\n`;
|
||||||
|
|
||||||
|
// Если есть только один gateway, создаем простую конфигурацию
|
||||||
|
if (Object.keys(gatewayGroups).length === 1) {
|
||||||
|
const [gateway, communities] = Object.entries(gatewayGroups)[0];
|
||||||
|
config += `if (\n`;
|
||||||
|
communities.forEach((community, index) => {
|
||||||
|
config += `(bgp-communities includes ${community})`;
|
||||||
|
if (index < communities.length - 1) {
|
||||||
|
config += ` \nor `;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config += `\n)\n{\n set gw ${gateway}; accept;\n}\nelse\n{\n reject;\n}\n`;
|
||||||
|
} else {
|
||||||
|
// Если несколько gateway, создаем каскадную структуру
|
||||||
|
const gatewayEntries = Object.entries(gatewayGroups);
|
||||||
|
gatewayEntries.forEach(([gateway, communities], gatewayIndex) => {
|
||||||
|
if (gatewayIndex === 0) {
|
||||||
|
config += `if (\n`;
|
||||||
|
} else {
|
||||||
|
config += `else if (\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
communities.forEach((community, index) => {
|
||||||
|
config += `(bgp-communities includes ${community})`;
|
||||||
|
if (index < communities.length - 1) {
|
||||||
|
config += ` \nor `;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config += `\n)\n{\n set gw ${gateway}; accept;\n}`;
|
||||||
|
|
||||||
|
if (gatewayIndex === gatewayEntries.length - 1) {
|
||||||
|
config += `\nelse\n{\n reject;\n}\n`;
|
||||||
|
} else {
|
||||||
|
config += `\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ config });
|
||||||
|
});
|
||||||
|
|
||||||
// The "catchall" handler: for any request that doesn't
|
// The "catchall" handler: for any request that doesn't
|
||||||
// match one above, send back React's index.html file.
|
// match one above, send back React's index.html file.
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
|
|||||||
+264
-319
@@ -25,58 +25,51 @@ import {
|
|||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
function FilterManager() {
|
function FilterManager() {
|
||||||
const [filters, setFilters] = useState([]);
|
|
||||||
const [newFilter, setNewFilter] = useState({
|
|
||||||
community: '',
|
|
||||||
gateway: '',
|
|
||||||
description: ''
|
|
||||||
});
|
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [editingFilter, setEditingFilter] = useState(null);
|
|
||||||
const [editingValues, setEditingValues] = useState({});
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
||||||
const [filterToDelete, setFilterToDelete] = useState(null);
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [sortField, setSortField] = useState('community');
|
|
||||||
const [sortOrder, setSortOrder] = useState('asc');
|
|
||||||
const [filterGateway, setFilterGateway] = useState('');
|
|
||||||
const editInputRef = useRef(null);
|
|
||||||
const pageSize = 10;
|
|
||||||
|
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
||||||
const [editModalFilter, setEditModalFilter] = useState(null);
|
|
||||||
|
|
||||||
// Состояние для предварительного просмотра конфигурации
|
// Состояние для предварительного просмотра конфигурации
|
||||||
const [previewModalOpen, setPreviewModalOpen] = useState(false);
|
const [previewModalOpen, setPreviewModalOpen] = useState(false);
|
||||||
const [generatedConfig, setGeneratedConfig] = useState('');
|
const [generatedConfig, setGeneratedConfig] = useState('');
|
||||||
|
|
||||||
// Состояние для модального окна добавления фильтра
|
// Состояния для управления серверами
|
||||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
|
||||||
|
|
||||||
// Новые состояния для управления серверами
|
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [selectedServer, setSelectedServer] = useState(null);
|
const [selectedServer, setSelectedServer] = useState(null);
|
||||||
const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false);
|
const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false);
|
||||||
const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true });
|
const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true });
|
||||||
const [serverConfigs, setServerConfigs] = useState({});
|
|
||||||
|
// Состояния для управления фильтрами сервера
|
||||||
|
const [serverFilters, setServerFilters] = useState([]);
|
||||||
|
const [newFilter, setNewFilter] = useState({ community: '', gateway: '', description: '' });
|
||||||
|
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||||
|
const [editFilterModalOpen, setEditFilterModalOpen] = useState(false);
|
||||||
|
const [editingFilter, setEditingFilter] = useState(null);
|
||||||
|
const [deleteFilterModalOpen, setDeleteFilterModalOpen] = useState(false);
|
||||||
|
const [filterToDelete, setFilterToDelete] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchFilters();
|
|
||||||
fetchServers();
|
fetchServers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchFilters = async () => {
|
// Загружаем фильтры при выборе сервера
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedServer) {
|
||||||
|
fetchServerFilters(selectedServer.id);
|
||||||
|
} else {
|
||||||
|
setServerFilters([]);
|
||||||
|
}
|
||||||
|
}, [selectedServer]);
|
||||||
|
|
||||||
|
const fetchServerFilters = async (serverId) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_URL}/filters`);
|
const response = await axios.get(`${API_URL}/server-filters/${serverId}`);
|
||||||
setFilters(response.data);
|
setServerFilters(response.data);
|
||||||
setError('');
|
setError('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching filters:', error);
|
console.error('Error fetching server filters:', error);
|
||||||
setError('Не удалось загрузить фильтры. Проверьте, запущен ли бэкенд.');
|
setServerFilters([]); // Если файл не существует, создаем пустой массив
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -301,139 +294,92 @@ function FilterManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddFilter = () => {
|
const handleAddFilter = async () => {
|
||||||
if (!newFilter.community.trim() || !newFilter.gateway.trim()) {
|
if (!selectedServer) {
|
||||||
setError('Поля Community и Gateway должны быть заполнены.');
|
setError('Сначала выберите сервер.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError('');
|
|
||||||
setFilters([...filters, { ...newFilter }]);
|
|
||||||
setNewFilter({ community: '', gateway: '', description: '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функции для модального окна добавления фильтра
|
|
||||||
const handleOpenAddFilterModal = () => {
|
|
||||||
setAddFilterModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseAddFilterModal = () => {
|
|
||||||
setAddFilterModalOpen(false);
|
|
||||||
setNewFilter({ community: '', gateway: '', description: '' });
|
|
||||||
setError('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddFilterFromModal = () => {
|
|
||||||
if (!newFilter.community.trim() || !newFilter.gateway.trim()) {
|
if (!newFilter.community.trim() || !newFilter.gateway.trim()) {
|
||||||
setError('Поля Community и Gateway должны быть заполнены.');
|
setError('Поля Community и Gateway должны быть заполнены.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setError('');
|
setError('');
|
||||||
setFilters([...filters, { ...newFilter }]);
|
const updatedFilters = [...serverFilters, { ...newFilter }];
|
||||||
|
setServerFilters(updatedFilters);
|
||||||
setNewFilter({ community: '', gateway: '', description: '' });
|
setNewFilter({ community: '', gateway: '', description: '' });
|
||||||
setAddFilterModalOpen(false);
|
setAddFilterModalOpen(false);
|
||||||
setSuccess('Фильтр успешно добавлен!');
|
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (filter) => {
|
// Сохраняем фильтры сервера
|
||||||
setEditModalFilter({ ...filter });
|
|
||||||
setEditModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditModalChange = (updatedFilter) => {
|
|
||||||
setEditModalFilter(updatedFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditModalSave = (updatedFilter) => {
|
|
||||||
setFilters(filters.map(f => f.community === updatedFilter.community ? { ...updatedFilter } : f));
|
|
||||||
setEditModalOpen(false);
|
|
||||||
setEditModalFilter(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditModalClose = () => {
|
|
||||||
setEditModalOpen(false);
|
|
||||||
setEditModalFilter(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (filter) => {
|
|
||||||
setFilterToDelete(filter);
|
|
||||||
setShowDeleteModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (filterToDelete) {
|
|
||||||
setFilters(filters.filter(f => f.community !== filterToDelete.community));
|
|
||||||
setShowDeleteModal(false);
|
|
||||||
setFilterToDelete(null);
|
|
||||||
setSuccess('Фильтр удален!');
|
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveChanges = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
await axios.post(`${API_URL}/filters`, { domains: filters });
|
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters });
|
||||||
setSuccess('Изменения сохранены в S3!');
|
setSuccess('Фильтр успешно добавлен!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving filters:', error);
|
console.error('Error saving server filters:', error);
|
||||||
setError('Не удалось сохранить изменения.');
|
setError('Не удалось сохранить фильтр.');
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSort = (field) => {
|
const handleEditFilter = (filter) => {
|
||||||
if (sortField === field) {
|
setEditingFilter({ ...filter });
|
||||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
setEditFilterModalOpen(true);
|
||||||
} else {
|
|
||||||
setSortField(field);
|
|
||||||
setSortOrder('asc');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditKeyDown = (e, filterCommunity) => {
|
const handleSaveEditFilter = async (updatedFilter) => {
|
||||||
if (e.key === 'Enter') {
|
const updatedFilters = serverFilters.map(f =>
|
||||||
handleSaveEdit(filterCommunity);
|
f.community === updatedFilter.community ? { ...updatedFilter } : f
|
||||||
} else if (e.key === 'Escape') {
|
);
|
||||||
handleCancelEdit(filterCommunity);
|
setServerFilters(updatedFilters);
|
||||||
}
|
setEditFilterModalOpen(false);
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveEdit = (filterCommunity) => {
|
|
||||||
const filter = filters.find(f => f.community === filterCommunity);
|
|
||||||
if (filter && editingValues[filterCommunity]) {
|
|
||||||
setFilters(filters.map(f =>
|
|
||||||
f.community === filterCommunity
|
|
||||||
? { ...f, ...editingValues[filterCommunity] }
|
|
||||||
: f
|
|
||||||
));
|
|
||||||
setEditingFilter(null);
|
|
||||||
setEditingValues({});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelEdit = (filterCommunity) => {
|
|
||||||
setEditingFilter(null);
|
setEditingFilter(null);
|
||||||
setEditingValues({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const startEdit = (filter) => {
|
// Сохраняем фильтры сервера
|
||||||
setEditingFilter(filter.community);
|
|
||||||
setEditingValues({ [filter.community]: { ...filter } });
|
|
||||||
setTimeout(() => {
|
|
||||||
if (editInputRef.current) {
|
|
||||||
editInputRef.current.focus();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для генерации конфигурации MikroTik
|
|
||||||
const generateMikrotikConfig = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_URL}/filters/generate-config`);
|
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters });
|
||||||
|
setSuccess('Фильтр успешно обновлен!');
|
||||||
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving server filters:', error);
|
||||||
|
setError('Не удалось сохранить фильтр.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteFilter = async (filter) => {
|
||||||
|
const updatedFilters = serverFilters.filter(f => f.community !== filter.community);
|
||||||
|
setServerFilters(updatedFilters);
|
||||||
|
setDeleteFilterModalOpen(false);
|
||||||
|
setFilterToDelete(null);
|
||||||
|
|
||||||
|
// Сохраняем фильтры сервера
|
||||||
|
try {
|
||||||
|
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters });
|
||||||
|
setSuccess('Фильтр успешно удален!');
|
||||||
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving server filters:', error);
|
||||||
|
setError('Не удалось удалить фильтр.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Удаляем старые функции редактирования
|
||||||
|
|
||||||
|
// Удаляем старые функции, которые больше не нужны
|
||||||
|
|
||||||
|
// Функция для генерации конфигурации MikroTik для выбранного сервера
|
||||||
|
const generateMikrotikConfig = async () => {
|
||||||
|
if (!selectedServer) {
|
||||||
|
return '// Сначала выберите сервер';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverFilters.length === 0) {
|
||||||
|
return '// Нет фильтров для генерации конфигурации';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_URL}/server-filters/generate-config`, {
|
||||||
|
filters: serverFilters
|
||||||
|
});
|
||||||
return response.data.config;
|
return response.data.config;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating config:', error);
|
console.error('Error generating config:', error);
|
||||||
@@ -496,26 +442,7 @@ function FilterManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Фильтрация и сортировка
|
// Удаляем старые переменные фильтрации
|
||||||
const filteredFilters = filters
|
|
||||||
.filter(filter => {
|
|
||||||
const matchesSearch = filter.community.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
filter.gateway.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
filter.description.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
const matchesGateway = !filterGateway || filter.gateway === filterGateway;
|
|
||||||
return matchesSearch && matchesGateway;
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aValue = a[sortField] || '';
|
|
||||||
const bValue = b[sortField] || '';
|
|
||||||
if (sortOrder === 'asc') {
|
|
||||||
return aValue.localeCompare(bValue);
|
|
||||||
} else {
|
|
||||||
return bValue.localeCompare(aValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const uniqueGateways = [...new Set(filters.map(f => f.gateway))].sort();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
@@ -527,43 +454,32 @@ function FilterManager() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-auto ms-auto d-print-none">
|
<div className="col-auto ms-auto d-print-none">
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
{selectedServer && (
|
||||||
className="btn btn-outline-primary"
|
<>
|
||||||
onClick={handlePreviewConfig}
|
<button
|
||||||
>
|
className="btn btn-outline-primary"
|
||||||
<IconEye className="me-2" />
|
onClick={handlePreviewConfig}
|
||||||
Предварительный просмотр
|
>
|
||||||
</button>
|
<IconEye className="me-2" />
|
||||||
<button
|
Предварительный просмотр
|
||||||
className="btn btn-outline-primary"
|
</button>
|
||||||
onClick={copyConfigToClipboard}
|
<button
|
||||||
>
|
className="btn btn-outline-primary"
|
||||||
<IconCopy className="me-2" />
|
onClick={copyConfigToClipboard}
|
||||||
Копировать конфигурацию
|
>
|
||||||
</button>
|
<IconCopy className="me-2" />
|
||||||
<button
|
Копировать конфигурацию
|
||||||
className="btn btn-outline-success"
|
</button>
|
||||||
onClick={exportConfigToS3}
|
<button
|
||||||
disabled={loading}
|
className="btn btn-outline-success"
|
||||||
>
|
onClick={() => handleSaveServerConfig(selectedServer.id)}
|
||||||
<IconDownload className="me-2" />
|
disabled={loading}
|
||||||
Экспорт в S3
|
>
|
||||||
</button>
|
<IconDeviceFloppy className="me-2" />
|
||||||
<button
|
Сохранить конфигурацию
|
||||||
className="btn btn-primary"
|
</button>
|
||||||
onClick={handleOpenAddFilterModal}
|
</>
|
||||||
>
|
)}
|
||||||
<IconPlus className="me-2" />
|
|
||||||
Добавить фильтр
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-success"
|
|
||||||
onClick={handleSaveChanges}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<IconDatabase className="me-2" />
|
|
||||||
{loading ? 'Сохранение...' : 'Сохранить в S3'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -648,37 +564,30 @@ function FilterManager() {
|
|||||||
<p className="text-muted small mb-3">{server.description}</p>
|
<p className="text-muted small mb-3">{server.description}</p>
|
||||||
)}
|
)}
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-primary btn-sm"
|
className={`btn btn-sm ${selectedServer?.id === server.id ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
onClick={() => handleLoadServerConfig(server.id)}
|
onClick={() => setSelectedServer(server)}
|
||||||
>
|
>
|
||||||
<IconEye className="me-1" />
|
<IconFilter className="me-1" />
|
||||||
Просмотр
|
{selectedServer?.id === server.id ? 'Выбран' : 'Выбрать'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-success btn-sm"
|
className="btn btn-outline-warning btn-sm"
|
||||||
onClick={() => handleSaveServerConfig(server.id)}
|
onClick={() => handleCopyConfigToAllServers(server.id)}
|
||||||
>
|
disabled={loading || servers.filter(s => s.id !== server.id && s.enabled).length === 0}
|
||||||
<IconDeviceFloppy className="me-1" />
|
title="Скопировать конфигурацию на все остальные серверы"
|
||||||
Сохранить
|
>
|
||||||
</button>
|
<IconCopy className="me-1" />
|
||||||
<button
|
Копировать на все
|
||||||
className="btn btn-outline-warning btn-sm"
|
</button>
|
||||||
onClick={() => handleCopyConfigToAllServers(server.id)}
|
<button
|
||||||
disabled={loading || servers.filter(s => s.id !== server.id && s.enabled).length === 0}
|
className="btn btn-outline-danger btn-sm"
|
||||||
title="Скопировать конфигурацию на все остальные серверы"
|
onClick={() => handleDeleteServer(server.id)}
|
||||||
>
|
>
|
||||||
<IconCopy className="me-1" />
|
<IconTrash className="me-1" />
|
||||||
Копировать на все
|
Удалить
|
||||||
</button>
|
</button>
|
||||||
<button
|
</div>
|
||||||
className="btn btn-outline-danger btn-sm"
|
|
||||||
onClick={() => handleDeleteServer(server.id)}
|
|
||||||
>
|
|
||||||
<IconTrash className="me-1" />
|
|
||||||
Удалить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -690,112 +599,148 @@ function FilterManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Фильтры и поиск */}
|
{/* Секция фильтров выбранного сервера */}
|
||||||
<div className="card w-100">
|
{selectedServer && (
|
||||||
<div className="card-header d-flex justify-content-between align-items-center">
|
<div className="row mb-4">
|
||||||
<h3 className="card-title mb-0">Список фильтров</h3>
|
<div className="col-12">
|
||||||
<div className="d-flex gap-2 w-75">
|
<div className="card">
|
||||||
{/* Фильтры и поиск */}
|
<div className="card-header">
|
||||||
<select className="form-select w-auto" value={filterGateway} onChange={e => setFilterGateway(e.target.value)}>
|
<h3 className="card-title">
|
||||||
<option value="">Все gateway</option>
|
<IconFilter className="me-2" />
|
||||||
{uniqueGateways.map(gateway => (
|
Фильтры сервера: {selectedServer.name}
|
||||||
<option key={gateway} value={gateway}>{gateway}</option>
|
<span className="badge bg-green-lt text-green ms-2">
|
||||||
))}
|
{serverFilters.length} фильтров
|
||||||
</select>
|
</span>
|
||||||
<div className="input-icon flex-grow-1">
|
</h3>
|
||||||
<span className="input-icon-addon">
|
<div className="card-actions">
|
||||||
<IconSearch size={18} />
|
<button
|
||||||
</span>
|
className="btn btn-outline-primary btn-sm me-2"
|
||||||
<input
|
onClick={handlePreviewConfig}
|
||||||
type="text"
|
>
|
||||||
className="form-control"
|
<IconEye className="me-2" />
|
||||||
placeholder="Поиск по фильтрам..."
|
Предварительный просмотр
|
||||||
value={searchTerm}
|
</button>
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
<button
|
||||||
/>
|
className="btn btn-outline-primary btn-sm me-2"
|
||||||
|
onClick={copyConfigToClipboard}
|
||||||
|
>
|
||||||
|
<IconCopy className="me-2" />
|
||||||
|
Копировать конфигурацию
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-success btn-sm me-2"
|
||||||
|
onClick={() => handleSaveServerConfig(selectedServer.id)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconDeviceFloppy className="me-2" />
|
||||||
|
Сохранить конфигурацию
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={() => setAddFilterModalOpen(true)}
|
||||||
|
>
|
||||||
|
<IconPlus className="me-2" />
|
||||||
|
Добавить фильтр
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
{serverFilters.length === 0 ? (
|
||||||
|
<div className="text-center text-muted py-4">
|
||||||
|
<IconFilter className="icon-lg mb-2" />
|
||||||
|
<p>Нет фильтров для этого сервера</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setAddFilterModalOpen(true)}
|
||||||
|
>
|
||||||
|
<IconPlus className="me-2" />
|
||||||
|
Добавить первый фильтр
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>COMMUNITY</th>
|
||||||
|
<th>GATEWAY</th>
|
||||||
|
<th>ОПИСАНИЕ</th>
|
||||||
|
<th className="text-end">ДЕЙСТВИЯ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{serverFilters.map((filter) => (
|
||||||
|
<tr key={filter.community}>
|
||||||
|
<td>
|
||||||
|
<span className="badge bg-blue-lt text-blue">{filter.community}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="badge bg-green-lt text-green">{filter.gateway}</span>
|
||||||
|
</td>
|
||||||
|
<td>{filter.description || '-'}</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary btn-icon btn-sm"
|
||||||
|
onClick={() => handleEditFilter(filter)}
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-danger btn-icon btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
setFilterToDelete(filter);
|
||||||
|
setDeleteFilterModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="table-responsive">
|
)}
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('community')}>
|
|
||||||
COMMUNITY
|
|
||||||
{sortField === 'community' && (
|
|
||||||
<span className="ms-1">
|
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('gateway')}>
|
|
||||||
GATEWAY
|
|
||||||
{sortField === 'gateway' && (
|
|
||||||
<span className="ms-1">
|
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th>ОПИСАНИЕ</th>
|
|
||||||
<th className="text-end">ДЕЙСТВИЯ</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredFilters.map((filter) => (
|
|
||||||
<tr key={filter.community}>
|
|
||||||
<td>
|
|
||||||
<span className="badge bg-blue-lt text-blue">{filter.community}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className="badge bg-green-lt text-green">{filter.gateway}</span>
|
|
||||||
</td>
|
|
||||||
<td>{filter.description || '-'}</td>
|
|
||||||
<td className="text-end">
|
|
||||||
<div className="btn-list">
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary btn-icon btn-sm"
|
|
||||||
onClick={() => handleEdit(filter)}
|
|
||||||
>
|
|
||||||
<IconEdit size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-danger btn-icon btn-sm"
|
|
||||||
onClick={() => handleDelete(filter)}
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Модальные окна */}
|
{/* Модальные окна */}
|
||||||
<AddFilterModal
|
<AddFilterModal
|
||||||
show={addFilterModalOpen}
|
show={addFilterModalOpen}
|
||||||
newFilter={newFilter}
|
newFilter={newFilter}
|
||||||
onNewFilterChange={setNewFilter}
|
onNewFilterChange={setNewFilter}
|
||||||
onAddFilter={handleAddFilterFromModal}
|
onAddFilter={handleAddFilter}
|
||||||
onClose={handleCloseAddFilterModal}
|
onClose={() => {
|
||||||
|
setAddFilterModalOpen(false);
|
||||||
|
setNewFilter({ community: '', gateway: '', description: '' });
|
||||||
|
}}
|
||||||
error={error}
|
error={error}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditFilterModal
|
<EditFilterModal
|
||||||
show={editModalOpen}
|
show={editFilterModalOpen}
|
||||||
filter={editModalFilter}
|
filter={editingFilter}
|
||||||
onChange={handleEditModalChange}
|
onChange={setEditingFilter}
|
||||||
onSave={handleEditModalSave}
|
onSave={handleSaveEditFilter}
|
||||||
onClose={handleEditModalClose}
|
onClose={() => {
|
||||||
|
setEditFilterModalOpen(false);
|
||||||
|
setEditingFilter(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteFilterModal
|
<DeleteFilterModal
|
||||||
show={showDeleteModal}
|
show={deleteFilterModalOpen}
|
||||||
filter={filterToDelete}
|
filter={filterToDelete}
|
||||||
onDelete={confirmDelete}
|
onDelete={handleDeleteFilter}
|
||||||
onClose={() => setShowDeleteModal(false)}
|
onClose={() => {
|
||||||
|
setDeleteFilterModalOpen(false);
|
||||||
|
setFilterToDelete(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PreviewConfigModal
|
<PreviewConfigModal
|
||||||
|
|||||||
Reference in New Issue
Block a user