fix(ui): адаптировать длинные данные и пустые списки трафика

This commit is contained in:
Denozordec committed 2026-09-24 09:53:10 +07:00
1 parent cb16c03e01
commit 094c812c37
3 files changed
+86 -12

No files matched your search

+17 -12
View File
@@ -323,7 +323,7 @@ function StatChip({ icon, label, value }: { icon: ReactNode; label: string; valu
return (
<div className="flex items-center gap-2">
<div className="flex items-center justify-center size-7 rounded-md bg-muted shrink-0">{icon}</div>
<div className="leading-tight">
<div className="leading-tight min-w-0 break-words">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="text-sm font-semibold tabular-nums">{value}</p>
</div>
@@ -475,13 +475,13 @@ function IfaceCard({ c, selected, onClick }: { c: BoundIfaceTraffic; selected: b
function IfaceRow({ c, showServer = false }: { c: BoundIfaceTraffic; showServer?: boolean }) {
return (
<div className={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-lg border bg-card hover:bg-muted/30 transition-colors",
"flex flex-wrap items-center gap-3 px-3 py-2.5 rounded-lg border bg-card hover:bg-muted/30 transition-colors",
c.status === "offline" && "opacity-55",
)}>
<StatusDot status={c.status} />
<div className="flex-1 min-w-0">
<div className="min-w-0 basis-full sm:basis-0 sm:flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-mono font-semibold">{boundIfaceLabel(c)}</span>
<span className="text-xs font-mono font-semibold break-all">{boundIfaceLabel(c)}</span>
<Badge variant={TYPE_VARIANT[c.interfaceType]} size="sm">{IFACE_TYPE_LABEL[c.interfaceType]}</Badge>
{showServer && (
<span className="inline-flex items-center gap-1 text-[10px] font-mono text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
@@ -558,7 +558,7 @@ function TotalsRow({ rxTotal, txTotal, rxSeries, txSeries }: {
rxTotal: number; txTotal: number; rxSeries: number[]; txSeries: number[]
}) {
return (
<div className="mt-4 pt-4 border-t grid grid-cols-2 gap-4">
<div className="mt-4 pt-4 border-t grid grid-cols-2 gap-4 [&>div]:min-w-0 [&_svg]:max-w-full">
<div>
<p className="text-xs text-muted-foreground mb-1">Получено за период</p>
<p className="text-lg font-semibold tabular-nums">
@@ -595,7 +595,7 @@ function ServerDetail({
<>
<DetailHeader range={range} setRange={setRange}>
<StatusDot status={sel.status} />
<h2 className="text-base font-semibold">{sel.name}</h2>
<h2 className="text-base font-semibold break-all">{sel.name}</h2>
<span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded flex items-center gap-1">
<Flag code={sel.country} />{sel.site}
</span>
@@ -662,8 +662,8 @@ function UserDetail({ sel, range, setRange }: { sel: UserTraffic; range: Range;
<UsersIcon className="size-4 text-muted-foreground" />
</div>
<div className="leading-tight">
<h2 className="text-base font-semibold">{sel.login}</h2>
<p className="text-xs text-muted-foreground">{sel.displayName} · {sel.role}</p>
<h2 className="text-base font-semibold break-all">{sel.login}</h2>
<p className="text-xs text-muted-foreground break-all">{sel.displayName} · {sel.role}</p>
</div>
</DetailHeader>
{!hasClients ? (
@@ -698,7 +698,7 @@ function IfaceDetail({ sel, range, setRange }: { sel: BoundIfaceTraffic; range:
<DetailHeader range={range} setRange={setRange}>
<StatusDot status={sel.status} />
<div className="leading-tight min-w-0">
<h2 className="text-base font-mono font-semibold">{boundIfaceLabel(sel)}</h2>
<h2 className="text-base font-mono font-semibold break-all">{boundIfaceLabel(sel)}</h2>
<p className="text-xs text-muted-foreground truncate">{sel.comment || sel.userLogin}</p>
</div>
<Badge variant={TYPE_VARIANT[sel.interfaceType]} size="sm">{IFACE_TYPE_LABEL[sel.interfaceType]}</Badge>
@@ -717,7 +717,7 @@ function IfaceDetail({ sel, range, setRange }: { sel: BoundIfaceTraffic; range:
<div className="mt-4 pt-4 border-t grid grid-cols-2 sm:grid-cols-3 gap-4">
<div>
<p className="text-xs text-muted-foreground mb-0.5">Сервер</p>
<p className="text-sm font-mono">{sel.serverName}</p>
<p className="text-sm font-mono break-all">{sel.serverName}</p>
</div>
<div>
<p className="text-xs text-muted-foreground mb-0.5">Тип</p>
@@ -725,7 +725,7 @@ function IfaceDetail({ sel, range, setRange }: { sel: BoundIfaceTraffic; range:
</div>
<div>
<p className="text-xs text-muted-foreground mb-0.5">Пользователь</p>
<p className="text-sm font-mono">{sel.userLogin}</p>
<p className="text-sm font-mono break-all">{sel.userLogin}</p>
</div>
</div>
</>
@@ -1178,7 +1178,7 @@ export default function TrafficPage() {
{/* ── mode tab bar ── */}
<div className="border-b bg-background shrink-0">
<div className="flex items-center overflow-x-auto px-3 sm:px-6">
<div role="group" aria-label="Разделы трафика" className="flex items-center overflow-x-auto px-3 sm:px-6">
{GROUP_MODES.map(({ mode, icon, label }) => (
<button key={mode} onClick={() => handleModeChange(mode)}
className={cn(
@@ -1360,6 +1360,11 @@ export default function TrafficPage() {
{isLive && trafficLoading && <p role="status" className="text-sm text-muted-foreground">Загрузка списка трафика…</p>}
{trafficError && <Alert variant="destructive" role="alert"><AlertTitle>Список трафика недоступен</AlertTitle><AlertDescription>{trafficError}<Button type="button" size="sm" variant="outline" onClick={() => setTrafficRetry(value => value + 1)}>Повторить загрузку списка</Button></AlertDescription></Alert>}
{(!isLive || trafficReady) && (effectiveMode === "servers" ? sortedServers.length : effectiveMode === "users" ? sortedUsers.length : sortedIfaces.length) === 0 && (
<p role="status" className="text-sm text-muted-foreground">
{search.trim() ? "По вашему запросу ничего не найдено" : effectiveMode === "servers" ? "Нет серверов с данными трафика" : effectiveMode === "users" ? "Нет клиентов с данными трафика" : "Нет привязанных интерфейсов с данными трафика"}
</p>
)}
{/* entity list */}
<div className="flex flex-col gap-2">
{effectiveMode === "servers" && sortedServers.map(s => (