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

This commit is contained in:
Denozordec
2026-05-12 20:11:22 +07:00
parent 6c8ca17590
commit 53ea0e74a3
2 changed files with 145 additions and 76 deletions
+144 -75
View File
@@ -93,6 +93,60 @@ function daysLeftBar(days: number, total = 365): number {
return Math.min(100, Math.round((days / total) * 100)) return Math.min(100, Math.round((days / total) * 100))
} }
function Field({
label,
hint,
required,
children,
}: {
label: string
hint?: string
required?: boolean
children: ReactNode
}) {
return (
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium">
{label}
{required && <span className="text-destructive ml-0.5">*</span>}
</label>
{children}
{hint && <p className="text-xs text-muted-foreground">{hint}</p>}
</div>
)
}
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={cn(
"relative inline-flex h-5 w-9 shrink-0 rounded-full border-2 border-transparent transition-colors",
checked ? "bg-primary" : "bg-input",
)}
>
<span
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm transition-transform",
checked ? "translate-x-4" : "translate-x-0",
)}
/>
</button>
)
}
function SectionTitle({ children }: { children: ReactNode }) {
return (
<div className="flex items-center gap-2 py-0.5">
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">{children}</span>
<div className="flex-1 h-px bg-border" />
</div>
)
}
function mockToDto(cert: (typeof routerCertificates)[number]): CertificateDto { function mockToDto(cert: (typeof routerCertificates)[number]): CertificateDto {
return { return {
id: cert.id, id: cert.id,
@@ -641,65 +695,75 @@ function CertPartIssueForm({
setIssueTrustApi: (v: boolean) => void setIssueTrustApi: (v: boolean) => void
}) { }) {
return ( return (
<div className="flex flex-col gap-4 py-4"> <div className="flex flex-col gap-5">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-4">
<label className="text-sm font-medium">Сервер</label> <SectionTitle>Основные</SectionTitle>
<select <Field label="Сервер" required hint="RouterOS 7.22+, куда импортируется сертификат">
className="h-9 rounded-md border border-input bg-background px-3 text-sm" <select
value={issueServerId} value={issueServerId}
onChange={(e) => setIssueServerId(e.target.value)} onChange={(e) => setIssueServerId(e.target.value)}
> className="h-8 w-full rounded-lg border border-input bg-background px-2.5 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
<option value="">Выберите сервер</option> >
{serverList.map((s) => ( <option value="" disabled>
<option key={s.id} value={s.id}> Выбрать сервер
{s.name}
</option> </option>
))} {serverList.map((s) => (
</select> <option key={s.id} value={s.id}>
</div> {s.name} ({s.site})
<div className="flex flex-col gap-1.5"> </option>
<label className="text-sm font-medium">Имя сертификата на роутере</label> ))}
<Input </select>
value={issueCertName} </Field>
onChange={(e) => setIssueCertName(e.target.value)} <Field label="Имя сертификата на роутере" required hint="Имя объекта /certificate на устройстве">
placeholder="router-le" <Input
/> className="font-mono"
</div> value={issueCertName}
<div className="flex flex-col gap-1.5"> onChange={(e) => setIssueCertName(e.target.value)}
<label className="text-sm font-medium">Common Name</label> placeholder="router-le"
<Input
value={issueCommonName}
onChange={(e) => setIssueCommonName(e.target.value)}
placeholder="vpn.example.com"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium">SAN (по одному в строке)</label>
<textarea
className="min-h-24 rounded-md border border-input bg-background px-3 py-2 text-sm"
value={issueSans}
onChange={(e) => setIssueSans(e.target.value)}
placeholder="www.example.com"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Trust store</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={issueTrustWww}
onChange={(e) => setIssueTrustWww(e.target.checked)}
/> />
www </Field>
</label> </div>
<label className="flex items-center gap-2 text-sm">
<input <div className="flex flex-col gap-4">
type="checkbox" <SectionTitle>Домены</SectionTitle>
checked={issueTrustApi} <Field label="Common Name" required hint="Основное имя в сертификате">
onChange={(e) => setIssueTrustApi(e.target.checked)} <Input
className="font-mono"
value={issueCommonName}
onChange={(e) => setIssueCommonName(e.target.value)}
placeholder="vpn.example.com"
/> />
api </Field>
</label> <Field label="SAN" hint="По одному имени в строке">
<textarea
className="min-h-24 w-full rounded-lg border border-input bg-background px-2.5 py-2 text-sm font-mono text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
value={issueSans}
onChange={(e) => setIssueSans(e.target.value)}
placeholder="www.example.com"
/>
</Field>
<div className="rounded-lg border border-border bg-muted/20 px-4 py-3 text-xs text-muted-foreground">
<p className="font-medium text-foreground mb-1">Let&apos;s Encrypt · DNS-01 (Cloudflare)</p>
<p>TXT-запись создаётся в Cloudflare, сертификат импортируется на выбранный RouterOS.</p>
</div>
</div>
<div className="flex flex-col gap-4">
<SectionTitle>Импорт на RouterOS</SectionTitle>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Trust store · www</p>
<p className="text-xs text-muted-foreground">Веб-интерфейс и HTTPS-сервисы</p>
</div>
<Toggle checked={issueTrustWww} onChange={setIssueTrustWww} />
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Trust store · api</p>
<p className="text-xs text-muted-foreground">REST API и управление</p>
</div>
<Toggle checked={issueTrustApi} onChange={setIssueTrustApi} />
</div>
</div> </div>
</div> </div>
) )
@@ -1027,33 +1091,38 @@ export default function CertificatesPage() {
</div> </div>
<Sheet open={issueOpen} onOpenChange={setIssueOpen}> <Sheet open={issueOpen} onOpenChange={setIssueOpen}>
<SheetContent className="sm:max-w-lg overflow-y-auto"> <SheetContent side="right" className="w-full sm:max-w-lg flex flex-col gap-0 p-0">
<SheetHeader> <SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
<SheetTitle>Выпуск сертификата</SheetTitle> <SheetTitle>Выпуск сертификата</SheetTitle>
<SheetDescription> <SheetDescription>
Let&apos;s Encrypt через DNS-01 (Cloudflare) и импорт на выбранный RouterOS. Let&apos;s Encrypt через DNS-01 (Cloudflare) и импорт на выбранный RouterOS.
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
<CertPartIssueForm
serverList={serverList} <div className="flex-1 overflow-y-auto px-6 py-5">
issueServerId={issueServerId} <CertPartIssueForm
setIssueServerId={setIssueServerId} serverList={serverList}
issueCertName={issueCertName} issueServerId={issueServerId}
setIssueCertName={setIssueCertName} setIssueServerId={setIssueServerId}
issueCommonName={issueCommonName} issueCertName={issueCertName}
setIssueCommonName={setIssueCommonName} setIssueCertName={setIssueCertName}
issueSans={issueSans} issueCommonName={issueCommonName}
setIssueSans={setIssueSans} setIssueCommonName={setIssueCommonName}
issueTrustWww={issueTrustWww} issueSans={issueSans}
setIssueTrustWww={setIssueTrustWww} setIssueSans={setIssueSans}
issueTrustApi={issueTrustApi} issueTrustWww={issueTrustWww}
setIssueTrustApi={setIssueTrustApi} setIssueTrustWww={setIssueTrustWww}
/> issueTrustApi={issueTrustApi}
<SheetFooter className="mt-4"> setIssueTrustApi={setIssueTrustApi}
<SheetClose render={<Button variant="outline" disabled={issueBusy} />}> />
</div>
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row gap-2">
<SheetClose render={<Button variant="outline" className="flex-1" disabled={issueBusy} />}>
Отмена Отмена
</SheetClose> </SheetClose>
<Button <Button
className="flex-1"
disabled={!liveReady || issueBusy} disabled={!liveReady || issueBusy}
onClick={() => { onClick={() => {
void handleIssue() void handleIssue()
+1 -1
View File
File diff suppressed because one or more lines are too long