Files
Homelab-manager/web/src/components/IntegrationForm.tsx
T
bobbanandClaude Sonnet 5 82ed25b63f Fix checkbox/label spacing on Notifications and other form checkboxes
Tabler's form-check-single modifier zeroes margin on .form-check-input,
which cancels the -2rem margin-inline-start the base .form-check rule
relies on to offset the checkbox into its label's padding gutter. That
left checkboxes floating almost flush against their label text.

Removed the form-check-single class from all 9 affected checkboxes:
6 in NotificationSettings, 1 each in DnsProviderForm, IntegrationForm,
and IntegrationEditForm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 01:43:45 +02:00

174 lines
5.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { api, type IntegrationField, type IntegrationType } from "../api/client";
const TYPE_LABELS: Record<IntegrationType, string> = {
tailscale: "Tailscale",
proxmox: "Proxmox",
synology: "Synology",
semaphore: "Semaphore",
gitea: "Gitea",
dockhand: "Dockhand",
};
export default function IntegrationForm({ onCreated, onCancel }: { onCreated: () => void; onCancel: () => void }) {
const [allFields, setAllFields] = useState<Partial<Record<IntegrationType, IntegrationField[]>> | null>(null);
const [type, setType] = useState<IntegrationType | null>(null);
const [name, setName] = useState("");
const [values, setValues] = useState<Record<string, string | boolean>>({});
const [testResult, setTestResult] = useState<{ ok: boolean; latencyMs?: number; error?: string } | null>(null);
const [testing, setTesting] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
api.integrations.fields().then((res) => {
setAllFields(res.fields);
const firstType = Object.keys(res.fields)[0] as IntegrationType | undefined;
if (firstType) setType(firstType);
});
}, []);
useEffect(() => {
setValues({});
setTestResult(null);
}, [type]);
const availableTypes = allFields ? (Object.keys(allFields) as IntegrationType[]) : [];
const fields = (type && allFields?.[type]) ?? [];
function setField(key: string, value: string | boolean) {
setValues((v) => ({ ...v, [key]: value }));
}
async function testConnection() {
if (!type) return;
setTesting(true);
setTestResult(null);
try {
const result = await api.integrations.test({ type, config: values });
setTestResult(result);
} catch (err) {
setTestResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
} finally {
setTesting(false);
}
}
async function submit(e: React.FormEvent) {
e.preventDefault();
if (!type) return;
setError(null);
setSaving(true);
try {
await api.integrations.create({ type, name, config: values });
onCreated();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setSaving(false);
}
}
if (allFields && availableTypes.length === 0) {
return (
<div className="card mb-3">
<div className="card-body text-secondary">
No integration types are implemented yet.
</div>
</div>
);
}
return (
<div className="card mb-3">
<div className="card-header">
<h3 className="card-title">Add an integration</h3>
</div>
<form onSubmit={submit}>
<div className="card-body row g-3">
{error && (
<div className="col-12">
<div className="alert alert-danger">{error}</div>
</div>
)}
<div className="col-md-4">
<label className="form-label">Integration</label>
<select
className="form-select"
value={type ?? ""}
onChange={(e) => setType(e.target.value as IntegrationType)}
>
{availableTypes.map((t) => (
<option key={t} value={t}>
{TYPE_LABELS[t]}
</option>
))}
</select>
</div>
<div className="col-md-4">
<label className="form-label">Display name</label>
<input
className="form-control"
required
placeholder="e.g. Home tailnet"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
{fields.map((field) => (
<div className="col-md-4" key={field.key}>
{field.type === "checkbox" ? (
<label className="form-check mt-4">
<input
type="checkbox"
className="form-check-input"
checked={Boolean(values[field.key])}
onChange={(e) => setField(field.key, e.target.checked)}
/>
<span className="form-check-label">{field.label}</span>
</label>
) : (
<>
<label className="form-label">{field.label}</label>
<input
type={field.type === "password" ? "password" : "text"}
className="form-control"
placeholder={field.placeholder}
required
value={(values[field.key] as string) ?? ""}
onChange={(e) => setField(field.key, e.target.value)}
/>
</>
)}
</div>
))}
{testResult && (
<div className="col-12">
{testResult.ok ? (
<div className="alert alert-success mb-0">
Connection OK{testResult.latencyMs !== undefined ? ` (${testResult.latencyMs}ms)` : ""}.
</div>
) : (
<div className="alert alert-danger mb-0">Connection failed: {testResult.error}</div>
)}
</div>
)}
</div>
<div className="card-footer d-flex gap-2">
<button type="button" className="btn btn-outline-secondary" onClick={testConnection} disabled={testing || !type}>
{testing ? "Testing…" : "Test connection"}
</button>
<button type="submit" className="btn btn-primary" disabled={saving || !type}>
Add integration
</button>
<button type="button" className="btn" onClick={onCancel}>
Cancel
</button>
</div>
</form>
</div>
);
}