Files
observer/web/src/components/ChainSwitcher.tsx
rob thijssen d6f4c0c21e
Some checks failed
deploy / build (push) Waiting to run
deploy / deploy-api (push) Has been cancelled
deploy / deploy-web (push) Has been cancelled
ui: only advertise chains we can actually read
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
2026-09-09 17:15:29 +03:00

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>
)
}