fix(ui): показывать неизвестные поля сертификатов явно

This commit is contained in:
Denozordec committed 2026-10-01 13:32:41 +07:00
1 parent bb74eae64c
commit 17f03e7af1
6 files changed
+101 -36

No files matched your search

@@ -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 (
<div className="flex flex-wrap items-center justify-between gap-1 text-xs">
<span className={cn("font-mono font-medium", daysLeftColor(cert.daysLeft))}>
{cert.daysLeft === null
? "Срок неизвестен"
: cert.daysLeft < 0
? "Истёк " + -cert.daysLeft + "д назад"
: cert.daysLeft + "д осталось"}
</span>
<span className="text-muted-foreground text-[10px] break-all">
{cert.validUntil ?? "Дата окончания неизвестна"}
</span>
</div>
)
}
@@ -1,21 +1,20 @@
"use client" "use client"
import type { CertificateDto } from "@mmapp/contracts/certificates" import type { CertificateView } from "@/lib/certificate-view"
function CertificateExpandedDetail({ cert }: { cert: CertificateView }) {
function CertificateExpandedDetail({ cert }: { cert: CertificateDto }) {
return ( return (
<div className="px-10 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-4 text-xs border-t border-border/50 pt-3"> <div className="px-10 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-4 text-xs border-t border-border/50 pt-3">
<div> <div>
<p className="text-muted-foreground mb-1">Key size</p> <p className="text-muted-foreground mb-1">Размер ключа</p>
<p className="font-mono font-medium">{cert.keySize} bit</p> <p className="font-mono font-medium">{cert.keySize === null ? "Неизвестен" : cert.keySize + " бит"}</p>
</div> </div>
<div> <div>
<p className="text-muted-foreground mb-1">Действителен с</p> <p className="text-muted-foreground mb-1">Действителен с</p>
<p className="font-mono">{cert.validFrom}</p> <p className="font-mono">{cert.validFrom ?? "Дата неизвестна"}</p>
</div> </div>
<div> <div>
<p className="text-muted-foreground mb-1">SAN / Alt Names</p> <p className="text-muted-foreground mb-1">Дополнительные имена (SAN)</p>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{cert.sans.length > 0 {cert.sans.length > 0
? cert.sans.map((s) => ( ? cert.sans.map((s) => (
@@ -27,9 +26,9 @@ function CertificateExpandedDetail({ cert }: { cert: CertificateDto }) {
</div> </div>
</div> </div>
<div> <div>
<p className="text-muted-foreground mb-1">Trusted</p> <p className="text-muted-foreground mb-1">Доверие</p>
<p className={cert.trusted ? "text-emerald-600 dark:text-emerald-400" : "text-red-500"}> <p className={cert.trusted === null ? "text-muted-foreground" : cert.trusted ? "text-success" : "text-destructive"}>
{cert.trusted ? "Да (доверенный)" : "Нет (не доверенный)"} {cert.trusted === null ? "Неизвестно" : cert.trusted ? "Да (доверенный)" : "Нет (не доверенный)"}
</p> </p>
</div> </div>
</div> </div>
@@ -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(<CertificateExpandedDetail cert={cert} />)
const days = renderToStaticMarkup(<CertDaysCell cert={cert} />)
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(<CertificateExpandedDetail cert={known} />)
assert.ok(details.includes("Нет (не доверенный)"))
assert.match(details, /2048 бит/)
const days = renderToStaticMarkup(<CertDaysCell cert={known} />)
assert.match(days, /0д осталось/)
assert.match(days, /2026-10-01T00:00:00Z/)
})
test("expired and positive observed days stay distinct", () => {
assert.match(renderToStaticMarkup(<CertDaysCell cert={{ ...cert, daysLeft: -3 }} />), /Истёк 3д назад/)
assert.match(renderToStaticMarkup(<CertDaysCell cert={{ ...cert, daysLeft: 40 }} />), /40д осталось/)
})
@@ -7,8 +7,9 @@ import {
type ColumnDef, type ColumnDef,
useTable, useTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import type { CertStatus, Server } from "@/lib/data" import type { Server } from "@/lib/data"
import type { CertificateDto } from "@mmapp/contracts/certificates" import type { CertificateView } from "@/lib/certificate-view"
import { CertDaysCell } from "@/components/data-grids/certificate-days-cell"
import { Flag } from "@/components/flag" import { Flag } from "@/components/flag"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell"
@@ -30,12 +31,19 @@ import {
ShieldAlertIcon, ShieldAlertIcon,
ShieldCheckIcon, ShieldCheckIcon,
ShieldOffIcon, ShieldOffIcon,
CircleHelpIcon,
} from "lucide-react" } from "lucide-react"
const STATUS_CONFIG: Record< const STATUS_CONFIG: Record<
CertStatus, CertificateView["status"],
{ label: string; icon: ReactNode; badge: string; row: string } { label: string; icon: ReactNode; badge: string; row: string }
> = { > = {
unknown: {
label: "Неизвестен",
icon: <CircleHelpIcon className="size-4 text-muted-foreground" />,
badge: "bg-muted text-muted-foreground border-border",
row: "",
},
valid: { valid: {
label: "Действителен", label: "Действителен",
icon: <BadgeCheckIcon className="size-4 text-emerald-500" />, icon: <BadgeCheckIcon className="size-4 text-emerald-500" />,
@@ -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 (
<>
<div className="flex items-center justify-between text-xs mb-1">
<span className={cn("font-mono font-medium", daysLeftColor(cert.daysLeft))}>
{cert.daysLeft < 0 ? `Истёк ${-cert.daysLeft}д назад` : `${cert.daysLeft}д осталось`}
</span>
<span className="text-muted-foreground text-[10px]">{cert.validUntil}</span>
</div>
</>
)
}
interface CertificatesDataGridProps { interface CertificatesDataGridProps {
certificates: CertificateDto[] certificates: CertificateView[]
serverMap: Map<string, Server> serverMap: Map<string, Server>
isLoading?: boolean isLoading?: boolean
} }
function CertificatesDataGrid({ certificates, serverMap, isLoading }: CertificatesDataGridProps) { function CertificatesDataGrid({ certificates, serverMap, isLoading }: CertificatesDataGridProps) {
const columns = useMemo<ColumnDef<DataGridFeatures, CertificateDto>[]>( const columns = useMemo<ColumnDef<DataGridFeatures, CertificateView>[]>(
() => [ () => [
{ {
id: "name", id: "name",
@@ -120,7 +108,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading }: Certificat
headerTitle: "Сертификат", headerTitle: "Сертификат",
headerClassName: DATA_GRID_CELL_PAD_FIRST, headerClassName: DATA_GRID_CELL_PAD_FIRST,
cellClassName: DATA_GRID_CELL_PAD_FIRST, cellClassName: DATA_GRID_CELL_PAD_FIRST,
expandedContent: (row: CertificateDto) => <CertificateExpandedDetail cert={row} />, expandedContent: (row: CertificateView) => <CertificateExpandedDetail cert={row} />,
}, },
}, },
{ {
+6
View File
@@ -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) ## Блок 11А.16.1 — достоверное отображение срока сертификата (2026-10-01)
- Удалены декоративные шкалы из таблицы и раскрытых деталей сертификата: они делили оставшиеся дни на выдуманный общий срок 365 дней. Сертификаты могут иметь другую продолжительность, поэтому процент не был достоверным. - Удалены декоративные шкалы из таблицы и раскрытых деталей сертификата: они делили оставшиеся дни на выдуманный общий срок 365 дней. Сертификаты могут иметь другую продолжительность, поэтому процент не был достоверным.
+4
View File
@@ -0,0 +1,4 @@
import type { CertificateDto, CertificateObserved } from "@mmapp/contracts/certificates"
/** Общий тип для демонстрационных строк и подтверждённых полей RouterOS. */
export type CertificateView = CertificateDto | CertificateObserved