From 16fa6eb5d84387e3b156509476a1fda5cdbf526e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 1 Oct 2026 14:42:11 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=B8=20=D0=B0=D0=B4=D0=B0=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/certificates/page.tsx | 46 +++++++++--------- .../certificate-expanded-detail.tsx | 6 ++- .../certificate-observed-view.test.tsx | 7 +++ .../data-grids/certificates-data-grid.tsx | 25 +++++----- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 11 ++++- docs/ux-ui-stage-11a.md | 17 +++++++ .../screens/certificates-details-desktop.png | Bin 0 -> 181382 bytes .../screens/certificates-details-mobile.png | Bin 0 -> 68249 bytes .../screens/certificates-details-narrow.png | Bin 0 -> 61939 bytes .../screens/certificates-issue-desktop.png | Bin 0 -> 228436 bytes .../screens/certificates-issue-mobile.png | Bin 0 -> 29830 bytes .../screens/certificates-issue-narrow.png | Bin 0 -> 28622 bytes .../screens/certificates-overview-desktop.png | Bin 0 -> 167344 bytes .../screens/certificates-overview-mobile.png | Bin 0 -> 62266 bytes .../screens/certificates-overview-narrow.png | Bin 0 -> 55522 bytes tests/e2e/certificates-layout.spec.ts | 24 +++++++++ 17 files changed, 101 insertions(+), 37 deletions(-) create mode 100644 docs/ux-ui/screens/certificates-details-desktop.png create mode 100644 docs/ux-ui/screens/certificates-details-mobile.png create mode 100644 docs/ux-ui/screens/certificates-details-narrow.png create mode 100644 docs/ux-ui/screens/certificates-issue-desktop.png create mode 100644 docs/ux-ui/screens/certificates-issue-mobile.png create mode 100644 docs/ux-ui/screens/certificates-issue-narrow.png create mode 100644 docs/ux-ui/screens/certificates-overview-desktop.png create mode 100644 docs/ux-ui/screens/certificates-overview-mobile.png create mode 100644 docs/ux-ui/screens/certificates-overview-narrow.png create mode 100644 tests/e2e/certificates-layout.spec.ts diff --git a/app/(main)/certificates/page.tsx b/app/(main)/certificates/page.tsx index 9584934c..4930cdce 100644 --- a/app/(main)/certificates/page.tsx +++ b/app/(main)/certificates/page.tsx @@ -4,7 +4,7 @@ import { Suspense, useMemo, useState } from "react" import { useSearchParams } from "next/navigation" import { readCertificatesUrl, patchCertificatesUrl, type CertificateStatusFilter } from "@/lib/certificates-url-state" import { PageHeader } from "@/components/page-header" -import { FormField, FormToggle, SectionTitle } from "@/components/form-kit" +import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit" import { routerCertificates, servers as mockServers } from "@/lib/data" import type { Server } from "@/lib/data" import type { CertificateDto } from "@mmapp/contracts/certificates" @@ -77,10 +77,10 @@ function mockToDto(cert: (typeof routerCertificates)[number]): CertificateDto { function CertPartAlertExpired({ expired }: { expired: CertificateView[] }) { return ( -
- +
+
-

+

{expired.length} {expired.length === 1 ? "истёкший сертификат" : "истёкших сертификата"}

@@ -93,10 +93,10 @@ function CertPartAlertExpired({ expired }: { expired: CertificateView[] }) { function CertPartAlertExpiring({ expiring }: { expiring: CertificateView[] }) { return ( -

- +
+
-

+

{expiring.length} {expiring.length === 1 ? "сертификат истекает" : "сертификата истекают"} в течение 30 дней

@@ -112,11 +112,13 @@ function CertPartKpi({ expiring, expired, complete, + totalKnown, }: { displayCerts: CertificateView[] expiring: CertificateView[] expired: CertificateView[] complete: boolean + totalKnown: boolean }) { return ( , iconClassName: "text-muted-foreground", }, @@ -508,27 +510,27 @@ function CertificatesContent() { /> } > -

+
{!isLive &&

Моковые данные

} {urlError &&
{urlError}
} {!urlError && selectedServerId !== ALL_SERVERS_ID && (isLive ? catalog.hasState && !snapshots[selectedServerId] : !mockServers.some(server => server.id === selectedServerId)) &&

Выбранный сервер недоступен в каталоге сертификатов. Выберите другой сервер.

} {isLive && backendStatus === false && ( -
+
Backend недоступен — live-операции отключены.
)} {loadError && isLive && ( -
+
{catalog.hasState ? "Последние данные сохранены. " : "Сертификаты пока неизвестны. "}{loadError}
)} - {isLive && serverError &&

{catalog.hasServers ? "Последние серверы сохранены. " : "Каталог серверов пока неизвестен. "}{serverError}

} + {isLive && serverError &&

{catalog.hasServers ? "Последние серверы сохранены. " : "Каталог серверов пока неизвестен. "}{serverError}

} {isLive && !canRead &&

Нет права mm:certificates:read.

} {isLive && !canReadServers &&

Нет права mm:servers:read. Выбор устройства для выпуска недоступен.

} {isLive && catalog.collectedAt &&

Последнее чтение каталога: {new Date(catalog.collectedAt).toLocaleString("ru-RU")}

} {isLive && Object.values(snapshots).filter(snapshot => selectedServerId === ALL_SERVERS_ID || snapshot.latest.serverId === selectedServerId).map(snapshot =>
- {snapshot.latest.error &&

{snapshot.latest.serverName}: {snapshot.latest.error}. {snapshot.confirmed ? "Последние сертификаты сохранены." : "Сертификаты пока неизвестны."}

} + {snapshot.latest.error &&

{snapshot.latest.serverName}: {snapshot.latest.error}. {snapshot.confirmed ? "Последние сертификаты сохранены." : "Сертификаты пока неизвестны."}

}

{snapshot.latest.serverName}: {snapshot.confirmed ? "Подтверждённые данные от " + new Date(snapshot.confirmed.collectedAt).toLocaleString("ru-RU") : "Успешного чтения нет"}

)} @@ -540,7 +542,7 @@ function CertificatesContent() { )} {!classificationsKnown &&

Сводка неполная: часть устройств, статусов или сроков неизвестна. Таблица показывает доступные снимки.

} - + @@ -551,17 +553,17 @@ function CertificatesContent() { search={search} onSearchChange={setSearch} searchPlaceholder="Поиск по имени, CN, эмитенту…" - segmented={{ - value: statusFilter, - onChange: setStatusFilter, - options: [ + filterControl={
} countLabel={isLive && !catalog.hasState ? "Количество неизвестно" : `${filtered.length} строк в снимках`} /> {urlError ?

Исправьте контекст ссылки

: isLive && scopedCerts.length === 0 && !scopeKnown ?

{loading ? "Загрузка сертификатов…" : "Список сертификатов пока неизвестен"}

: { setIssueOpen(v); if (!v) setIssueStep(1) }}> - + Выпуск сертификата @@ -586,7 +588,7 @@ function CertificatesContent() { -
+
{(issue.error || issue.storageError) &&

{issue.error || issue.storageError}

} {issue.checkpoint &&

Закрытие формы не отменяет серверную задачу. Её можно проверить повторно.

} {issue.checkpoint &&

Задача {issue.checkpoint.request.requestId}: {issue.checkpoint.job ? ({ queued: "В очереди", running: "Выполняется", done: "Завершена", failed: "Ошибка" }[issue.checkpoint.job.status]) + " · " + issue.checkpoint.job.step : issue.checkpoint.accepted ? "Принята; результат пока неизвестен" : "Принятие запроса пока не подтверждено"}. Сервер {issue.checkpoint.request.serverId}, сертификат {issue.checkpoint.request.certName}.

} diff --git a/components/data-grids/certificate-expanded-detail.tsx b/components/data-grids/certificate-expanded-detail.tsx index 2d59fc63..7ccc088d 100644 --- a/components/data-grids/certificate-expanded-detail.tsx +++ b/components/data-grids/certificate-expanded-detail.tsx @@ -4,13 +4,13 @@ import type { CertificateView } from "@/lib/certificate-view" function CertificateExpandedDetail({ cert }: { cert: CertificateView }) { return ( -
+

Размер ключа

{cert.keySize === null ? "Неизвестен" : cert.keySize + " бит"}

-

Действителен с

+

Начало срока

{cert.validFrom ?? "Дата неизвестна"}

@@ -31,6 +31,8 @@ function CertificateExpandedDetail({ cert }: { cert: CertificateView }) { {cert.trusted === null ? "Неизвестно" : cert.trusted ? "Да (доверенный)" : "Нет (не доверенный)"}

+ {"rawValidFrom" in cert && !cert.validFrom && cert.rawValidFrom &&

Начало срока не удалось определить. Значение RouterOS: {cert.rawValidFrom}

} + {"rawValidUntil" in cert && !cert.validUntil && cert.rawValidUntil &&

Окончание срока не удалось определить. Значение RouterOS: {cert.rawValidUntil}

}
) } diff --git a/components/data-grids/certificate-observed-view.test.tsx b/components/data-grids/certificate-observed-view.test.tsx index f5fe56d0..5e5b22c3 100644 --- a/components/data-grids/certificate-observed-view.test.tsx +++ b/components/data-grids/certificate-observed-view.test.tsx @@ -38,3 +38,10 @@ test("expired and positive observed days stay distinct", () => { assert.match(renderToStaticMarkup(), /Истёк 3д назад/) assert.match(renderToStaticMarkup(), /40д осталось/) }) + +test("unparsed RouterOS dates remain explicit raw values without claiming valid dates", () => { + const details = renderToStaticMarkup() + assert.match(details, /Начало срока не удалось определить/) + assert.match(details, /Окончание срока не удалось определить/) + assert.ok(details.includes("oct/01/2026 12:00:00")) +}) diff --git a/components/data-grids/certificates-data-grid.tsx b/components/data-grids/certificates-data-grid.tsx index a87cda5b..bfddb32b 100644 --- a/components/data-grids/certificates-data-grid.tsx +++ b/components/data-grids/certificates-data-grid.tsx @@ -10,6 +10,7 @@ import { import type { Server } from "@/lib/data" import type { CertificateView } from "@/lib/certificate-view" import { CertDaysCell } from "@/components/data-grids/certificate-days-cell" +import { Button } from "@/components/ui/button" import { Flag } from "@/components/flag" import { cn } from "@/lib/utils" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" @@ -40,27 +41,27 @@ const STATUS_CONFIG: Record< > = { unknown: { label: "Неизвестен", - icon: , + icon: