fix(ui): сохранять пропуски в мини-графиках измерений

This commit is contained in:
Denozordec committed 2026-09-24 15:58:23 +07:00
1 parent f3568d836b
commit 1d98a2a66e
3 files changed
+51 -14

No files matched your search

+24 -13
View File
@@ -1,5 +1,7 @@
import { Fragment } from "react"
interface SparklineProps {
data: number[]
data: Array<number | null | undefined>
width?: number
height?: number
color?: string
@@ -7,25 +9,34 @@ interface SparklineProps {
}
export function Sparkline({ data, width = 120, height = 28, color = "currentColor", filled = false }: SparklineProps) {
const valid = data.filter((v) => v != null && isFinite(v))
if (valid.length < 2) return <span className="text-muted-foreground/50 text-xs font-mono">—</span>
const valid = data.filter((value): value is number => typeof value === "number" && Number.isFinite(value))
if (!valid.length) return <span className="text-muted-foreground/50 text-xs font-mono" aria-label="Нет истории измерений">—</span>
const max = Math.max(...valid)
const min = Math.min(...valid)
const span = Math.max(1, max - min)
const pts = data.map((v, i) => [
(i / (data.length - 1)) * width,
height - ((v - min) / span) * (height - 2) - 1,
const segments: Array<Array<[number, number]>> = []
let segment: Array<[number, number]> = []
data.forEach((value, index) => {
if (typeof value !== "number" || !Number.isFinite(value)) { segment = []; return }
if (!segment.length) segments.push(segment)
segment.push([
data.length === 1 ? width / 2 : index / (data.length - 1) * width,
height - ((value - min) / span) * (height - 2) - 1,
])
const lineD = pts.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" ")
const areaD = `${lineD} L${width},${height} L0,${height} Z`
})
return (
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{ display: "block" }}>
{filled && <path d={areaD} style={{ fill: color, fillOpacity: 0.15 }} />}
<path d={lineD} fill="none" style={{ stroke: color }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<svg role="img" aria-label="История измерений" width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{ display: "block" }}>
{segments.map((points, index) => {
if (points.length === 1) return <circle key={index} cx={points[0][0]} cy={points[0][1]} r="1.5" fill={color} />
const line = points.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" ")
const area = `${line} L${points[points.length - 1][0]},${height} L${points[0][0]},${height} Z`
return <Fragment key={index}>
{filled && <path d={area} style={{ fill: color, fillOpacity: 0.15 }} />}
<path d={line} fill="none" style={{ stroke: color }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</Fragment>
})}
</svg>
)
}
+7
View File
@@ -29,6 +29,13 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
## Блок 8.4З — разрывы общего Sparkline (2026-09-24)
- Sparkline принимает null/undefined и исключает нечисловые значения из расчётов, сохраняя исходную позицию каждой точки. Линия и заливка разделены на сегменты, пропуски не соединяются. Одиночная точка (включая ноль) отображается; пустая история имеет доступное имя. Масштаб для валидных значений сохранён.
- 16 E2E трафика/дашборда прошли, включая новый сценарий с null между сегментами, координатой следующего сегмента, одиночным нулём и пустой историей. Дополнительно прошли 4 профильных сценария мониторинга RTT/Resources URL. Это профильная, не полная регрессия мониторинга.
- Lint компонента/теста и diff-check чистые. Production build/TypeScript прошли (34 страницы). Использовано 62% пятичасового и 87% недельного лимита; локальный коммит без push.
- Далее: свежесть трафика, смена backend/сервера и поздний SSE, исправление оставшегося flowIface-effect и общая приёмка этапа 8. Реальные IPFIX/RouterOS не проверены; этап 8 не объявлен завершённым.
## Блок 8.4Ж — история в карточках трафика (2026-09-24)
- Две реализации MiniAreaChart заменены общим TrafficMiniChart. Пустые/полностью невалидные серии показывают «Нет истории трафика»; одна точка не создаёт NaN; нулевой TX не скрывается. Координаты RX/TX используют общую длину временной шкалы. Невалидные точки разделяют линии и заливку на сегменты.
+20 -1
View File
@@ -1,7 +1,7 @@
import { test, expect, type Page } from '@playwright/test'
async function setup(page: Page) {
const control = { holdRange: '', failIface: false, series: undefined as number[] | undefined, txSeries: undefined as number[] | undefined, release: () => {} }
const control = { holdRange: '', failIface: false, series: undefined as Array<number | null> | undefined, txSeries: undefined as number[] | undefined, release: () => {} }
const gate = new Promise<void>(resolve => { control.release = resolve })
const server = { id: '1', name: 'Первый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] }
await page.route('**/health', route => route.fulfill({ json: { ok: true } }))
@@ -124,3 +124,22 @@ test('Карточка трафика: пустая история, одна т
await expect(card.locator('[data-direction="tx"] polyline')).toHaveCount(1)
expect(await card.locator('svg').last().innerHTML()).not.toMatch(/NaN|Infinity/)
})
test('Итоги трафика: Sparkline сохраняет разрывы и одиночные измерения', async ({ page }) => {
const control = await setup(page)
control.series = [1, 2, null, 3, 4]
await page.getByRole('button', { name: '24ч', exact: true }).click()
const spark = page.getByRole('img', { name: 'История измерений', exact: true }).first()
await expect(spark.locator('path[fill="none"]')).toHaveCount(2)
await expect(spark.locator('path[fill="none"]').nth(1)).toHaveAttribute('d', /^M135\.0,/)
expect(await spark.innerHTML()).not.toMatch(/NaN|Infinity/)
control.series = [0]
await page.getByRole('button', { name: '15м', exact: true }).click()
await expect(spark.locator('circle')).toHaveCount(1)
await expect(spark.locator('circle')).toHaveAttribute('cx', '90')
control.series = []
await page.getByRole('button', { name: '4ч', exact: true }).click()
await expect(page.getByLabel('Нет истории измерений')).toHaveCount(2)
await expect(page.getByRole('img', { name: 'История измерений', exact: true })).toHaveCount(0)
})