fix(ui): сохранять пропуски в мини-графиках измерений
This commit is contained in:
1 parent
f3568d836b
commit
1d98a2a66e
3 files changed
+52
-15
No files matched your search
+25
-14
@@ -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 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`
|
||||
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,
|
||||
])
|
||||
})
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -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 используют общую длину временной шкалы. Невалидные точки разделяют линии и заливку на сегменты.
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
Reference in new issue
Block a user