fix(ui): адаптировать строки Ping-проб для телефона

This commit is contained in:
Denozordec committed 2026-09-21 09:58:43 +07:00
1 parent dfb993e1a3
commit e9e5030047
5 files changed
+133 -38

No files matched your search

+35 -32
View File
@@ -1,5 +1,7 @@
"use client"
import probeStyles from "./probe-rows.module.css"
import { Suspense, useState, useMemo, useEffect, useRef, useCallback } from "react"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
@@ -2422,7 +2424,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
</div>
{/* ── probe groups ── */}
<div className="p-6 flex flex-col gap-4">
<div className="p-4 md:p-6 flex flex-col gap-4">
{grouped.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-2">
@@ -2439,18 +2441,18 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
const warnCount = allServerProbes.filter(p => p.status === "warn").length
return (
<Frame key={srv.id} dense className="w-full overflow-hidden">
<Frame key={srv.id} dense className={cn("w-full overflow-hidden", probeStyles.container)}>
<FramePanel className="p-0 overflow-hidden">
{/* server header */}
<button onClick={() => toggleCollapse(srv.id)}
className="w-full flex items-center gap-2.5 px-4 py-3 hover:bg-muted/30 transition-colors text-left border-b">
<button type="button" aria-expanded={!isCollapsed} onClick={() => toggleCollapse(srv.id)}
className="w-full flex flex-wrap items-center gap-2.5 px-4 py-3 hover:bg-muted/30 transition-colors text-left border-b">
{isCollapsed
? <ChevronRightIcon className="size-4 text-muted-foreground shrink-0" />
: <ChevronDownIcon className="size-4 text-muted-foreground shrink-0" />}
<StatusDot status={srv.status} pulse={srv.status === "online"} />
<Flag code={srv.country} size={16} />
<span className="font-mono text-sm font-semibold">{srv.name}</span>
<span className={cn(probeStyles.serverName, "font-mono text-sm font-semibold min-w-0 wrap-anywhere")}>{srv.name}</span>
<TypeChip type={srv.type} />
<span className="text-xs text-muted-foreground">{srv.site}</span>
<div className="ml-auto flex items-center gap-3 text-xs">
@@ -2475,10 +2477,10 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
const groupBusy = !!probeGroupPingBusy[groupBusyKey]
return (
<div key={`${group.name}|${group.target}`}>
<div className="px-4 py-2 border-b bg-muted/20 flex items-center gap-2 text-xs">
<div className="px-4 py-2 border-b bg-muted/20 flex flex-wrap items-center gap-2 text-xs">
<ArrowRightIcon className="size-3.5 text-muted-foreground/60 shrink-0" />
<span className="font-medium truncate min-w-0">{group.name}</span>
<span className="font-mono text-muted-foreground shrink-0">{group.target}</span>
<span className="font-medium min-w-0 wrap-anywhere">{group.name}</span>
<span className="font-mono text-muted-foreground min-w-0 wrap-anywhere">{group.target}</span>
<div className="ml-auto flex items-center gap-2 shrink-0">
<span className="text-muted-foreground whitespace-nowrap">{group.probes.length} интерф.</span>
<Button
@@ -2496,8 +2498,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
</div>
</div>
<div className="grid items-center gap-3 px-4 py-1.5 bg-muted/30 border-b text-[10px] font-semibold uppercase tracking-widest text-muted-foreground"
style={{ gridTemplateColumns: "36px 16px 130px 120px 140px 70px 44px minmax(132px,1fr) 96px 36px 72px" }}>
<div className={cn(probeStyles.header, "items-center gap-3 px-4 py-1.5 bg-muted/30 border-b text-[10px] font-semibold uppercase tracking-widest text-muted-foreground")}>
<span />
<span />
<span>Интерфейс</span>
@@ -2517,33 +2518,33 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
<div key={p.id} data-probe-id={p.id} className="border-b border-border/40 last:border-b-0">
<div
className={cn(
"grid items-center gap-3 px-4 py-2.5 hover:bg-muted/20 transition-colors",
probeStyles.row, "items-center gap-3 px-4 py-2.5 hover:bg-muted/20 transition-colors",
!p.enabled && "opacity-40",
)}
style={{ gridTemplateColumns: "36px 16px 130px 120px 140px 70px 44px minmax(132px,1fr) 96px 36px 72px" }}>
<FormToggle checked={p.enabled} onChange={v => toggleProbe(p.id, v)} />
<StatusDot
>
<label className={probeStyles.toggle}><span className="sr-only">Включить пробу {p.name}</span><span className={probeStyles.label} aria-hidden="true">Включена</span><FormToggle checked={p.enabled} onChange={v => toggleProbe(p.id, v)} /></label>
<div className={probeStyles.status}><StatusDot
status={p.status === "up" ? "online" : p.status === "warn" ? "degraded" : "offline"}
pulse={p.status === "up" && p.enabled}
/>
<span className="font-mono text-xs text-muted-foreground truncate">{p.srcInterface || "auto"}</span>
<span className="text-sm font-medium truncate">{p.name}</span>
{p.filter === "—"
/><span className={probeStyles.mobile}>{!p.enabled ? "Отключена" : p.status === "up" ? "Доступна" : p.status === "warn" ? "Предупреждение" : "Недоступна"}</span></div>
<div className={probeStyles.iface}><span className={probeStyles.label}>Интерфейс</span><span className="font-mono text-xs text-muted-foreground">{p.srcInterface || "auto"}</span></div>
<div className={probeStyles.name}><span className="text-sm font-medium">{p.name}</span></div>
<div className={probeStyles.filter}><span className={probeStyles.label}>Фильтр</span>{p.filter === "—"
? <span className="text-muted-foreground/40 text-xs">—</span>
: <span className="text-[11px] font-medium text-sky-600 dark:text-sky-400 truncate">{p.filter}</span>
}
<span className={cn("font-mono text-sm font-semibold tabular-nums", rttColor(p.rtt, p.loss))}>
: <span className="text-[11px] font-medium text-sky-600 dark:text-sky-400">{p.filter}</span>
}</div>
<div className={probeStyles.rtt}><span className={probeStyles.label}>RTT</span><span className={cn("font-mono text-sm font-semibold tabular-nums", rttColor(p.rtt, p.loss))}>
{p.rtt === null ? "—" : `${p.rtt}мс`}
</span>
<span className={cn("font-mono text-xs tabular-nums",
</span></div>
<div className={probeStyles.loss}><span className={probeStyles.label}>Потери</span><span className={cn("font-mono text-xs tabular-nums",
p.loss === 0 ? "text-muted-foreground" :
p.loss >= 100 ? "text-red-600 dark:text-red-400" :
p.loss > 5 ? "text-red-600 dark:text-red-400" :
"text-amber-600 dark:text-amber-400",
)}>
{p.loss}%
</span>
<div className="min-w-0 flex flex-col gap-0.5 justify-center">
</span></div>
<div className={cn(probeStyles.bt, "min-w-0 flex flex-col gap-0.5 justify-center")}><span className={probeStyles.label}>BT · ping</span>
{!linkedSp?.lastRunAt ? (
<span className="text-muted-foreground/35 text-[10px]">—</span>
) : linkedSp.lastStatus === "error" && linkedSp.lastError ? (
@@ -2577,8 +2578,8 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
</>
)}
</div>
<Sparkline data={p.series} width={96} height={24} color={probeSparkColor(p.status)} filled />
<div className="flex justify-center">
<div className={probeStyles.graph}><span className={probeStyles.label}>График RTT</span><Sparkline data={p.series} width={96} height={24} color={probeSparkColor(p.status)} filled /></div>
<div className={probeStyles.star}><span className={probeStyles.label}>На дашборде</span>
<Button
type="button"
size="sm"
@@ -2586,6 +2587,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
className="size-7 p-0 text-muted-foreground hover:text-amber-500"
onClick={() => toggleDashboardStar(p.id)}
title={p.showOnDashboard ? "Убрать с дашборда" : "Показать на дашборде"}
aria-label={p.showOnDashboard ? "Убрать с дашборда" : "Показать на дашборде"}
aria-pressed={p.showOnDashboard === true}
>
<StarIcon
@@ -2598,15 +2600,15 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
/>
</Button>
</div>
<div className="flex justify-end gap-0.5">
<div className={cn(probeStyles.actions, "flex justify-end gap-1")}>
<Button size="sm" variant="ghost"
className="size-7 p-0 text-muted-foreground hover:text-foreground"
onClick={() => openEditProbe(p)} title="Редактировать пробу">
type="button" aria-label="Редактировать пробу" onClick={() => openEditProbe(p)} title="Редактировать пробу">
<PencilIcon className="size-3.5" />
</Button>
<Button size="sm" variant="ghost"
className="size-7 p-0 text-muted-foreground hover:text-destructive"
onClick={() => deleteProbe(p.id)} title="Удалить пробу">
type="button" aria-label="Удалить пробу" onClick={() => deleteProbe(p.id)} title="Удалить пробу">
<TrashIcon className="size-3.5" />
</Button>
</div>
@@ -2615,7 +2617,7 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
open={probeRttChartOpen[p.id] ?? false}
onOpenChange={(open) => setProbeRttChartOpen((m) => ({ ...m, [p.id]: open }))}
>
<CollapsibleTrigger
<CollapsibleTrigger type="button"
className={cn(
"flex w-full items-center gap-2 border-t border-border/50 bg-muted/15 px-4 py-1.5 text-left text-xs text-muted-foreground",
"outline-none hover:bg-muted/30 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
@@ -2657,7 +2659,8 @@ function UptimePageContent({ selectedProbeId, allProbesHref }: { selectedProbeId
setNewName(group.name); setNewTarget(group.target); setNewFilter("—")
setSheetOpen(true)
}}
className="w-full flex items-center gap-2 px-4 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
type="button"
className="w-full flex items-center gap-2 px-4 py-2 text-left wrap-anywhere text-xs text-muted-foreground hover:text-foreground hover:bg-muted/20 transition-colors border-t">
<PlusIcon className="size-3.5" />
Добавить интерфейс для {group.name} ({group.target})
</button>
+33
View File
@@ -0,0 +1,33 @@
.container { container-type: inline-size; }
.header { display: none; }
.serverName { flex: 1 1 160px; }
.row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas: "name name" "status toggle" "iface filter" "rtt loss" "bt graph" "star actions";
}
.row > * { min-width: 0; overflow-wrap: anywhere; }
.name { grid-area: name; }
.status { grid-area: status; display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; }
.toggle { grid-area: toggle; display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; justify-content: flex-end; }
.iface { grid-area: iface; }
.filter { grid-area: filter; }
.rtt { grid-area: rtt; }
.loss { grid-area: loss; }
.bt { grid-area: bt; }
.graph { grid-area: graph; }
.star { grid-area: star; display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; }
.actions { grid-area: actions; }
.label { display: block; font-size: 0.625rem; color: var(--muted-foreground); }
.actions button, .star button { min-width: 2rem; min-height: 2rem; }
@container (min-width: 1100px) {
.header, .row {
display: grid;
grid-template-columns: 36px 16px 130px 120px 140px 70px 44px minmax(132px, 1fr) 96px 36px 72px;
grid-template-areas: "toggle status iface name filter rtt loss bt graph star actions";
}
.label, .mobile { display: none; }
.toggle, .star { justify-content: center; }
.actions button, .star button { min-width: unset; min-height: unset; }
}