feat: Implement simple filters management in FilterManager with API integration for fetching, adding, and deleting filters, enhancing user experience and functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m46s

This commit is contained in:
2025-07-14 15:13:32 +07:00
parent e6a9c17aa0
commit c62666bd42
2 changed files with 188 additions and 16 deletions
+68
View File
@@ -785,6 +785,74 @@ app.post('/api/server-filters/:serverId', async (req, res) => {
}
});
// --- Simple Filters Routes ---
// Get simple filters
app.get('/api/simple-filters', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: 'simple-filters.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 simple 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 simple filters from S3');
}
}
});
// Update simple filters
app.post('/api/simple-filters', async (req, res) => {
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: 'simple-filters.json',
Body: JSON.stringify(filters, null, 2), // Pretty print JSON
ContentType: 'application/json',
};
try {
await s3.putObject(params).promise();
res.send('Simple filters updated successfully');
} catch (error) {
console.error(error);
res.status(500).send('Error writing simple filters to S3');
}
});
// The "catchall" handler: for any request that doesn't
// match one above, send back React's index.html file.
app.get('*', (req, res) => {
+120 -16
View File
@@ -65,6 +65,7 @@ function FilterManager() {
useEffect(() => {
fetchServers();
fetchSimpleFilters();
}, []);
// Загружаем фильтры при выборе сервера
@@ -102,6 +103,19 @@ function FilterManager() {
}
};
const fetchSimpleFilters = async () => {
setSimpleLoading(true);
try {
const response = await axios.get(`${API_URL}/simple-filters`);
setSimpleFilters(response.data);
} catch (error) {
console.error('Error fetching simple filters:', error);
setSimpleFilters([]);
} finally {
setSimpleLoading(false);
}
};
const handleAddServer = async () => {
if (!newServer.name.trim()) {
setError('Имя сервера обязательно для заполнения.');
@@ -581,22 +595,54 @@ function FilterManager() {
};
// Функции для работы с фильтрами в упрощённом режиме
const handleSimpleAddFilter = () => {
const handleSimpleAddFilter = async () => {
if (!simpleAddFilter.community.trim() || !simpleAddFilter.gateway.trim()) {
setSimpleError('Поля Community и Gateway должны быть заполнены.');
return;
}
setSimpleFilters([...simpleFilters, { ...simpleAddFilter }]);
const updatedFilters = [...simpleFilters, { ...simpleAddFilter }];
setSimpleFilters(updatedFilters);
setSimpleAddFilter({ community: '', gateway: '', description: '' });
setSimpleError('');
// Сохраняем фильтры
try {
await axios.post(`${API_URL}/simple-filters`, { filters: updatedFilters });
setSimpleSuccess('Фильтр успешно добавлен!');
setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) {
console.error('Error saving simple filters:', error);
setSimpleError('Не удалось сохранить фильтр.');
}
};
const handleSimpleDeleteFilter = (filter) => {
setSimpleFilters(simpleFilters.filter(f => f !== filter));
const handleSimpleDeleteFilter = async (filter) => {
const updatedFilters = simpleFilters.filter(f => f !== filter);
setSimpleFilters(updatedFilters);
// Сохраняем фильтры
try {
await axios.post(`${API_URL}/simple-filters`, { filters: updatedFilters });
setSimpleSuccess('Фильтр успешно удален!');
setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) {
console.error('Error saving simple filters:', error);
setSimpleError('Не удалось удалить фильтр.');
}
};
const handleSimpleEditFilter = (index, field, value) => {
const handleSimpleEditFilter = async (index, field, value) => {
const updated = [...simpleFilters];
updated[index][field] = value;
setSimpleFilters(updated);
// Сохраняем фильтры
try {
await axios.post(`${API_URL}/simple-filters`, { filters: updated });
} catch (error) {
console.error('Error saving simple filters:', error);
setSimpleError('Не удалось сохранить изменения.');
}
};
return (
@@ -1181,9 +1227,21 @@ function FilterManager() {
{mode === 'simple' ? (
<div className="card mb-4">
<div className="card-header">
<h3 className="card-title">
<IconFilter className="me-2" /> Фильтры (упрощённый режим)
</h3>
<div className="row align-items-center">
<div className="col">
<h3 className="card-title">
<IconFilter className="me-2" /> Фильтры (упрощённый режим)
</h3>
<div className="text-muted">
Управление фильтрами community/gateway для всех серверов
</div>
</div>
<div className="col-auto">
<div className="badge bg-blue-lt text-blue">
{simpleFilters.length} фильтров
</div>
</div>
</div>
</div>
<div className="card-body">
{simpleError && (
@@ -1203,7 +1261,16 @@ function FilterManager() {
</tr>
</thead>
<tbody>
{simpleFilters.length === 0 ? (
{simpleLoading ? (
<tr>
<td colSpan={4} className="text-center text-muted py-4">
<div className="spinner-border spinner-border-sm me-2" role="status">
<span className="visually-hidden">Загрузка...</span>
</div>
Загрузка фильтров...
</td>
</tr>
) : simpleFilters.length === 0 ? (
<tr><td colSpan={4} className="text-center text-muted">Нет фильтров</td></tr>
) : (
simpleFilters.map((filter, idx) => (
@@ -1275,13 +1342,50 @@ function FilterManager() {
</button>
</div>
</div>
<button
className="btn btn-outline-success"
onClick={handleDeploySimpleFiltersToAll}
disabled={simpleFilters.length === 0 || simpleLoading}
>
<IconRocket className="me-2" /> Раскатать фильтры на все серверы
</button>
<div className="btn-list">
<button
className="btn btn-outline-success"
onClick={handleDeploySimpleFiltersToAll}
disabled={simpleFilters.length === 0 || simpleLoading}
>
<IconRocket className="me-2" /> Раскатать фильтры на все серверы
</button>
<button
className="btn btn-outline-primary"
onClick={async () => {
try {
await axios.post(`${API_URL}/simple-filters`, { filters: simpleFilters });
setSimpleSuccess('Фильтры успешно сохранены!');
setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) {
setSimpleError('Ошибка при сохранении фильтров: ' + (error.response?.data || error.message));
}
}}
disabled={simpleLoading}
>
<IconDeviceFloppy className="me-2" /> Сохранить фильтры
</button>
{simpleFilters.length > 0 && (
<button
className="btn btn-outline-danger"
onClick={async () => {
if (window.confirm('Вы уверены, что хотите очистить все фильтры?')) {
try {
await axios.post(`${API_URL}/simple-filters`, { filters: [] });
setSimpleFilters([]);
setSimpleSuccess('Все фильтры очищены!');
setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) {
setSimpleError('Ошибка при очистке фильтров: ' + (error.response?.data || error.message));
}
}
}}
disabled={simpleLoading}
>
<IconTrash className="me-2" /> Очистить все
</button>
)}
</div>
</div>
</div>
) : (