Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m23s
Docker images / frontend-image (push) Successful in 3m30s
Docker images / updater-image (push) Successful in 52s
Docker images / backend-image (push) Successful in 3m8s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 14s
Сопоставить клиентов с ifIndex как на карте трафика, чтобы KPI пользователей не обнулялся. На экране — разрез остальных измерений и сводная матрица. Co-authored-by: Cursor <[email protected]>
90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo } from "react"
|
|
import { CompactDataGrid, type CompactDataGridColumn } from "@/components/data-grids/compact-data-grid"
|
|
import { formatBytes } from "@/lib/fmt-rate"
|
|
import { cn } from "@/lib/utils"
|
|
import type { StatisticsPivotDto } from "@mmapp/contracts/statistics"
|
|
|
|
interface PivotGridRow {
|
|
id: string
|
|
label: string
|
|
total: number
|
|
[key: string]: string | number
|
|
}
|
|
|
|
export function StatisticsPivotGrid({
|
|
data,
|
|
onCellClick,
|
|
isLoading,
|
|
}: {
|
|
data: StatisticsPivotDto
|
|
onCellClick?: (rowId: string, colId: string) => void
|
|
isLoading?: boolean
|
|
}) {
|
|
const rows: PivotGridRow[] = useMemo(
|
|
() =>
|
|
data.rows.map((r) => {
|
|
const next: PivotGridRow = { id: r.id, label: r.label, total: r.total }
|
|
for (const col of data.columns) {
|
|
next[`c:${col.id}`] = r.cells[col.id] ?? 0
|
|
}
|
|
return next
|
|
}),
|
|
[data],
|
|
)
|
|
|
|
const columns: CompactDataGridColumn<PivotGridRow>[] = [
|
|
{
|
|
id: "label",
|
|
header: "Измерение",
|
|
accessorKey: "label",
|
|
cell: (row) => <span className="truncate font-medium">{row.label}</span>,
|
|
},
|
|
...data.columns.map((col) => ({
|
|
id: `c:${col.id}`,
|
|
header: col.label,
|
|
accessorKey: `c:${col.id}` as const,
|
|
cell: (row: PivotGridRow) => {
|
|
const value = Number(row[`c:${col.id}`] ?? 0)
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
"tabular-nums text-left hover:underline",
|
|
col.id === "__other__" || row.id === "__other__" ? "text-muted-foreground" : "",
|
|
)}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
if (row.id === "__other__" || col.id === "__other__") return
|
|
onCellClick?.(row.id, col.id)
|
|
}}
|
|
>
|
|
{data.metric === "packets" ? value.toLocaleString("ru-RU") : formatBytes(value)}
|
|
</button>
|
|
)
|
|
},
|
|
})),
|
|
{
|
|
id: "total",
|
|
header: "Итого",
|
|
accessorKey: "total",
|
|
cell: (row) => (
|
|
<span className="tabular-nums font-medium">
|
|
{data.metric === "packets" ? row.total.toLocaleString("ru-RU") : formatBytes(row.total)}
|
|
</span>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<CompactDataGrid
|
|
data={rows}
|
|
columns={columns}
|
|
isLoading={isLoading}
|
|
emptyTitle="Нет данных сводной"
|
|
emptyDescription="Выберите разные измерения строк и колонок."
|
|
/>
|
|
)
|
|
}
|