The nav listed every chain telemetry names, with the ones we hold no RPC endpoint for as disabled chips saying "no endpoint". The reasoning was that they are part of the network and hiding them misrepresents its shape. In practice it put five dead chips above the standings on every page — four of which have never had anything behind them — and made the one live chain the smallest thing in the row. Nothing is blocked. `/:chain/...` still resolves for any chain the backend knows, `/v1/chains` still reports every one of them with its `tracking`, and promoting one is still a single `[[chains]]` entry. This is only about what the page offers unprompted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
/**
|
|
* The chain nav.
|
|
*
|
|
* Only the chains this observer holds an RPC endpoint for, ordered by node
|
|
* count descending — the busiest is the one most people came for, and the order
|
|
* is a real fact about the network rather than a config ordering.
|
|
*
|
|
* The rest are **not advertised**. They were listed as disabled chips for a
|
|
* while, on the reasoning that they are part of the network and hiding them
|
|
* misrepresents its shape. In practice that put five dead chips above the
|
|
* standings on every page, four of which had never had anything behind them,
|
|
* and the one live chain was the smallest thing in the row. Nothing is
|
|
* blocked: `/:chain/...` still resolves for any chain the backend knows, and
|
|
* the API still reports every one of them with its `tracking`. This is only
|
|
* about what the page offers unprompted.
|
|
*/
|
|
|
|
import { Link } from 'react-router-dom'
|
|
|
|
import type { ChainInfo } from '../api/generated/ChainInfo'
|
|
import type { Window as WindowName } from '../api/generated/Window'
|
|
import { href } from '../lib/routes'
|
|
|
|
function reason(chain: ChainInfo): string {
|
|
switch (chain.status) {
|
|
case 'awaiting':
|
|
return `${chain.display_name} has not produced a block yet.`
|
|
case 'unreachable':
|
|
return `${chain.display_name}'s node is not answering; the last known standings are shown.`
|
|
case 'syncing':
|
|
return `${chain.display_name}'s node is still importing history.`
|
|
default:
|
|
return chain.display_name
|
|
}
|
|
}
|
|
|
|
export function ChainSwitcher({
|
|
chains,
|
|
active,
|
|
window: windowName,
|
|
}: {
|
|
chains: ChainInfo[]
|
|
active: string | null
|
|
/** Carried across the switch: changing chain is not a reason to change span. */
|
|
window: WindowName
|
|
}) {
|
|
const navigable = chains.filter((chain) => chain.tracking === 'full')
|
|
if (navigable.length === 0) return null
|
|
|
|
return (
|
|
<nav className="chains" aria-label="Chain">
|
|
<span className="eyebrow chains-label">Chains</span>
|
|
<div className="chains-list">
|
|
{navigable.map((chain) => {
|
|
const isActive = chain.id === active
|
|
// A link, not a button: a chain is a page with an address, and the
|
|
// chip is how most people reach it — so it has to survive a middle
|
|
// click and a right-click-copy like any other link on the site.
|
|
return (
|
|
<Link
|
|
key={chain.genesis ?? chain.id}
|
|
to={href({ chain: chain.id, window: windowName })}
|
|
className={isActive ? 'chain-chip is-active' : 'chain-chip'}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
title={reason(chain)}
|
|
>
|
|
<span className="chain-name">{chain.display_name}</span>
|
|
{chain.node_count !== null && (
|
|
<span className="chain-nodes" title={`${chain.node_count} nodes on telemetry`}>
|
|
{chain.node_count}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
</nav>
|
|
)
|
|
}
|