feat(NetworkConfigManager): implement category-based accordion view for gateways and interfaces, improving organization and user navigation
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s
This commit is contained in:
@@ -29,6 +29,8 @@ import {
|
|||||||
IconCode,
|
IconCode,
|
||||||
IconArrowsRightLeft,
|
IconArrowsRightLeft,
|
||||||
IconLock,
|
IconLock,
|
||||||
|
IconChevronDown,
|
||||||
|
IconChevronUp,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +120,7 @@ function NetworkConfigManager() {
|
|||||||
const [gatewayTypeFilter, setGatewayTypeFilter] = useState(''); // Для фильтрации gateway по типу
|
const [gatewayTypeFilter, setGatewayTypeFilter] = useState(''); // Для фильтрации gateway по типу
|
||||||
const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table'
|
const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table'
|
||||||
const [ipRegistrySearch, setIpRegistrySearch] = useState(''); // Поиск в реестре IP
|
const [ipRegistrySearch, setIpRegistrySearch] = useState(''); // Поиск в реестре IP
|
||||||
|
const [expandedCategories, setExpandedCategories] = useState(new Set(['home', 'jumphost', 'exit'])); // Развернутые категории аккордеонов
|
||||||
|
|
||||||
// === Modals ===
|
// === Modals ===
|
||||||
const [gatewayModalOpen, setGatewayModalOpen] = useState(false);
|
const [gatewayModalOpen, setGatewayModalOpen] = useState(false);
|
||||||
@@ -247,6 +250,45 @@ function NetworkConfigManager() {
|
|||||||
return server.dns || server.ip;
|
return server.dns || server.ip;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === Определение категории сервера ===
|
||||||
|
const getServerCategory = (serverId) => {
|
||||||
|
if (!serverId || serverId === '__unassigned__') return 'other';
|
||||||
|
const server = getServerInfo(serverId);
|
||||||
|
if (!server) return 'other';
|
||||||
|
const serverType = server.type?.toLowerCase();
|
||||||
|
if (serverType === 'home') return 'home';
|
||||||
|
if (serverType === 'jumphost') return 'jumphost';
|
||||||
|
if (serverType === 'exit') return 'exit';
|
||||||
|
return 'other';
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Получение названия категории ===
|
||||||
|
const getCategoryLabel = (category) => {
|
||||||
|
switch (category) {
|
||||||
|
case 'home':
|
||||||
|
return 'Домашние (входные роутеры)';
|
||||||
|
case 'jumphost':
|
||||||
|
return 'Jumphosts';
|
||||||
|
case 'exit':
|
||||||
|
return 'Exit nodes';
|
||||||
|
default:
|
||||||
|
return 'Прочие';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Переключение аккордеона категории ===
|
||||||
|
const toggleCategory = (category) => {
|
||||||
|
setExpandedCategories(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(category)) {
|
||||||
|
newSet.delete(category);
|
||||||
|
} else {
|
||||||
|
newSet.add(category);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// === Уникальные серверы для фильтра ===
|
// === Уникальные серверы для фильтра ===
|
||||||
const uniqueServersInConfig = useMemo(() => {
|
const uniqueServersInConfig = useMemo(() => {
|
||||||
const allServerIds = new Set([
|
const allServerIds = new Set([
|
||||||
@@ -376,6 +418,45 @@ function NetworkConfigManager() {
|
|||||||
return grouped;
|
return grouped;
|
||||||
}, [filteredInterfaces]);
|
}, [filteredInterfaces]);
|
||||||
|
|
||||||
|
// === Группировка по категориям серверов ===
|
||||||
|
const gatewaysByCategory = useMemo(() => {
|
||||||
|
const grouped = {
|
||||||
|
home: {},
|
||||||
|
jumphost: {},
|
||||||
|
exit: {},
|
||||||
|
other: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(gatewaysByServer).forEach(([serverId, gateways]) => {
|
||||||
|
const category = getServerCategory(serverId);
|
||||||
|
if (!grouped[category][serverId]) {
|
||||||
|
grouped[category][serverId] = [];
|
||||||
|
}
|
||||||
|
grouped[category][serverId] = gateways;
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}, [gatewaysByServer, servers]);
|
||||||
|
|
||||||
|
const interfacesByCategory = useMemo(() => {
|
||||||
|
const grouped = {
|
||||||
|
home: {},
|
||||||
|
jumphost: {},
|
||||||
|
exit: {},
|
||||||
|
other: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(interfacesByServer).forEach(([serverId, interfaces]) => {
|
||||||
|
const category = getServerCategory(serverId);
|
||||||
|
if (!grouped[category][serverId]) {
|
||||||
|
grouped[category][serverId] = [];
|
||||||
|
}
|
||||||
|
grouped[category][serverId] = interfaces;
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}, [interfacesByServer, servers]);
|
||||||
|
|
||||||
// === Сброс фильтров ===
|
// === Сброс фильтров ===
|
||||||
const resetFilters = () => {
|
const resetFilters = () => {
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
@@ -2041,6 +2122,64 @@ function NetworkConfigManager() {
|
|||||||
return t?.color || 'secondary';
|
return t?.color || 'secondary';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === Рендер категории с аккордеоном ===
|
||||||
|
const renderCategoryAccordion = (category, itemsByServer, type) => {
|
||||||
|
const categoryItems = Object.entries(itemsByServer);
|
||||||
|
if (categoryItems.length === 0) return null;
|
||||||
|
|
||||||
|
const isExpanded = expandedCategories.has(category);
|
||||||
|
const totalItems = categoryItems.reduce((sum, [, items]) => sum + items.length, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={category} className="card mb-3">
|
||||||
|
{/* Заголовок категории (кликабельный) */}
|
||||||
|
<div
|
||||||
|
className="card-header py-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(32, 107, 196, 0.05)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none'
|
||||||
|
}}
|
||||||
|
onClick={() => toggleCategory(category)}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
{/* Иконка сворачивания */}
|
||||||
|
{isExpanded ? (
|
||||||
|
<IconChevronUp size={20} className="me-2 text-muted" />
|
||||||
|
) : (
|
||||||
|
<IconChevronDown size={20} className="me-2 text-muted" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h3 className="card-title mb-0 me-2">{getCategoryLabel(category)}</h3>
|
||||||
|
<span className="badge bg-blue-lt text-blue ms-auto">
|
||||||
|
{categoryItems.length} {categoryItems.length === 1 ? 'сервер' : categoryItems.length < 5 ? 'сервера' : 'серверов'} • {totalItems} {type === 'gateway' ? (totalItems === 1 ? 'gateway' : 'gateways') : (totalItems === 1 ? 'интерфейс' : 'интерфейсов')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Контент (сворачиваемый) */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="card-body p-3">
|
||||||
|
{categoryItems
|
||||||
|
.sort(([a], [b]) => {
|
||||||
|
// Сортируем: сначала серверы с информацией, потом без привязки
|
||||||
|
if (a === '__unassigned__') return 1;
|
||||||
|
if (b === '__unassigned__') return -1;
|
||||||
|
const serverA = getServerInfo(a);
|
||||||
|
const serverB = getServerInfo(b);
|
||||||
|
const nameA = serverA?.dns || serverA?.ip || a;
|
||||||
|
const nameB = serverB?.dns || serverB?.ip || b;
|
||||||
|
return nameA.localeCompare(nameB);
|
||||||
|
})
|
||||||
|
.map(([serverId, items]) =>
|
||||||
|
renderServerGroup(serverId, items, type)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// === Рендер карточки сервера с его элементами ===
|
// === Рендер карточки сервера с его элементами ===
|
||||||
const renderServerGroup = (serverId, items, type) => {
|
const renderServerGroup = (serverId, items, type) => {
|
||||||
const server = getServerInfo(serverId);
|
const server = getServerInfo(serverId);
|
||||||
@@ -2768,10 +2907,10 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'cards' ? (
|
) : viewMode === 'cards' ? (
|
||||||
// Grouped cards view
|
// Grouped cards view with category accordions
|
||||||
<div>
|
<div>
|
||||||
{Object.entries(gatewaysByServer).map(([serverId, gateways]) =>
|
{['home', 'jumphost', 'exit', 'other'].map(category =>
|
||||||
renderServerGroup(serverId, gateways, 'gateway')
|
renderCategoryAccordion(category, gatewaysByCategory[category], 'gateway')
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -2944,10 +3083,10 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'cards' ? (
|
) : viewMode === 'cards' ? (
|
||||||
// Grouped cards view
|
// Grouped cards view with category accordions
|
||||||
<div>
|
<div>
|
||||||
{Object.entries(interfacesByServer).map(([serverId, interfaces]) =>
|
{['home', 'jumphost', 'exit', 'other'].map(category =>
|
||||||
renderServerGroup(serverId, interfaces, 'interface')
|
renderCategoryAccordion(category, interfacesByCategory[category], 'interface')
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user