diff --git a/components/data-grids/certificate-days-cell.tsx b/components/data-grids/certificate-days-cell.tsx new file mode 100644 index 00000000..f4ed6b63 --- /dev/null +++ b/components/data-grids/certificate-days-cell.tsx @@ -0,0 +1,28 @@ +"use client" + +import type { CertificateView } from "@/lib/certificate-view" +import { cn } from "@/lib/utils" + +function daysLeftColor(days: number | null): string { + if (days === null) return "text-muted-foreground" + if (days <= 7) return "text-destructive" + if (days <= 30) return "text-warning" + return "text-success" +} + +export function CertDaysCell({ cert }: { cert: CertificateView }) { + return ( +
+ + {cert.daysLeft === null + ? "Срок неизвестен" + : cert.daysLeft < 0 + ? "Истёк " + -cert.daysLeft + "д назад" + : cert.daysLeft + "д осталось"} + + + {cert.validUntil ?? "Дата окончания неизвестна"} + +
+ ) +} diff --git a/components/data-grids/certificate-expanded-detail.tsx b/components/data-grids/certificate-expanded-detail.tsx index 899e9257..2d59fc63 100644 --- a/components/data-grids/certificate-expanded-detail.tsx +++ b/components/data-grids/certificate-expanded-detail.tsx @@ -1,21 +1,20 @@ "use client" -import type { CertificateDto } from "@mmapp/contracts/certificates" +import type { CertificateView } from "@/lib/certificate-view" - -function CertificateExpandedDetail({ cert }: { cert: CertificateDto }) { +function CertificateExpandedDetail({ cert }: { cert: CertificateView }) { return (
-

Key size

-

{cert.keySize} bit

+

Размер ключа

+

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

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

-

{cert.validFrom}

+

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

-

SAN / Alt Names

+

Дополнительные имена (SAN)

{cert.sans.length > 0 ? cert.sans.map((s) => ( @@ -27,9 +26,9 @@ function CertificateExpandedDetail({ cert }: { cert: CertificateDto }) {
-

Trusted

-

- {cert.trusted ? "Да (доверенный)" : "Нет (не доверенный)"} +

Доверие

+

+ {cert.trusted === null ? "Неизвестно" : cert.trusted ? "Да (доверенный)" : "Нет (не доверенный)"}

diff --git a/components/data-grids/certificate-observed-view.test.tsx b/components/data-grids/certificate-observed-view.test.tsx new file mode 100644 index 00000000..f5fe56d0 --- /dev/null +++ b/components/data-grids/certificate-observed-view.test.tsx @@ -0,0 +1,40 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { renderToStaticMarkup } from "react-dom/server" +import type { CertificateObserved } from "@mmapp/contracts/certificates" +import { CertificateExpandedDetail } from "./certificate-expanded-detail" +import { CertDaysCell } from "./certificate-days-cell" + +const cert: CertificateObserved = { + id: "1:*A", rosId: "*A", name: "unknown-cert", serverId: "1", commonName: "test.example", + sans: [], issuedBy: "—", validFrom: null, validUntil: null, rawValidFrom: null, + rawValidUntil: null, daysLeft: null, keySize: null, trusted: null, status: "unknown", usage: [], +} + +test("unknown observed fields are explicit and do not become zero or untrusted", () => { + const details = renderToStaticMarkup() + const days = renderToStaticMarkup() + assert.match(details, /Неизвестен/) + assert.match(details, /Дата неизвестна/) + assert.match(details, /Неизвестно/) + assert.ok(!details.includes("0 бит")) + assert.ok(!details.includes("Нет (не доверенный)")) + assert.match(days, /Срок неизвестен/) + assert.match(days, /Дата окончания неизвестна/) + assert.doesNotMatch(days, /0д осталось/) +}) + +test("known false trust and zero remaining days are preserved", () => { + const known = { ...cert, trusted: false, keySize: 2048, daysLeft: 0, validUntil: "2026-10-01T00:00:00Z" } + const details = renderToStaticMarkup() + assert.ok(details.includes("Нет (не доверенный)")) + assert.match(details, /2048 бит/) + const days = renderToStaticMarkup() + assert.match(days, /0д осталось/) + assert.match(days, /2026-10-01T00:00:00Z/) +}) + +test("expired and positive observed days stay distinct", () => { + assert.match(renderToStaticMarkup(), /Истёк 3д назад/) + assert.match(renderToStaticMarkup(), /40д осталось/) +}) diff --git a/components/data-grids/certificates-data-grid.tsx b/components/data-grids/certificates-data-grid.tsx index 5246d443..c7e27aff 100644 --- a/components/data-grids/certificates-data-grid.tsx +++ b/components/data-grids/certificates-data-grid.tsx @@ -7,8 +7,9 @@ import { type ColumnDef, useTable, } from "@tanstack/react-table" -import type { CertStatus, Server } from "@/lib/data" -import type { CertificateDto } from "@mmapp/contracts/certificates" +import type { Server } from "@/lib/data" +import type { CertificateView } from "@/lib/certificate-view" +import { CertDaysCell } from "@/components/data-grids/certificate-days-cell" import { Flag } from "@/components/flag" import { cn } from "@/lib/utils" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" @@ -30,12 +31,19 @@ import { ShieldAlertIcon, ShieldCheckIcon, ShieldOffIcon, + CircleHelpIcon, } from "lucide-react" const STATUS_CONFIG: Record< - CertStatus, + CertificateView["status"], { label: string; icon: ReactNode; badge: string; row: string } > = { + unknown: { + label: "Неизвестен", + icon: , + badge: "bg-muted text-muted-foreground border-border", + row: "", + }, valid: { label: "Действителен", icon: , @@ -56,34 +64,14 @@ const STATUS_CONFIG: Record< }, } -function daysLeftColor(days: number): string { - if (days < 0) return "text-red-500" - if (days <= 7) return "text-red-500" - if (days <= 30) return "text-amber-500" - return "text-emerald-600 dark:text-emerald-400" -} - -function CertDaysCell({ cert }: { cert: CertificateDto }) { - return ( - <> -
- - {cert.daysLeft < 0 ? `Истёк ${-cert.daysLeft}д назад` : `${cert.daysLeft}д осталось`} - - {cert.validUntil} -
- - ) -} - interface CertificatesDataGridProps { - certificates: CertificateDto[] + certificates: CertificateView[] serverMap: Map isLoading?: boolean } function CertificatesDataGrid({ certificates, serverMap, isLoading }: CertificatesDataGridProps) { - const columns = useMemo[]>( + const columns = useMemo[]>( () => [ { id: "name", @@ -120,7 +108,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading }: Certificat headerTitle: "Сертификат", headerClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST, - expandedContent: (row: CertificateDto) => , + expandedContent: (row: CertificateView) => , }, }, { diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index ac283b80..5a42f265 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -36,6 +36,12 @@ +## Блок 11А.17.1 — nullable представление сертификатов (2026-10-01) + +- CertificatesDataGrid и раскрытые детали принимают CertificateView, совместимый с legacy/mock DTO и новым CertificateObserved. Добавлен неизвестный статус; null срок, размер ключа, дата и доверие отображаются явно, без 0/false. Известные false и 0 дней сохранены. +- Срок вынесен в CertDaysCell; неизвестные и известные значения используют семантические токены. Установленная композиция ReUI и API операций сохранены. +- 3 проверки фактического HTML компонентов, TypeScript и профильный ESLint прошли. Страница пока использует legacy reader; следующий блок 11А.17.2 — подключение state каталога, независимое чтение серверов, частичные ошибки и последние данные. + ## Блок 11А.16.1 — достоверное отображение срока сертификата (2026-10-01) - Удалены декоративные шкалы из таблицы и раскрытых деталей сертификата: они делили оставшиеся дни на выдуманный общий срок 365 дней. Сертификаты могут иметь другую продолжительность, поэтому процент не был достоверным. diff --git a/lib/certificate-view.ts b/lib/certificate-view.ts new file mode 100644 index 00000000..a4ca1a56 --- /dev/null +++ b/lib/certificate-view.ts @@ -0,0 +1,4 @@ +import type { CertificateDto, CertificateObserved } from "@mmapp/contracts/certificates" + +/** Общий тип для демонстрационных строк и подтверждённых полей RouterOS. */ +export type CertificateView = CertificateDto | CertificateObserved