fix(ui): обозначить выбранные состояния и поиск трафика
This commit is contained in:
1 parent
460cf67313
commit
48b7a061c1
6 files changed
+44
-10
No files matched your search
@@ -338,7 +338,7 @@ function StatChip({ icon, label, value }: { icon: ReactNode; label: string; valu
|
|||||||
|
|
||||||
function ServerCard({ s, selected, onClick }: { s: ServerTraffic; selected: boolean; onClick: () => void }) {
|
function ServerCard({ s, selected, onClick }: { s: ServerTraffic; selected: boolean; onClick: () => void }) {
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} className={cn(
|
<button type="button" aria-pressed={selected} onClick={onClick} className={cn(
|
||||||
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
||||||
selected ? "border-primary bg-primary/5" : "border-border bg-card",
|
selected ? "border-primary bg-primary/5" : "border-border bg-card",
|
||||||
s.status === "offline" && "opacity-60",
|
s.status === "offline" && "opacity-60",
|
||||||
@@ -375,7 +375,7 @@ function ServerCard({ s, selected, onClick }: { s: ServerTraffic; selected: bool
|
|||||||
function UserCard({ u, selected, onClick }: { u: UserTraffic; selected: boolean; onClick: () => void }) {
|
function UserCard({ u, selected, onClick }: { u: UserTraffic; selected: boolean; onClick: () => void }) {
|
||||||
const hasClients = u.interfaces.length > 0
|
const hasClients = u.interfaces.length > 0
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} className={cn(
|
<button type="button" aria-pressed={selected} onClick={onClick} className={cn(
|
||||||
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
||||||
selected ? "border-primary bg-primary/5" : "border-border bg-card",
|
selected ? "border-primary bg-primary/5" : "border-border bg-card",
|
||||||
!hasClients && "opacity-60",
|
!hasClients && "opacity-60",
|
||||||
@@ -417,7 +417,7 @@ function UserCard({ u, selected, onClick }: { u: UserTraffic; selected: boolean;
|
|||||||
|
|
||||||
function IfaceCard({ c, selected, onClick }: { c: BoundIfaceTraffic; selected: boolean; onClick: () => void }) {
|
function IfaceCard({ c, selected, onClick }: { c: BoundIfaceTraffic; selected: boolean; onClick: () => void }) {
|
||||||
return (
|
return (
|
||||||
<button onClick={onClick} className={cn(
|
<button type="button" aria-pressed={selected} onClick={onClick} className={cn(
|
||||||
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
||||||
selected ? "border-primary bg-primary/5" : "border-border bg-card",
|
selected ? "border-primary bg-primary/5" : "border-border bg-card",
|
||||||
c.status === "offline" && "opacity-60",
|
c.status === "offline" && "opacity-60",
|
||||||
@@ -516,7 +516,7 @@ function DetailHeader({ range, setRange, children }: {
|
|||||||
<div className="flex items-center gap-2 flex-wrap min-w-0">{children}</div>
|
<div className="flex items-center gap-2 flex-wrap min-w-0">{children}</div>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{TRAFFIC_RANGE_KEYS.map((r) => (
|
{TRAFFIC_RANGE_KEYS.map((r) => (
|
||||||
<Button key={r} size="sm" variant={r === range ? "default" : "ghost"}
|
<Button key={r} type="button" aria-pressed={range === r} size="sm" variant={r === range ? "default" : "ghost"}
|
||||||
className="h-7 px-2 text-xs" onClick={() => setRange(r)}>
|
className="h-7 px-2 text-xs" onClick={() => setRange(r)}>
|
||||||
{TRAFFIC_RANGE_LABELS[r]}
|
{TRAFFIC_RANGE_LABELS[r]}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1178,7 +1178,7 @@ export default function TrafficPage() {
|
|||||||
<div className="border-b bg-background shrink-0">
|
<div className="border-b bg-background shrink-0">
|
||||||
<div role="group" aria-label="Разделы трафика" className="flex items-center overflow-x-auto px-3 sm:px-6">
|
<div role="group" aria-label="Разделы трафика" className="flex items-center overflow-x-auto px-3 sm:px-6">
|
||||||
{GROUP_MODES.map(({ mode, icon, label }) => (
|
{GROUP_MODES.map(({ mode, icon, label }) => (
|
||||||
<button key={mode} onClick={() => handleModeChange(mode)}
|
<button type="button" aria-pressed={effectiveMode === mode} key={mode} onClick={() => handleModeChange(mode)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
|
"flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors -mb-px",
|
||||||
effectiveMode === mode
|
effectiveMode === mode
|
||||||
@@ -1224,6 +1224,7 @@ export default function TrafficPage() {
|
|||||||
<div role="group" aria-label="Источник потоков" className="flex gap-1">
|
<div role="group" aria-label="Источник потоков" className="flex gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-pressed={flowScope === "servers"}
|
||||||
onClick={() => { setFlowScope("servers"); setFlowIface("__all__") }}
|
onClick={() => { setFlowScope("servers"); setFlowIface("__all__") }}
|
||||||
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",
|
||||||
@@ -1236,6 +1237,7 @@ export default function TrafficPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-pressed={flowScope === "users"}
|
||||||
onClick={() => { setFlowScope("users"); setFlowIface("__all__"); if (range === "30d") setRange("1h") }}
|
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",
|
||||||
@@ -1253,7 +1255,7 @@ export default function TrafficPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="Поиск…"
|
aria-label="Поиск потоков" placeholder="Поиск…"
|
||||||
className="w-full pl-8 pr-3 h-8 text-xs bg-muted/50 border border-border rounded-md outline-none focus:border-primary transition-colors"
|
className="w-full pl-8 pr-3 h-8 text-xs bg-muted/50 border border-border rounded-md outline-none focus:border-primary transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1341,7 +1343,7 @@ export default function TrafficPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
placeholder="Поиск…"
|
aria-label="Поиск трафика" placeholder="Поиск…"
|
||||||
className="w-full pl-8 pr-3 h-8 text-xs bg-muted/50 border border-border rounded-md outline-none focus:border-primary transition-colors"
|
className="w-full pl-8 pr-3 h-8 text-xs bg-muted/50 border border-border rounded-md outline-none focus:border-primary transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1350,7 +1352,7 @@ export default function TrafficPage() {
|
|||||||
<div className="flex gap-1 flex-wrap">
|
<div className="flex gap-1 flex-wrap">
|
||||||
<span className="text-[10px] text-muted-foreground self-center mr-0.5">Сортировка:</span>
|
<span className="text-[10px] text-muted-foreground self-center mr-0.5">Сортировка:</span>
|
||||||
{visibleSortFields.map(({ field, label }) => (
|
{visibleSortFields.map(({ field, label }) => (
|
||||||
<button key={field} onClick={() => toggleSort(field)}
|
<button type="button" aria-pressed={sortField === field} key={field} onClick={() => toggleSort(field)}
|
||||||
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",
|
||||||
sortField === field
|
sortField === field
|
||||||
@@ -1391,6 +1393,7 @@ export default function TrafficPage() {
|
|||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="text-[11px] text-muted-foreground mr-1">Интерфейс:</span>
|
<span className="text-[11px] text-muted-foreground mr-1">Интерфейс:</span>
|
||||||
<button
|
<button
|
||||||
|
type="button" aria-pressed={hideDisabledIfaces}
|
||||||
onClick={() => setHideDisabledIfaces((v) => !v)}
|
onClick={() => setHideDisabledIfaces((v) => !v)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium transition-all",
|
"inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium transition-all",
|
||||||
@@ -1402,6 +1405,7 @@ export default function TrafficPage() {
|
|||||||
{hideDisabledIfaces ? "Показать отключенные" : "Скрыть отключенные"}
|
{hideDisabledIfaces ? "Показать отключенные" : "Скрыть отключенные"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button" aria-pressed={selectedIface === "__all__"}
|
||||||
onClick={() => setSelectedIface("__all__")}
|
onClick={() => setSelectedIface("__all__")}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-[10px] font-medium transition-all",
|
"inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-[10px] font-medium transition-all",
|
||||||
@@ -1417,6 +1421,7 @@ export default function TrafficPage() {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`${iface.name}:${index}`}
|
key={`${iface.name}:${index}`}
|
||||||
|
type="button" aria-pressed={active}
|
||||||
onClick={() => setSelectedIface(iface.name)}
|
onClick={() => setSelectedIface(iface.name)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-medium transition-all",
|
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[10px] font-medium transition-all",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export function FlowEntityCardView({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-pressed={selected}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
"text-left w-full rounded-lg border p-3 transition-colors hover:bg-muted/50",
|
||||||
|
|||||||
+1
-1
@@ -88,7 +88,7 @@ flowchart LR
|
|||||||
|
|
||||||
Ближайший оставшийся объём:
|
Ближайший оставшийся объём:
|
||||||
|
|
||||||
1. Молчащий SSE и CRLF/heartbeat проверены в блоках 8.6–8.7. Одиночные точки графика исправлены в 8.8; ожидание первого sample проверено в 8.9. Выбор месяца реализован в 8.10, каталог неактивных серверов — в 8.11. Следующий шаг — доступность и итоговая приёмка.
|
1. Молчащий SSE и CRLF/heartbeat проверены в блоках 8.6–8.7. Одиночные точки графика исправлены в 8.8; ожидание первого sample проверено в 8.9. Выбор месяца реализован в 8.10, каталог неактивных серверов — в 8.11. Подписи и выбранные состояния исправлены в 8.12; следующий шаг — оставшийся аудит доступности и итоговая регрессия.
|
||||||
2. Итоговая приёмка этапа 8: ReUI/accessibility checklist и регрессия; удалённые серверы остаются ограничением. Промежуточная матрица проверок сохранена в [этапе 8](ux-ui-stage-8.md).
|
2. Итоговая приёмка этапа 8: ReUI/accessibility checklist и регрессия; удалённые серверы остаются ограничением. Промежуточная матрица проверок сохранена в [этапе 8](ux-ui-stage-8.md).
|
||||||
3. После фиксации результата этапа 8 — этап 9.
|
3. После фиксации результата этапа 8 — этап 9.
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,20 @@
|
|||||||
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
| 5 | Состояния данных, свежесть, частичные сбои, гонки polling | Завершён 2026-09-20 для дашборда; границы проверок ниже |
|
||||||
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
| 6 | Навигация и дашборд | Завершён 2026-09-20 в границах локальной приёмки; ограничения ниже |
|
||||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||||
| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.11; каталог месячного отчёта готов, далее доступность и приёмка |
|
| 8 | Трафик и статистика | В работе: выполнены блоки 8.1–8.12; подписи/состояния исправлены, далее итоговая регрессия |
|
||||||
| 9 | VPN, Firewall и Filters | Не начат |
|
| 9 | VPN, Firewall и Filters | Не начат |
|
||||||
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
| 10 | Маршрутизация, диагностика, карта и терминал | Не начат |
|
||||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||||
|
|
||||||
|
## Блок 8.12 — доступные имена и выбранные состояния (2026-09-28)
|
||||||
|
|
||||||
|
- Получен актуальный ReUI get_audit_checklist. Поисковые поля трафика/потоков получили доступные имена. Карточки, разделы, источник flow, периоды и интерфейсы обозначают выбранное состояние через aria-pressed; затронутые native-кнопки получили type=button.
|
||||||
|
- Новый E2E проверяет выбор flow-сервера через Enter, сохранение его заголовка при поиске другой карточки, открытие месяца через Space и aria-pressed. Сценарий, lint, diff-check и production build/TypeScript прошли (34 страницы).
|
||||||
|
- Это профильный блок доступности, не полный аудит: ещё проверить общую Tab-навигацию/видимый фокус, слои/Escape, статусы загрузки и статистику, затем выполнить итоговый набор трафика/статистики и backend-switch. Этап 8 не закрыт. Реальные RouterOS/IPFIX/JWT не проверены; удалённые серверы и экспорт остаются ограничениями.
|
||||||
|
- Использовано 97% пятичасового и 31% недельного лимита. Следующий запуск начать с лимита и итоговой регрессии после оставшегося аудита. Локальный коммит без push.
|
||||||
|
|
||||||
## Блок 8.11 — каталог серверов месячного отчёта (2026-09-28)
|
## Блок 8.11 — каталог серверов месячного отчёта (2026-09-28)
|
||||||
|
|
||||||
- Для месячного режима список загружается через существующий listServers, независимо от активности IPFIX за 24 часа. Выбор доступен даже при полном отсутствии активных экспортёров. В списке только имена; скорости, графики и статус без подтверждения не подставляются. Сортировка по live-метрикам скрыта, поиск работает по имени.
|
- Для месячного режима список загружается через существующий listServers, независимо от активности IPFIX за 24 часа. Выбор доступен даже при полном отсутствии активных экспортёров. В списке только имена; скорости, графики и статус без подтверждения не подставляются. Сортировка по live-метрикам скрыта, поиск работает по имени.
|
||||||
|
|||||||
@@ -54,3 +54,5 @@
|
|||||||
Обновление 8.10 (2026-09-28): добавлен выбор прошлого/текущего месяца UTC; валидация не отправляет пустые/будущие значения, ключ запроса защищает от позднего отчёта. 23 прежних и 1 новый flow-E2E прошли, lint/build чистые. Ограничение списка серверов по активности 24 часа остаётся следующим шагом; полный аудит доступности ещё не завершён.
|
Обновление 8.10 (2026-09-28): добавлен выбор прошлого/текущего месяца UTC; валидация не отправляет пустые/будущие значения, ключ запроса защищает от позднего отчёта. 23 прежних и 1 новый flow-E2E прошли, lint/build чистые. Ограничение списка серверов по активности 24 часа остаётся следующим шагом; полный аудит доступности ещё не завершён.
|
||||||
|
|
||||||
Обновление 8.11 (2026-09-28): месячный режим выбирает сервер из каталога независимо от активности за 24 часа. 25 flow-E2E и дополнительная проверка пустого списка экспортёров прошли, как и lint/build. Удалённые из каталога серверы не перечисляются; далее доступность и итоговая регрессия этапа 8.
|
Обновление 8.11 (2026-09-28): месячный режим выбирает сервер из каталога независимо от активности за 24 часа. 25 flow-E2E и дополнительная проверка пустого списка экспортёров прошли, как и lint/build. Удалённые из каталога серверы не перечисляются; далее доступность и итоговая регрессия этапа 8.
|
||||||
|
|
||||||
|
Обновление 8.12: получен checklist ReUI; имена поиска и aria-pressed исправлены. Клавиатурный E2E Enter/Space, lint/build прошли. Полный аудит Tab/фокуса/слоёв/статистики и итоговая регрессия ещё нужны; этап не закрыт.
|
||||||
@@ -427,3 +427,22 @@ test('Flow: месячный отчёт доступен серверу без
|
|||||||
await expect(inactive).toHaveAttribute('aria-pressed', 'true')
|
await expect(inactive).toHaveAttribute('aria-pressed', 'true')
|
||||||
await expect(page.getByRole('region', { name: 'Месячный отчёт' })).toBeVisible()
|
await expect(page.getByRole('region', { name: 'Месячный отчёт' })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('Flow: клавиатурный выбор сервера и месяца имеет доступное состояние', async ({ page }) => {
|
||||||
|
await setup(page)
|
||||||
|
await expect(page.getByRole('group', { name: 'Разделы трафика' }).getByRole('button', { name: 'Потоки', exact: true })).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
const second = page.getByRole('button').filter({ hasText: 'Экспортёр второй' })
|
||||||
|
await second.focus()
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
await expect(second).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
await expect(page.getByRole('heading', { name: 'Экспортёр второй', exact: true })).toBeVisible()
|
||||||
|
await page.getByRole('textbox', { name: 'Поиск потоков', exact: true }).fill('Экспортёр тест')
|
||||||
|
await expect(page.getByRole('heading', { name: 'Экспортёр второй', exact: true })).toBeVisible()
|
||||||
|
await page.getByRole('textbox', { name: 'Поиск потоков', exact: true }).fill('')
|
||||||
|
const month = page.getByRole('button', { name: 'Месяц', exact: true })
|
||||||
|
await month.focus()
|
||||||
|
await page.keyboard.press('Space')
|
||||||
|
await expect(month).toHaveAttribute('aria-pressed', 'true')
|
||||||
|
await expect(page.getByLabel('Месяц отчёта (UTC)', { exact: true })).toBeVisible()
|
||||||
|
})
|
||||||
Reference in new issue
Block a user