fix(ui): сохранять контекст и источник сертификатов

This commit is contained in:
Denozordec committed 2026-10-01 13:46:12 +07:00
1 parent b184922f61
commit adb6589816
11 files changed
+138 -13

No files matched your search

+29 -7
View File
@@ -1,6 +1,8 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { Suspense, useCallback, useEffect, 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 { FileImportDialog } from "@/components/file-import-dialog"
@@ -418,6 +420,10 @@ function CertPartIssueForm({
}
export default function CertificatesPage() {
return <Suspense fallback={<p role="status">Загрузка контекста сертификатов…</p>}><CertificatesSource /></Suspense>
}
function CertificatesSource() {
const { mode, backendUrl, prefsHydrated } = useDataSource()
if (!prefsHydrated) return <p role="status" className="p-5">Загрузка настроек источника…</p>
return <CertificatesContent key={mode + ":" + backendUrl} />
@@ -428,14 +434,25 @@ function CertificatesContent() {
const isLive = prefsHydrated && mode === "live"
const liveReady = isLive && backendStatus === true
const [search, setSearch] = useState("")
const params = useSearchParams()
const context = readCertificatesUrl(params)
const search = context.search
const statusFilter = context.status
const selectedServerId = context.server
const urlError = context.error
function writeContext(changes: Record<string, string | null>, replace = false) {
const href = patchCertificatesUrl(new URLSearchParams(window.location.search), changes) + window.location.hash
if (replace) window.history.replaceState(null, "", href); else window.history.pushState(null, "", href)
}
const setSearch = (value: string) => writeContext({ q: value }, true)
const setStatusFilter = (value: CertificateStatusFilter) => writeContext({ status: value === "all" ? null : value })
const setSelectedServerId = (value: string) => writeContext({ server: value === ALL_SERVERS_ID ? null : value })
const canRead = can("mm:certificates:read")
const canWrite = can("mm:certificates:write")
const canReadServers = can("mm:servers:read")
const catalog = useCertificateCatalog(backendUrl, isLive, canRead, canReadServers)
const { snapshots, loading, error: loadError, serverError } = catalog
const serverList = useMemo(() => catalog.servers.map(toFrontendServer), [catalog.servers])
const [statusFilter, setStatusFilter] = useState<CertificateView["status"] | "all">("all")
const [issueOpen, setIssueOpen] = useState(false)
const [issueStep, setIssueStep] = useState(1)
@@ -448,7 +465,6 @@ function CertificatesContent() {
const [issueTrustWww, setIssueTrustWww] = useState(true)
const [issueTrustApi, setIssueTrustApi] = useState(true)
const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID)
const [acmeDirectoryUrl, setAcmeDirectoryUrl] = useState(
"https://acme-v02.api.letsencrypt.org/directory",
@@ -467,9 +483,10 @@ function CertificatesContent() {
const displayServers = isLive ? serverList : mockServers
const scopedCerts = useMemo(() => {
if (urlError) return []
if (selectedServerId === ALL_SERVERS_ID) return displayCerts
return displayCerts.filter((c) => c.serverId === selectedServerId)
}, [displayCerts, selectedServerId])
}, [displayCerts, selectedServerId, urlError])
const certRailItems = useMemo<ServerTileItem[]>(() => (
displayServers.map((s) => ({
@@ -516,7 +533,7 @@ function CertificatesContent() {
const expired = useMemo(() => scopedCerts.filter((c) => c.status === "expired"), [scopedCerts])
const scopeSnapshots = Object.values(snapshots).filter(snapshot => selectedServerId === ALL_SERVERS_ID || snapshot.latest.serverId === selectedServerId)
const scopeKnown = !isLive || canRead && catalog.hasState && !loadError && (selectedServerId === ALL_SERVERS_ID || !!snapshots[selectedServerId]) && scopeSnapshots.every(snapshot => snapshot.latest.error === null)
const scopeKnown = !urlError && (!isLive && (selectedServerId === ALL_SERVERS_ID || mockServers.some(server => server.id === selectedServerId)) || canRead && catalog.hasState && !loadError && (selectedServerId === ALL_SERVERS_ID || !!snapshots[selectedServerId]) && scopeSnapshots.every(snapshot => snapshot.latest.error === null))
const classificationsKnown = scopeKnown && scopedCerts.every(cert => cert.status !== "unknown" && cert.daysLeft !== null)
const filtered = useMemo(() => {
@@ -665,6 +682,9 @@ function CertificatesContent() {
}
>
<div className="flex flex-col gap-5">
{!isLive && <p role="status" className="text-xs text-muted-foreground">Моковые данные</p>}
{urlError && <div role="alert" aria-label="Контекст сертификатов">{urlError} <Button variant="outline" size="sm" onClick={() => writeContext({ server: null, status: null, q: null }, true)}>Сбросить контекст</Button></div>}
{!urlError && selectedServerId !== ALL_SERVERS_ID && (isLive ? catalog.hasState && !snapshots[selectedServerId] : !mockServers.some(server => server.id === selectedServerId)) && <p role="alert">Выбранный сервер недоступен в каталоге сертификатов. Выберите другой сервер.</p>}
{isLive && backendStatus === false && (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-4 py-3 text-sm text-amber-700 dark:text-amber-300">
Backend недоступен — live-операции отключены.
@@ -734,9 +754,11 @@ function CertificatesContent() {
}}
countLabel={isLive && !catalog.hasState ? "Количество неизвестно" : `${filtered.length} строк в снимках`}
/>
{isLive && scopedCerts.length === 0 && !scopeKnown ? <p role="status" className="p-5">{loading ? "Загрузка сертификатов…" : "Список сертификатов пока неизвестен"}</p> : <CertificatesDataGrid
{urlError ? <p role="status" className="p-5">Исправьте контекст ссылки</p> : isLive && scopedCerts.length === 0 && !scopeKnown ? <p role="status" className="p-5">{loading ? "Загрузка сертификатов…" : "Список сертификатов пока неизвестен"}</p> : <CertificatesDataGrid
certificates={filtered}
serverMap={serverById}
emptyTitle={search || statusFilter !== "all" ? "Нет совпадений" : "Нет сертификатов"}
emptyDescription={search || statusFilter !== "all" ? "Измените фильтр или поиск" : "Подтверждённый каталог пуст"}
isLoading={isLive && loading && displayCerts.length === 0}
/>}
</DataPageCard>