[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:
@@ -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}`}>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user