Add block summary

This commit is contained in:
Victor Oliva
2024-11-01 18:20:29 +01:00
parent 6154acf967
commit dbcd64ca7b
4 changed files with 169 additions and 39 deletions
+13 -34
View File
@@ -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>
),
}
+107 -4
View File
@@ -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,
+41
View File
@@ -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",
}
+8 -1
View File
@@ -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(