Add test connection feature for database targets

This commit introduces a new endpoint to test database connection. The frontend now includes a button to test the connection before creating a target, with real-time feedback on success or failure. Related styles and components were updated for better user experience.
This commit is contained in:
2026-02-12 11:56:32 +01:00
parent 2f5529a93a
commit 3e025bcf1b
4 changed files with 117 additions and 5 deletions

View File

@@ -20,6 +20,7 @@ export function TargetsPage() {
const [form, setForm] = useState(emptyForm);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const [testState, setTestState] = useState({ loading: false, message: "", ok: null });
const canManage = me?.role === "admin" || me?.role === "operator";
@@ -50,6 +51,31 @@ export function TargetsPage() {
}
};
const testConnection = async () => {
setTestState({ loading: true, message: "", ok: null });
try {
const result = await apiFetch(
"/targets/test-connection",
{
method: "POST",
body: JSON.stringify({
host: form.host,
port: form.port,
dbname: form.dbname,
username: form.username,
password: form.password,
sslmode: form.sslmode,
}),
},
tokens,
refresh
);
setTestState({ loading: false, message: `${result.message} (PostgreSQL ${result.server_version})`, ok: true });
} catch (e) {
setTestState({ loading: false, message: String(e.message || e), ok: false });
}
};
const deleteTarget = async (id) => {
if (!confirm("Delete target?")) return;
try {
@@ -66,7 +92,7 @@ export function TargetsPage() {
{error && <div className="card error">{error}</div>}
{canManage && (
<details className="card collapsible" open>
<details className="card collapsible">
<summary className="collapse-head">
<div>
<h3>New Target</h3>
@@ -111,9 +137,17 @@ export function TargetsPage() {
</small>
</div>
<div className="field submit-field">
<button className="primary-btn">Create target</button>
<div className="target-actions">
<button type="button" className="secondary-btn" onClick={testConnection} disabled={testState.loading}>
{testState.loading ? "Testing..." : "Test connection"}
</button>
<button className="primary-btn">Create target</button>
</div>
</div>
</form>
{testState.message && (
<div className={`test-connection-result ${testState.ok ? "ok" : "fail"}`}>{testState.message}</div>
)}
</details>
)}

View File

@@ -269,8 +269,8 @@ button {
border-color: #3384cb;
background: linear-gradient(180deg, #15528d, #114170);
box-shadow: inset 0 1px 0 #5f8de144;
padding: 7px 12px;
min-height: 38px;
padding: 6px 12px;
min-height: 34px;
}
.primary-btn:hover {
@@ -278,6 +278,48 @@ button {
background: linear-gradient(180deg, #1a63a9, #14558f);
}
.submit-field {
align-self: end;
}
.target-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.secondary-btn {
font-weight: 600;
border-color: #3f6ea9;
background: linear-gradient(180deg, #14365f, #102c4f);
padding: 6px 12px;
min-height: 34px;
}
.secondary-btn:hover {
border-color: #69a9de;
}
.test-connection-result {
margin-top: 10px;
font-size: 13px;
border-radius: 10px;
padding: 8px 10px;
border: 1px solid transparent;
}
.test-connection-result.ok {
color: #b9f3cf;
border-color: #2f8f63;
background: #123727;
}
.test-connection-result.fail {
color: #fecaca;
border-color: #b64a4a;
background: #3a1c22;
}
.collapsible {
padding-top: 12px;
}