|
|
|
|
@@ -26,6 +26,7 @@ interface BackendFormValues {
|
|
|
|
|
port: number
|
|
|
|
|
health_check_path?: string
|
|
|
|
|
active: boolean
|
|
|
|
|
domain_ids?: number[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function listBackends(): Promise<Backend[]> {
|
|
|
|
|
@@ -35,16 +36,25 @@ async function listBackends(): Promise<Backend[]> {
|
|
|
|
|
return payload.backends ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DomainLite {
|
|
|
|
|
// DomainFull mirrors the API contract; we need the full body to PUT
|
|
|
|
|
// the domain back when re-attaching/detaching it (the handler does
|
|
|
|
|
// a full-row replace, not a patch). Fields beyond what the form
|
|
|
|
|
// shows are passed through verbatim.
|
|
|
|
|
interface DomainFull {
|
|
|
|
|
id: number
|
|
|
|
|
name: string
|
|
|
|
|
active: boolean
|
|
|
|
|
primary_backend_id?: number | null
|
|
|
|
|
http_to_https: boolean
|
|
|
|
|
hsts_enabled: boolean
|
|
|
|
|
notes?: string | null
|
|
|
|
|
created_at?: string
|
|
|
|
|
updated_at?: string
|
|
|
|
|
}
|
|
|
|
|
async function listDomains(): Promise<DomainLite[]> {
|
|
|
|
|
async function listDomains(): Promise<DomainFull[]> {
|
|
|
|
|
const r = await apiClient.get('/domains')
|
|
|
|
|
if (!isEnvelope(r.data)) return []
|
|
|
|
|
return (r.data.data as { domains?: DomainLite[] }).domains ?? []
|
|
|
|
|
return (r.data.data as { domains?: DomainFull[] }).domains ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function BackendsPage() {
|
|
|
|
|
@@ -65,27 +75,72 @@ export default function BackendsPage() {
|
|
|
|
|
const [creating, setCreating] = useState(false)
|
|
|
|
|
const [form] = Form.useForm<BackendFormValues>()
|
|
|
|
|
|
|
|
|
|
// syncDomainAttachments diff-applies the multi-select to the
|
|
|
|
|
// domains table — domains that should now point at this backend
|
|
|
|
|
// get a PUT with primary_backend_id=backend.id; domains that used
|
|
|
|
|
// to point at it but were unchecked get a PUT with null.
|
|
|
|
|
// Domains are kept canonical in their own table; this is just the
|
|
|
|
|
// reverse-edit affordance the operator asked for.
|
|
|
|
|
async function syncDomainAttachments(backendID: number, selected: number[]) {
|
|
|
|
|
const all = domains ?? []
|
|
|
|
|
const wasAttached = new Set(all.filter(d => d.primary_backend_id === backendID).map(d => d.id))
|
|
|
|
|
const want = new Set(selected)
|
|
|
|
|
const adds = [...want].filter(id => !wasAttached.has(id))
|
|
|
|
|
const removes = [...wasAttached].filter(id => !want.has(id))
|
|
|
|
|
|
|
|
|
|
const puts: Promise<unknown>[] = []
|
|
|
|
|
for (const id of adds) {
|
|
|
|
|
const d = all.find(x => x.id === id)
|
|
|
|
|
if (!d) continue
|
|
|
|
|
puts.push(apiClient.put(`/domains/${id}`, {
|
|
|
|
|
name: d.name, active: d.active,
|
|
|
|
|
http_to_https: d.http_to_https, hsts_enabled: d.hsts_enabled,
|
|
|
|
|
notes: d.notes ?? '', primary_backend_id: backendID,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
for (const id of removes) {
|
|
|
|
|
const d = all.find(x => x.id === id)
|
|
|
|
|
if (!d) continue
|
|
|
|
|
puts.push(apiClient.put(`/domains/${id}`, {
|
|
|
|
|
name: d.name, active: d.active,
|
|
|
|
|
http_to_https: d.http_to_https, hsts_enabled: d.hsts_enabled,
|
|
|
|
|
notes: d.notes ?? '', primary_backend_id: null,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
if (puts.length > 0) await Promise.all(puts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const create = useMutation({
|
|
|
|
|
mutationFn: async (v: BackendFormValues) => {
|
|
|
|
|
await apiClient.post('/backends', v)
|
|
|
|
|
const { domain_ids, ...body } = v
|
|
|
|
|
const r = await apiClient.post('/backends', body)
|
|
|
|
|
const env = r.data
|
|
|
|
|
const created = isEnvelope(env) ? (env.data as Backend) : null
|
|
|
|
|
if (created && domain_ids && domain_ids.length > 0) {
|
|
|
|
|
await syncDomainAttachments(created.id, domain_ids)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
message.success(t('common.save'))
|
|
|
|
|
setCreating(false)
|
|
|
|
|
form.resetFields()
|
|
|
|
|
void qc.invalidateQueries({ queryKey: ['backends'] })
|
|
|
|
|
void qc.invalidateQueries({ queryKey: ['domains'] })
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const update = useMutation({
|
|
|
|
|
mutationFn: async ({ id, v }: { id: number; v: BackendFormValues }) => {
|
|
|
|
|
await apiClient.put(`/backends/${id}`, v)
|
|
|
|
|
const { domain_ids, ...body } = v
|
|
|
|
|
await apiClient.put(`/backends/${id}`, body)
|
|
|
|
|
await syncDomainAttachments(id, domain_ids ?? [])
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
message.success(t('common.save'))
|
|
|
|
|
setEditing(null)
|
|
|
|
|
form.resetFields()
|
|
|
|
|
void qc.invalidateQueries({ queryKey: ['backends'] })
|
|
|
|
|
void qc.invalidateQueries({ queryKey: ['domains'] })
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@@ -124,6 +179,7 @@ export default function BackendsPage() {
|
|
|
|
|
port: row.port,
|
|
|
|
|
health_check_path: row.health_check_path ?? undefined,
|
|
|
|
|
active: row.active,
|
|
|
|
|
domain_ids: domainsForBackend(row.id).map(d => d.id),
|
|
|
|
|
})
|
|
|
|
|
}}>{t('common.edit')}</Button>
|
|
|
|
|
<Popconfirm
|
|
|
|
|
@@ -181,6 +237,25 @@ export default function BackendsPage() {
|
|
|
|
|
<Form.Item label={t('backends.active')} name="active" valuePropName="checked">
|
|
|
|
|
<Switch />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item
|
|
|
|
|
label={t('backends.attachedDomains')}
|
|
|
|
|
name="domain_ids"
|
|
|
|
|
extra={t('backends.attachedDomainsHint')}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
mode="multiple"
|
|
|
|
|
allowClear
|
|
|
|
|
showSearch
|
|
|
|
|
optionFilterProp="label"
|
|
|
|
|
placeholder={t('backends.selectDomains')}
|
|
|
|
|
options={(domains ?? []).map(d => ({
|
|
|
|
|
value: d.id,
|
|
|
|
|
label: d.primary_backend_id && d.primary_backend_id !== editing?.id
|
|
|
|
|
? `${d.name} (zur Zeit: #${d.primary_backend_id})`
|
|
|
|
|
: d.name,
|
|
|
|
|
}))}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
</div>
|
|
|
|
|
|