Add block summary
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { lookup$ } from "@/chain.state"
|
||||
import { CircularProgress } from "@/components/CircularProgress"
|
||||
import { groupBy } from "@/lib/groupBy"
|
||||
import { getDynamicBuilder } from "@polkadot-api/metadata-builders"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { FC, ReactNode } from "react"
|
||||
import { map } from "rxjs"
|
||||
import { blockInfo$, blockInfoState$, BlockState } from "./block.state"
|
||||
import { filterEvt } from "./Events"
|
||||
import { CircularProgress } from "@/components/CircularProgress"
|
||||
import { CircleAlert, CircleCheck, CircleX, Clock } from "lucide-react"
|
||||
import { FC } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { map } from "rxjs"
|
||||
import { blockInfoState$ } from "./block.state"
|
||||
import { BlockStatusIcon, statusText } from "./Detail/BlockState"
|
||||
import { filterEvt } from "./Events"
|
||||
|
||||
const maxBlockWeight$ = state(
|
||||
lookup$.pipe(
|
||||
@@ -60,7 +60,13 @@ export const BlockPopover: FC<{ hash: string }> = ({ hash }) => {
|
||||
</span>
|
||||
</Link>
|
||||
</h3>
|
||||
<p>Status: {statusRep[block.status]}</p>
|
||||
<p>
|
||||
Status:{" "}
|
||||
<span className="inline-flex gap-1 items-center align-middle">
|
||||
<BlockStatusIcon state={block.status} />
|
||||
{statusText[block.status]}
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex justify-between items-start">
|
||||
{eventGroups && (
|
||||
<div>
|
||||
@@ -134,30 +140,3 @@ function addWeight(a: Weight, b: Weight | null): Weight {
|
||||
ref_time: a.ref_time + b.ref_time,
|
||||
}
|
||||
}
|
||||
|
||||
const statusRep: Record<BlockState, ReactNode> = {
|
||||
[BlockState.Best]: (
|
||||
<span className="inline-flex gap-1 items-center align-middle">
|
||||
<Clock className="text-blue-400" />
|
||||
Pending
|
||||
</span>
|
||||
),
|
||||
[BlockState.Fork]: (
|
||||
<span className="inline-flex gap-1 items-center align-middle">
|
||||
<CircleAlert className="text-orange-400" />
|
||||
Fork
|
||||
</span>
|
||||
),
|
||||
[BlockState.Finalized]: (
|
||||
<span className="inline-flex gap-1 items-center align-middle">
|
||||
<CircleCheck className="text-green-400" />
|
||||
Finalized
|
||||
</span>
|
||||
),
|
||||
[BlockState.Pruned]: (
|
||||
<span className="inline-flex gap-1 items-center align-middle">
|
||||
<CircleX className="text-red-400" />
|
||||
Pruned
|
||||
</span>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -20,12 +20,19 @@ import {
|
||||
Variant,
|
||||
} from "@polkadot-api/substrate-bindings"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { combineKeys } from "@react-rxjs/utils"
|
||||
import { Codec, HexString } from "polkadot-api"
|
||||
import { FC, useEffect, useRef, useState } from "react"
|
||||
import { useLocation, useParams } from "react-router-dom"
|
||||
import { map } from "rxjs"
|
||||
import { Link, useLocation, useParams } from "react-router-dom"
|
||||
import { map, startWith, switchMap, take } from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { blockInfoState$ } from "../block.state"
|
||||
import {
|
||||
blockInfo$,
|
||||
blockInfoState$,
|
||||
blocksByHeight$,
|
||||
BlockState,
|
||||
} from "../block.state"
|
||||
import { BlockStatusIcon, statusText } from "./BlockState"
|
||||
|
||||
type ApplyExtrinsicEvent = SystemEvent & { phase: { type: "ApplyExtrinsic" } }
|
||||
export const BlockDetail = () => {
|
||||
@@ -52,7 +59,33 @@ export const BlockDetail = () => {
|
||||
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<div>Summary</div>
|
||||
<div className="p-2">
|
||||
<div className="flex flex-wrap justify-between">
|
||||
<h2 className="font-bold text-xl whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
Block {block.hash}
|
||||
</h2>
|
||||
<p className="text-xl">{block.number.toLocaleString()}</p>
|
||||
</div>
|
||||
<p className="flex gap-1 items-center py-1">
|
||||
Status:
|
||||
<BlockStatusIcon state={block.status} />
|
||||
{statusText[block.status]}
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div>Parent</div>
|
||||
<BlockLink hash={block.parent} />
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<div>Children</div>
|
||||
<BlockChildren hash={block.hash} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-slate-200 py-2">
|
||||
<p>State root: {block.header?.stateRoot}</p>
|
||||
<p>Extrinsic root: {block.header?.extrinsicRoot}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<h3>Extrinsics TODO separate unsigned</h3>
|
||||
<ol>
|
||||
@@ -70,6 +103,76 @@ export const BlockDetail = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const childBlocks$ = state(
|
||||
(hash: string) =>
|
||||
blockInfo$(hash).pipe(
|
||||
take(1),
|
||||
switchMap(({ hash, number }) =>
|
||||
combineKeys(
|
||||
blocksByHeight$.pipe(
|
||||
map((v) => v[number + 1]),
|
||||
map((v) =>
|
||||
v
|
||||
? [...v.values()]
|
||||
.filter((block) => block.parent === hash)
|
||||
.map((block) => block.hash)
|
||||
: [],
|
||||
),
|
||||
),
|
||||
(hash) => blockInfo$(hash).pipe(startWith({ hash })),
|
||||
),
|
||||
),
|
||||
map((children) =>
|
||||
[...children.values()]
|
||||
.sort((a, b) => {
|
||||
const valueOf = (v: typeof a) =>
|
||||
"status" in v ? statusValue[v.status] : 0
|
||||
return valueOf(a) - valueOf(b)
|
||||
})
|
||||
.map((v) => v.hash),
|
||||
),
|
||||
),
|
||||
[],
|
||||
)
|
||||
const statusValue: Record<BlockState, number> = {
|
||||
[BlockState.Finalized]: 3,
|
||||
[BlockState.Best]: 2,
|
||||
[BlockState.Fork]: 1,
|
||||
[BlockState.Pruned]: 0,
|
||||
}
|
||||
|
||||
const BlockChildren: FC<{ hash: string }> = ({ hash }) => {
|
||||
const childBlocks = useStateObservable(childBlocks$(hash))
|
||||
|
||||
return childBlocks.length ? (
|
||||
<span className="inline-flex gap-2 align-middle">
|
||||
{childBlocks.map((hash) => (
|
||||
<BlockLink key={hash} hash={hash} />
|
||||
))}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-400">N/A</span>
|
||||
)
|
||||
}
|
||||
|
||||
const BlockLink: FC<{ hash: string }> = ({ hash }) => {
|
||||
const block = useStateObservable(blockInfoState$(hash))
|
||||
|
||||
if (!block) {
|
||||
return <span className="align-middle">{hash.slice(0, 12)}…</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className="text-polkadot-200 hover:text-polkadot-300 align-middle inline-flex items-center gap-1"
|
||||
to={`../${hash}`}
|
||||
>
|
||||
{<BlockStatusIcon state={block.status} size={20} />}
|
||||
{hash.slice(0, 12)}…
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const extrinsicDecoder$ = state(
|
||||
lookup$.pipe(map((lookup) => createExtrinsicCodec(lookup))),
|
||||
null,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ComponentType, FC } from "react"
|
||||
import { BlockState } from "../block.state"
|
||||
import {
|
||||
CircleAlert,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
Clock,
|
||||
LucideProps,
|
||||
} from "lucide-react"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
const statusIcon: Record<BlockState, ComponentType<LucideProps>> = {
|
||||
[BlockState.Best]: Clock,
|
||||
[BlockState.Fork]: CircleAlert,
|
||||
[BlockState.Finalized]: CircleCheck,
|
||||
[BlockState.Pruned]: CircleX,
|
||||
}
|
||||
const statusClassName: Record<BlockState, string> = {
|
||||
[BlockState.Best]: "text-blue-400",
|
||||
[BlockState.Fork]: "text-orange-400",
|
||||
[BlockState.Finalized]: "text-green-400",
|
||||
[BlockState.Pruned]: "text-red-400",
|
||||
}
|
||||
|
||||
export const BlockStatusIcon: FC<
|
||||
{
|
||||
state: BlockState
|
||||
} & LucideProps
|
||||
> = ({ state, className, ...props }) => {
|
||||
const Icon = statusIcon[state]
|
||||
|
||||
return (
|
||||
<Icon {...props} className={twMerge(statusClassName[state], className)} />
|
||||
)
|
||||
}
|
||||
export const statusText: Record<BlockState, string> = {
|
||||
[BlockState.Best]: "Pending",
|
||||
[BlockState.Fork]: "Fork",
|
||||
[BlockState.Finalized]: "Finalized",
|
||||
[BlockState.Pruned]: "Pruned",
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { FollowEventWithRuntime } from "@polkadot-api/substrate-client"
|
||||
import { state, withDefault } from "@react-rxjs/core"
|
||||
import { partitionByKey } from "@react-rxjs/utils"
|
||||
import { HexString } from "polkadot-api"
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
@@ -56,7 +57,13 @@ export interface BlockInfo {
|
||||
number: number
|
||||
body: string[] | null
|
||||
events: SystemEvent[] | null
|
||||
header: unknown | null
|
||||
header: {
|
||||
parentHash: HexString
|
||||
number: number
|
||||
stateRoot: HexString
|
||||
extrinsicRoot: HexString
|
||||
digests: unknown[]
|
||||
} | null
|
||||
status: BlockState
|
||||
}
|
||||
export const [blockInfo$, recordedBlocks$] = partitionByKey(
|
||||
|
||||
Reference in New Issue
Block a user