feat: go to block (#91)

This commit is contained in:
Josep M Sobrepere
2025-10-20 10:38:40 +03:00
committed by GitHub
parent a3378df16b
commit 334b232d40
6 changed files with 101 additions and 13 deletions

View File

@@ -0,0 +1,21 @@
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { forwardRef } from "react"
export const SearchInput = forwardRef<
HTMLInputElement,
Omit<React.ComponentProps<"input">, "type">
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<input
type="text"
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
))

View File

@@ -1,6 +1,6 @@
import { CopyText } from "@/components/Copy"
import { Popover } from "@/components/Popover"
import { Link } from "@/hashParams"
import { Link, useNavigate } from "@/hashParams"
import { client$ } from "@/state/chains/chain.state"
import { state, useStateObservable } from "@react-rxjs/core"
import { FC } from "react"
@@ -9,6 +9,9 @@ import { twMerge } from "tailwind-merge"
import { BlockInfo, blocksByHeight$, finalized$ } from "./block.state"
import { BlockPopover } from "./BlockPopover"
import * as Finalizing from "./FinalizingTable"
import { Button } from "@/components/ui/button"
import { SearchInput } from "@/components/ui/search-input"
import { Search } from "lucide-react"
const best$ = client$.pipeState(
switchMap((client) => client.bestBlocks$.pipe(map(([best]) => best))),
@@ -84,6 +87,34 @@ const blockTable$ = state(
[],
)
export const BlockInput: FC = () => {
const navigate = useNavigate()
return (
<form
className="grow p-0 -my-2 flex"
onSubmit={(e) => {
const blockTarget = new FormData(e.currentTarget).get("blockTarget") as string
const isHex = blockTarget?.match(/^0[xX][0-9a-fA-F]+$/)
if (
(isHex && blockTarget.length === 66) ||
(!isHex && !Number.isNaN(Number(blockTarget)))
) {
navigate(blockTarget)
}
e.preventDefault()
e.stopPropagation()
}}
>
<div className="grow">
<SearchInput placeholder="Block hash or height" name="blockTarget" />
</div>
<Button className="grow-0">
<Search />{" "}
</Button>
</form>
)
}
export const BlockTable = () => {
const rows = useStateObservable(blockTable$)
const finalized = useStateObservable(finalized$)
@@ -100,7 +131,7 @@ export const BlockTable = () => {
return (
<Finalizing.Root>
<Finalizing.Title>Recent Blocks</Finalizing.Title>
<Finalizing.Title search={<BlockInput />}>Recent Blocks</Finalizing.Title>
<Finalizing.Table>
{rows.map((row, i) => (
<Finalizing.Row

View File

@@ -6,8 +6,8 @@ import { BlockBody } from "./BlockBody"
import { BlockInfoView } from "./BlockInfo"
export const BlockDetail = () => {
const { hash } = useParams()
const block = useStateObservable(blockInfoState$(hash ?? ""))
const { hashOrHeight } = useParams()
const block = useStateObservable(blockInfoState$(hashOrHeight ?? ""))
if (!block) return <Loading>Loading block</Loading>

View File

@@ -9,7 +9,7 @@ import { Summary } from "./Summary"
export const Explorer = withSubscribe(
() => (
<Routes>
<Route path=":hash" element={<BlockDetail />} />
<Route path=":hashOrHeight" element={<BlockDetail />} />
<Route
path="*"
element={

View File

@@ -1,8 +1,14 @@
import { FC, PropsWithChildren } from "react"
import { FC, PropsWithChildren, ReactNode } from "react"
import { twMerge } from "tailwind-merge"
export const Title: FC<PropsWithChildren> = ({ children }) => (
<h2 className="font-bold p-2 border-b border-slate-400 mb-2">{children}</h2>
export const Title: FC<PropsWithChildren<{ search?: ReactNode }>> = ({
children,
search,
}) => (
<h2 className="font-bold p-2 border-b border-slate-400 mb-2 flex">
<span className="grow-0">{children}</span>
{search}
</h2>
)
export const Table: FC<PropsWithChildren> = ({ children }) => (

View File

@@ -3,7 +3,7 @@ import { chainClient$, client$ } from "@/state/chains/chain.state"
import { SystemEvent } from "@polkadot-api/observable-client"
import { state } from "@react-rxjs/core"
import { combineKeys, partitionByKey } from "@react-rxjs/utils"
import { HexString, PolkadotClient } from "polkadot-api"
import { FixedSizeBinary, HexString, PolkadotClient } from "polkadot-api"
import {
catchError,
combineLatest,
@@ -244,13 +244,43 @@ export const inMemoryBlocks$ = state(
)
inMemoryBlocks$.subscribe()
const blockHash$ = (hashOrHeight: string) =>
hashOrHeight.length > 63
? of(hashOrHeight.startsWith("0x") ? hashOrHeight : `0x${hashOrHeight}`)
: client$.pipe(
switchMap((client) =>
from(
client._request<HexString[], [number]>("archive_v1_hashByHeight", [
Number(hashOrHeight),
]),
).pipe(
map((x) => {
if (x.length) return x[0]
throw null
}),
catchError(() =>
client
.getUnsafeApi()
.query.System.BlockHash.getValue(Number(hashOrHeight))
.then((x: FixedSizeBinary<32>) => x.asHex()),
),
),
),
)
export const blockInfoState$ = state(
(hash: string) =>
(hashOrHeight: string) =>
inMemoryBlocks$.pipe(
take(1),
switchMap((blocks) =>
blocks.has(hash) ? blockInfo$(hash) : getUnpinnedBlockInfo$(hash),
),
switchMap((blocks) => {
if (blocks.has(hashOrHeight)) return blockInfo$(hashOrHeight)
const potentialHeight = Number(hashOrHeight)
const target = Array.from(blocks.values()).find(
(x) => x?.number === potentialHeight,
)
if (target) return blockInfo$(target.hash)
return blockHash$(hashOrHeight).pipe(mergeMap(getUnpinnedBlockInfo$))
}),
),
null,
)