fix(ui): показывать подтверждённые данные месячного отчёта
This commit is contained in:
1 parent
24313340d5
commit
8ac40ba78a
4 files changed
+71
-52
No files matched your search
+14
-47
@@ -79,42 +79,6 @@ function flowEmptyHint(stats: FlowStatsDto | null, collectorAlive?: boolean): st
|
|||||||
return "IPFIX ещё не доходит до коллектора. На jump-host у target Src должен быть 0.0.0.0 (авто). На хосте MM проверьте bind 10.255.254.1:4739 после wg-flow."
|
return "IPFIX ещё не доходит до коллектора. На jump-host у target Src должен быть 0.0.0.0 (авто). На хосте MM проверьте bind 10.255.254.1:4739 после wg-flow."
|
||||||
}
|
}
|
||||||
|
|
||||||
function monthlyToAnalytics(m: FlowMonthlyDto): FlowAnalyticsDto {
|
|
||||||
const emptySeries = Array(60).fill(0) as number[]
|
|
||||||
return {
|
|
||||||
bpsNow: 0,
|
|
||||||
bytes: m.bytes,
|
|
||||||
packets: 0,
|
|
||||||
conversations: 0,
|
|
||||||
conversationsRaw: 0,
|
|
||||||
uniqueSrc: 0,
|
|
||||||
uniqueDst: 0,
|
|
||||||
topProto: "—",
|
|
||||||
topCategory: "—",
|
|
||||||
rxSeries: emptySeries,
|
|
||||||
txSeries: emptySeries,
|
|
||||||
applications: [],
|
|
||||||
protocols: [],
|
|
||||||
sources: [],
|
|
||||||
destinations: [],
|
|
||||||
interfaces: [],
|
|
||||||
asns: m.asns,
|
|
||||||
countries: m.countries,
|
|
||||||
categories: [],
|
|
||||||
services: m.services,
|
|
||||||
mapEdges: [],
|
|
||||||
conversationsList: [],
|
|
||||||
paths: [],
|
|
||||||
ifaces: [],
|
|
||||||
live: false,
|
|
||||||
degraded: false,
|
|
||||||
bytesPayload: m.bytes,
|
|
||||||
bytesOverlay: 0,
|
|
||||||
bytesMesh: 0,
|
|
||||||
bytesWire: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── data model ───────────────────────────────────────────────────────────────
|
// ─── data model ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface BoundIfaceTraffic {
|
interface BoundIfaceTraffic {
|
||||||
@@ -858,7 +822,7 @@ export default function TrafficPage() {
|
|||||||
const flowExporters = useMemo(() => currentFlows?.exporters ?? [], [currentFlows])
|
const flowExporters = useMemo(() => currentFlows?.exporters ?? [], [currentFlows])
|
||||||
const flowClients = useMemo(() => currentFlows?.clients ?? [], [currentFlows])
|
const flowClients = useMemo(() => currentFlows?.clients ?? [], [currentFlows])
|
||||||
const [analyticsRetry, setAnalyticsRetry] = useState(0)
|
const [analyticsRetry, setAnalyticsRetry] = useState(0)
|
||||||
const [analyticsResult, setAnalyticsResult] = useState<{ key: string; data?: FlowAnalyticsDto; error?: string } | null>(null)
|
const [analyticsResult, setAnalyticsResult] = useState<{ key: string; data?: FlowAnalyticsDto; monthly?: FlowMonthlyDto; error?: string } | null>(null)
|
||||||
const [flowIface, setFlowIface] = useState("__all__")
|
const [flowIface, setFlowIface] = useState("__all__")
|
||||||
const [flowDedup, setFlowDedup] = useState(true)
|
const [flowDedup, setFlowDedup] = useState(true)
|
||||||
const [flowExcludeMesh, setFlowExcludeMesh] = useState(true)
|
const [flowExcludeMesh, setFlowExcludeMesh] = useState(true)
|
||||||
@@ -928,9 +892,9 @@ export default function TrafficPage() {
|
|||||||
if (!isLive || effectiveMode !== "flows") return
|
if (!isLive || effectiveMode !== "flows") return
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
getTrafficFlows(backendUrl, range),
|
getTrafficFlows(backendUrl, range === "30d" ? "24h" : range),
|
||||||
getFlowExporters(backendUrl, range),
|
getFlowExporters(backendUrl, range === "30d" ? "24h" : range),
|
||||||
getFlowClients(backendUrl, range),
|
getFlowClients(backendUrl, range === "30d" ? "24h" : range),
|
||||||
]).then(([stats, exporters, clients]) => {
|
]).then(([stats, exporters, clients]) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setFlowResult({ key: flowKey, stats, exporters: exporters.exporters, clients: clients.clients })
|
setFlowResult({ key: flowKey, stats, exporters: exporters.exporters, clients: clients.clients })
|
||||||
@@ -951,14 +915,14 @@ export default function TrafficPage() {
|
|||||||
if (!isLive || effectiveMode !== "flows" || !selectedId) return
|
if (!isLive || effectiveMode !== "flows" || !selectedId) return
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
const request = range === "30d"
|
const request = range === "30d"
|
||||||
? getFlowMonthly(backendUrl, { month: new Date().toISOString().slice(0, 7), serverId: flowScope === "servers" ? selectedId : undefined }).then(monthlyToAnalytics)
|
? getFlowMonthly(backendUrl, { month: new Date().toISOString().slice(0, 7), serverId: flowScope === "servers" ? selectedId : undefined }).then(monthly => ({ monthly }))
|
||||||
: getFlowAnalytics(backendUrl, {
|
: getFlowAnalytics(backendUrl, {
|
||||||
range, serverId: flowScope === "servers" ? selectedId : undefined,
|
range, serverId: flowScope === "servers" ? selectedId : undefined,
|
||||||
userId: flowScope === "users" ? selectedId : undefined,
|
userId: flowScope === "users" ? selectedId : undefined,
|
||||||
iface: flowIface, dedup: flowDedup, excludeMesh: flowExcludeMesh, excludeOverlay: flowExcludeOverlay,
|
iface: flowIface, dedup: flowDedup, excludeMesh: flowExcludeMesh, excludeOverlay: flowExcludeOverlay,
|
||||||
})
|
}).then(data => ({ data }))
|
||||||
void request.then(data => {
|
void request.then(result => {
|
||||||
if (!cancelled) setAnalyticsResult({ key: analyticsKey, data })
|
if (!cancelled) setAnalyticsResult({ key: analyticsKey, ...result })
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
if (!cancelled) setAnalyticsResult({ key: analyticsKey, error: e instanceof Error ? e.message : "Не удалось загрузить аналитику" })
|
if (!cancelled) setAnalyticsResult({ key: analyticsKey, error: e instanceof Error ? e.message : "Не удалось загрузить аналитику" })
|
||||||
})
|
})
|
||||||
@@ -1231,7 +1195,7 @@ export default function TrafficPage() {
|
|||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
{(!isLive || !(effectiveMode === "flows" ? liveError : trafficError)) && <KpiStatGrid
|
{!(effectiveMode === "flows" && range === "30d") && (!isLive || !(effectiveMode === "flows" ? liveError : trafficError)) && <KpiStatGrid
|
||||||
isLoading={isLive && (effectiveMode === "flows" ? liveBusy : trafficLoading)}
|
isLoading={isLive && (effectiveMode === "flows" ? liveBusy : trafficLoading)}
|
||||||
aria-label="Сводка трафика"
|
aria-label="Сводка трафика"
|
||||||
items={effectiveMode === "flows" ? flowKpiItems : counterKpiItems}
|
items={effectiveMode === "flows" ? flowKpiItems : counterKpiItems}
|
||||||
@@ -1257,7 +1221,7 @@ export default function TrafficPage() {
|
|||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-[300px_1fr] gap-5 items-start">
|
<div className="grid grid-cols-[300px_1fr] gap-5 items-start">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex gap-1">
|
<div role="group" aria-label="Источник потоков" className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setFlowScope("servers"); setFlowIface("__all__") }}
|
onClick={() => { setFlowScope("servers"); setFlowIface("__all__") }}
|
||||||
@@ -1272,7 +1236,7 @@ export default function TrafficPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setFlowScope("users"); setFlowIface("__all__") }}
|
onClick={() => { setFlowScope("users"); setFlowIface("__all__"); if (range === "30d") setRange("1h") }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-[10px] px-2 py-0.5 rounded border transition-colors",
|
"text-[10px] px-2 py-0.5 rounded border transition-colors",
|
||||||
flowScope === "users"
|
flowScope === "users"
|
||||||
@@ -1311,6 +1275,7 @@ export default function TrafficPage() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{range === "30d" && <p className="text-xs text-muted-foreground">Выбор сервера: активность за последние 24 часа. Месячный отчёт — справа.</p>}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{sortedFlowCards.map((card) => (
|
{sortedFlowCards.map((card) => (
|
||||||
<FlowEntityCardView
|
<FlowEntityCardView
|
||||||
@@ -1334,6 +1299,8 @@ export default function TrafficPage() {
|
|||||||
<FlowAnalyticsDetail
|
<FlowAnalyticsDetail
|
||||||
card={selFlowCard}
|
card={selFlowCard}
|
||||||
analytics={displayedFlow}
|
analytics={displayedFlow}
|
||||||
|
monthly={currentAnalytics?.monthly}
|
||||||
|
monthlyAvailable={flowScope === "servers"}
|
||||||
range={range}
|
range={range}
|
||||||
onRange={(r) => setRange(r as Range)}
|
onRange={(r) => setRange(r as Range)}
|
||||||
selectedIface={flowIface}
|
selectedIface={flowIface}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-
|
|||||||
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { type ColumnDef, useTable } from "@tanstack/react-table"
|
import { type ColumnDef, useTable } from "@tanstack/react-table"
|
||||||
import type { FlowAnalyticsDto, FlowBreakdownRow, FlowEntityCard, FlowPathRow, FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
|
import type { FlowMonthlyDto, FlowAnalyticsDto, FlowBreakdownRow, FlowEntityCard, FlowPathRow, FlowTalkerDto } from "@mmapp/contracts/traffic-flow"
|
||||||
import { ArrowDownIcon, ArrowUpIcon, GitBranchIcon, GlobeIcon, LayersIcon, NetworkIcon, RouteIcon, ShieldIcon, UsersIcon } from "lucide-react"
|
import { ArrowDownIcon, ArrowUpIcon, GitBranchIcon, GlobeIcon, LayersIcon, NetworkIcon, RouteIcon, ShieldIcon, UsersIcon } from "lucide-react"
|
||||||
import { Badge } from "@/components/reui/badge"
|
import { Badge } from "@/components/reui/badge"
|
||||||
import { KpiStatGrid, type KpiStatItem } from "@/components/reui-kit/kpi-stat-grid"
|
import { KpiStatGrid, type KpiStatItem } from "@/components/reui-kit/kpi-stat-grid"
|
||||||
@@ -39,7 +39,7 @@ const RANGE_LABELS: Record<string, string> = {
|
|||||||
"1h": "1ч",
|
"1h": "1ч",
|
||||||
"4h": "4ч",
|
"4h": "4ч",
|
||||||
"24h": "24ч",
|
"24h": "24ч",
|
||||||
"30d": "месяц",
|
"30d": "Текущий месяц",
|
||||||
}
|
}
|
||||||
|
|
||||||
function MiniAreaChart({ rx, tx, height = 44 }: { rx: number[]; tx: number[]; height?: number }) {
|
function MiniAreaChart({ rx, tx, height = 44 }: { rx: number[]; tx: number[]; height?: number }) {
|
||||||
@@ -137,11 +137,13 @@ function FlowBreakdownGrid({
|
|||||||
rows,
|
rows,
|
||||||
empty,
|
empty,
|
||||||
country,
|
country,
|
||||||
|
monthly = false,
|
||||||
onPick,
|
onPick,
|
||||||
}: {
|
}: {
|
||||||
rows: FlowBreakdownRow[]
|
rows: FlowBreakdownRow[]
|
||||||
empty?: string
|
empty?: string
|
||||||
country?: boolean
|
country?: boolean
|
||||||
|
monthly?: boolean
|
||||||
onPick?: (row: FlowBreakdownRow) => void
|
onPick?: (row: FlowBreakdownRow) => void
|
||||||
}) {
|
}) {
|
||||||
const columns = useMemo<ColumnDef<DataGridFeatures, FlowBreakdownRow>[]>(
|
const columns = useMemo<ColumnDef<DataGridFeatures, FlowBreakdownRow>[]>(
|
||||||
@@ -197,12 +199,13 @@ function FlowBreakdownGrid({
|
|||||||
[country],
|
[country],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const visibleColumns = useMemo(() => monthly ? columns.filter(column => column.id !== "rate") : columns, [columns, monthly])
|
||||||
const table = useTable({
|
const table = useTable({
|
||||||
features: dataGridFeatures,
|
features: dataGridFeatures,
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
manualSorting: true,
|
manualSorting: true,
|
||||||
data: rows,
|
data: rows,
|
||||||
columns,
|
columns: visibleColumns,
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -316,6 +319,8 @@ function FlowPathsGrid({
|
|||||||
export function FlowAnalyticsDetail({
|
export function FlowAnalyticsDetail({
|
||||||
card,
|
card,
|
||||||
analytics,
|
analytics,
|
||||||
|
monthly,
|
||||||
|
monthlyAvailable = true,
|
||||||
range,
|
range,
|
||||||
onRange,
|
onRange,
|
||||||
selectedIface,
|
selectedIface,
|
||||||
@@ -331,6 +336,8 @@ export function FlowAnalyticsDetail({
|
|||||||
}: {
|
}: {
|
||||||
card: FlowEntityCard | null
|
card: FlowEntityCard | null
|
||||||
analytics: FlowAnalyticsDto | null
|
analytics: FlowAnalyticsDto | null
|
||||||
|
monthly?: FlowMonthlyDto
|
||||||
|
monthlyAvailable?: boolean
|
||||||
range: string
|
range: string
|
||||||
onRange: (r: string) => void
|
onRange: (r: string) => void
|
||||||
selectedIface: string
|
selectedIface: string
|
||||||
@@ -379,6 +386,7 @@ export function FlowAnalyticsDetail({
|
|||||||
<div className="flex items-center gap-3 shrink-0">
|
<div className="flex items-center gap-3 shrink-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={range === "30d"}
|
||||||
id="flow-dedup"
|
id="flow-dedup"
|
||||||
checked={dedup}
|
checked={dedup}
|
||||||
onCheckedChange={onDedup}
|
onCheckedChange={onDedup}
|
||||||
@@ -389,6 +397,7 @@ export function FlowAnalyticsDetail({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={range === "30d"}
|
||||||
id="flow-exclude-overlay"
|
id="flow-exclude-overlay"
|
||||||
checked={excludeOverlay}
|
checked={excludeOverlay}
|
||||||
onCheckedChange={onExcludeOverlay}
|
onCheckedChange={onExcludeOverlay}
|
||||||
@@ -399,6 +408,7 @@ export function FlowAnalyticsDetail({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={range === "30d"}
|
||||||
id="flow-exclude-mesh"
|
id="flow-exclude-mesh"
|
||||||
checked={excludeMesh}
|
checked={excludeMesh}
|
||||||
onCheckedChange={onExcludeMesh}
|
onCheckedChange={onExcludeMesh}
|
||||||
@@ -408,7 +418,7 @@ export function FlowAnalyticsDetail({
|
|||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{RANGE_KEYS.map((r) => (
|
{RANGE_KEYS.filter(r => r !== "30d" || monthlyAvailable).map((r) => (
|
||||||
<button
|
<button
|
||||||
key={r}
|
key={r}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -427,6 +437,13 @@ export function FlowAnalyticsDetail({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{range === "30d" && <p className="text-sm text-muted-foreground mb-3">Календарный месяц по UTC. Фильтры интерфейса, дублей, overlay и mesh к этому отчёту не применяются.</p>}
|
||||||
|
{monthly && range === "30d" && <section aria-label="Месячный отчёт" className="flex flex-col gap-4">
|
||||||
|
<p className="text-sm">Период: {monthly.month} (UTC) · Объём: {formatBytes(monthly.bytes)}</p>
|
||||||
|
<div><h3 className="text-sm font-medium mb-2">Страны</h3><FlowBreakdownGrid rows={monthly.countries} country monthly /></div>
|
||||||
|
<div><h3 className="text-sm font-medium mb-2">Сервисы</h3><FlowBreakdownGrid rows={monthly.services} monthly /></div>
|
||||||
|
<div><h3 className="text-sm font-medium mb-2">ASN</h3><FlowBreakdownGrid rows={monthly.asns} monthly /></div>
|
||||||
|
</section>}
|
||||||
{analytics?.ifaces && analytics.ifaces.length > 0 ? (
|
{analytics?.ifaces && analytics.ifaces.length > 0 ? (
|
||||||
<div className="mb-3 pb-3 border-b">
|
<div className="mb-3 pb-3 border-b">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
|||||||
@@ -29,6 +29,14 @@
|
|||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 8.3В — достоверный месячный отчёт (2026-09-24)
|
||||||
|
|
||||||
|
- «Текущий месяц» явно означает календарный месяц UTC. Удалён адаптер, заполнявший отсутствующие скорости, сессии и графики нулями. Месячный DTO отображается отдельно: объём, страны, сервисы и ASN; колонки скорости скрыты.
|
||||||
|
- Общая оперативная сводка скрыта в месячном режиме. Выбор экспортёров использует явный период 24h с пояснением; месячный отчёт справа запрашивает month/serverId. Фильтры дублей, overlay и mesh отключены с пояснением; интерфейсный фильтр не отображается.
|
||||||
|
- Месячный период доступен только для серверов: при переходе к клиентам выбран 1h. Иначе прежний API monthly возвращал глобальную сумму вместо клиентского среза. Группе переключения источника добавлено доступное имя.
|
||||||
|
- 5 flow-E2E прошли, включая контракт monthly, отсутствие неподтверждённых метрик, отключённые фильтры и переход к клиентам. Lint панели/тестов чистый, production build/TypeScript прошли (34 страницы). Реальная агрегация IPFIX не проверялась.
|
||||||
|
- Ограничение: выбор сервера пока зависит от списка активности 24h; архивный сервер без этой активности не появляется в списке. Выбор прошлых месяцев не добавлен. Следующий блок: 8.4 — адаптивность 320/390/1440, подписи единиц и пустые состояния; остаются отдельные проверки смены backend/сервера и позднего SSE, строгая валидация sample. На остановке использовано 84% пятичасового и 60% недельного лимита.
|
||||||
|
|
||||||
## Блок 8.3Б3 — контекст flow-аналитики (2026-09-24)
|
## Блок 8.3Б3 — контекст flow-аналитики (2026-09-24)
|
||||||
|
|
||||||
- Списки flow и аналитика привязаны к backend, режиму, периоду, объекту и фильтрам. Поздние ответы отменённого эффекта игнорируются. Ошибки списка/аналитики получили отдельные Alert и повтор без сброса контекста; ошибка списка не выдаётся за отсутствие экспортёров.
|
- Списки flow и аналитика привязаны к backend, режиму, периоду, объекту и фильтрам. Поздние ответы отменённого эффекта игнорируются. Ошибки списка/аналитики получили отдельные Alert и повтор без сброса контекста; ошибка списка не выдаётся за отсутствие экспортёров.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ async function setup(page: Page) {
|
|||||||
await page.route('**/health', r => r.fulfill({ json: { ok: true } }))
|
await page.route('**/health', r => r.fulfill({ json: { ok: true } }))
|
||||||
await page.route('**/api/**', async route => {
|
await page.route('**/api/**', async route => {
|
||||||
const url = new URL(route.request().url()), path = url.pathname, range = url.searchParams.get('range')
|
const url = new URL(route.request().url()), path = url.pathname, range = url.searchParams.get('range')
|
||||||
|
if (path === '/api/traffic/flow/monthly') return route.fulfill({ json: { month: url.searchParams.get('month'), bytes: 1500, countries: [{ id: 'RU', label: 'Россия', bytes: 1500, packets: 1, bps: 0, percent: 100 }], services: [], asns: [] } })
|
||||||
if (path === '/api/traffic/flow/analytics') {
|
if (path === '/api/traffic/flow/analytics') {
|
||||||
if (control.hold === range) await gate
|
if (control.hold === range) await gate
|
||||||
if (control.fail) return route.fulfill({ status: 500, json: { error: 'Срез временно недоступен' } })
|
if (control.fail) return route.fulfill({ status: 500, json: { error: 'Срез временно недоступен' } })
|
||||||
@@ -17,7 +18,7 @@ async function setup(page: Page) {
|
|||||||
if (path === '/api/traffic/flow/exporters' && control.failList) return route.fulfill({ status: 403, json: { error: 'Нет доступа к экспортёрам' } })
|
if (path === '/api/traffic/flow/exporters' && control.failList) return route.fulfill({ status: 403, json: { error: 'Нет доступа к экспортёрам' } })
|
||||||
const json = path === '/api/auth/config' ? { required: false }
|
const json = path === '/api/auth/config' ? { required: false }
|
||||||
: path === '/api/traffic/flow/exporters' ? { exporters: [card] }
|
: path === '/api/traffic/flow/exporters' ? { exporters: [card] }
|
||||||
: path === '/api/traffic/flow/clients' ? { clients: [] }
|
: path === '/api/traffic/flow/clients' ? { clients: [{ ...card, id: 'client1', name: 'Клиент тест' }] }
|
||||||
: path === '/api/traffic/flows' ? { exportersOnline: 1, bytesPerMin: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', talkers: [], listenerBound: true }
|
: path === '/api/traffic/flows' ? { exportersOnline: 1, bytesPerMin: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', talkers: [], listenerBound: true }
|
||||||
: path === '/api/traffic/servers' ? { servers: [] }
|
: path === '/api/traffic/servers' ? { servers: [] }
|
||||||
: path.endsWith('/interfaces') || path.endsWith('/bound-interfaces') ? { interfaces: [] }
|
: path.endsWith('/interfaces') || path.endsWith('/bound-interfaces') ? { interfaces: [] }
|
||||||
@@ -75,3 +76,29 @@ test('Flow: завершённый SSE не помечает последние
|
|||||||
await expect(page.getByText('Последние данные · live недоступен', { exact: true }).filter({ visible: true })).toHaveCount(2)
|
await expect(page.getByText('Последние данные · live недоступен', { exact: true }).filter({ visible: true })).toHaveCount(2)
|
||||||
await expect(page.getByText('live', { exact: true })).toHaveCount(0)
|
await expect(page.getByText('live', { exact: true })).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('Flow: месяц показывает только доступные агрегаты и не применяется к клиенту', async ({ page }) => {
|
||||||
|
await setup(page)
|
||||||
|
const monthlyRequest = page.waitForRequest(r => r.url().includes('/flow/monthly?'))
|
||||||
|
await page.getByRole('button', { name: 'Текущий месяц', exact: true }).click()
|
||||||
|
const url = new URL((await monthlyRequest).url())
|
||||||
|
expect(url.searchParams.get('serverId')).toBe('1')
|
||||||
|
expect(url.searchParams.get('month')).toMatch(/^\d{4}-\d{2}$/)
|
||||||
|
expect([...url.searchParams.keys()].sort()).toEqual(['month', 'serverId'])
|
||||||
|
const report = page.getByRole('region', { name: 'Месячный отчёт' })
|
||||||
|
await expect(report).toContainText('1.5 КБ')
|
||||||
|
await expect(report.getByRole('columnheader').filter({ hasText: 'Скорость' })).toHaveCount(0)
|
||||||
|
await expect(page.getByLabel('Скорость потоков')).toHaveCount(0)
|
||||||
|
await expect(page.getByLabel('Сводка трафика')).toHaveCount(0)
|
||||||
|
await expect(page.getByRole('switch', { name: 'Без дублей' })).toBeDisabled()
|
||||||
|
await expect(page.getByRole('switch', { name: 'Без overlay' })).toBeDisabled()
|
||||||
|
await expect(page.getByRole('switch', { name: 'Без mesh' })).toBeDisabled()
|
||||||
|
const clientRequest = page.waitForRequest(r => r.url().includes('/flow/analytics?') && r.url().includes('userId=client1'))
|
||||||
|
await page.getByRole('group', { name: 'Источник потоков' }).getByRole('button', { name: 'Клиенты', exact: true }).click()
|
||||||
|
expect(new URL((await clientRequest).url()).searchParams.get('range')).toBe('1h')
|
||||||
|
await expect(page.getByLabel('Скорость потоков')).toBeVisible()
|
||||||
|
await expect(report).toHaveCount(0)
|
||||||
|
await expect(page.getByRole('button', { name: 'Текущий месяц', exact: true })).toHaveCount(0)
|
||||||
|
await expect(page.getByRole('switch', { name: 'Без дублей' })).toBeEnabled()
|
||||||
|
})
|
||||||
Reference in new issue
Block a user