diff --git a/src/components/ui/search-input.tsx b/src/components/ui/search-input.tsx new file mode 100644 index 0000000..f5ac780 --- /dev/null +++ b/src/components/ui/search-input.tsx @@ -0,0 +1,21 @@ +import { Search } from "lucide-react" +import { cn } from "@/lib/utils" +import { forwardRef } from "react" + +export const SearchInput = forwardRef< + HTMLInputElement, + Omit, "type"> +>(({ className, ...props }, ref) => ( +
+ + +
+)) diff --git a/src/pages/Explorer/BlockTable.tsx b/src/pages/Explorer/BlockTable.tsx index 6221de3..b315853 100644 --- a/src/pages/Explorer/BlockTable.tsx +++ b/src/pages/Explorer/BlockTable.tsx @@ -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 ( +
{ + 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() + }} + > +
+ +
+ +
+ ) +} + export const BlockTable = () => { const rows = useStateObservable(blockTable$) const finalized = useStateObservable(finalized$) @@ -100,7 +131,7 @@ export const BlockTable = () => { return ( - Recent Blocks + }>Recent Blocks {rows.map((row, i) => ( { - const { hash } = useParams() - const block = useStateObservable(blockInfoState$(hash ?? "")) + const { hashOrHeight } = useParams() + const block = useStateObservable(blockInfoState$(hashOrHeight ?? "")) if (!block) return Loading block… diff --git a/src/pages/Explorer/Explorer.tsx b/src/pages/Explorer/Explorer.tsx index ce39052..f5e76ce 100644 --- a/src/pages/Explorer/Explorer.tsx +++ b/src/pages/Explorer/Explorer.tsx @@ -9,7 +9,7 @@ import { Summary } from "./Summary" export const Explorer = withSubscribe( () => ( - } /> + } /> = ({ children }) => ( -

{children}

+export const Title: FC> = ({ + children, + search, +}) => ( +

+ {children} + {search} +

) export const Table: FC = ({ children }) => ( diff --git a/src/pages/Explorer/block.state.ts b/src/pages/Explorer/block.state.ts index 8ecb84e..2ab4848 100644 --- a/src/pages/Explorer/block.state.ts +++ b/src/pages/Explorer/block.state.ts @@ -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("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, )