fix(ui): улучшить отображение таблицы сертификатов и добавить классы для сетки
This commit is contained in:
@@ -78,6 +78,9 @@ const STATUS_CONFIG: Record<
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CERT_TABLE_GRID_CLASS =
|
||||||
|
"grid grid-cols-[1.25rem_minmax(0,1.35fr)_minmax(0,0.85fr)_minmax(0,1fr)_9.5rem_minmax(0,8.5rem)_6rem] gap-3"
|
||||||
|
|
||||||
function daysLeftColor(days: number): string {
|
function daysLeftColor(days: number): string {
|
||||||
if (days < 0) return "text-red-500"
|
if (days < 0) return "text-red-500"
|
||||||
if (days <= 7) return "text-red-500"
|
if (days <= 7) return "text-red-500"
|
||||||
@@ -197,7 +200,10 @@ function CertListRow({
|
|||||||
return (
|
return (
|
||||||
<div className={cn("border-b last:border-b-0", cfg.row)}>
|
<div className={cn("border-b last:border-b-0", cfg.row)}>
|
||||||
<div
|
<div
|
||||||
className="grid grid-cols-[20px_1fr_1fr_1fr_160px_auto_auto] gap-3 px-4 py-3 items-center hover:bg-muted/30 transition-colors cursor-pointer"
|
className={cn(
|
||||||
|
CERT_TABLE_GRID_CLASS,
|
||||||
|
"px-4 py-3 items-center hover:bg-muted/30 transition-colors cursor-pointer",
|
||||||
|
)}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -211,11 +217,15 @@ function CertListRow({
|
|||||||
{expanded ? <ChevronDownIcon className="size-3.5" /> : <ChevronRightIcon className="size-3.5" />}
|
{expanded ? <ChevronDownIcon className="size-3.5" /> : <ChevronRightIcon className="size-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
{cfg.icon}
|
<span className="shrink-0">{cfg.icon}</span>
|
||||||
<span className="font-medium text-sm truncate">{cert.name}</span>
|
<span className="font-medium text-sm truncate" title={cert.name}>
|
||||||
|
{cert.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground font-mono mt-0.5 truncate">{cert.commonName}</p>
|
<p className="text-xs text-muted-foreground font-mono mt-0.5 truncate" title={cert.commonName}>
|
||||||
|
{cert.commonName}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground min-w-0">
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground min-w-0">
|
||||||
{server
|
{server
|
||||||
@@ -232,11 +242,13 @@ function CertListRow({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground truncate">{cert.issuedBy}</p>
|
<p className="text-xs text-muted-foreground truncate min-w-0" title={cert.issuedBy}>
|
||||||
<div>
|
{cert.issuedBy}
|
||||||
|
</p>
|
||||||
|
<div className="min-w-0">
|
||||||
<CertPartDays cert={cert} pct={pct} />
|
<CertPartDays cert={cert} pct={pct} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1 min-w-0 overflow-hidden">
|
||||||
{cert.usage.map((u) => (
|
{cert.usage.map((u) => (
|
||||||
<span
|
<span
|
||||||
key={u}
|
key={u}
|
||||||
@@ -246,7 +258,12 @@ function CertListRow({
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className={cn("text-[11px] font-mono px-2 py-0.5 rounded border whitespace-nowrap", cfg.badge)}>
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[11px] font-mono px-2 py-0.5 rounded border whitespace-nowrap shrink-0 justify-self-end",
|
||||||
|
cfg.badge,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{cfg.label}
|
{cfg.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -400,14 +417,25 @@ function CertPartAcmeSettings({
|
|||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Публичные Let's Encrypt для зон в Cloudflare выпускаются на backend и импортируются на RouterOS 7.22+.
|
Публичные Let's Encrypt для зон в Cloudflare выпускаются на backend и импортируются на RouterOS 7.22+.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid gap-3 md:grid-cols-2">
|
<form
|
||||||
|
className="grid gap-3 md:grid-cols-2"
|
||||||
|
autoComplete="off"
|
||||||
|
onSubmit={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-sm font-medium">ACME directory URL</label>
|
<label className="text-sm font-medium">ACME directory URL</label>
|
||||||
<Input value={acmeDirectoryUrl} onChange={(e) => setAcmeDirectoryUrl(e.target.value)} />
|
<Input
|
||||||
|
autoComplete="off"
|
||||||
|
name="acme-directory-url"
|
||||||
|
value={acmeDirectoryUrl}
|
||||||
|
onChange={(e) => setAcmeDirectoryUrl(e.target.value)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-sm font-medium">Cloudflare zone id (опционально)</label>
|
<label className="text-sm font-medium">Cloudflare zone id (опционально)</label>
|
||||||
<Input
|
<Input
|
||||||
|
autoComplete="off"
|
||||||
|
name="cloudflare-zone-id"
|
||||||
value={acmeZoneId}
|
value={acmeZoneId}
|
||||||
onChange={(e) => setAcmeZoneId(e.target.value)}
|
onChange={(e) => setAcmeZoneId(e.target.value)}
|
||||||
placeholder="Авто по домену"
|
placeholder="Авто по домену"
|
||||||
@@ -417,6 +445,8 @@ function CertPartAcmeSettings({
|
|||||||
<label className="text-sm font-medium">Cloudflare API token</label>
|
<label className="text-sm font-medium">Cloudflare API token</label>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
name="cloudflare-api-token"
|
||||||
value={acmeTokenDraft}
|
value={acmeTokenDraft}
|
||||||
onChange={(e) => setAcmeTokenDraft(e.target.value)}
|
onChange={(e) => setAcmeTokenDraft(e.target.value)}
|
||||||
placeholder={
|
placeholder={
|
||||||
@@ -426,7 +456,7 @@ function CertPartAcmeSettings({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button variant="outline" size="sm" disabled={acmeSaveBusy} onClick={onTest}>
|
<Button variant="outline" size="sm" disabled={acmeSaveBusy} onClick={onTest}>
|
||||||
Проверить Cloudflare
|
Проверить Cloudflare
|
||||||
@@ -512,7 +542,12 @@ function CertPartTableHeaderUsage() {
|
|||||||
|
|
||||||
function CertPartTableHeader() {
|
function CertPartTableHeader() {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-[20px_1fr_1fr_1fr_160px_auto_auto] gap-3 px-4 py-2 border-b text-[10px] font-semibold uppercase tracking-widest text-muted-foreground bg-muted/20">
|
<div
|
||||||
|
className={cn(
|
||||||
|
CERT_TABLE_GRID_CLASS,
|
||||||
|
"px-4 py-2 border-b text-[10px] font-semibold uppercase tracking-widest text-muted-foreground bg-muted/20",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<span />
|
<span />
|
||||||
<span>Имя / CN</span>
|
<span>Имя / CN</span>
|
||||||
<span>Сервер</span>
|
<span>Сервер</span>
|
||||||
@@ -962,25 +997,29 @@ export default function CertificatesPage() {
|
|||||||
setStatusFilter={setStatusFilter}
|
setStatusFilter={setStatusFilter}
|
||||||
filteredCount={filtered.length}
|
filteredCount={filtered.length}
|
||||||
/>
|
/>
|
||||||
<CertPartTableHeader />
|
<div className="overflow-x-auto">
|
||||||
{prefsHydrated && isLive && loadState === "loading" && displayCerts.length === 0 ? (
|
<div className="min-w-[48rem]">
|
||||||
<div className="py-16 text-center text-sm text-muted-foreground">Загрузка сертификатов…</div>
|
<CertPartTableHeader />
|
||||||
) : filtered.length === 0 ? (
|
{prefsHydrated && isLive && loadState === "loading" && displayCerts.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-center text-muted-foreground">
|
<div className="py-16 text-center text-sm text-muted-foreground">Загрузка сертификатов…</div>
|
||||||
<ShieldCheckIcon className="size-10 mb-3 opacity-20" />
|
) : filtered.length === 0 ? (
|
||||||
<p className="text-sm font-medium">Сертификаты не найдены</p>
|
<div className="flex flex-col items-center justify-center py-16 text-center text-muted-foreground">
|
||||||
|
<ShieldCheckIcon className="size-10 mb-3 opacity-20" />
|
||||||
|
<p className="text-sm font-medium">Сертификаты не найдены</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filtered.map((cert) => (
|
||||||
|
<CertRow
|
||||||
|
key={cert.id}
|
||||||
|
cert={cert}
|
||||||
|
server={serverById.get(cert.serverId)}
|
||||||
|
expanded={expandedIds.has(cert.id)}
|
||||||
|
onToggle={() => toggleExpand(cert.id)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
filtered.map((cert) => (
|
|
||||||
<CertRow
|
|
||||||
key={cert.id}
|
|
||||||
cert={cert}
|
|
||||||
server={serverById.get(cert.serverId)}
|
|
||||||
expanded={expandedIds.has(cert.id)}
|
|
||||||
onToggle={() => toggleExpand(cert.id)}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<CertPartReference />
|
<CertPartReference />
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user