fix(ui): ограничить боковой каталог устройств

This commit is contained in:
Denozordec committed 2026-10-04 03:22:39 +07:00
1 parent 2bdcc1621f
commit 1cce712833
6 files changed
+141 -10

No files matched your search

+45 -5
View File
@@ -1,6 +1,6 @@
"use client"
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from "react"
import { useEffect, useMemo, useRef, useState, useId, type KeyboardEvent, type ReactNode } from "react"
import type { ServerStatus, ServerType } from "@/lib/data"
import { Flag } from "@/components/flag"
import { StatusDot } from "@/components/status-dot"
@@ -27,6 +27,9 @@ import {
InputGroupInput,
} from "@/components/ui/input-group"
import { cn } from "@/lib/utils"
import { useTable } from "@tanstack/react-table"
import { DataGrid, DataGridPagination, dataGridFeatures } from "@/components/reui/data-grid"
import { DATA_GRID_LABELS_RU } from "@/lib/data-grid-labels-ru"
import { LayersIcon, SearchIcon, ServerIcon } from "lucide-react"
/** Preview: https://reui.io/preview/base/list-9 · https://reui.io/docs/components/base/icon-tile · https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/badge */
@@ -63,7 +66,7 @@ function typeBadgeLabel(type: ServerType): string {
function ServerTypeBadge({ type }: { type: ServerType }) {
return (
<Badge variant={typeBadgeVariant(type)} size="xs" className="font-mono">
<Badge variant={typeBadgeVariant(type)} size="xs" className="font-mono text-foreground">
{typeBadgeLabel(type)}
</Badge>
)
@@ -112,6 +115,7 @@ function ServerTileRail({
preserveSelection?: boolean
className?: string
}) {
const railId = useId()
const [query, setQuery] = useState("")
const q = query.trim().toLowerCase()
const listRef = useRef<HTMLDivElement>(null)
@@ -158,6 +162,35 @@ function ServerTileRail({
if (resolvedId && resolvedId !== selectedId) onSelect(resolvedId)
}, [onSelect, resolvedId, selectedId])
const orderedItems = useMemo(() => groups.flatMap(group => group.items), [groups])
const table = useTable({
features: dataGridFeatures,
data: orderedItems,
columns: [],
getRowId: item => item.id,
manualPagination: false,
autoResetPageIndex: false,
initialState: { pagination: { pageIndex: 0, pageSize: 50 } },
})
const { pageIndex, pageSize } = table.state.pagination
useEffect(() => {
const lastPage = Math.max(0, Math.ceil(orderedItems.length / pageSize) - 1)
if (pageIndex > lastPage) table.setPageIndex(lastPage)
}, [orderedItems.length, pageIndex, pageSize, table])
const setPageIndex = table.setPageIndex
const selectedIndex = orderedItems.findIndex(item => item.id === resolvedId)
useEffect(() => {
setPageIndex(selectedIndex < 0 ? 0 : Math.floor(selectedIndex / pageSize))
}, [resolvedId, selectedIndex, q, pageSize, setPageIndex])
useEffect(() => {
const viewport = listRef.current?.closest<HTMLElement>('[data-slot="scroll-area-viewport"]')
viewport?.scrollTo({ top: 0 })
listRef.current?.querySelector<HTMLElement>('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" })
}, [pageIndex, resolvedId, q, pageSize])
const visibleIds = new Set(table.getRowModel().rows.map(row => row.id))
const visibleGroups = groups.map(group => ({ ...group, items: group.items.filter(item => visibleIds.has(item.id)) })).filter(group => group.items.length > 0)
const activeId = resolvedId === ALL_SERVERS_ID && showAllTile || visibleIds.has(resolvedId) ? resolvedId : undefined
const selectableIds = useMemo(() => {
const ids: string[] = []
if (showAllTile) ids.push(ALL_SERVERS_ID)
@@ -211,6 +244,7 @@ function ServerTileRail({
}
return (
<DataGrid table={table} recordCount={orderedItems.length} i18n={{ labels: { ...DATA_GRID_LABELS_RU, goToPage: page => `Страница устройств ${page}` } }}>
<Frame dense spacing="sm" className={cn("flex h-full min-h-0 w-full flex-col", className)}>
<FramePanel className="flex min-h-0 flex-1 flex-col gap-0 p-0">
<FrameHeader className="flex flex-col gap-2 border-b px-3 py-2">
@@ -249,12 +283,13 @@ function ServerTileRail({
className="flex flex-col gap-0.5 p-1.5"
role="listbox"
aria-label="Серверы"
aria-activedescendant={resolvedId ? `server-tile-${resolvedId}` : undefined}
aria-activedescendant={activeId ? `${railId}-server-tile-${activeId}` : undefined}
tabIndex={0}
onKeyDown={handleListKeyDown}
>
{showAllTile ? (
<ServerTileButton
id={`${railId}-server-tile-${ALL_SERVERS_ID}`}
item={allItem}
selected={resolvedId === ALL_SERVERS_ID}
onSelect={() => onSelect(ALL_SERVERS_ID)}
@@ -267,7 +302,7 @@ function ServerTileRail({
}
/>
) : null}
{groups.map((group) => (
{visibleGroups.map((group) => (
<div key={group.site || "__flat__"} className="flex flex-col gap-0.5">
{groupBySite && group.site ? (
<p className="sticky top-0 z-[1] bg-background/95 px-2 py-1 text-[10px] font-bold uppercase tracking-wide text-muted-foreground backdrop-blur-sm">
@@ -277,6 +312,7 @@ function ServerTileRail({
{group.items.map((item) => (
<ServerTileButton
key={item.id}
id={`${railId}-server-tile-${item.id}`}
item={item}
selected={resolvedId === item.id}
onSelect={() => onSelect(item.id)}
@@ -307,12 +343,15 @@ function ServerTileRail({
) : null}
</div>
</ScrollArea>
{(orderedItems.length > 50 || pageSize !== 50) && <DataGridPagination moreLimit={3} sizes={[25, 50, 100]} rowsPerPageLabel="Устройств на странице" previousPageLabel="Предыдущие устройства" nextPageLabel="Следующие устройства" className="grow-0 shrink-0 border-t px-2 sm:flex-col [&>div]:flex-col [&>div]:gap-2" />}
</FramePanel>
</Frame>
</DataGrid>
)
}
function ServerTileButton({
id,
item,
selected,
onSelect,
@@ -321,6 +360,7 @@ function ServerTileButton({
showSite = true,
icon,
}: {
id: string
item: ServerTileItem
selected: boolean
onSelect: () => void
@@ -337,7 +377,7 @@ function ServerTileButton({
return (
<Item
id={`server-tile-${item.id}`}
id={id}
size="xs"
variant={selected ? "muted" : "default"}
render={