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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user