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
|
||||
// match one above, send back React's index.html file.
|
||||
app.get('*', (req, res) => {
|
||||
|
||||
+266
-321
@@ -25,58 +25,51 @@ import {
|
||||
const API_URL = '/api';
|
||||
|
||||
function FilterManager() {
|
||||
const [filters, setFilters] = useState([]);
|
||||
const [newFilter, setNewFilter] = useState({
|
||||
community: '',
|
||||
gateway: '',
|
||||
description: ''
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [editingFilter, setEditingFilter] = useState(null);
|
||||
const [editingValues, setEditingValues] = useState({});
|
||||
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 [generatedConfig, setGeneratedConfig] = useState('');
|
||||
|
||||
// Состояние для модального окна добавления фильтра
|
||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||
|
||||
// Новые состояния для управления серверами
|
||||
// Состояния для управления серверами
|
||||
const [servers, setServers] = useState([]);
|
||||
const [selectedServer, setSelectedServer] = useState(null);
|
||||
const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false);
|
||||
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(() => {
|
||||
fetchFilters();
|
||||
fetchServers();
|
||||
}, []);
|
||||
|
||||
const fetchFilters = async () => {
|
||||
// Загружаем фильтры при выборе сервера
|
||||
useEffect(() => {
|
||||
if (selectedServer) {
|
||||
fetchServerFilters(selectedServer.id);
|
||||
} else {
|
||||
setServerFilters([]);
|
||||
}
|
||||
}, [selectedServer]);
|
||||
|
||||
const fetchServerFilters = async (serverId) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/filters`);
|
||||
setFilters(response.data);
|
||||
const response = await axios.get(`${API_URL}/server-filters/${serverId}`);
|
||||
setServerFilters(response.data);
|
||||
setError('');
|
||||
} catch (error) {
|
||||
console.error('Error fetching filters:', error);
|
||||
setError('Не удалось загрузить фильтры. Проверьте, запущен ли бэкенд.');
|
||||
console.error('Error fetching server filters:', error);
|
||||
setServerFilters([]); // Если файл не существует, создаем пустой массив
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -301,139 +294,92 @@ function FilterManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddFilter = () => {
|
||||
if (!newFilter.community.trim() || !newFilter.gateway.trim()) {
|
||||
setError('Поля Community и Gateway должны быть заполнены.');
|
||||
const handleAddFilter = async () => {
|
||||
if (!selectedServer) {
|
||||
setError('Сначала выберите сервер.');
|
||||
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()) {
|
||||
setError('Поля Community и Gateway должны быть заполнены.');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setFilters([...filters, { ...newFilter }]);
|
||||
const updatedFilters = [...serverFilters, { ...newFilter }];
|
||||
setServerFilters(updatedFilters);
|
||||
setNewFilter({ community: '', gateway: '', description: '' });
|
||||
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 {
|
||||
await axios.post(`${API_URL}/filters`, { domains: filters });
|
||||
setSuccess('Изменения сохранены в S3!');
|
||||
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters });
|
||||
setSuccess('Фильтр успешно добавлен!');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
} catch (error) {
|
||||
console.error('Error saving filters:', error);
|
||||
setError('Не удалось сохранить изменения.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
console.error('Error saving server filters:', error);
|
||||
setError('Не удалось сохранить фильтр.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSort = (field) => {
|
||||
if (sortField === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortOrder('asc');
|
||||
}
|
||||
const handleEditFilter = (filter) => {
|
||||
setEditingFilter({ ...filter });
|
||||
setEditFilterModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditKeyDown = (e, filterCommunity) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSaveEdit(filterCommunity);
|
||||
} else if (e.key === 'Escape') {
|
||||
handleCancelEdit(filterCommunity);
|
||||
}
|
||||
};
|
||||
|
||||
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) => {
|
||||
const handleSaveEditFilter = async (updatedFilter) => {
|
||||
const updatedFilters = serverFilters.map(f =>
|
||||
f.community === updatedFilter.community ? { ...updatedFilter } : f
|
||||
);
|
||||
setServerFilters(updatedFilters);
|
||||
setEditFilterModalOpen(false);
|
||||
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 {
|
||||
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;
|
||||
} catch (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 (
|
||||
<div className="container-xl">
|
||||
@@ -527,43 +454,32 @@ function FilterManager() {
|
||||
</div>
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handlePreviewConfig}
|
||||
>
|
||||
<IconEye className="me-2" />
|
||||
Предварительный просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={copyConfigToClipboard}
|
||||
>
|
||||
<IconCopy className="me-2" />
|
||||
Копировать конфигурацию
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success"
|
||||
onClick={exportConfigToS3}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconDownload className="me-2" />
|
||||
Экспорт в S3
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleOpenAddFilterModal}
|
||||
>
|
||||
<IconPlus className="me-2" />
|
||||
Добавить фильтр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={handleSaveChanges}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconDatabase className="me-2" />
|
||||
{loading ? 'Сохранение...' : 'Сохранить в S3'}
|
||||
</button>
|
||||
{selectedServer && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handlePreviewConfig}
|
||||
>
|
||||
<IconEye className="me-2" />
|
||||
Предварительный просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={copyConfigToClipboard}
|
||||
>
|
||||
<IconCopy className="me-2" />
|
||||
Копировать конфигурацию
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success"
|
||||
onClick={() => handleSaveServerConfig(selectedServer.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconDeviceFloppy className="me-2" />
|
||||
Сохранить конфигурацию
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -648,37 +564,30 @@ function FilterManager() {
|
||||
<p className="text-muted small mb-3">{server.description}</p>
|
||||
)}
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={() => handleLoadServerConfig(server.id)}
|
||||
>
|
||||
<IconEye className="me-1" />
|
||||
Просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success btn-sm"
|
||||
onClick={() => handleSaveServerConfig(server.id)}
|
||||
>
|
||||
<IconDeviceFloppy className="me-1" />
|
||||
Сохранить
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-warning btn-sm"
|
||||
onClick={() => handleCopyConfigToAllServers(server.id)}
|
||||
disabled={loading || servers.filter(s => s.id !== server.id && s.enabled).length === 0}
|
||||
title="Скопировать конфигурацию на все остальные серверы"
|
||||
>
|
||||
<IconCopy className="me-1" />
|
||||
Копировать на все
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => handleDeleteServer(server.id)}
|
||||
>
|
||||
<IconTrash className="me-1" />
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={`btn btn-sm ${selectedServer?.id === server.id ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||
onClick={() => setSelectedServer(server)}
|
||||
>
|
||||
<IconFilter className="me-1" />
|
||||
{selectedServer?.id === server.id ? 'Выбран' : 'Выбрать'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-warning btn-sm"
|
||||
onClick={() => handleCopyConfigToAllServers(server.id)}
|
||||
disabled={loading || servers.filter(s => s.id !== server.id && s.enabled).length === 0}
|
||||
title="Скопировать конфигурацию на все остальные серверы"
|
||||
>
|
||||
<IconCopy className="me-1" />
|
||||
Копировать на все
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
onClick={() => handleDeleteServer(server.id)}
|
||||
>
|
||||
<IconTrash className="me-1" />
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -690,112 +599,148 @@ function FilterManager() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Фильтры и поиск */}
|
||||
<div className="card w-100">
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<h3 className="card-title mb-0">Список фильтров</h3>
|
||||
<div className="d-flex gap-2 w-75">
|
||||
{/* Фильтры и поиск */}
|
||||
<select className="form-select w-auto" value={filterGateway} onChange={e => setFilterGateway(e.target.value)}>
|
||||
<option value="">Все gateway</option>
|
||||
{uniqueGateways.map(gateway => (
|
||||
<option key={gateway} value={gateway}>{gateway}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="input-icon flex-grow-1">
|
||||
<span className="input-icon-addon">
|
||||
<IconSearch size={18} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Поиск по фильтрам..."
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
{/* Секция фильтров выбранного сервера */}
|
||||
{selectedServer && (
|
||||
<div className="row mb-4">
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<IconFilter className="me-2" />
|
||||
Фильтры сервера: {selectedServer.name}
|
||||
<span className="badge bg-green-lt text-green ms-2">
|
||||
{serverFilters.length} фильтров
|
||||
</span>
|
||||
</h3>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm me-2"
|
||||
onClick={handlePreviewConfig}
|
||||
>
|
||||
<IconEye className="me-2" />
|
||||
Предварительный просмотр
|
||||
</button>
|
||||
<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 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
|
||||
show={addFilterModalOpen}
|
||||
newFilter={newFilter}
|
||||
onNewFilterChange={setNewFilter}
|
||||
onAddFilter={handleAddFilterFromModal}
|
||||
onClose={handleCloseAddFilterModal}
|
||||
onAddFilter={handleAddFilter}
|
||||
onClose={() => {
|
||||
setAddFilterModalOpen(false);
|
||||
setNewFilter({ community: '', gateway: '', description: '' });
|
||||
}}
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<EditFilterModal
|
||||
show={editModalOpen}
|
||||
filter={editModalFilter}
|
||||
onChange={handleEditModalChange}
|
||||
onSave={handleEditModalSave}
|
||||
onClose={handleEditModalClose}
|
||||
show={editFilterModalOpen}
|
||||
filter={editingFilter}
|
||||
onChange={setEditingFilter}
|
||||
onSave={handleSaveEditFilter}
|
||||
onClose={() => {
|
||||
setEditFilterModalOpen(false);
|
||||
setEditingFilter(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteFilterModal
|
||||
show={showDeleteModal}
|
||||
show={deleteFilterModalOpen}
|
||||
filter={filterToDelete}
|
||||
onDelete={confirmDelete}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
onDelete={handleDeleteFilter}
|
||||
onClose={() => {
|
||||
setDeleteFilterModalOpen(false);
|
||||
setFilterToDelete(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
<PreviewConfigModal
|
||||
|
||||
Reference in New Issue
Block a user