From 8e60fb775fc1d6ef6f34340d8b1823cc4e1c14e8 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Tue, 5 May 2026 13:42:35 +0200 Subject: [PATCH] improve UI --- src/components/TokenAmount.tsx | 4 +- src/hashParams.tsx | 11 +- src/pages/Explorer/Detail/Extrinsic.tsx | 18 ++- .../Explorer/Detail/MortalityAnalyzer.tsx | 150 +++++++++++++++++- .../Explorer/Detail/SignedExtensions.tsx | 143 ++++++++++------- .../Extrinsics/Analyzer/ExtrinsicAnalyzer.tsx | 44 +++-- .../Extrinsics/Analyzer/ExtrinsicDecoder.tsx | 141 +++++++++++----- src/pages/Extrinsics/Analyzer/Priority.tsx | 97 +++++++++-- 8 files changed, 467 insertions(+), 141 deletions(-) diff --git a/src/components/TokenAmount.tsx b/src/components/TokenAmount.tsx index d99d045..2bbb963 100644 --- a/src/components/TokenAmount.tsx +++ b/src/components/TokenAmount.tsx @@ -18,7 +18,9 @@ export const TokenAmount: FC<{ const formattedValue = ( Number(children) / 10 ** properties.tokenDecimals - ).toLocaleString(undefined) + ).toLocaleString(undefined, { + maximumSignificantDigits: 3, + }) return ( ( useLayoutEffect( () => - setHashParams({ - [key]: getFn(...dependencies), - }), + setHashParams( + { + [key]: getFn(...dependencies), + }, + location, + ), // eslint-disable-next-line react-hooks/exhaustive-deps dependencies, ) @@ -116,6 +119,6 @@ export const useHashParamState = ( return [ getHashParams(location).get(key) ?? init?.() ?? (null as any), - (value: string | null) => setHashParams({ [key]: value }), + (value: string | null) => setHashParams({ [key]: value }, location), ] as const } diff --git a/src/pages/Explorer/Detail/Extrinsic.tsx b/src/pages/Explorer/Detail/Extrinsic.tsx index 826e032..92553db 100644 --- a/src/pages/Explorer/Detail/Extrinsic.tsx +++ b/src/pages/Explorer/Detail/Extrinsic.tsx @@ -130,15 +130,19 @@ export const EventDisplay: FC<{ ) } -export const Sender: React.FC<{ +export const senderToAddress = ( + sender: Enum<{ Id: SS58String }> | SS58String | HexString, +) => + typeof sender === "string" + ? sender + : "type" in sender && sender.type === "Id" + ? sender.value + : null + +const Sender: React.FC<{ sender: Enum<{ Id: SS58String }> | SS58String | HexString }> = ({ sender }) => { - const value: string | null = - typeof sender === "string" - ? sender - : "type" in sender && sender.type === "Id" - ? sender.value - : null + const value = senderToAddress(sender) return ( value && (
diff --git a/src/pages/Explorer/Detail/MortalityAnalyzer.tsx b/src/pages/Explorer/Detail/MortalityAnalyzer.tsx index cecc9b4..e0524ac 100644 --- a/src/pages/Explorer/Detail/MortalityAnalyzer.tsx +++ b/src/pages/Explorer/Detail/MortalityAnalyzer.tsx @@ -1,5 +1,6 @@ import { client$ } from "@/state/chains/chain.state" import { useStateObservable, withDefault } from "@react-rxjs/core" +import { jsonSerialize } from "polkadot-api/utils" import { FC, useContext } from "react" import { map, switchMap } from "rxjs" import { BlockContext } from "./blockContext" @@ -17,9 +18,31 @@ export const MortalityAnalyzer: FC<{ const finalizedNumber = useStateObservable(finalizedNumber$) const selectedBlockNumber = selectedBlock?.number ?? finalizedNumber - if (mortality.type === "Immortal") return
Immortal
+ if (mortality.type === "Immortal") { + return ( +
+
+ Lifetime +
+
Immortal
+
+ ) + } + const parseResult = /^Mortal(\d+)$/.exec(mortality.type) - if (parseResult === null) return null + if (parseResult === null) { + return ( +
+
+ Lifetime +
+
+ {mortality.type} {mortality.value} +
+
+ ) + } + const first = BigInt(parseResult[1]) const second = BigInt(mortality.value) // from polkadot-sdk primitives runtime generic era fn decode @@ -34,13 +57,126 @@ export const MortalityAnalyzer: FC<{ period + phase : null + const deathBlock = birthBlock == null ? null : birthBlock + period + const blocksRemaining = + birthBlock == null || deathBlock == null || selectedBlockNumber == null + ? null + : deathBlock - selectedBlockNumber + const contextProgress = + birthBlock == null || deathBlock == null || selectedBlockNumber == null + ? null + : getTimelineProgress(birthBlock, deathBlock, selectedBlockNumber) return ( -
- period={period} phase={phase}{" "} - {birthBlock != null - ? `currentBlock=${selectedBlockNumber?.toLocaleString()} from=${birthBlock.toLocaleString()} to=${(birthBlock + period).toLocaleString()}` - : null} +
+
+
+
+ Lifetime +
+
+
+ +
+ + +
+ +
+
+
+ Validity Window +
+
+ {blocksRemaining == null + ? null + : `${blocksRemaining.toLocaleString()} blocks remaining`} +
+
+ +
+
+ {contextProgress != null ? ( +
+
+
90 ? "-95%" : "-50%"})`, + }} + > + {selectedBlockNumber?.toLocaleString()} +
+
+ ) : null} +
+ +
+
+
+ Start +
+
+ {formatOptional(birthBlock)} +
+
+
+
+ End +
+
+ {formatOptional(deathBlock)} +
+
+
+
+
) } + +const Metric: FC<{ label: string; value: string }> = ({ label, value }) => ( +
+
+ {label} +
+
+ {value} +
+
+) + +const formatOptional = (value: number | null) => + value == null ? "N/A" : value.toLocaleString() + +const getTimelineProgress = ( + start: number, + end: number, + value: number, +): number => { + if (end <= start) return 0 + const raw = ((value - start) / (end - start)) * 100 + return Math.max(0, Math.min(100, raw)) +} + +export const InlineMortality: FC<{ + mortality: { type: string; value: number } +}> = ({ mortality }) => { + if (mortality.type === "Immortal") return "Immortal" + + const parseResult = /^Mortal(\d+)$/.exec(mortality.type) + if (parseResult === null) return JSON.stringify(mortality, jsonSerialize) + + const first = BigInt(parseResult[1]) + const second = BigInt(mortality.value) + // from polkadot-sdk primitives runtime generic era fn decode + const encoded = first + (second << 8n) + const period = Number(2n << (encoded % (1n << 4n))) + const factor = period >> 12 || 1 + const phase = Number(encoded >> 4n) * factor + + return JSON.stringify({ period, phase }) + " " + JSON.stringify(mortality) +} diff --git a/src/pages/Explorer/Detail/SignedExtensions.tsx b/src/pages/Explorer/Detail/SignedExtensions.tsx index da58892..b574de2 100644 --- a/src/pages/Explorer/Detail/SignedExtensions.tsx +++ b/src/pages/Explorer/Detail/SignedExtensions.tsx @@ -1,73 +1,108 @@ import { ExpandBtn } from "@/components/Expand" import { JsonDisplay } from "@/components/JsonDisplay" import { Dot } from "lucide-react" -import { toHex } from "polkadot-api/utils" -import { ComponentType, FC, useState } from "react" -import { MortalityAnalyzer } from "./MortalityAnalyzer" +import { jsonSerialize } from "polkadot-api/utils" +import { + ComponentType, + FC, + PropsWithChildren, + ReactNode, + useState, +} from "react" +import { InlineMortality, MortalityAnalyzer } from "./MortalityAnalyzer" -export const SignedExtensions: FC<{ extra: Record }> = ({ - extra, -}) => ( +export const SignedExtensions: FC<{ + extra: Record + title?: boolean +}> = ({ extra, title = true }) => (
-

Signed extensions

-
    - {Object.entries(extra).map(([key, value]) => { - const KnownExtension = knownSignedExtensions[key] - return KnownExtension ? ( - - ) : ( - - ) - })} + {title ? ( +

    Signed extensions

    + ) : null} +
      + {Object.entries(extra).map(([key, value]) => ( + + ))}
) const SignedExtension: FC<{ id: string; value: unknown }> = ({ id, value }) => { - const [expanded, setExpanded] = useState(false) - const inlineJson = JSON.stringify(value, (_, v) => - typeof v === "bigint" ? String(v) : v instanceof Uint8Array ? toHex(v) : v, - ) - - if (!inlineJson || inlineJson.length < 40) { - return ( -
  • -
    - - {id} -
    - {inlineJson ? ( -
    - - {inlineJson} -
    - ) : null} -
  • - ) + const knownExtension = knownSignedExtensions[id] + if (knownExtension) { + const { expanded: ExpandedView, inline: InlineView } = knownExtension + return ExpandedView ? ( + : null} + > + + + ) : InlineView ? ( + : null} + /> + ) : null } - return ( -
  • -
    - setExpanded((e) => !e)} /> - {id} -
    - {expanded && } -
  • + + const inlineJson = JSON.stringify(value, jsonSerialize) + return !inlineJson || inlineJson.length < 40 ? ( + + ) : ( + + + ) } export const knownSignedExtensions: Record< string, - ComponentType<{ id: string; value: unknown }> + { + inline?: ComponentType<{ id: string; value: unknown }> + expanded?: ComponentType<{ id: string; value: unknown }> + } > = { - CheckMortality: ({ id, value }) => { - return ( -
  • -
    - - {id} -
    - -
  • - ) + CheckMortality: { + expanded: ({ value }) => , + inline: ({ value }) => , }, } + +const ExpandableLine: FC< + PropsWithChildren<{ id: string; inlineContent?: ReactNode }> +> = ({ id, inlineContent, children }) => { + const [expanded, setExpanded] = useState(false) + + return ( +
  • +
    + setExpanded((e) => !e)} /> + {id} + {inlineContent ? ( +
    + - {inlineContent} +
    + ) : null} +
    + {expanded && children} +
  • + ) +} + +const InlineLine: FC<{ id: string; inlineContent?: ReactNode }> = ({ + id, + inlineContent, +}) => ( +
  • +
    + + {id} +
    + {inlineContent ? ( +
    + - {inlineContent} +
    + ) : null} +
  • +) diff --git a/src/pages/Extrinsics/Analyzer/ExtrinsicAnalyzer.tsx b/src/pages/Extrinsics/Analyzer/ExtrinsicAnalyzer.tsx index 029c571..af1e12e 100644 --- a/src/pages/Extrinsics/Analyzer/ExtrinsicAnalyzer.tsx +++ b/src/pages/Extrinsics/Analyzer/ExtrinsicAnalyzer.tsx @@ -12,27 +12,41 @@ export const ExtrinsicAnalyzer: FC = () => { ) return ( -
    -

    Analyze Extrinsic

    -
    -
    -