diff --git a/frontend/src/pages/TargetDetailPage.jsx b/frontend/src/pages/TargetDetailPage.jsx index 21c26e1..8337190 100644 --- a/frontend/src/pages/TargetDetailPage.jsx +++ b/frontend/src/pages/TargetDetailPage.jsx @@ -76,6 +76,10 @@ function didMetricSeriesChange(prev = [], next = []) { 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) { const { from, to } = toQueryRange(range); return apiFetch( @@ -99,6 +103,7 @@ export function TargetDetailPage() { const [targetMeta, setTargetMeta] = useState(null); const [owners, setOwners] = useState([]); const [groupTargets, setGroupTargets] = useState([]); + const [offlineState, setOfflineState] = useState(null); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); const refreshRef = useRef(refresh); @@ -114,22 +119,16 @@ export function TargetDetailPage() { setLoading(true); } 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, "xacts_total", 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}/owners`, {}, tokens, refreshRef.current), apiFetch("/targets", {}, tokens, refreshRef.current), ]); if (!active) return; setSeries({ connections, xacts, cache }); - setLocks(locksTable); - setActivity(activityTable); - setOverview(overviewData); setTargetMeta(targetInfo); setOwners(ownerRows); const groupId = targetInfo?.tags?.monitor_group_id; @@ -141,6 +140,34 @@ export function TargetDetailPage() { } else { 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(""); } catch (e) { if (active) setError(String(e.message || e)); @@ -281,6 +308,17 @@ export function TargetDetailPage() { Responsible users: {owners.length > 0 ? owners.map((item) => {item.email}) : none assigned} + {offlineState && ( +
+

Target Offline

+

{offlineState.message}

+
+ Host: {offlineState.host} + Port: {offlineState.port} + {offlineState.requestId ? Request ID: {offlineState.requestId} : null} +
+
+ )} {uiMode === "easy" && overview && easySummary && ( <>
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 25d10d0..9c1b75f 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2022,6 +2022,29 @@ select:-webkit-autofill { 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 { background: #0f1934ee; border: 1px solid #2f4a8b;