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

This commit is contained in:
2025-07-14 11:13:35 +07:00
parent 077fdcd258
commit 022c8dddc5
2 changed files with 397 additions and 321 deletions
+131
View File
@@ -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) => {
+266 -321
View File
@@ -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