feat(ServerManager): implement server grouping in cards view and enhance server modal for groupKey input
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s

This commit is contained in:
2026-02-17 10:37:23 +07:00
parent 29fcee8ab1
commit d799c38edf
5 changed files with 216 additions and 14 deletions
+62 -14
View File
@@ -31,6 +31,7 @@ import {
LinkGeneratorModal,
ServerSidebar,
ServerCard,
ServerGroupCard,
ServerBulkActions
} from './components/server/index.js';
import PageHeader from './components/PageHeader.jsx';
@@ -217,9 +218,44 @@ function ServerManager() {
return result;
}, [servers, filters, searchTerm, sortField, sortOrder]);
// Элементы для отображения: серверы с одинаковым groupKey (и type home) объединяются в одну карточку
const displayItems = useMemo(() => {
const keyToServers = {};
filteredServers.forEach((s) => {
const k = (s.groupKey && String(s.groupKey).trim()) || null;
if (k) {
if (!keyToServers[k]) keyToServers[k] = [];
keyToServers[k].push(s);
}
});
const seenKeys = new Set();
const items = [];
for (const s of filteredServers) {
const k = (s.groupKey && String(s.groupKey).trim()) || null;
if (!k) {
items.push({ type: 'server', server: s });
} else {
if (!seenKeys.has(k)) {
seenKeys.add(k);
const group = keyToServers[k];
items.push(group.length >= 2 ? { type: 'group', groupKey: k, servers: group } : { type: 'server', server: group[0] });
}
}
}
return items;
}, [filteredServers]);
// === Пагинация ===
const totalPages = Math.ceil(filteredServers.length / pageSize);
const totalPagesCards = Math.ceil(displayItems.length / pageSize);
const totalPagesTable = Math.ceil(filteredServers.length / pageSize);
const totalPages = viewMode === 'cards' ? totalPagesCards : totalPagesTable;
const paginatedDisplayItems = displayItems.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const paginatedServers = filteredServers.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const totalItemsCount = viewMode === 'cards' ? displayItems.length : filteredServers.length;
useEffect(() => {
if (currentPage > totalPages && totalPages >= 1) setCurrentPage(1);
}, [viewMode, totalPages]);
// === Handlers ===
const handleSort = (field) => {
@@ -252,7 +288,7 @@ function ServerManager() {
if (selectedServers.size === filteredServers.length) {
setSelectedServers(new Set());
} else {
setSelectedServers(new Set(filteredServers.map(s => s.ip)));
setSelectedServers(new Set(filteredServers.map((s) => s.ip)));
}
};
@@ -792,18 +828,30 @@ function ServerManager() {
</div>
</div>
) : viewMode === 'cards' ? (
/* Cards view */
/* Cards view: одиночные серверы и группы (groupKey) */
<div>
{paginatedServers.map(server => (
<ServerCard
key={server.id || server.ip}
server={server}
selected={selectedServers.has(server.ip)}
onSelect={toggleServerSelection}
onEdit={handleEdit}
onDelete={confirmDelete}
/>
))}
{paginatedDisplayItems.map((item) =>
item.type === 'group' ? (
<ServerGroupCard
key={`group-${item.groupKey}`}
groupKey={item.groupKey}
servers={item.servers}
selectedSet={selectedServers}
onSelect={toggleServerSelection}
onEdit={handleEdit}
onDelete={confirmDelete}
/>
) : (
<ServerCard
key={item.server.id || item.server.ip}
server={item.server}
selected={selectedServers.has(item.server.ip)}
onSelect={toggleServerSelection}
onEdit={handleEdit}
onDelete={confirmDelete}
/>
)
)}
</div>
) : (
/* Table view */
@@ -892,7 +940,7 @@ function ServerManager() {
<div className="card mt-3">
<div className="card-footer d-flex align-items-center justify-content-between">
<div className="text-muted">
{(currentPage - 1) * pageSize + 1} {Math.min(currentPage * pageSize, filteredServers.length)} из {filteredServers.length}
{(currentPage - 1) * pageSize + 1} {Math.min(currentPage * pageSize, totalItemsCount)} из {totalItemsCount}
</div>
<ul className="pagination m-0">
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>