feat: add block picker to storage
This commit is contained in:
@@ -25,8 +25,8 @@ export const ButtonGroup: FC<{
|
||||
key={value}
|
||||
className={twMerge(
|
||||
"text-secondary-foreground px-3 py-1",
|
||||
"hover:text-polkadot-500",
|
||||
"data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
"hover:text-polkadot-500 font-light",
|
||||
"data-[state=on]:bg-accent data-[state=on]:text-accent-foreground data-[state=on]:font-bold",
|
||||
disabled && "opacity-50 pointer-events-none",
|
||||
)}
|
||||
disabled={disabled}
|
||||
|
||||
+18
-9
@@ -1,24 +1,24 @@
|
||||
import { lookup$, runtimeCtx$ } from "@/state/chains/chain.state"
|
||||
import { ViewCodec } from "@/codec-components/ViewCodec"
|
||||
import { ButtonGroup } from "@/components/ButtonGroup"
|
||||
import { DocsRenderer } from "@/components/DocsRenderer"
|
||||
import { ExpandBtn } from "@/components/Expand"
|
||||
import { LoadingMetadata } from "@/components/Loading"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { withSubscribe } from "@/components/withSuspense"
|
||||
import { CodecComponentType } from "@polkadot-api/react-builder"
|
||||
import { lookup$, runtimeCtx$ } from "@/state/chains/chain.state"
|
||||
import { getTypeComplexity } from "@/utils/shape"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { CodecComponentType } from "@polkadot-api/react-builder"
|
||||
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { Dot } from "lucide-react"
|
||||
import { HexString } from "polkadot-api"
|
||||
import { FC, useState } from "react"
|
||||
import { map } from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { ValueDisplay } from "./Storage/StorageSubscriptions"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
const metadataConstants$ = state(
|
||||
lookup$.pipe(
|
||||
@@ -154,11 +154,14 @@ const ConstantEntry: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const defaultedCtx$ = runtimeCtx$.pipeState(withDefault(null))
|
||||
const ConstantValue: FC<{ type: number; decoded: unknown }> = ({
|
||||
type,
|
||||
decoded,
|
||||
}) => {
|
||||
const [mode, setMode] = useState<"json" | "decoded">("decoded")
|
||||
const ctx = useStateObservable(defaultedCtx$)
|
||||
if (!ctx) return null
|
||||
|
||||
return (
|
||||
<div className="pl-6 py-2 flex flex-col gap-2 items-start overflow-hidden w-full">
|
||||
@@ -177,7 +180,13 @@ const ConstantValue: FC<{ type: number; decoded: unknown }> = ({
|
||||
]}
|
||||
/>
|
||||
<div className="overflow-auto w-full">
|
||||
<ValueDisplay mode={mode} type={type} value={decoded} title="Value" />
|
||||
<ValueDisplay
|
||||
mode={mode}
|
||||
ctx={ctx}
|
||||
type={type}
|
||||
value={decoded}
|
||||
title="Value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -244,7 +244,7 @@ export const inMemoryBlocks$ = state(
|
||||
)
|
||||
inMemoryBlocks$.subscribe()
|
||||
|
||||
const blockHash$ = (hashOrHeight: string) =>
|
||||
export const blockHash$ = (hashOrHeight: string) =>
|
||||
hashOrHeight.length > 63
|
||||
? of(hashOrHeight.startsWith("0x") ? hashOrHeight : `0x${hashOrHeight}`)
|
||||
: client$.pipe(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { PathsRoot } from "@/codec-components/common/paths.state"
|
||||
import { ButtonGroup } from "@/components/ButtonGroup"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import { runtimeCtx$ } from "@/state/chains/chain.state"
|
||||
import { useStateObservable, withDefault } from "@react-rxjs/core"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { FC, useState } from "react"
|
||||
import { ValueDisplay } from "../Storage/StorageSubscriptions"
|
||||
@@ -9,7 +11,6 @@ import {
|
||||
runtimeCallResult$,
|
||||
runtimeCallResultKeys$,
|
||||
} from "./runtimeCalls.state"
|
||||
import { PathsRoot } from "@/codec-components/common/paths.state"
|
||||
|
||||
export const RuntimeCallResults: FC = () => {
|
||||
const keys = useStateObservable(runtimeCallResultKeys$)
|
||||
@@ -71,10 +72,14 @@ const RuntimeCallResultBox: FC<{ subscription: string }> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const defaultedCtx$ = runtimeCtx$.pipeState(withDefault(null))
|
||||
const ResultDisplay: FC<{
|
||||
runtimeCallResult: RuntimeCallResult
|
||||
mode: "json" | "decoded"
|
||||
}> = ({ runtimeCallResult, mode }) => {
|
||||
const ctx = useStateObservable(defaultedCtx$)
|
||||
if (!ctx) return null
|
||||
|
||||
if ("error" in runtimeCallResult) {
|
||||
return (
|
||||
<div className="text-sm">
|
||||
@@ -92,6 +97,7 @@ const ResultDisplay: FC<{
|
||||
<div className="max-h-[60svh] overflow-auto">
|
||||
<ValueDisplay
|
||||
mode={mode}
|
||||
ctx={ctx}
|
||||
type={runtimeCallResult.type}
|
||||
value={runtimeCallResult.result}
|
||||
title={"Result"}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Spinner } from "@/components/Icons"
|
||||
import { runtimeCtx$, runtimeCtxAt$ } from "@/state/chains/chain.state"
|
||||
import { RuntimeContext } from "@polkadot-api/observable-client"
|
||||
import { Input } from "@polkahub/ui-components"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { Check, X } from "lucide-react"
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
filter,
|
||||
map,
|
||||
Observable,
|
||||
startWith,
|
||||
switchMap,
|
||||
} from "rxjs"
|
||||
import { blockHash$ } from "../Explorer/block.state"
|
||||
|
||||
const [valueChange$, setValue] = createSignal<string>()
|
||||
const inputValue$ = state(valueChange$, "Latest")
|
||||
|
||||
const loadedCtx$ = state(
|
||||
inputValue$.pipe(
|
||||
debounceTime(200),
|
||||
switchMap((v) => {
|
||||
const inner$: Observable<{
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
|
||||
hash?: string
|
||||
}> =
|
||||
!v.trim() || v === "Latest"
|
||||
? runtimeCtx$.pipe(map((ctx) => ({ ctx })))
|
||||
: blockHash$(v).pipe(
|
||||
switchMap((hash) =>
|
||||
runtimeCtxAt$(hash).pipe(map((ctx) => ({ ctx, hash }))),
|
||||
),
|
||||
)
|
||||
|
||||
return inner$.pipe(
|
||||
map((value) => ({
|
||||
type: "success" as const,
|
||||
value,
|
||||
})),
|
||||
catchError((value) => [
|
||||
{
|
||||
type: "error" as const,
|
||||
value,
|
||||
},
|
||||
]),
|
||||
startWith({
|
||||
type: "loading" as const,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
{ type: "loading" as const },
|
||||
)
|
||||
|
||||
export const selectedBlock$ = loadedCtx$.pipeState(
|
||||
filter((v) => v.type === "success"),
|
||||
map((v) => v.value),
|
||||
)
|
||||
|
||||
export const BlockPicker = () => {
|
||||
const value = useStateObservable(inputValue$)
|
||||
const loadedCtx = useStateObservable(loadedCtx$)
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
placeholder="Block hash or number"
|
||||
value={value}
|
||||
onChange={(evt) => setValue(evt.target.value)}
|
||||
onFocus={() => {
|
||||
if (value === "Latest") {
|
||||
setValue("")
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!value.trim()) {
|
||||
setValue("Latest")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{loadedCtx.type === "loading" ? (
|
||||
<Spinner className="text-muted-foreground" />
|
||||
) : loadedCtx.type === "error" ? (
|
||||
<X className="text-red-500" />
|
||||
) : (
|
||||
<Check className="text-green-600" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import { Chopsticks } from "@/components/Icons"
|
||||
import { LoadingMetadata } from "@/components/Loading"
|
||||
import { SearchableSelect } from "@/components/Select"
|
||||
import { withSubscribe } from "@/components/withSuspense"
|
||||
import { canSetStorage$, lookup$ } from "@/state/chains/chain.state"
|
||||
import { canSetStorage$ } from "@/state/chains/chain.state"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { FC, useState } from "react"
|
||||
import { map } from "rxjs"
|
||||
import { BlockPicker, selectedBlock$ } from "./BlockPicker"
|
||||
import { partialEntry$, selectedEntry$, selectEntry } from "./storage.state"
|
||||
import { StorageDecode } from "./StorageDecode"
|
||||
import { StorageQuery } from "./StorageQuery"
|
||||
@@ -15,11 +16,11 @@ import { StorageSet } from "./StorageSet"
|
||||
import { StorageSubscriptions } from "./StorageSubscriptions"
|
||||
|
||||
const metadataStorage$ = state(
|
||||
lookup$.pipe(
|
||||
map((lookup) => ({
|
||||
lookup,
|
||||
selectedBlock$.pipe(
|
||||
map(({ ctx }) => ({
|
||||
lookup: ctx.lookup,
|
||||
entries: Object.fromEntries(
|
||||
lookup.metadata.pallets
|
||||
ctx.lookup.metadata.pallets
|
||||
.filter((p) => p.storage)
|
||||
.map((p) => [
|
||||
p.name,
|
||||
@@ -41,6 +42,10 @@ export const Storage = withSubscribe(
|
||||
return (
|
||||
<div className="p-4 pb-0 flex flex-col gap-2 items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<label>
|
||||
Block
|
||||
<BlockPicker />
|
||||
</label>
|
||||
<label>
|
||||
Pallet
|
||||
<SearchableSelect
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { ActionButton } from "@/components/ActionButton"
|
||||
import { dynamicBuilder$, runtimeCtx$ } from "@/state/chains/chain.state"
|
||||
import { NOTIN } from "@polkadot-api/react-builder"
|
||||
import { state, useStateObservable } from "@react-rxjs/core"
|
||||
import { createSignal } from "@react-rxjs/utils"
|
||||
import { FC } from "react"
|
||||
import { combineLatest, firstValueFrom, map, of } from "rxjs"
|
||||
import { selectedBlock$ } from "./BlockPicker"
|
||||
import { addStorageSubscription, selectedEntry$ } from "./storage.state"
|
||||
|
||||
const [valueChange, setValue] = createSignal<string>()
|
||||
const value$ = state(valueChange, "")
|
||||
|
||||
const valueDecoder$ = combineLatest([selectedEntry$, dynamicBuilder$]).pipe(
|
||||
map(([selectedEntry, builder]) =>
|
||||
selectedEntry ? builder.buildDefinition(selectedEntry.value).dec : null,
|
||||
const valueDecoder$ = combineLatest([selectedEntry$, selectedBlock$]).pipe(
|
||||
map(([selectedEntry, { ctx }]) =>
|
||||
selectedEntry
|
||||
? ctx.dynamicBuilder.buildDefinition(selectedEntry.value).dec
|
||||
: null,
|
||||
),
|
||||
)
|
||||
const decodedValue$ = state(
|
||||
@@ -34,8 +36,8 @@ export const StorageDecode: FC = () => {
|
||||
const decoded = useStateObservable(decodedValue$)
|
||||
|
||||
const submit = async () => {
|
||||
const [entry, ctx] = await firstValueFrom(
|
||||
combineLatest([selectedEntry$, runtimeCtx$]),
|
||||
const [entry, { ctx }] = await firstValueFrom(
|
||||
combineLatest([selectedEntry$, selectedBlock$]),
|
||||
)
|
||||
|
||||
addStorageSubscription({
|
||||
@@ -46,6 +48,7 @@ export const StorageDecode: FC = () => {
|
||||
type: entry!.value,
|
||||
payload: decoded,
|
||||
ctx: ctx!,
|
||||
hash: null,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import { CopyText } from "@/components/Copy"
|
||||
import SliderToggle from "@/components/Toggle"
|
||||
import {
|
||||
chainClient$,
|
||||
dynamicBuilder$,
|
||||
runtimeCtx$,
|
||||
runtimeCtxAt$,
|
||||
unsafeApi$,
|
||||
} from "@/state/chains/chain.state"
|
||||
@@ -23,6 +21,7 @@ import { Binary } from "polkadot-api"
|
||||
import { FC } from "react"
|
||||
import {
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
@@ -32,9 +31,11 @@ import {
|
||||
scan,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { selectedBlock$ } from "./BlockPicker"
|
||||
import {
|
||||
addStorageSubscription,
|
||||
selectedEntry$,
|
||||
@@ -49,74 +50,97 @@ export const StorageQuery: FC = () => {
|
||||
if (!selectedEntry) return null
|
||||
|
||||
const submit = async () => {
|
||||
const [entry, unsafeApi, keyValues, keysEnabled] = await firstValueFrom(
|
||||
combineLatest([selectedEntry$, unsafeApi$, keyValues$, keysEnabled$]),
|
||||
)
|
||||
const [entry, unsafeApi, keyValues, keysEnabled, { hash }] =
|
||||
await firstValueFrom(
|
||||
combineLatest([
|
||||
selectedEntry$,
|
||||
unsafeApi$,
|
||||
keyValues$,
|
||||
keysEnabled$,
|
||||
selectedBlock$,
|
||||
]),
|
||||
)
|
||||
const args = keyValues.slice(0, keysEnabled)
|
||||
const storageEntry = unsafeApi.query[entry!.pallet][entry!.entry]
|
||||
const single = keyValues.length === keysEnabled
|
||||
const argString = [...args.map(stringifyArg), ...(single ? [] : ["…"])]
|
||||
|
||||
addStorageSubscription({
|
||||
name: `${entry!.pallet}.${entry!.entry}(${argString})`,
|
||||
args,
|
||||
single,
|
||||
keyCodec: (hash) =>
|
||||
runtimeCtxAt$(hash).pipe(
|
||||
map(
|
||||
(ctx) =>
|
||||
ctx.dynamicBuilder.buildStorage(entry!.pallet, entry!.entry).keys,
|
||||
),
|
||||
),
|
||||
at: (hash) => {
|
||||
const value$ = from(
|
||||
single
|
||||
? storageEntry.getValue(...args, {
|
||||
at: hash,
|
||||
})
|
||||
: storageEntry.getEntries(...args, {
|
||||
at: hash,
|
||||
}),
|
||||
)
|
||||
const hash$ = single
|
||||
? chainClient$.pipe(
|
||||
switchMap(({ chainHead }) =>
|
||||
chainHead.storage$(hash, "hash", (ctx) =>
|
||||
ctx.dynamicBuilder
|
||||
.buildStorage(entry!.pallet, entry!.entry)
|
||||
.keys.enc(...args),
|
||||
),
|
||||
const at = (hash: string) => {
|
||||
const value$ = from(
|
||||
single
|
||||
? storageEntry.getValue(...args, {
|
||||
at: hash,
|
||||
})
|
||||
: storageEntry.getEntries(...args, {
|
||||
at: hash,
|
||||
}),
|
||||
)
|
||||
const hash$ = single
|
||||
? chainClient$.pipe(
|
||||
switchMap(({ chainHead }) =>
|
||||
chainHead.storage$(hash, "hash", (ctx) =>
|
||||
ctx.dynamicBuilder
|
||||
.buildStorage(entry!.pallet, entry!.entry)
|
||||
.keys.enc(...args),
|
||||
),
|
||||
),
|
||||
)
|
||||
: of(null)
|
||||
const ctxType$ = runtimeCtxAt$(hash).pipe(
|
||||
map((ctx) => {
|
||||
const pallet = ctx.lookup.metadata.pallets.find(
|
||||
(p) => p.name === entry!.pallet,
|
||||
)
|
||||
const ctxEntry = pallet?.storage?.items.find(
|
||||
(it) => it.name === entry!.entry,
|
||||
)
|
||||
if (!ctxEntry) {
|
||||
throw new Error(
|
||||
`Storage entry ${entry?.pallet}.${entry?.entry} not found in ${hash}`,
|
||||
)
|
||||
: of(null)
|
||||
const ctxType$ = runtimeCtxAt$(hash).pipe(
|
||||
map((ctx) => {
|
||||
const pallet = ctx.lookup.metadata.pallets.find(
|
||||
(p) => p.name === entry!.pallet,
|
||||
)
|
||||
const ctxEntry = pallet?.storage?.items.find(
|
||||
(it) => it.name === entry!.entry,
|
||||
)
|
||||
if (!ctxEntry) {
|
||||
throw new Error(
|
||||
`Storage entry ${entry?.pallet}.${entry?.entry} not found in ${hash}`,
|
||||
)
|
||||
}
|
||||
const type =
|
||||
ctxEntry.type.tag === "plain"
|
||||
? ctxEntry.type.value
|
||||
: ctxEntry.type.value.value
|
||||
}
|
||||
const type =
|
||||
ctxEntry.type.tag === "plain"
|
||||
? ctxEntry.type.value
|
||||
: ctxEntry.type.value.value
|
||||
|
||||
return { ctx, type }
|
||||
}),
|
||||
)
|
||||
return { ctx, type }
|
||||
}),
|
||||
)
|
||||
|
||||
return combineLatest([
|
||||
combineLatest({ value: value$, hash: hash$ }),
|
||||
ctxType$,
|
||||
]).pipe(map(([a, b]) => ({ ...a, ...b })))
|
||||
},
|
||||
})
|
||||
return combineLatest([
|
||||
combineLatest({ payload: value$, hash: hash$ }),
|
||||
ctxType$,
|
||||
]).pipe(
|
||||
map(([a, b]) => ({ ...a, ...b })),
|
||||
take(1),
|
||||
)
|
||||
}
|
||||
const keyCodec = (hash: string) =>
|
||||
runtimeCtxAt$(hash).pipe(
|
||||
map(
|
||||
(ctx) =>
|
||||
ctx.dynamicBuilder.buildStorage(entry!.pallet, entry!.entry).keys,
|
||||
),
|
||||
)
|
||||
|
||||
if (hash) {
|
||||
addStorageSubscription({
|
||||
name: `${entry!.pallet}.${entry!.entry}(${argString})`,
|
||||
args,
|
||||
single,
|
||||
keyCodec,
|
||||
value: at(hash),
|
||||
})
|
||||
} else {
|
||||
addStorageSubscription({
|
||||
name: `${entry!.pallet}.${entry!.entry}(${argString})`,
|
||||
args,
|
||||
single,
|
||||
keyCodec,
|
||||
at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -134,6 +158,7 @@ const keys$ = selectedEntry$.pipeState(
|
||||
filter((e) => !!e),
|
||||
map((entry) => entry.key),
|
||||
withDefault([] as number[]),
|
||||
distinctUntilChanged((a, b) => a.join(",") === b.join(",")),
|
||||
)
|
||||
|
||||
const hashers$ = selectedEntry$.pipeState(
|
||||
@@ -222,8 +247,8 @@ export const StorageKeysInput: FC<{
|
||||
}
|
||||
|
||||
const builderState$ = state(
|
||||
runtimeCtx$.pipe(
|
||||
map((ctx) => ({
|
||||
selectedBlock$.pipe(
|
||||
map(({ ctx }) => ({
|
||||
...ctx.dynamicBuilder,
|
||||
lookup: ctx.lookup,
|
||||
})),
|
||||
@@ -356,10 +381,13 @@ const StorageKeyInput: FC<{
|
||||
}
|
||||
|
||||
const keyCodec$ = state(
|
||||
combineLatest([dynamicBuilder$, selectedEntry$]).pipe(
|
||||
map(([builder, selectedEntry]) =>
|
||||
combineLatest([selectedBlock$, selectedEntry$]).pipe(
|
||||
map(([{ ctx }, selectedEntry]) =>
|
||||
selectedEntry
|
||||
? builder.buildStorage(selectedEntry.pallet, selectedEntry.entry).keys
|
||||
? ctx.dynamicBuilder.buildStorage(
|
||||
selectedEntry.pallet,
|
||||
selectedEntry.entry,
|
||||
).keys
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -10,24 +10,26 @@ import { RuntimeContext } from "@polkadot-api/observable-client"
|
||||
import { CodecComponentType, NOTIN } from "@polkadot-api/react-builder"
|
||||
import { Button } from "@polkahub/ui-components"
|
||||
import { useStateObservable } from "@react-rxjs/core"
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
PauseCircle,
|
||||
PlayCircle,
|
||||
Trash2,
|
||||
} from "lucide-react"
|
||||
import { ChevronLeft, ChevronRight, StopCircle, Trash2 } from "lucide-react"
|
||||
import { Binary, Enum } from "polkadot-api"
|
||||
import { FC, ReactNode, useMemo, useState } from "react"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
import { BlockState } from "../Explorer/block.state"
|
||||
import { BlockStatusIcon } from "../Explorer/Detail/BlockState"
|
||||
import {
|
||||
KeyCodec,
|
||||
removeStorageSubscription,
|
||||
stopStorageSubscription,
|
||||
storageSubscription$,
|
||||
storageSubscriptionKeys$,
|
||||
StorageSubscriptionValue,
|
||||
toggleSubscriptionPause,
|
||||
} from "./storage.state"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Link } from "@/hashParams"
|
||||
|
||||
export const StorageSubscriptions: FC = () => {
|
||||
const keys = useStateObservable(storageSubscriptionKeys$)
|
||||
@@ -77,10 +79,22 @@ const ValueSubscriptionBox: FC<{ subscription: string }> = ({
|
||||
if (!storageSubscription) return null
|
||||
const status = storageSubscription.status
|
||||
if (status.type !== "value") return null
|
||||
const { ctx, payload, type } = status.value
|
||||
const { ctx, payload, type, hash } = status.value
|
||||
|
||||
return (
|
||||
<SubscriptionBox subscription={subscription}>
|
||||
<SubscriptionBox
|
||||
subscription={subscription}
|
||||
actions={
|
||||
hash ? (
|
||||
<div className="text-center">
|
||||
<p className="text-sm">Hash</p>
|
||||
<Link to={`/explorer/${hash}`} className="underline">
|
||||
<p className="font-mono text-xs">{shortStr(hash, 6)}</p>
|
||||
</Link>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{(mode) => (
|
||||
<ResultDisplay
|
||||
id={subscription}
|
||||
@@ -88,7 +102,7 @@ const ValueSubscriptionBox: FC<{ subscription: string }> = ({
|
||||
result: Enum("success", {
|
||||
ctx,
|
||||
type,
|
||||
value: payload,
|
||||
payload: payload,
|
||||
hash: null,
|
||||
}),
|
||||
}}
|
||||
@@ -126,32 +140,51 @@ const ValuesSubscriptionBox: FC<{ subscription: string }> = ({
|
||||
<SubscriptionBox
|
||||
subscription={subscription}
|
||||
actions={
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
disabled={!hasPrev}
|
||||
onClick={() => setTarget(status.value[targetValueIdx - 1].height)}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<div className="text-xs text-center">
|
||||
<p>{targetValue?.height}</p>
|
||||
<p className="font-mono">
|
||||
{targetValue ? shortStr(targetValue.blockHash, 6) : null}
|
||||
</p>
|
||||
targetValue ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="has-[>svg]:px-1"
|
||||
disabled={!hasPrev}
|
||||
onClick={() => setTarget(status.value[targetValueIdx - 1].height)}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
<div className="text-center">
|
||||
<div className="text-sm flex items-center gap-1 justify-center">
|
||||
<p>{targetValue.height}</p>
|
||||
<BlockStatusIcon
|
||||
size={20}
|
||||
state={
|
||||
targetValue.settled ? BlockState.Finalized : BlockState.Best
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
to={`/explorer/${targetValue.blockHash}`}
|
||||
className="underline"
|
||||
>
|
||||
<p className="font-mono text-xs">
|
||||
{shortStr(targetValue.blockHash, 6)}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
disabled={!hasNext}
|
||||
variant="secondary"
|
||||
className="has-[>svg]:px-1"
|
||||
onClick={() =>
|
||||
setTarget(
|
||||
targetValueIdx === status.value.length - 2
|
||||
? "best"
|
||||
: status.value[targetValueIdx + 1].height,
|
||||
)
|
||||
}
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
disabled={!hasNext}
|
||||
onClick={() =>
|
||||
setTarget(
|
||||
targetValueIdx === status.value.length - 2
|
||||
? "best"
|
||||
: status.value[targetValueIdx + 1].height,
|
||||
)
|
||||
}
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{(mode) =>
|
||||
@@ -207,18 +240,20 @@ const SubscriptionBox: FC<{
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{storageSubscription.completed ? null : (
|
||||
<button onClick={() => toggleSubscriptionPause(subscription)}>
|
||||
{storageSubscription.paused ? (
|
||||
<PlayCircle {...iconButtonProps} />
|
||||
) : (
|
||||
<PauseCircle {...iconButtonProps} />
|
||||
)}
|
||||
{storageSubscription.completed ? (
|
||||
<button onClick={() => removeStorageSubscription(subscription)}>
|
||||
<Trash2 {...iconButtonProps} />
|
||||
</button>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button onClick={() => stopStorageSubscription(subscription)}>
|
||||
<StopCircle {...iconButtonProps} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop subscription</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<button onClick={() => removeStorageSubscription(subscription)}>
|
||||
<Trash2 {...iconButtonProps} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{children(mode)}
|
||||
@@ -249,7 +284,7 @@ const DecodedResultDisplay: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const { value, ctx, type } = subValue.result.value
|
||||
const { payload: value, ctx, type } = subValue.result.value
|
||||
|
||||
if (single) {
|
||||
return (
|
||||
@@ -267,7 +302,7 @@ const DecodedResultDisplay: FC<{
|
||||
)
|
||||
}
|
||||
|
||||
const values = subValue.result.value.value as Array<{
|
||||
const values = subValue.result.value.payload as Array<{
|
||||
keyArgs: unknown[]
|
||||
value: unknown
|
||||
}>
|
||||
@@ -321,7 +356,7 @@ const JsonResultDisplay: FC<{
|
||||
}
|
||||
return (
|
||||
<div className="max-h-[60svh] overflow-auto">
|
||||
<JsonDisplay src={subValue.result.value.value} />
|
||||
<JsonDisplay src={subValue.result.value.payload} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { bytesToString } from "@/components/BinaryInput"
|
||||
import { getHashParams } from "@/hashParams"
|
||||
import {
|
||||
client$,
|
||||
runtimeCtx$,
|
||||
selectedChainChanged$,
|
||||
} from "@/state/chains/chain.state"
|
||||
import { client$, selectedChainChanged$ } from "@/state/chains/chain.state"
|
||||
import {
|
||||
BlockInfo,
|
||||
concatMapEager,
|
||||
@@ -23,7 +19,6 @@ import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
concat,
|
||||
distinctUntilChanged,
|
||||
EMPTY,
|
||||
filter,
|
||||
ignoreElements,
|
||||
@@ -41,6 +36,7 @@ import {
|
||||
withLatestFrom,
|
||||
} from "rxjs"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { selectedBlock$ } from "./BlockPicker"
|
||||
|
||||
export type StorageMetadataEntry = {
|
||||
pallet: string
|
||||
@@ -56,8 +52,8 @@ export const [entryChange$, selectEntry] = createSignal<{
|
||||
entry?: string | null
|
||||
}>()
|
||||
|
||||
const palletEntries$ = runtimeCtx$.pipe(
|
||||
map((ctx) =>
|
||||
const palletEntries$ = selectedBlock$.pipe(
|
||||
map(({ ctx }) =>
|
||||
Object.fromEntries(
|
||||
ctx.lookup.metadata.pallets.map((p) => [p.name, p.storage?.items ?? []]),
|
||||
),
|
||||
@@ -104,9 +100,9 @@ export const partialEntry$ = state(
|
||||
)
|
||||
|
||||
export const selectedEntry$ = state(
|
||||
partialEntry$.pipe(
|
||||
withLatestFrom(palletEntries$, runtimeCtx$),
|
||||
map(([partialEntry, entries, ctx]): StorageMetadataEntry | null => {
|
||||
combineLatest([partialEntry$, selectedBlock$]).pipe(
|
||||
withLatestFrom(palletEntries$),
|
||||
map(([[partialEntry, { ctx }], entries]): StorageMetadataEntry | null => {
|
||||
const entry = partialEntry.pallet
|
||||
? entries[partialEntry.pallet]?.find(
|
||||
(v) => v.name === partialEntry.entry,
|
||||
@@ -176,17 +172,18 @@ export const [newStorageSubscription$, addStorageSubscription] = createSignal<{
|
||||
type: number
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
|
||||
hash: HexString | null
|
||||
value: unknown
|
||||
payload: unknown
|
||||
}>
|
||||
value?: Observable<{
|
||||
type: number
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
|
||||
hash: string | null
|
||||
payload: unknown
|
||||
}>
|
||||
}>()
|
||||
export const [removeStorageSubscription$, removeStorageSubscription] =
|
||||
createKeyedSignal<string>()
|
||||
export const [togglePause$, toggleSubscriptionPause] =
|
||||
export const [stopStorageSubscription$, stopStorageSubscription] =
|
||||
createKeyedSignal<string>()
|
||||
|
||||
export type StorageSubscriptionValue = {
|
||||
@@ -199,7 +196,7 @@ export type StorageSubscriptionValue = {
|
||||
hash: string | null
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
|
||||
type: number
|
||||
value: unknown
|
||||
payload: unknown
|
||||
}
|
||||
error: string
|
||||
}>
|
||||
@@ -208,11 +205,11 @@ export type StorageSubscription = {
|
||||
name: string
|
||||
args: unknown[] | null
|
||||
single: boolean
|
||||
paused: boolean
|
||||
completed: boolean
|
||||
status: Enum<{
|
||||
loading: undefined
|
||||
value: {
|
||||
hash: string | null
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
|
||||
type: number
|
||||
payload: unknown
|
||||
@@ -226,7 +223,7 @@ const getStatus$ = (
|
||||
type: number
|
||||
ctx: Pick<RuntimeContext, "lookup" | "dynamicBuilder">
|
||||
hash: HexString | null
|
||||
value: unknown
|
||||
payload: unknown
|
||||
}>,
|
||||
single: boolean,
|
||||
keyCodec?: (hash: HexString) => Observable<KeyCodec>,
|
||||
@@ -362,10 +359,6 @@ const [getStorageSubscription$, storageSubscriptionKeyChange$] = partitionByKey(
|
||||
keyCodec,
|
||||
...props
|
||||
}): Observable<StorageSubscription> => {
|
||||
const paused$ = togglePause$(id).pipe(
|
||||
scan((v) => !v, false),
|
||||
startWith(false),
|
||||
)
|
||||
const status$: Observable<StorageSubscription["status"]> = value
|
||||
? value.pipe(map((v) => Enum("value", v)))
|
||||
: getStatus$(at!, props.single, keyCodec)
|
||||
@@ -379,16 +372,16 @@ const [getStorageSubscription$, storageSubscriptionKeyChange$] = partitionByKey(
|
||||
status: Enum("loading"),
|
||||
}),
|
||||
shareReplay(1),
|
||||
takeUntil(stopStorageSubscription$(id)),
|
||||
)
|
||||
const completed$ = concat(
|
||||
of(false),
|
||||
result$.pipe(ignoreElements()),
|
||||
of(true),
|
||||
)
|
||||
return combineLatest([paused$, completed$, result$]).pipe(
|
||||
map(([paused, completed, result]) => ({
|
||||
return combineLatest([completed$, result$]).pipe(
|
||||
map(([completed, result]) => ({
|
||||
...result,
|
||||
paused,
|
||||
completed,
|
||||
})),
|
||||
)
|
||||
@@ -409,16 +402,7 @@ export const storage$ = storageSubscriptionKeys$
|
||||
|
||||
export const storageSubscription$ = state(
|
||||
(key: string): Observable<StorageSubscription> =>
|
||||
getStorageSubscription$(key).pipe(
|
||||
// Don't propagate if paused
|
||||
distinctUntilChanged((prev, current) => {
|
||||
// if it's not paused, mark it as "not equal" for the update to go through
|
||||
if (!current.paused) return false
|
||||
// otherwise, don't propagate if we were previously paused
|
||||
// but do propagate if we weren't: Because we still need to show the "paused" status.
|
||||
return prev.paused === current.paused
|
||||
}),
|
||||
),
|
||||
getStorageSubscription$(key),
|
||||
null,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user