fix(ui): добавить новые компоненты для управления сертификатами
This commit is contained in:
@@ -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'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's Encrypt через DNS-01 (Cloudflare) и импорт на выбранный RouterOS.
|
Let'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()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user