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([
|
const allServerIds = new Set([
|
||||||
...(config.gateways || []).map(g => g.serverId).filter(Boolean),
|
...(config.gateways || []).map(g => g.serverId).filter(Boolean),
|
||||||
...(config.tunnelInterfaces || []).map(i => i.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 => ({
|
return Array.from(allServerIds).map(id => ({
|
||||||
id,
|
id,
|
||||||
label: getServerLabel(id),
|
label: getServerLabel(id),
|
||||||
server: getServerInfo(id),
|
server: getServerInfo(id),
|
||||||
}));
|
}));
|
||||||
}, [config.gateways, config.tunnelInterfaces, servers]);
|
}, [config.gateways, config.tunnelInterfaces, config.ipPools, servers]);
|
||||||
|
|
||||||
// === Фильтрация ===
|
// === Фильтрация ===
|
||||||
const filteredGateways = useMemo(() => {
|
const filteredGateways = useMemo(() => {
|
||||||
@@ -289,17 +290,25 @@ function NetworkConfigManager() {
|
|||||||
const filteredPools = useMemo(() => {
|
const filteredPools = useMemo(() => {
|
||||||
let result = [...(config.ipPools || [])];
|
let result = [...(config.ipPools || [])];
|
||||||
|
|
||||||
|
if (serverFilter) {
|
||||||
|
result = result.filter(p => p.serverId === serverFilter);
|
||||||
|
}
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const term = searchTerm.toLowerCase();
|
const term = searchTerm.toLowerCase();
|
||||||
result = result.filter(p =>
|
result = result.filter(p => {
|
||||||
p.name?.toLowerCase().includes(term) ||
|
const server = getServerInfo(p.serverId);
|
||||||
p.cidr?.toLowerCase().includes(term) ||
|
return (
|
||||||
p.description?.toLowerCase().includes(term)
|
p.name?.toLowerCase().includes(term) ||
|
||||||
);
|
p.cidr?.toLowerCase().includes(term) ||
|
||||||
|
p.description?.toLowerCase().includes(term) ||
|
||||||
|
getServerLabel(p.serverId).toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [config.ipPools, searchTerm]);
|
}, [config.ipPools, serverFilter, searchTerm, servers]);
|
||||||
|
|
||||||
// === Группировка по серверам ===
|
// === Группировка по серверам ===
|
||||||
const gatewaysByServer = useMemo(() => {
|
const gatewaysByServer = useMemo(() => {
|
||||||
@@ -1170,7 +1179,9 @@ function NetworkConfigManager() {
|
|||||||
<div className="row row-cards">
|
<div className="row row-cards">
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<div key={item.id} className="col-12 col-md-6 col-lg-6">
|
<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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1214,6 +1225,100 @@ function NetworkConfigManager() {
|
|||||||
return null;
|
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 ===
|
// === Рендер карточки Gateway ===
|
||||||
const renderGatewayCard = (gateway) => {
|
const renderGatewayCard = (gateway) => {
|
||||||
const server = getServerInfo(gateway.serverId);
|
const server = getServerInfo(gateway.serverId);
|
||||||
@@ -1578,7 +1683,7 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Server filter */}
|
{/* 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">
|
<div className="col-6 col-md-3 col-lg-2">
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
@@ -2002,77 +2107,59 @@ function NetworkConfigManager() {
|
|||||||
{activeTab === 'pools' && (
|
{activeTab === 'pools' && (
|
||||||
<>
|
<>
|
||||||
{filteredPools.length === 0 ? (
|
{filteredPools.length === 0 ? (
|
||||||
<div className="card">
|
<div className="empty">
|
||||||
<div className="card-body text-center py-5">
|
<div className="empty-img">
|
||||||
<div className="mb-3">
|
<IconDatabase size={48} />
|
||||||
<span className="avatar avatar-xl bg-green-lt">
|
</div>
|
||||||
<IconDatabase size={40} className="text-green" />
|
<p className="empty-title">IP пулы не найдены</p>
|
||||||
</span>
|
<p className="empty-subtitle text-muted">
|
||||||
</div>
|
{hasActiveFilters
|
||||||
<h3>IP пулы не найдены</h3>
|
? 'Попробуйте изменить параметры фильтрации'
|
||||||
<p className="text-muted">
|
: 'Добавьте пулы IP-адресов для организации сетей'
|
||||||
{hasActiveFilters
|
}
|
||||||
? 'Попробуйте изменить параметры фильтрации'
|
</p>
|
||||||
: 'Добавьте пулы IP-адресов для организации сетей'
|
{!hasActiveFilters && (
|
||||||
}
|
<div className="empty-action">
|
||||||
</p>
|
|
||||||
{!hasActiveFilters && (
|
|
||||||
<button className="btn btn-primary" onClick={handleAddPool}>
|
<button className="btn btn-primary" onClick={handleAddPool}>
|
||||||
<IconPlus size={16} className="me-1" />
|
<IconPlus size={16} className="me-2" />
|
||||||
Добавить IP пул
|
Добавить IP пул
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="row row-cards">
|
<>
|
||||||
{filteredPools.map(pool => (
|
{/* Группировка по серверам */}
|
||||||
<div key={pool.id} className="col-sm-6 col-lg-4">
|
{(serverFilter || filteredPools.some(p => p.serverId)) && (() => {
|
||||||
<div className="card">
|
const poolsByServer = {};
|
||||||
<div className="card-body">
|
filteredPools.forEach(pool => {
|
||||||
<div className="d-flex align-items-center mb-3">
|
const key = pool.serverId || '__unassigned__';
|
||||||
<span className="avatar avatar-sm bg-green-lt me-2">
|
if (!poolsByServer[key]) {
|
||||||
<IconDatabase size={16} className="text-green" />
|
poolsByServer[key] = [];
|
||||||
</span>
|
}
|
||||||
<div className="flex-fill">
|
poolsByServer[key].push(pool);
|
||||||
<div className="fw-medium">{pool.name || '—'}</div>
|
});
|
||||||
</div>
|
|
||||||
<div className="dropdown">
|
return Object.entries(poolsByServer)
|
||||||
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
|
.sort(([a], [b]) => {
|
||||||
<IconEdit size={16} />
|
if (a === '__unassigned__') return 1;
|
||||||
</button>
|
if (b === '__unassigned__') return -1;
|
||||||
<div className="dropdown-menu dropdown-menu-end">
|
return a.localeCompare(b);
|
||||||
<button className="dropdown-item" onClick={() => handleEditPool(pool)}>
|
})
|
||||||
<IconEdit size={16} className="me-2" />Редактировать
|
.map(([serverId, pools]) => renderServerGroup(serverId, pools, 'pool'));
|
||||||
</button>
|
})()}
|
||||||
<button className="dropdown-item text-danger" onClick={() => handleDeletePool(pool)}>
|
|
||||||
<IconTrash size={16} className="me-2" />Удалить
|
{/* Если нет фильтра по серверу и все пулы без сервера - показываем без группировки */}
|
||||||
</button>
|
{!serverFilter && !filteredPools.some(p => p.serverId) && (
|
||||||
</div>
|
<div className="row row-cards">
|
||||||
</div>
|
{filteredPools.map(pool => (
|
||||||
</div>
|
<div key={pool.id} className="col-12 col-md-6 col-lg-6">
|
||||||
|
{renderPoolCard(pool)}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user