Add multi-database discovery and grouping features
Some checks are pending
PostgreSQL Compatibility Matrix / PG14 smoke (push) Waiting to run
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 28s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 6s
Some checks are pending
PostgreSQL Compatibility Matrix / PG14 smoke (push) Waiting to run
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 28s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 6s
This update introduces optional automatic discovery and onboarding of all databases on a PostgreSQL instance. It also enhances the frontend UI with grouped target display and navigation, making it easier to view and manage related databases. Additionally, new backend endpoints and logic ensure seamless integration of these features.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||
import { apiFetch } from "../api";
|
||||
import { useAuth } from "../state";
|
||||
@@ -75,6 +75,7 @@ async function loadMetric(targetId, metric, range, tokens, refresh) {
|
||||
|
||||
export function TargetDetailPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { tokens, refresh, uiMode } = useAuth();
|
||||
const [range, setRange] = useState("1h");
|
||||
const [liveMode, setLiveMode] = useState(false);
|
||||
@@ -84,6 +85,7 @@ export function TargetDetailPage() {
|
||||
const [overview, setOverview] = useState(null);
|
||||
const [targetMeta, setTargetMeta] = useState(null);
|
||||
const [owners, setOwners] = useState([]);
|
||||
const [groupTargets, setGroupTargets] = useState([]);
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const refreshRef = useRef(refresh);
|
||||
@@ -99,7 +101,7 @@ export function TargetDetailPage() {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo, ownerRows] = await Promise.all([
|
||||
const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo, ownerRows, allTargets] = await Promise.all([
|
||||
loadMetric(id, "connections_total", range, tokens, refreshRef.current),
|
||||
loadMetric(id, "xacts_total", range, tokens, refreshRef.current),
|
||||
loadMetric(id, "cache_hit_ratio", range, tokens, refreshRef.current),
|
||||
@@ -108,6 +110,7 @@ export function TargetDetailPage() {
|
||||
apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}`, {}, tokens, refreshRef.current),
|
||||
apiFetch(`/targets/${id}/owners`, {}, tokens, refreshRef.current),
|
||||
apiFetch("/targets", {}, tokens, refreshRef.current),
|
||||
]);
|
||||
if (!active) return;
|
||||
setSeries({ connections, xacts, cache });
|
||||
@@ -116,6 +119,15 @@ export function TargetDetailPage() {
|
||||
setOverview(overviewData);
|
||||
setTargetMeta(targetInfo);
|
||||
setOwners(ownerRows);
|
||||
const groupId = targetInfo?.tags?.monitor_group_id;
|
||||
if (groupId) {
|
||||
const sameGroup = allTargets
|
||||
.filter((item) => item?.tags?.monitor_group_id === groupId)
|
||||
.sort((a, b) => (a.dbname || "").localeCompare(b.dbname || ""));
|
||||
setGroupTargets(sameGroup);
|
||||
} else {
|
||||
setGroupTargets([]);
|
||||
}
|
||||
setError("");
|
||||
} catch (e) {
|
||||
if (active) setError(String(e.message || e));
|
||||
@@ -232,6 +244,26 @@ export function TargetDetailPage() {
|
||||
Target Detail {targetMeta?.name || `#${id}`}
|
||||
{targetMeta?.dbname ? ` (${targetMeta.dbname})` : ""}
|
||||
</h2>
|
||||
{groupTargets.length > 1 && (
|
||||
<div className="field target-db-switcher">
|
||||
<label>Database in this target group</label>
|
||||
<select
|
||||
value={String(id)}
|
||||
onChange={(e) => {
|
||||
const targetId = e.target.value;
|
||||
if (targetId && String(targetId) !== String(id)) {
|
||||
navigate(`/targets/${targetId}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{groupTargets.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.dbname} ({item.name})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="owner-row">
|
||||
<span className="muted">Responsible users:</span>
|
||||
{owners.length > 0 ? owners.map((item) => <span key={item.user_id} className="owner-pill">{item.email}</span>) : <span className="muted">none assigned</span>}
|
||||
|
||||
Reference in New Issue
Block a user