feat(traffic): разделить плоскости IPFIX и показать путь JH→EN
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Failing after 1m31s
Docker images / frontend-image (push) Successful in 2m36s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 42s
Docker images / publish-release (push) Skipped
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Failing after 1m31s
Docker images / frontend-image (push) Successful in 2m36s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 42s
Docker images / publish-release (push) Skipped
Считать payload отдельно от overlay GRE/ESP и mesh; вкладка Пути и KPI Wire из счётчиков iface. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { type ColumnDef, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||
import type { FlowAnalyticsDto, FlowBreakdownRow, FlowEntityCard, FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
|
||||
import { ArrowDownIcon, ArrowUpIcon, GitBranchIcon, GlobeIcon, LayersIcon, UsersIcon } from "lucide-react"
|
||||
import type { FlowAnalyticsDto, FlowBreakdownRow, FlowEntityCard, FlowPathRow, FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
|
||||
import { ArrowDownIcon, ArrowUpIcon, GitBranchIcon, GlobeIcon, LayersIcon, NetworkIcon, RouteIcon, ShieldIcon, UsersIcon } from "lucide-react"
|
||||
import { Badge } from "@/components/reui/badge"
|
||||
import { KpiStatGrid } from "@/components/reui-kit/kpi-stat-grid"
|
||||
import { KpiStatGrid, type KpiStatItem } from "@/components/reui-kit/kpi-stat-grid"
|
||||
import { TrafficRxTxChart } from "@/components/reui-kit/traffic-rx-tx-chart"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
@@ -110,7 +110,7 @@ export function FlowEntityCardView({
|
||||
}
|
||||
|
||||
type SessionFilter = {
|
||||
kind: "application" | "category" | "service" | "asn" | "country" | "protocol" | "source" | "destination" | "iface"
|
||||
kind: "application" | "category" | "service" | "asn" | "country" | "protocol" | "source" | "destination" | "iface" | "client" | "en"
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
@@ -126,6 +126,8 @@ function talkerMatchesFilter(row: FlowTalkerDto, filter: SessionFilter): boolean
|
||||
case "source": return row.src === filter.value
|
||||
case "destination": return row.dst === filter.value
|
||||
case "iface": return row.inIface === filter.value
|
||||
case "client": return (row.clientId || "unknown") === filter.value
|
||||
case "en": return (row.enId || "") === filter.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +212,101 @@ function FlowBreakdownGrid({
|
||||
)
|
||||
}
|
||||
|
||||
function FlowPathsGrid({
|
||||
rows,
|
||||
empty,
|
||||
onPick,
|
||||
}: {
|
||||
rows: FlowPathRow[]
|
||||
empty?: string
|
||||
onPick?: (row: FlowPathRow) => void
|
||||
}) {
|
||||
const columns = useMemo<ColumnDef<FlowPathRow>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "client",
|
||||
accessorKey: "clientName",
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">Клиент</span>,
|
||||
cell: ({ row }) => <span className="text-sm font-medium">{row.original.clientName}</span>,
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST },
|
||||
},
|
||||
{
|
||||
id: "ifaces",
|
||||
accessorKey: "ifaces",
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">Ifaces</span>,
|
||||
cell: ({ row }) => <span className="font-mono text-xs text-muted-foreground">{row.original.ifaces}</span>,
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "jh",
|
||||
accessorKey: "serverName",
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">JH</span>,
|
||||
cell: ({ row }) => <span className="text-xs">{row.original.serverName}</span>,
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "in",
|
||||
accessorKey: "inIface",
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">In</span>,
|
||||
cell: ({ row }) => <span className="font-mono text-xs">{row.original.inIface}</span>,
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "en",
|
||||
accessorKey: "enName",
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">EN</span>,
|
||||
cell: ({ row }) => <span className="text-xs">{row.original.enName || "—"}</span>,
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "dst",
|
||||
accessorKey: "dst",
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">Dest</span>,
|
||||
cell: ({ row }) => (
|
||||
<span className="flex min-w-0 flex-col gap-0.5 text-xs">
|
||||
<span className="font-mono">{row.original.dst}</span>
|
||||
<span className="text-[10px] text-muted-foreground truncate">
|
||||
{[row.original.category, row.original.service].filter(Boolean).join(" · ")}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "rate",
|
||||
accessorFn: (r) => r.bps,
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">Скорость</span>,
|
||||
cell: ({ row }) => <span className="text-xs tabular-nums">{fmtRate(row.original.bps / 1_000_000)}</span>,
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD, cellClassName: DATA_GRID_CELL_PAD },
|
||||
},
|
||||
{
|
||||
id: "bytes",
|
||||
accessorKey: "bytes",
|
||||
header: () => <span className="text-xs font-medium text-muted-foreground">Байты</span>,
|
||||
cell: ({ row }) => <span className="text-xs tabular-nums">{formatBytes(row.original.bytes)}</span>,
|
||||
meta: { headerClassName: DATA_GRID_CELL_PAD_LAST, cellClassName: DATA_GRID_CELL_PAD_LAST },
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (row) => row.id,
|
||||
})
|
||||
|
||||
return (
|
||||
<DataGridShell
|
||||
table={table}
|
||||
recordCount={rows.length}
|
||||
emptyMessage={empty ?? "Нет путей"}
|
||||
onRowClick={onPick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function FlowAnalyticsDetail({
|
||||
card,
|
||||
analytics,
|
||||
@@ -219,6 +316,10 @@ export function FlowAnalyticsDetail({
|
||||
onIface,
|
||||
dedup,
|
||||
onDedup,
|
||||
excludeMesh,
|
||||
onExcludeMesh,
|
||||
excludeOverlay,
|
||||
onExcludeOverlay,
|
||||
liveHint,
|
||||
emptyHint,
|
||||
}: {
|
||||
@@ -230,6 +331,10 @@ export function FlowAnalyticsDetail({
|
||||
onIface: (name: string) => void
|
||||
dedup: boolean
|
||||
onDedup: (value: boolean) => void
|
||||
excludeMesh: boolean
|
||||
onExcludeMesh: (value: boolean) => void
|
||||
excludeOverlay: boolean
|
||||
onExcludeOverlay: (value: boolean) => void
|
||||
liveHint?: string
|
||||
emptyHint?: string
|
||||
}) {
|
||||
@@ -276,6 +381,26 @@ export function FlowAnalyticsDetail({
|
||||
Без дублей
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="flow-exclude-overlay"
|
||||
checked={excludeOverlay}
|
||||
onCheckedChange={onExcludeOverlay}
|
||||
/>
|
||||
<Label htmlFor="flow-exclude-overlay" className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
Без overlay
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="flow-exclude-mesh"
|
||||
checked={excludeMesh}
|
||||
onCheckedChange={onExcludeMesh}
|
||||
/>
|
||||
<Label htmlFor="flow-exclude-mesh" className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
Без mesh
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{RANGE_KEYS.map((r) => (
|
||||
<button
|
||||
@@ -344,7 +469,7 @@ export function FlowAnalyticsDetail({
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<KpiStatGrid
|
||||
aria-label="Скорость потоков"
|
||||
items={[
|
||||
items={([
|
||||
{
|
||||
id: "bps-now",
|
||||
label: "Скорость сейчас",
|
||||
@@ -355,11 +480,38 @@ export function FlowAnalyticsDetail({
|
||||
},
|
||||
{
|
||||
id: "bytes",
|
||||
label: "Байт за период",
|
||||
label: "Payload",
|
||||
value: formatBytes(bytes),
|
||||
hint: "inner IPFIX",
|
||||
icon: <ArrowUpIcon className="size-4" />,
|
||||
iconClassName: "text-info",
|
||||
},
|
||||
{
|
||||
id: "overlay",
|
||||
label: "JH↔EN overlay",
|
||||
value: fmtRate((analytics?.bpsOverlay ?? 0) / 1_000_000),
|
||||
hint: analytics?.bytesOverlay ? formatBytes(analytics.bytesOverlay) : undefined,
|
||||
icon: <ShieldIcon className="size-4" />,
|
||||
iconClassName: "text-warning",
|
||||
},
|
||||
{
|
||||
id: "wire",
|
||||
label: "Wire GRE",
|
||||
value: fmtRate((analytics?.bpsWire ?? 0) / 1_000_000),
|
||||
hint: "счётчик iface",
|
||||
icon: <RouteIcon className="size-4" />,
|
||||
iconClassName: "text-primary",
|
||||
},
|
||||
...(!excludeMesh
|
||||
? [{
|
||||
id: "mesh",
|
||||
label: "Mesh",
|
||||
value: formatBytes(analytics?.bytesMesh ?? 0),
|
||||
hint: "клиент↔клиент",
|
||||
icon: <NetworkIcon className="size-4" />,
|
||||
iconClassName: "text-muted-foreground",
|
||||
}]
|
||||
: []),
|
||||
{
|
||||
id: "flows",
|
||||
label: "Сессии",
|
||||
@@ -384,7 +536,7 @@ export function FlowAnalyticsDetail({
|
||||
icon: <LayersIcon className="size-4" />,
|
||||
iconClassName: "text-primary",
|
||||
},
|
||||
]}
|
||||
] satisfies KpiStatItem[])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -404,6 +556,7 @@ export function FlowAnalyticsDetail({
|
||||
<TabsTrigger value="protocols">Протоколы</TabsTrigger>
|
||||
<TabsTrigger value="sources">Источники</TabsTrigger>
|
||||
<TabsTrigger value="destinations">Назначения</TabsTrigger>
|
||||
<TabsTrigger value="paths">Пути</TabsTrigger>
|
||||
<TabsTrigger value="sessions">Сессии</TabsTrigger>
|
||||
<TabsTrigger value="interfaces">Интерфейсы</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -440,6 +593,20 @@ export function FlowAnalyticsDetail({
|
||||
<TabsContent value="destinations">
|
||||
<FlowBreakdownGrid rows={analytics?.destinations ?? []} onPick={(row) => pickBreakdown("destination", row)} />
|
||||
</TabsContent>
|
||||
<TabsContent value="paths">
|
||||
<FlowPathsGrid
|
||||
rows={analytics?.paths ?? []}
|
||||
empty="Нет путей за период"
|
||||
onPick={(row) => {
|
||||
setSessionFilter({
|
||||
kind: "client",
|
||||
value: row.clientId,
|
||||
label: `${row.clientName} → ${row.enName || row.dst}`,
|
||||
})
|
||||
setSlice("sessions")
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="sessions">
|
||||
{sessionFilter ? (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
|
||||
Reference in New Issue
Block a user