feat: Add IP ranges management API endpoints and integrate with frontend for viewing and updating IP ranges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m20s

This commit is contained in:
2025-07-14 19:40:27 +07:00
parent a54c09ae15
commit 2bee891548
16 changed files with 804 additions and 0 deletions
+3
View File
@@ -23,6 +23,7 @@ import DataManager from './DataManager';
import ServerManager from './ServerManager';
import FilterManager from './FilterManager';
import DomainsNewManager from './DomainsNewManager';
import IPRangesManager from './IPRangesManager';
import './App.css';
import axios from 'axios';
import {
@@ -52,6 +53,7 @@ function MainLayout() {
const navItems = [
{ id: 'domains', title: 'Домены', icon: IconWorld, path: '/domains' },
{ id: 'domains-new', title: 'Домены New', icon: IconWorld, path: '/domains-new' },
{ id: 'ip-ranges', title: 'IP-диапазоны', icon: IconNetwork, path: '/ip-ranges' },
{ id: 'asns', title: 'AS', icon: IconNetwork, path: '/asns' },
{ id: 'servers', title: 'Серверы', icon: IconServer, path: '/servers' },
{ id: 'filters', title: 'Фильтры', icon: IconFilter, path: '/filters' },
@@ -98,6 +100,7 @@ function MainLayout() {
/>
} />
<Route path="/domains-new" element={<DomainsNewManager />} />
<Route path="/ip-ranges" element={<IPRangesManager />} />
<Route path="/asns" element={
<DataManager
entityName="AS"
+374
View File
@@ -0,0 +1,374 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import {
IconPlus,
IconSearch,
IconEdit,
IconTrash,
IconCheck,
IconX,
IconDatabase,
IconRefresh,
IconDownload,
IconUpload,
IconAlertTriangle,
IconFilter,
IconChevronDown,
IconLink,
IconCopy,
IconEye,
IconServer,
IconSettings,
IconDeviceFloppy,
IconHash,
IconRocket,
IconWorld,
IconNetwork
} from '@tabler/icons-react';
const API_URL = '/api';
function IPRangesManager() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [editingIndex, setEditingIndex] = useState(null);
const [editingItem, setEditingItem] = useState({ ipRange: '', community: '' });
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
setLoading(true);
try {
const response = await axios.get(`${API_URL}/ip-ranges`);
setItems(response.data);
setError('');
} catch (error) {
console.error('Error fetching ip-ranges:', error);
setItems([]);
} finally {
setLoading(false);
}
};
const handleAdd = () => {
setEditingIndex(null);
setEditingItem({ ipRange: '', community: '' });
};
const handleEdit = (index) => {
setEditingIndex(index);
setEditingItem({ ...items[index] });
};
const handleSave = () => {
if (!editingItem.ipRange.trim() || !editingItem.community.trim()) {
setError('IP-диапазон и Community обязательны для заполнения.');
return;
}
const updatedItems = [...items];
if (editingIndex !== null) {
updatedItems[editingIndex] = { ...editingItem };
} else {
updatedItems.push({ ...editingItem });
}
setItems(updatedItems);
setEditingIndex(null);
setEditingItem({ ipRange: '', community: '' });
};
const handleCancel = () => {
setEditingIndex(null);
setEditingItem({ ipRange: '', community: '' });
};
const handleDelete = (index) => {
const updatedItems = items.filter((_, i) => i !== index);
setItems(updatedItems);
};
const handleSaveToS3 = async () => {
setLoading(true);
try {
await axios.post(`${API_URL}/ip-ranges`, { ipRanges: items });
setSuccess('IP-диапазоны успешно сохранены!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
console.error('Error saving ip-ranges:', error);
setError('Не удалось сохранить IP-диапазоны.');
} finally {
setLoading(false);
}
};
const handleUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
const lines = content.split('\n').filter(line => line.trim());
const parsedItems = lines.map(line => {
const parts = line.trim().split(/\s+/);
return {
ipRange: parts[0] || '',
community: parts[1] || ''
};
});
setItems(parsedItems);
setSuccess(`Загружено ${parsedItems.length} IP-диапазонов!`);
setTimeout(() => setSuccess(''), 3000);
};
reader.readAsText(file);
};
const handleDownload = () => {
const content = items.map(item => `${item.ipRange} ${item.community}`).join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ips.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const filteredItems = items.filter(item =>
item.ipRange.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.community.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="container-xl">
<div className="page-header d-print-none mb-4">
<div className="row align-items-center">
<div className="col">
<h2 className="page-title">Управление IP-диапазонами</h2>
<div className="page-pretitle">Главная / IP-диапазоны</div>
</div>
<div className="col-auto ms-auto d-print-none">
<div className="btn-list">
<button
className="btn btn-outline-primary"
onClick={handleDownload}
disabled={items.length === 0}
>
<IconDownload className="me-2" />
Скачать
</button>
<label className="btn btn-outline-primary">
<IconUpload className="me-2" />
Загрузить
<input
type="file"
accept=".txt"
onChange={handleUpload}
style={{ display: 'none' }}
/>
</label>
<button
className="btn btn-primary"
onClick={handleAdd}
>
<IconPlus className="me-2" />
Добавить IP-диапазон
</button>
</div>
</div>
</div>
</div>
{/* Уведомления */}
{error && (
<div className="alert alert-danger alert-dismissible" role="alert">
<IconAlertTriangle className="me-2" />
{error}
<button type="button" className="btn-close" onClick={() => setError('')}></button>
</div>
)}
{success && (
<div className="alert alert-success alert-dismissible" role="alert">
<IconCheck className="me-2" />
{success}
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
</div>
)}
{/* Панель поиска и сохранения */}
<div className="card mb-4">
<div className="card-body">
<div className="row g-3 align-items-center">
<div className="col-md-6">
<div className="input-group">
<span className="input-group-text">
<IconSearch />
</span>
<input
type="text"
className="form-control"
placeholder="Поиск по IP-диапазону или Community..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="col-md-6">
<div className="d-flex gap-2 justify-content-end">
<button
className="btn btn-outline-secondary"
onClick={fetchItems}
disabled={loading}
>
<IconRefresh className="me-2" />
Обновить
</button>
<button
className="btn btn-success"
onClick={handleSaveToS3}
disabled={loading}
>
<IconDeviceFloppy className="me-2" />
Сохранить в S3
</button>
</div>
</div>
</div>
</div>
</div>
{/* Таблица IP-диапазонов */}
<div className="card">
<div className="card-header">
<h3 className="card-title">
<IconNetwork className="me-2" />
IP-диапазоны ({filteredItems.length} из {items.length})
</h3>
</div>
<div className="card-body">
{loading ? (
<div className="text-center text-muted py-4">
<div className="spinner-border spinner-border-sm me-2" role="status">
<span className="visually-hidden">Загрузка...</span>
</div>
Загрузка IP-диапазонов...
</div>
) : filteredItems.length === 0 ? (
<div className="text-center text-muted py-4">
<IconNetwork className="icon-lg mb-2" />
<p>Нет IP-диапазонов</p>
<button
className="btn btn-primary"
onClick={handleAdd}
>
<IconPlus className="me-2" />
Добавить первый IP-диапазон
</button>
</div>
) : (
<div className="table-responsive">
<table className="table card-table table-vcenter table-nowrap mb-0">
<thead>
<tr>
<th style={{ width: '40%' }}>IP-ДИАПАЗОН</th>
<th style={{ width: '40%' }}>COMMUNITY</th>
<th style={{ width: '20%' }} className="text-end">ДЕЙСТВИЯ</th>
</tr>
</thead>
<tbody>
{filteredItems.map((item, index) => (
<tr key={index}>
<td>
{editingIndex === index ? (
<input
type="text"
className="form-control form-control-sm"
value={editingItem.ipRange}
onChange={(e) => setEditingItem({ ...editingItem, ipRange: e.target.value })}
placeholder="192.168.1.0/24"
/>
) : (
<div className="d-flex align-items-center">
<span className="badge bg-blue-lt text-blue me-2">
<IconNetwork size={12} />
</span>
<code className="text-blue">{item.ipRange}</code>
</div>
)}
</td>
<td>
{editingIndex === index ? (
<input
type="text"
className="form-control form-control-sm"
value={editingItem.community}
onChange={(e) => setEditingItem({ ...editingItem, community: e.target.value })}
placeholder="65001:100"
/>
) : (
<div className="d-flex align-items-center">
<span className="badge bg-green-lt text-green me-2">
<IconHash size={12} />
</span>
<code className="text-green">{item.community}</code>
</div>
)}
</td>
<td className="text-end">
{editingIndex === index ? (
<div className="btn-list">
<button
className="btn btn-outline-success btn-icon btn-sm"
onClick={handleSave}
title="Сохранить"
>
<IconCheck size={16} />
</button>
<button
className="btn btn-outline-secondary btn-icon btn-sm"
onClick={handleCancel}
title="Отмена"
>
<IconX size={16} />
</button>
</div>
) : (
<div className="btn-list">
<button
className="btn btn-outline-primary btn-icon btn-sm"
onClick={() => handleEdit(index)}
title="Редактировать"
>
<IconEdit size={16} />
</button>
<button
className="btn btn-outline-danger btn-icon btn-sm"
onClick={() => handleDelete(index)}
title="Удалить"
>
<IconTrash size={16} />
</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
}
export default IPRangesManager;