Files
MikrotikManager/components/reui-kit/traffic-rx-tx-chart.tsx
T
DenozordecandCursor 36c5305db7
Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m13s
Docker images / frontend-image (push) Successful in 3m14s
Docker images / updater-image (push) Successful in 47s
Docker images / backend-image (push) Successful in 2m44s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 14s
feat(dashboard): собрать ops-дашборд на ReUI Frame с живыми данными
Co-authored-by: Cursor <[email protected]>
2026-09-08 15:13:27 +07:00

142 lines
4.0 KiB
TypeScript

"use client"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
import { Frame, FramePanel } from "@/components/reui/frame"
import { ChartContainer, ChartTooltip, type ChartConfig } from "@/components/ui/chart"
import { fmtRate, formatRangeAgo, TRAFFIC_RANGE_MINUTES } from "@/lib/fmt-rate"
/**
* История RX/TX — adapt ReUI PRO chart-23 (multi-line + tooltip).
* @see https://reui.io/preview/base/chart-23
* @see https://reui.io/blocks
*/
const chartConfig = {
rx: { label: "RX", color: "var(--chart-rx)" },
tx: { label: "TX", color: "var(--chart-tx)" },
} satisfies ChartConfig
function ChartLegendItem({ label, color }: { label: string; color: string }) {
return (
<div className="flex items-center gap-2">
<div className="size-2 rounded-full" style={{ backgroundColor: color }} />
<span className="text-muted-foreground text-xs font-medium">{label}</span>
</div>
)
}
function CustomTooltip({
active,
payload,
}: {
active?: boolean
payload?: {
dataKey: string
color: string
value: number
}[]
}) {
if (!active || !payload?.length) return null
return (
<div className="min-w-[120px] flex flex-col gap-1.5 rounded-lg bg-popover p-3 text-popover-foreground shadow-lg ring-1 ring-foreground/10">
{payload.map((item) => (
<div key={item.dataKey}>
<div className="text-[10px] font-medium tracking-wider uppercase opacity-70">
{item.dataKey === "rx" ? "RX" : "TX"}:
</div>
<div className="text-sm font-semibold tabular-nums">
{fmtRate(item.value)}
</div>
</div>
))}
</div>
)
}
function toChartData(rx: number[], tx: number[], rangeMinutes: number) {
const n = Math.max(rx.length, tx.length, 1)
const denom = Math.max(n - 1, 1)
return Array.from({ length: n }, (_, i) => {
const minutesAgo = Math.round((1 - i / denom) * rangeMinutes)
return {
time: formatRangeAgo(minutesAgo),
rx: rx[i] ?? 0,
tx: tx[i] ?? 0,
}
})
}
export function TrafficRxTxChart({
rx,
tx,
range = "1h",
embedded = false,
}: {
rx: number[]
tx: number[]
range?: string
/** Skip outer Frame when already inside OpsPanel / Frame. */
embedded?: boolean
}) {
const rangeMinutes = TRAFFIC_RANGE_MINUTES[range] ?? 60
const data = toChartData(rx, tx, rangeMinutes)
const tickEvery = Math.max(1, Math.ceil(data.length / 6))
const chart = (
<div className="flex flex-col gap-4">
<ChartContainer config={chartConfig} className="-ms-4 aspect-auto h-[220px] w-full">
<LineChart data={data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<CartesianGrid
strokeDasharray="4 8"
vertical={false}
stroke="var(--border)"
/>
<XAxis
dataKey="time"
axisLine={false}
tickLine={false}
tick={{ fontSize: 11 }}
tickMargin={10}
interval={tickEvery - 1}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 11 }}
tickFormatter={(v: number) => fmtRate(Number(v))}
tickMargin={8}
width={72}
/>
<ChartTooltip content={<CustomTooltip />} />
<Line
dataKey="rx"
type="monotone"
stroke="var(--chart-rx)"
strokeWidth={2}
dot={false}
/>
<Line
dataKey="tx"
type="monotone"
stroke="var(--chart-tx)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
<div className="mb-1 flex items-center justify-center gap-6">
<ChartLegendItem label="RX (входящий)" color="var(--chart-rx)" />
<ChartLegendItem label="TX (исходящий)" color="var(--chart-tx)" />
</div>
</div>
)
if (embedded) return chart
return (
<Frame className="w-full">
<FramePanel className="flex flex-col gap-6">{chart}</FramePanel>
</Frame>
)
}