[NX-103 Issue] Add offline state handling for unreachable targets

Introduced a mechanism to detect and handle when a target is unreachable, including a detailed offline state message with host and port information. Updated the UI to display a card notifying users of the target's offline status and styled the card accordingly in CSS.
This commit is contained in:
2026-02-14 15:58:22 +01:00
parent 1ad237d750
commit 5c566cd90d
2 changed files with 68 additions and 7 deletions

View File

@@ -76,6 +76,10 @@ function didMetricSeriesChange(prev = [], next = []) {
return prevLast?.ts !== nextLast?.ts || Number(prevLast?.value) !== Number(nextLast?.value); return prevLast?.ts !== nextLast?.ts || Number(prevLast?.value) !== Number(nextLast?.value);
} }
function isTargetUnreachableError(err) {
return err?.code === "target_unreachable" || err?.status === 503;
}
async function loadMetric(targetId, metric, range, tokens, refresh) { async function loadMetric(targetId, metric, range, tokens, refresh) {
const { from, to } = toQueryRange(range); const { from, to } = toQueryRange(range);
return apiFetch( return apiFetch(
@@ -99,6 +103,7 @@ export function TargetDetailPage() {
const [targetMeta, setTargetMeta] = useState(null); const [targetMeta, setTargetMeta] = useState(null);
const [owners, setOwners] = useState([]); const [owners, setOwners] = useState([]);
const [groupTargets, setGroupTargets] = useState([]); const [groupTargets, setGroupTargets] = useState([]);
const [offlineState, setOfflineState] = useState(null);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const refreshRef = useRef(refresh); const refreshRef = useRef(refresh);
@@ -114,22 +119,16 @@ export function TargetDetailPage() {
setLoading(true); setLoading(true);
} }
try { try {
const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo, ownerRows, allTargets] = await Promise.all([ const [connections, xacts, cache, targetInfo, ownerRows, allTargets] = await Promise.all([
loadMetric(id, "connections_total", range, tokens, refreshRef.current), loadMetric(id, "connections_total", range, tokens, refreshRef.current),
loadMetric(id, "xacts_total", range, tokens, refreshRef.current), loadMetric(id, "xacts_total", range, tokens, refreshRef.current),
loadMetric(id, "cache_hit_ratio", range, tokens, refreshRef.current), loadMetric(id, "cache_hit_ratio", range, tokens, refreshRef.current),
apiFetch(`/targets/${id}/locks`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/activity`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}`, {}, tokens, refreshRef.current), apiFetch(`/targets/${id}`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/owners`, {}, tokens, refreshRef.current), apiFetch(`/targets/${id}/owners`, {}, tokens, refreshRef.current),
apiFetch("/targets", {}, tokens, refreshRef.current), apiFetch("/targets", {}, tokens, refreshRef.current),
]); ]);
if (!active) return; if (!active) return;
setSeries({ connections, xacts, cache }); setSeries({ connections, xacts, cache });
setLocks(locksTable);
setActivity(activityTable);
setOverview(overviewData);
setTargetMeta(targetInfo); setTargetMeta(targetInfo);
setOwners(ownerRows); setOwners(ownerRows);
const groupId = targetInfo?.tags?.monitor_group_id; const groupId = targetInfo?.tags?.monitor_group_id;
@@ -141,6 +140,34 @@ export function TargetDetailPage() {
} else { } else {
setGroupTargets([]); setGroupTargets([]);
} }
try {
const [locksTable, activityTable, overviewData] = await Promise.all([
apiFetch(`/targets/${id}/locks`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/activity`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current),
]);
if (!active) return;
setLocks(locksTable);
setActivity(activityTable);
setOverview(overviewData);
setOfflineState(null);
} catch (liveErr) {
if (!active) return;
if (isTargetUnreachableError(liveErr)) {
setLocks([]);
setActivity([]);
setOverview(null);
setOfflineState({
message:
"Target is currently unreachable. Check host/port, network route, SSL mode, and database availability.",
host: liveErr?.details?.host || targetInfo?.host || "-",
port: liveErr?.details?.port || targetInfo?.port || "-",
requestId: liveErr?.requestId || null,
});
} else {
throw liveErr;
}
}
setError(""); setError("");
} catch (e) { } catch (e) {
if (active) setError(String(e.message || e)); if (active) setError(String(e.message || e));
@@ -281,6 +308,17 @@ export function TargetDetailPage() {
<span className="muted">Responsible users:</span> <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>} {owners.length > 0 ? owners.map((item) => <span key={item.user_id} className="owner-pill">{item.email}</span>) : <span className="muted">none assigned</span>}
</div> </div>
{offlineState && (
<div className="card target-offline-card">
<h3>Target Offline</h3>
<p>{offlineState.message}</p>
<div className="target-offline-meta">
<span><strong>Host:</strong> {offlineState.host}</span>
<span><strong>Port:</strong> {offlineState.port}</span>
{offlineState.requestId ? <span><strong>Request ID:</strong> {offlineState.requestId}</span> : null}
</div>
</div>
)}
{uiMode === "easy" && overview && easySummary && ( {uiMode === "easy" && overview && easySummary && (
<> <>
<div className={`card easy-status ${easySummary.health}`}> <div className={`card easy-status ${easySummary.health}`}>

View File

@@ -2022,6 +2022,29 @@ select:-webkit-autofill {
font-size: 12px; font-size: 12px;
} }
.target-offline-card {
border-color: #a85757;
background: linear-gradient(130deg, #2c1724 0%, #1f1f38 100%);
}
.target-offline-card h3 {
margin: 0 0 8px;
color: #fecaca;
}
.target-offline-card p {
margin: 0 0 10px;
color: #fde2e2;
}
.target-offline-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
font-size: 12px;
color: #d4d4f5;
}
.chart-tooltip { .chart-tooltip {
background: #0f1934ee; background: #0f1934ee;
border: 1px solid #2f4a8b; border: 1px solid #2f4a8b;