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:
@@ -8,7 +8,7 @@ from app.core.deps import get_current_user, require_roles
|
||||
from app.models.models import Metric, QueryStat, Target, User
|
||||
from app.schemas.metric import MetricOut, QueryStatOut
|
||||
from app.schemas.overview import DatabaseOverviewOut
|
||||
from app.schemas.target import TargetCreate, TargetOut, TargetUpdate
|
||||
from app.schemas.target import TargetConnectionTestRequest, TargetCreate, TargetOut, TargetUpdate
|
||||
from app.services.audit import write_audit_log
|
||||
from app.services.collector import build_target_dsn
|
||||
from app.services.crypto import encrypt_secret
|
||||
@@ -23,6 +23,33 @@ async def list_targets(user: User = Depends(get_current_user), db: AsyncSession
|
||||
return [TargetOut.model_validate(item) for item in targets]
|
||||
|
||||
|
||||
@router.post("/test-connection")
|
||||
async def test_target_connection(
|
||||
payload: TargetConnectionTestRequest,
|
||||
user: User = Depends(require_roles("admin", "operator")),
|
||||
) -> dict:
|
||||
_ = user
|
||||
ssl = False if payload.sslmode == "disable" else True
|
||||
conn = None
|
||||
try:
|
||||
conn = await asyncpg.connect(
|
||||
host=payload.host,
|
||||
port=payload.port,
|
||||
database=payload.dbname,
|
||||
user=payload.username,
|
||||
password=payload.password,
|
||||
ssl=ssl,
|
||||
timeout=8,
|
||||
)
|
||||
version = await conn.fetchval("SHOW server_version")
|
||||
return {"ok": True, "message": "Connection successful", "server_version": version}
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=f"Connection failed: {exc}")
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
|
||||
@router.post("", response_model=TargetOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_target(
|
||||
payload: TargetCreate,
|
||||
|
||||
@@ -16,6 +16,15 @@ class TargetCreate(TargetBase):
|
||||
password: str
|
||||
|
||||
|
||||
class TargetConnectionTestRequest(BaseModel):
|
||||
host: str
|
||||
port: int = 5432
|
||||
dbname: str
|
||||
username: str
|
||||
password: str
|
||||
sslmode: str = "prefer"
|
||||
|
||||
|
||||
class TargetUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
host: str | None = None
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user