feat(ui): SSL-Domain-Picker — Management-FQDN + Cluster-Nodes + Free-Text
Vorher: SSL-Issue-Form bot nur die operator-managed /domains an. Wenn der Operator ein Cert für die Management-FQDN (utm-1.netcell-it.de aus setup.json) wollte, war diese nicht in der Auswahl — er hätte sie erst als Domain-Row anlegen müssen. Jetzt: AutoComplete (statt Select) mit drei Quellen kombiniert: * Management-FQDN aus /setup/status — als erste Option mit Hint * Alle Cluster-Node-FQDNs aus /cluster/nodes * Operator-/domains Plus: jede beliebige FQDN ist eintippbar (DNS muss zeigen). (combobox-mode in AntD ist deprecated — AutoComplete ist die empfohlene Variante für free-text-with-suggestions.) Version 1.0.15. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ const NAV: NavSection[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const VERSION = '1.0.14'
|
||||
const VERSION = '1.0.15'
|
||||
|
||||
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -249,7 +249,10 @@
|
||||
"uploadIntro": "Eigenes Zertifikat hochladen. Format: PEM-encoded. Cert + optional Chain + Private Key. EdgeGuard prüft die Cert/Key-Übereinstimmung vor dem Schreiben.",
|
||||
"uploadHint": "Tipp: bei Let's-Encrypt-Renewals nicht hier hochladen — den LE-Tab nutzen.",
|
||||
"domain": "Domain",
|
||||
"selectDomain": "Domain wählen",
|
||||
"selectDomain": "Domain wählen oder eintippen",
|
||||
"domainExtra": "Inkludiert Management-FQDN (aus Setup), Cluster-Knoten und Operator-Domains. Du kannst auch eine andere Domain tippen, sofern DNS schon auf die Box zeigt.",
|
||||
"fqdnHintMgmt": "Management-FQDN",
|
||||
"fqdnHintCluster": "Cluster · {{role}}",
|
||||
"issuer": "Issuer",
|
||||
"status": "Status",
|
||||
"expiresIn": "Gültig noch",
|
||||
|
||||
@@ -249,7 +249,10 @@
|
||||
"uploadIntro": "Upload your own certificate. Format: PEM-encoded. Cert + optional chain + private key. EdgeGuard validates cert/key match before writing.",
|
||||
"uploadHint": "Tip: for Let's Encrypt renewals don't upload here — use the LE tab.",
|
||||
"domain": "Domain",
|
||||
"selectDomain": "Select domain",
|
||||
"selectDomain": "Pick or type a domain",
|
||||
"domainExtra": "Includes the management FQDN (from Setup), cluster nodes and operator domains. You can also type any other domain — as long as DNS resolves to this box.",
|
||||
"fqdnHintMgmt": "management FQDN",
|
||||
"fqdnHintCluster": "cluster · {{role}}",
|
||||
"issuer": "Issuer",
|
||||
"status": "Status",
|
||||
"expiresIn": "Expires in",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Alert, Button, Card, Form, Input, Select, Space, Tabs, Tag, Typography, message } from 'antd'
|
||||
import { Alert, AutoComplete, Button, Card, Form, Input, Space, Tabs, Tag, Typography, message } from 'antd'
|
||||
import { SafetyCertificateOutlined } from '@ant-design/icons'
|
||||
import PageHeader from '../../components/PageHeader'
|
||||
import ActionButtons from '../../components/ActionButtons'
|
||||
@@ -47,6 +47,23 @@ async function listDomains(): Promise<Domain[]> {
|
||||
return (r.data.data as { domains?: Domain[] }).domains ?? []
|
||||
}
|
||||
|
||||
interface SetupStatus { completed: boolean; fqdn?: string }
|
||||
async function fetchSetupStatus(): Promise<SetupStatus | null> {
|
||||
try {
|
||||
const r = await apiClient.get('/setup/status')
|
||||
return isEnvelope(r.data) ? (r.data.data as SetupStatus) : null
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
interface ClusterNode { id: string; fqdn: string; role: string }
|
||||
async function listClusterNodes(): Promise<ClusterNode[]> {
|
||||
try {
|
||||
const r = await apiClient.get('/cluster/nodes')
|
||||
if (!isEnvelope(r.data)) return []
|
||||
return (r.data.data as { nodes?: ClusterNode[] }).nodes ?? []
|
||||
} catch { return [] }
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<TLSCert['status'], string> = {
|
||||
active: 'green',
|
||||
renewing: 'blue',
|
||||
@@ -68,6 +85,24 @@ export default function SSLPage() {
|
||||
|
||||
const { data: certs, isLoading } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
|
||||
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
||||
const { data: setup } = useQuery({ queryKey: ['setup-status'], queryFn: fetchSetupStatus })
|
||||
const { data: nodes } = useQuery({ queryKey: ['cluster', 'nodes'], queryFn: listClusterNodes })
|
||||
|
||||
// Domain-Picker-Optionen: operator-managed Domains + Management-FQDN
|
||||
// (utm-1.netcell-it.de aus setup.json) + alle Cluster-Node-FQDNs.
|
||||
// Plus Combobox-Mode, sodass der Operator jede beliebige Domain
|
||||
// tippen kann (z.B. wenn DNS schon zeigt aber Domain noch nicht
|
||||
// unter /domains angelegt wurde).
|
||||
const domainOptions: { value: string; label: string }[] = []
|
||||
const seen = new Set<string>()
|
||||
const add = (name: string, hint?: string) => {
|
||||
if (!name || seen.has(name)) return
|
||||
seen.add(name)
|
||||
domainOptions.push({ value: name, label: hint ? `${name} — ${hint}` : name })
|
||||
}
|
||||
if (setup?.fqdn) add(setup.fqdn, t('ssl.fqdnHintMgmt'))
|
||||
for (const n of nodes ?? []) add(n.fqdn, t('ssl.fqdnHintCluster', { role: n.role }))
|
||||
for (const d of domains ?? []) add(d.name)
|
||||
|
||||
const [issueForm] = Form.useForm<IssueValues>()
|
||||
const [uploadForm] = Form.useForm<UploadValues>()
|
||||
@@ -141,11 +176,15 @@ export default function SSLPage() {
|
||||
<Typography.Paragraph type="secondary">{t('ssl.leIntro')}</Typography.Paragraph>
|
||||
{issueErr && <Alert type="error" closable showIcon style={{ marginBottom: 12 }} message={issueErr} onClose={() => setIssueErr(null)} />}
|
||||
<Form form={issueForm} layout="vertical" onFinish={(v) => issueMut.mutate(v)}>
|
||||
<Form.Item label={t('ssl.domain')} name="domain" rules={[{ required: true }]}>
|
||||
<Select
|
||||
showSearch
|
||||
<Form.Item
|
||||
label={t('ssl.domain')} name="domain" rules={[{ required: true }]}
|
||||
extra={t('ssl.domainExtra')}
|
||||
>
|
||||
<AutoComplete
|
||||
placeholder={t('ssl.selectDomain')}
|
||||
options={(domains ?? []).map((d) => ({ value: d.name, label: d.name }))}
|
||||
options={domainOptions}
|
||||
filterOption={(input, opt) => String(opt?.value ?? '').toLowerCase().includes(input.toLowerCase())}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={issueMut.isPending}>
|
||||
|
||||
Reference in New Issue
Block a user