fix(ui): сохранять контекст и источник сертификатов
This commit is contained in:
1 parent
b184922f61
commit
adb6589816
11 files changed
+138
-13
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user