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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ const messages = {
checkDiskWarning: 'Check disk/mount',
seedingDataText: 'Awaiting completion',
uptimeWarning: 'Failed to determine when the server last restarted',
pending: 'Loading...'
pending: 'Loading...',
nodeType: 'Node Type',
peers: 'Peers',
currentHeight: 'Current Height',
storageType: 'Storage Type',
lastRestart: 'Last Restart',
gitSha: 'Git SHA',
viewConsole: 'View Console'
}

type ServiceDetailProps = {
Expand Down Expand Up @@ -118,6 +125,7 @@ const NodeOverview = ({
}: NodeOverviewProps) => {
const { isOpen, onClick, onClose } = useModalControls()
const { health, status, error } = useNodeHealth(endpoint, serviceType)
const isValidator = serviceType === ServiceType.Validator

let healthDetails = null
if (status === 'pending') {
Expand All @@ -139,6 +147,48 @@ const NodeOverview = ({
}
/>
)
} else if (isValidator) {
healthDetails = (
<>
{health?.nodeType && (
<ServiceDetail label={messages.nodeType} value={health.nodeType} />
)}
{typeof health?.peerCount === 'number' && (
<ServiceDetail label={messages.peers} value={health.peerCount} />
)}
{typeof health?.currentHeight === 'number' && (
<ServiceDetail
label={messages.currentHeight}
value={health.currentHeight.toLocaleString()}
/>
)}
{health?.storageType && (
<ServiceDetail
label={messages.storageType}
value={health.storageType}
/>
)}
{health?.startedAt && (
<ServiceDetail
label={messages.lastRestart}
value={
<TextWithIcon
icon={<>{timeSince(health.startedAt)}</>}
text={`ago (${health.startedAt.toLocaleString()})`}
/>
}
/>
)}
{health?.gitSha && (
<ServiceDetail
label={messages.gitSha}
value={
<span title={health.gitSha}>{health.gitSha.slice(0, 7)}</span>
}
/>
)}
</>
)
} else {
healthDetails = (
<>
Expand Down Expand Up @@ -320,6 +370,23 @@ const NodeOverview = ({
direction='column'
alignItems='flex-end'
>
{isValidator && endpoint && !isDeregistered && (
<Box>
<Button
onClick={() =>
window.open(
`${endpoint.replace(/\/$/, '')}/console`,
'_blank',
'noopener,noreferrer'
)
}
type={ButtonType.PRIMARY}
text={messages.viewConsole}
className={clsx(styles.modifyBtn)}
textClassName={styles.modifyBtnText}
/>
</Box>
)}
{!isDeregistered && isUnregistered && (
<Box>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,16 @@ const ServiceTable: React.FC<ServiceTableProps> = ({
return (
<div className={styles.rowContainer}>
<div className={clsx(styles.rowCol, styles.colEndpoint)}>
<ReactCountryFlag
className={styles.countryFlag}
countryCode={data.country}
/>
{data.country && /^[A-Za-z]{2}$/.test(data.country) ? (
<ReactCountryFlag
className={styles.countryFlag}
countryCode={data.country}
/>
) : (
<span className={styles.countryFlag} aria-label='Unknown location'>
🏁
</span>
)}
{data.endpoint}
</div>
<div className={clsx(styles.rowCol, styles.colVersion)}>
Expand Down
36 changes: 13 additions & 23 deletions packages/protocol-dashboard/src/containers/Node/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,19 @@ const Validator: React.FC<ValidatorProps> = ({
const isOwner = accountWallet === validator?.owner

return (
<>
<div className={styles.section}>
<NodeOverview
spID={spID}
serviceType={ServiceType.Validator}
version={validator?.version}
endpoint={validator?.endpoint}
operatorWallet={validator?.owner}
delegateOwnerWallet={validator?.delegateOwnerWallet}
isOwner={isOwner}
isDeregistered={validator?.isDeregistered}
isLoading={status === Status.Loading}
/>
</div>
{validator ? (
<div className={clsx(styles.section, styles.chart)}>
<IndividualNodeUptimeChart
nodeType={ServiceType.Validator}
node={validator.endpoint}
/>
</div>
) : null}
</>
<div className={styles.section}>
<NodeOverview
spID={spID}
serviceType={ServiceType.Validator}
version={validator?.version}
endpoint={validator?.endpoint}
operatorWallet={validator?.owner}
delegateOwnerWallet={validator?.delegateOwnerWallet}
isOwner={isOwner}
isDeregistered={validator?.isDeregistered}
isLoading={status === Status.Loading}
/>
</div>
)
}

Expand Down
125 changes: 123 additions & 2 deletions packages/protocol-dashboard/src/hooks/useNodeHealth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { ServiceType } from 'types'
const bytesToGb = (bytes: number) => Math.floor(bytes / 10 ** 9)

const useNodeHealth = (endpoint: string, serviceType: ServiceType) => {
const isValidator = serviceType === ServiceType.Validator
const healthPath = isValidator ? '/health-check' : '/health_check'
const { data, status, error } = useQuery({
queryKey: ['health', { endpoint }],
queryKey: ['health', { endpoint, healthPath }],
queryFn: async () => {
const response = await fetch(`${endpoint}/health_check`)
const response = await fetch(`${endpoint}${healthPath}`)
if (!response.ok) {
throw new Error(
`Failed fetching health check from ${endpoint}: ${response.status} ${response.statusText}`
Expand Down Expand Up @@ -37,6 +39,125 @@ const useNodeHealth = (endpoint: string, serviceType: ServiceType) => {
return { status, error, health: null }
}

if (isValidator) {
const core = data?.core ?? {}

const hasPrimitiveProps = (o: Record<string, unknown>) =>
Object.values(o).some(
(x) => x === null || typeof x !== 'object' || Array.isArray(x)
)
const countPeers = (val: any): number => {
if (val == null) return 0
if (Array.isArray(val)) {
return val.reduce(
(sum: number, item) =>
sum +
(item && typeof item === 'object' && !Array.isArray(item)
? hasPrimitiveProps(item)
? 1
: countPeers(item)
: 1),
0
)
}
if (typeof val !== 'object') return 0
const subObjects = Object.values(val).filter(
(v): v is Record<string, unknown> =>
v !== null && typeof v === 'object' && !Array.isArray(v)
)
const arrays = Object.values(val).filter((v): v is unknown[] =>
Array.isArray(v)
)
if (subObjects.length === 0 && arrays.length === 0) return 0
if (subObjects.some(hasPrimitiveProps)) {
return subObjects.length + arrays.reduce((s, a) => s + countPeers(a), 0)
}
return (
subObjects.reduce((s, o) => s + countPeers(o), 0) +
arrays.reduce((s, a) => s + countPeers(a), 0)
)
}
const peerCount = countPeers(core?.peers)

const syncInfo = core?.sync_info ?? {}
const findHeight = (obj: any): number | undefined => {
if (!obj || typeof obj !== 'object') return undefined
for (const k of [
'latest_block_height',
'block_height',
'height',
'current_height'
]) {
const v = obj[k]
if (typeof v === 'number') return v
if (typeof v === 'string' && /^\d+$/.test(v)) return Number(v)
}
for (const v of Object.values(obj)) {
const found = findHeight(v)
if (found !== undefined) return found
}
return undefined
}
const currentHeight = findHeight(syncInfo) ?? findHeight(core)

const parseDurationMs = (s: unknown): number | undefined => {
if (typeof s !== 'string') return undefined
let total = 0
const re = /(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g
let match: RegExpExecArray | null
let matched = false
while ((match = re.exec(s)) !== null) {
matched = true
const n = parseFloat(match[1])
switch (match[2]) {
case 'ns':
total += n / 1e6
break
case 'us':
case 'µs':
total += n / 1e3
break
case 'ms':
total += n
break
case 's':
total += n * 1000
break
case 'm':
total += n * 60 * 1000
break
case 'h':
total += n * 60 * 60 * 1000
break
}
}
return matched ? total : undefined
}
let startedAt: Date | undefined
const uptimeMs = parseDurationMs(data?.uptime)
const ts = data?.timestamp ? Date.parse(data.timestamp) : NaN
if (!isNaN(ts) && uptimeMs !== undefined) {
startedAt = new Date(ts - uptimeMs)
}

return {
status,
error: null,
health: {
version: data?.data?.version ?? data?.version,
chainId: core?.chain_info?.chain_id,
nodeType: core?.node_info?.node_type,
ethAddress: core?.node_info?.eth_address ?? data?.signer,
peerCount,
currentHeight,
storageType: core?.storage_info?.storage_type,
startedAt,
gitSha: typeof data?.git === 'string' ? data.git : undefined,
delegateOwnerWallet: data?.signer
}
}
}

const { data: health } = data
let res = {}

Expand Down
Loading