Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions packages/admin-ui/src/__mock-backend__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,20 +479,26 @@ export const otherCalls: Omit<Routes, keyof typeof namedSpacedCalls> = {
gnosis: null
}),

validatorsFilterAttestingByNetwork: async () => ({
mainnet: { validators: ["0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"] },
hoodi: { validators: [] },
gnosis: null
}),

validatorsBalancesByNetwork: async () => ({
validatorsDataByNetwork: async () => ({
mainnet: {
active: { validators: ["0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"] },
attesting: { validators: ["0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"] },
balances: {
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef": "32000000000"
balances: {
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef": "32000000000"
}
}
},
hoodi: { balances: {} },
gnosis: null
hoodi: {
active: { validators: [] },
attesting: { validators: [] },
balances: { balances: {} }
},
gnosis: {
active: null,
attesting: null,
balances: null
}
}),

signerByNetworkGet: async () => ({
Expand Down
60 changes: 0 additions & 60 deletions packages/admin-ui/src/hooks/PWA/useSystemHealth.ts

This file was deleted.

209 changes: 179 additions & 30 deletions packages/admin-ui/src/hooks/useNetworkStats.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { DashboardSupportedNetwork, Network, NetworkStats, NodeStatus } from "@dappnode/types";
import { useApi } from "api";
import { useState, useEffect, useCallback, useRef } from "react";
import {
ClientError,
ClientResult,
DashboardSupportedNetwork,
Network,
NetworkStats,
NodeStatus,
NodeStatusByNetwork,
SignerStatus,
ValidatorsDataByNetwork
} from "@dappnode/types";
import { api, useApi } from "api";
import EthLogo from "img/logos/eth-logo.svg?react";
import GnosisLogo from "img/logos/gnosis-logo.svg?react";
import LuksoLogo from "img/logos/lukso-logo.svg?react";
Expand Down Expand Up @@ -43,31 +54,151 @@ const networkFeatures: Record<DashboardSupportedNetwork, NetworkFeatures> = {
[Network.Sepolia]: { hasValidators: false, logo: EthLogo }
};

/**
* Collects all non-null dnpNames from the consensus and execution client maps.
*/
function collectDnpNames(
consensusClients: Partial<Record<Network, string | null | undefined>> | undefined,
executionClients: Partial<Record<Network, string | null | undefined>> | undefined
): Set<string> {
const names = new Set<string>();
if (consensusClients) {
for (const dnpName of Object.values(consensusClients)) {
if (dnpName) names.add(dnpName);
}
}
if (executionClients) {
for (const dnpName of Object.values(executionClients)) {
if (dnpName) names.add(dnpName);
}
}
return names;
}

/**
* Determines which supported networks have at least one installed client package
* (either consensus or execution) based on the installed packages.
*/
function getNetworksWithInstalledClients(
consensusClients: Partial<Record<Network, string | null | undefined>> | undefined,
executionClients: Partial<Record<Network, string | null | undefined>> | undefined,
installedDnpNames: Set<string>
): DashboardSupportedNetwork[] {
const networks: DashboardSupportedNetwork[] = [];
for (const network of supportedNetworks) {
const cc = consensusClients?.[network];
const ec = executionClients?.[network];
const ccInstalled = cc ? installedDnpNames.has(cc) : false;
const ecInstalled = ec ? installedDnpNames.has(ec) : false;
if (ccInstalled || ecInstalled) {
networks.push(network);
}
}
return networks;
}

type SignersStatusByNetwork = Partial<Record<Network, SignerStatus>>;

export function useNetworkStats() {
const nodesStatusReq = useApi.nodeStatusGetByNetwork({ networks: supportedNetworks });
// Step 1: Fetch consensus and execution clients for all supported networks
const consensusClientsReq = useApi.consensusClientsGetByNetworks({ networks: supportedNetworks });
const executionClientsReq = useApi.executionClientsGetByNetworks({ networks: supportedNetworks });
const validatorsFilterActiveReq = useApi.validatorsFilterActiveByNetwork({ networks: supportedNetworks });
const validatorsFilterAttestingReq = useApi.validatorsFilterAttestingByNetwork({ networks: supportedNetworks });
const validatorsBalancesReq = useApi.validatorsBalancesByNetwork({ networks: supportedNetworks });
const signersStatusReq = useApi.signerByNetworkGet({ networks: supportedNetworks });

const nodesStatusByNetwork = nodesStatusReq.data;
const consensusClientsByNetwork = consensusClientsReq.data;
const executionClientsByNetwork = executionClientsReq.data;
const validatorsActiveByNetwork = validatorsFilterActiveReq.data;
const validatorsAttestingByNetwork = validatorsFilterAttestingReq.data;
const validatorsBalancesByNetwork = validatorsBalancesReq.data;
const signersStatusByNetwork = signersStatusReq.data;

const clientsLoading =
nodesStatusReq.isValidating || consensusClientsReq.isValidating || executionClientsReq.isValidating;
// States fetched only for networks with installed clients
const [nodesStatusByNetwork, setNodesStatusByNetwork] = useState<NodeStatusByNetwork | undefined>(undefined);
const [nodesStatusLoading, setNodesStatusLoading] = useState(false);
const [validatorsData, setValidatorsData] = useState<ValidatorsDataByNetwork | undefined>(undefined);
const [signersStatusByNetwork, setSignersStatusByNetwork] = useState<SignersStatusByNetwork | undefined>(undefined);
const [validatorsLoading, setValidatorsLoading] = useState(false);
const [installedDnpNames, setInstalledDnpNames] = useState<Set<string>>(new Set());

const lastFetchedNetworksKey = useRef<string>("");

const fetchNetworkData = useCallback(async () => {
if (!consensusClientsByNetwork || !executionClientsByNetwork) return;

const validatorsLoading =
validatorsFilterActiveReq.isValidating ||
validatorsFilterAttestingReq.isValidating ||
validatorsBalancesReq.isValidating ||
signersStatusReq.isValidating;
// Step 2: Collect all dnpNames from both client responses
const dnpNames = collectDnpNames(consensusClientsByNetwork, executionClientsByNetwork);
if (dnpNames.size === 0) {
setNodesStatusByNetwork({});
setValidatorsData({});
setSignersStatusByNetwork({});
return;
}

// Step 3: Verify which packages are installed
let installedDnpNames: Set<string>;
try {
const installedPackages = await api.packagesGet();
const allInstalledNames = new Set(installedPackages.map((pkg) => pkg.dnpName));
installedDnpNames = new Set([...dnpNames].filter((name) => allInstalledNames.has(name)));
setInstalledDnpNames(installedDnpNames);
} catch (e) {
console.error("Error fetching installed packages for node status", e);
return;
}

if (installedDnpNames.size === 0) {
setNodesStatusByNetwork({});
setValidatorsData({});
setSignersStatusByNetwork({});
return;
}

const networksWithClients = getNetworksWithInstalledClients(
consensusClientsByNetwork,
executionClientsByNetwork,
installedDnpNames
);

if (networksWithClients.length === 0) {
setNodesStatusByNetwork({});
setValidatorsData({});
setSignersStatusByNetwork({});
return;
}

// Avoid re-fetching if the networks list hasn't changed
const networksKey = JSON.stringify(networksWithClients);
if (networksKey === lastFetchedNetworksKey.current) return;
lastFetchedNetworksKey.current = networksKey;

// Step 4: Fetch node status, combined validators data, and signer data
// validatorsDataByNetwork combines active, attesting, and balances in a single
// backend call, reducing beacon chain API requests
try {
setNodesStatusLoading(true);
setValidatorsLoading(true);

const [nodeStatus, validatorsResult, signersStatus] = await Promise.all([
api.nodeStatusGetByNetwork({ networks: networksWithClients }),
api.validatorsDataByNetwork({ networks: networksWithClients }),
api.signerByNetworkGet({ networks: networksWithClients })
]);

setNodesStatusByNetwork(nodeStatus);
setValidatorsData(validatorsResult);
setSignersStatusByNetwork(signersStatus);
} catch (e) {
console.error("Error fetching network data", e);
} finally {
setNodesStatusLoading(false);
setValidatorsLoading(false);
}
}, [consensusClientsByNetwork, executionClientsByNetwork]);

useEffect(() => {
fetchNetworkData();
}, [fetchNetworkData]);

const clientsLoading =
nodesStatusLoading ||
consensusClientsReq.isValidating ||
executionClientsReq.isValidating ||
nodesStatusByNetwork === undefined;

const networkStats: NetworkStats = {};

Expand All @@ -76,9 +207,21 @@ export function useNetworkStats() {
const nodeStatusData: NodeStatus | undefined = nodesStatusByNetwork?.[network];
const consensusClientDnp = consensusClientsByNetwork?.[network];
const executionClientDnp = executionClientsByNetwork?.[network];
const validatorsActive = validatorsActiveByNetwork?.[network];
const validatorsAttesting = validatorsAttestingByNetwork?.[network];
const balancesObj = validatorsBalancesByNetwork?.[network]?.balances;

// Only include client results if the corresponding package is actually installed
const ecInstalled = executionClientDnp ? installedDnpNames.has(executionClientDnp) : false;
const ccInstalled = consensusClientDnp ? installedDnpNames.has(consensusClientDnp) : false;

const filteredNodeStatus: NodeStatus | undefined = nodeStatusData
? {
ec: ecInstalled ? nodeStatusData.ec : null,
cc: ccInstalled ? nodeStatusData.cc : null
}
: undefined;

const validatorsActive = validatorsData?.[network]?.active;
const validatorsAttesting = validatorsData?.[network]?.attesting;
const balancesObj = validatorsData?.[network]?.balances?.balances;
const signerStatus = signersStatusByNetwork?.[network] ?? { isInstalled: false, brainRunning: false };

let total = 0;
Expand All @@ -90,15 +233,15 @@ export function useNetworkStats() {
total = validatorsActive.validators.length;
beaconError = validatorsActive.beaconError;
}
if (features.hasValidators && validatorsAttesting) {
if (features.hasValidators && validatorsAttesting && !validatorsAttesting.beaconError) {
attesting = validatorsAttesting.validators.length;
}
if (features.hasValidators && balancesObj && validatorsActive) {
// Sum balances for active validators
balance = validatorsActive.validators.reduce((acc, pk) => acc + (parseFloat(balancesObj[pk]) || 0), 0);
}

const validatorsData = features.hasValidators
const validatorsInfo = features.hasValidators
? {
validators: {
total,
Expand All @@ -111,15 +254,17 @@ export function useNetworkStats() {
}
: undefined;

// Remove network where no node data is available
if (nodeStatusData && (nodeStatusData.ec || nodeStatusData.cc)) {
// Show network when any client has data or an error (but not when both are null)
const hasEc = filteredNodeStatus?.ec !== null && filteredNodeStatus?.ec !== undefined;
const hasCc = filteredNodeStatus?.cc !== null && filteredNodeStatus?.cc !== undefined;
if (filteredNodeStatus && (hasEc || hasCc)) {
networkStats[network] = {
nodeStatus: nodeStatusData,
nodeStatus: filteredNodeStatus,
clientsDnps: {
ecDnp: executionClientDnp || null,
ccDnp: consensusClientDnp || null
ecDnp: ecInstalled ? executionClientDnp || null : null,
ccDnp: ccInstalled ? consensusClientDnp || null : null
},
...validatorsData,
...validatorsInfo,
hasValidators: features.hasValidators,
beaconExplorer: features.beaconExplorer || undefined
};
Expand All @@ -135,3 +280,7 @@ export function useNetworkStats() {

return { networkStats, clientsLoading, getNetworkLogo, validatorsLoading };
}

export function isClientError(result: ClientResult): result is ClientError {
return result !== null && typeof result === "object" && "error" in result;
}
4 changes: 2 additions & 2 deletions packages/admin-ui/src/hooks/useSystemHealth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export function useSystemHealth() {
return {
cpuUsage: cpuStats.data ? Math.floor(cpuStats.data.usedPercentage) : 0,
cpuTemp: cpuStats.data ? Math.floor(cpuStats.data.temperatureAverage) : 0,
memoryUsed: memoryStats.data ? humanFileSize(memoryStats.data.used, false, 0) : null,
memoryUsed: memoryStats.data ? humanFileSize(memoryStats.data.used, true, 0) : null,
memoryTotal: memoryStats.data ? humanFileSize(memoryStats.data.total) : null,
memoryPercentage: memoryStats.data ? Math.floor(memoryStats.data.usedPercentage) : 0,
diskUsed: diskStats.data ? humanFileSize(diskStats.data.used, false) : null,
diskUsed: diskStats.data ? humanFileSize(diskStats.data.used) : null,
diskTotal: diskStats.data ? humanFileSize(diskStats.data.total) : null,
diskPercentage: diskStats.data ? Math.floor(diskStats.data.usedPercentage) : 0,
uptime: hostUptime.data ? formatUptime(hostUptime.data) : null,
Expand Down
Loading
Loading