feat(NetworkConfigManager): add IP Pool management features; implement filtering and rendering for IP Pools, including server grouping and enhanced UI elements
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2026-01-22 18:33:40 +07:00
parent b94b7c67e2
commit 8f5cd700c3
+160 -73
View File
@@ -215,13 +215,14 @@ function NetworkConfigManager() {
const allServerIds = new Set([
...(config.gateways || []).map(g => g.serverId).filter(Boolean),
...(config.tunnelInterfaces || []).map(i => i.serverId).filter(Boolean),
...(config.ipPools || []).map(p => p.serverId).filter(Boolean),
]);
return Array.from(allServerIds).map(id => ({
id,
label: getServerLabel(id),
server: getServerInfo(id),
}));
}, [config.gateways, config.tunnelInterfaces, servers]);
}, [config.gateways, config.tunnelInterfaces, config.ipPools, servers]);
// === Фильтрация ===
const filteredGateways = useMemo(() => {
@@ -289,17 +290,25 @@ function NetworkConfigManager() {
const filteredPools = useMemo(() => {
let result = [...(config.ipPools || [])];
if (serverFilter) {
result = result.filter(p => p.serverId === serverFilter);
}
if (searchTerm) {
const term = searchTerm.toLowerCase();
result = result.filter(p =>
p.name?.toLowerCase().includes(term) ||
p.cidr?.toLowerCase().includes(term) ||
p.description?.toLowerCase().includes(term)
);
result = result.filter(p => {
const server = getServerInfo(p.serverId);
return (
p.name?.toLowerCase().includes(term) ||
p.cidr?.toLowerCase().includes(term) ||
p.description?.toLowerCase().includes(term) ||
getServerLabel(p.serverId).toLowerCase().includes(term)
);
});
}
return result;
}, [config.ipPools, searchTerm]);
}, [config.ipPools, serverFilter, searchTerm, servers]);
// === Группировка по серверам ===
const gatewaysByServer = useMemo(() => {
@@ -1170,7 +1179,9 @@ function NetworkConfigManager() {
<div className="row row-cards">
{items.map(item => (
<div key={item.id} className="col-12 col-md-6 col-lg-6">
{type === 'gateway' ? renderGatewayCard(item) : renderInterfaceCard(item, serverId)}
{type === 'gateway' ? renderGatewayCard(item) :
type === 'interface' ? renderInterfaceCard(item, serverId) :
type === 'pool' ? renderPoolCard(item) : null}
</div>
))}
</div>
@@ -1214,6 +1225,100 @@ function NetworkConfigManager() {
return null;
};
// === Рендер карточки IP Pool ===
const renderPoolCard = (pool) => {
const server = getServerInfo(pool.serverId);
const serverCountry = server?.country || '';
// Определяем цвет индикатора (зеленый для IP пулов)
const indicatorColor = 'green';
const IndicatorIcon = IconDatabase;
return (
<div className="card mb-2" style={{ borderRadius: '12px', maxWidth: '100%' }}>
<div className="card-body p-0">
<div className="d-flex align-items-stretch">
{/* Левая часть: тип индикатора */}
<div
className={`d-flex align-items-center justify-content-center px-3 bg-${indicatorColor}-lt`}
style={{
minWidth: '100px',
width: '100px',
borderTopLeftRadius: '11px',
borderBottomLeftRadius: '11px'
}}
>
<div className="text-center">
<div className="mb-1">
<IndicatorIcon size={20} className={`text-${indicatorColor}`} />
</div>
<div className={`fw-bold text-${indicatorColor}`} style={{ fontSize: '0.65rem', letterSpacing: '0.5px' }}>
Pool
</div>
</div>
</div>
{/* Центральная часть: информация */}
<div className="flex-grow-1 py-2 px-3" style={{ minWidth: 0, flex: '1 1 auto', overflow: 'hidden' }}>
{/* Верхняя строка: флаг, название */}
<div className="d-flex align-items-center mb-1">
<div className="d-flex align-items-center" style={{ flex: '0 0 auto', minWidth: 0 }}>
{serverCountry && (
<span className="me-2" style={{ fontSize: '1.25rem', flexShrink: 0 }}>{countryToFlag(serverCountry)}</span>
)}
<div>
<span className="fw-semibold">{pool.name || '—'}</span>
</div>
</div>
</div>
{/* CIDR под названием */}
{pool.cidr && (
<div className="mb-1">
<code className="text-muted" style={{ fontSize: '0.875rem' }}>
{pool.cidr}
</code>
</div>
)}
{/* Нижняя строка: DNS сервера */}
<div className="d-flex align-items-center gap-2 flex-wrap">
{server && (
<code className="text-muted" style={{ fontSize: '0.8rem', wordBreak: 'break-all', overflowWrap: 'break-word' }}>
{server.dns || server.ip || pool.serverId}
</code>
)}
{!server && pool.serverId && (
<code className="text-muted" style={{ fontSize: '0.8rem' }}>
{pool.serverId}
</code>
)}
</div>
</div>
{/* Правая часть: действия */}
<div className="d-flex align-items-center gap-2 px-3 border-start" style={{ flexShrink: 0 }}>
<button
className="btn btn-outline-secondary btn-sm"
onClick={() => handleEditPool(pool)}
>
<IconEdit size={16} className="me-1" />
Изменить
</button>
<button
className="btn btn-outline-danger btn-sm"
onClick={() => handleDeletePool(pool)}
>
<IconTrash size={16} className="me-1" />
Удалить
</button>
</div>
</div>
</div>
</div>
);
};
// === Рендер карточки Gateway ===
const renderGatewayCard = (gateway) => {
const server = getServerInfo(gateway.serverId);
@@ -1578,7 +1683,7 @@ function NetworkConfigManager() {
</div>
{/* Server filter */}
{(activeTab === 'gateways' || activeTab === 'interfaces') && uniqueServersInConfig.length > 0 && (
{(activeTab === 'gateways' || activeTab === 'interfaces' || activeTab === 'pools') && uniqueServersInConfig.length > 0 && (
<div className="col-6 col-md-3 col-lg-2">
<select
className="form-select"
@@ -2002,77 +2107,59 @@ function NetworkConfigManager() {
{activeTab === 'pools' && (
<>
{filteredPools.length === 0 ? (
<div className="card">
<div className="card-body text-center py-5">
<div className="mb-3">
<span className="avatar avatar-xl bg-green-lt">
<IconDatabase size={40} className="text-green" />
</span>
</div>
<h3>IP пулы не найдены</h3>
<p className="text-muted">
{hasActiveFilters
? 'Попробуйте изменить параметры фильтрации'
: 'Добавьте пулы IP-адресов для организации сетей'
}
</p>
{!hasActiveFilters && (
<div className="empty">
<div className="empty-img">
<IconDatabase size={48} />
</div>
<p className="empty-title">IP пулы не найдены</p>
<p className="empty-subtitle text-muted">
{hasActiveFilters
? 'Попробуйте изменить параметры фильтрации'
: 'Добавьте пулы IP-адресов для организации сетей'
}
</p>
{!hasActiveFilters && (
<div className="empty-action">
<button className="btn btn-primary" onClick={handleAddPool}>
<IconPlus size={16} className="me-1" />
<IconPlus size={16} className="me-2" />
Добавить IP пул
</button>
)}
</div>
</div>
)}
</div>
) : (
<div className="row row-cards">
{filteredPools.map(pool => (
<div key={pool.id} className="col-sm-6 col-lg-4">
<div className="card">
<div className="card-body">
<div className="d-flex align-items-center mb-3">
<span className="avatar avatar-sm bg-green-lt me-2">
<IconDatabase size={16} className="text-green" />
</span>
<div className="flex-fill">
<div className="fw-medium">{pool.name || '—'}</div>
</div>
<div className="dropdown">
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
<IconEdit size={16} />
</button>
<div className="dropdown-menu dropdown-menu-end">
<button className="dropdown-item" onClick={() => handleEditPool(pool)}>
<IconEdit size={16} className="me-2" />Редактировать
</button>
<button className="dropdown-item text-danger" onClick={() => handleDeletePool(pool)}>
<IconTrash size={16} className="me-2" />Удалить
</button>
</div>
</div>
</div>
<div className="d-flex align-items-center gap-2 mb-2">
<code className="flex-fill">{pool.cidr || '—'}</code>
{pool.cidr && (
<button
className="btn btn-ghost-secondary btn-icon btn-sm"
onClick={() => copyToClipboard(pool.cidr)}
title="Копировать CIDR"
>
<IconCopy size={14} />
</button>
)}
</div>
{pool.description && (
<div className="text-muted small">{pool.description}</div>
)}
<>
{/* Группировка по серверам */}
{(serverFilter || filteredPools.some(p => p.serverId)) && (() => {
const poolsByServer = {};
filteredPools.forEach(pool => {
const key = pool.serverId || '__unassigned__';
if (!poolsByServer[key]) {
poolsByServer[key] = [];
}
poolsByServer[key].push(pool);
});
return Object.entries(poolsByServer)
.sort(([a], [b]) => {
if (a === '__unassigned__') return 1;
if (b === '__unassigned__') return -1;
return a.localeCompare(b);
})
.map(([serverId, pools]) => renderServerGroup(serverId, pools, 'pool'));
})()}
{/* Если нет фильтра по серверу и все пулы без сервера - показываем без группировки */}
{!serverFilter && !filteredPools.some(p => p.serverId) && (
<div className="row row-cards">
{filteredPools.map(pool => (
<div key={pool.id} className="col-12 col-md-6 col-lg-6">
{renderPoolCard(pool)}
</div>
</div>
))}
</div>
))}
</div>
)}
</>
)}
</>
)}