add storage subscriptions

This commit is contained in:
Victor Oliva
2024-10-24 15:58:14 +02:00
parent d64d214191
commit 687a2b537d
9 changed files with 221 additions and 31 deletions

View File

@@ -32,7 +32,8 @@
"react18-json-view": "^0.2.8",
"rxjs": "^7.8.1",
"save-as": "^0.1.8",
"tailwind-merge": "^2.5.4"
"tailwind-merge": "^2.5.4",
"uuid": "^10.0.0"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
@@ -40,6 +41,7 @@
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/react-portal": "^4.0.7",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.11.1",

17
pnpm-lock.yaml generated
View File

@@ -73,6 +73,9 @@ importers:
tailwind-merge:
specifier: ^2.5.4
version: 2.5.4
uuid:
specifier: ^10.0.0
version: 10.0.0
devDependencies:
'@eslint/js':
specifier: ^9.11.1
@@ -89,6 +92,9 @@ importers:
'@types/react-portal':
specifier: ^4.0.7
version: 4.0.7
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
'@vitejs/plugin-react':
specifier: ^4.3.2
version: 4.3.3(vite@5.4.9(@types/node@22.7.8))
@@ -1037,6 +1043,9 @@ packages:
'@types/react@18.3.11':
resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==}
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
'@typescript-eslint/eslint-plugin@8.11.0':
resolution: {integrity: sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2276,6 +2285,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@@ -3217,6 +3230,8 @@ snapshots:
'@types/prop-types': 15.7.13
csstype: 3.1.3
'@types/uuid@10.0.0': {}
'@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.13.0(jiti@1.21.6))(typescript@5.6.3)':
dependencies:
'@eslint-community/regexpp': 4.11.1
@@ -4478,6 +4493,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@10.0.0: {}
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0

View File

@@ -0,0 +1,30 @@
import { bytesToString } from "@/components/BinaryInput"
import { Binary } from "@polkadot-api/substrate-bindings"
import { FC } from "react"
import ReactJson from "react18-json-view"
import "react18-json-view/src/dark.css"
import "react18-json-view/src/style.css"
import "./jsonDisplay.css"
export const JsonDisplay: FC<{
src: unknown
}> = ({ src }) => (
<ReactJson
src={src}
dark
theme="a11y"
replacer={(v) => (v instanceof Binary ? bytesToString(v) : v)}
customizeCopy={(v) =>
JSON.stringify(
v,
(_, v) =>
typeof v === "bigint"
? `${v}n`
: v instanceof Binary
? bytesToString(v)
: v,
2,
)
}
/>
)

View File

@@ -0,0 +1,9 @@
.json-view .json-view--copy {
opacity: 0.4;
}
.json-view .json-view--copy:hover {
opacity: 1;
}
.json-view .jv-button {
padding: 0 0.3rem;
}

View File

@@ -1,10 +1,5 @@
import { bytesToString } from "@/components/BinaryInput"
import { Binary } from "@polkadot-api/substrate-bindings"
import { JsonDisplay } from "@/components/JsonDisplay"
import { FC, useMemo } from "react"
import ReactJson from "react18-json-view"
import "react18-json-view/src/style.css"
import "react18-json-view/src/dark.css"
import "./jsonView.css"
export const JsonMode: FC<{
decode: (value: Uint8Array) => unknown
@@ -14,24 +9,7 @@ export const JsonMode: FC<{
return (
<div className="overflow-auto p-2 bg-polkadot-900 text-sm">
<ReactJson
src={src}
dark
theme="a11y"
replacer={(v) => (v instanceof Binary ? bytesToString(v) : v)}
customizeCopy={(v) =>
JSON.stringify(
v,
(_, v) =>
typeof v === "bigint"
? `${v}n`
: v instanceof Binary
? bytesToString(v)
: v,
2,
)
}
/>
<JsonDisplay src={src} />
</div>
)
}

View File

@@ -5,8 +5,9 @@ import { withSubscribe } from "@/components/withSuspense"
import { state, useStateObservable } from "@react-rxjs/core"
import { FC, useEffect, useState } from "react"
import { map } from "rxjs"
import { StorageQuery } from "./StorageQuery"
import { selectedEntry$, setSelectedEntry } from "./storage.state"
import { StorageQuery } from "./StorageQuery"
import { StorageSubscriptions } from "./StorageSubscriptions"
const metadataStorage$ = state(
lookup$.pipe(
@@ -111,6 +112,7 @@ export const Storage = withSubscribe(() => {
)}
</div>
<StorageEntry />
<StorageSubscriptions />
</div>
)
})

View File

@@ -1,6 +1,7 @@
import { lookup$ } from "@/chain.state"
import { lookup$, unsafeApi$ } from "@/chain.state"
import { EditCodec } from "@/codec-components/EditCodec"
import { ActionButton } from "@/components/ActionButton"
import { bytesToString } from "@/components/BinaryInput"
import { CopyText } from "@/components/Copy"
import { BinaryEdit } from "@/components/Icons"
import SliderToggle from "@/components/Toggle"
@@ -14,19 +15,59 @@ import { state, useStateObservable, withDefault } from "@react-rxjs/core"
import { createSignal } from "@react-rxjs/utils"
import { Binary } from "polkadot-api"
import { FC, useState } from "react"
import { combineLatest, filter, map, scan, startWith, switchMap } from "rxjs"
import {
combineLatest,
filter,
firstValueFrom,
from,
map,
scan,
startWith,
switchMap,
} from "rxjs"
import { twMerge } from "tailwind-merge"
import { BinaryEditModal } from "../Extrinsics/BinaryEditModal"
import { selectedEntry$ } from "./storage.state"
import { addStorageSubscription, selectedEntry$ } from "./storage.state"
export const StorageQuery: FC = () => {
const isReady = useStateObservable(isReady$)
const submit = async () => {
const [entry, unsafeApi, keyValues, keysEnabled] = await firstValueFrom(
combineLatest([selectedEntry$, unsafeApi$, keyValues$, keysEnabled$]),
)
const args = keyValues.slice(0, keysEnabled)
const storageEntry = unsafeApi.query[entry!.pallet][entry!.entry]
const single = keyValues.length === keysEnabled
const stream = single
? storageEntry.watchValue(...args)
: from(storageEntry.getEntries(...args))
const stringifyArg = (value: unknown) => {
if (typeof value === "object" && value !== null) {
if (value instanceof Binary) {
return bytesToString(value)
}
return "arg"
}
return JSON.stringify(value)
}
addStorageSubscription({
name: `${entry!.pallet}.${entry!.entry}(${args.map(stringifyArg).join(", ")})`,
single,
stream,
type: entry!.value,
})
}
return (
<div className="p-2 flex flex-col gap-4 items-start w-full">
<StorageKeysInput />
<KeyDisplay />
<ActionButton disabled={!isReady}>Query</ActionButton>
<ActionButton disabled={!isReady} onClick={submit}>
Query
</ActionButton>
</div>
)
}

View File

@@ -0,0 +1,58 @@
import { useStateObservable } from "@react-rxjs/core"
import { FC } from "react"
import {
removeStorageSubscription,
storageSubscription$,
storageSubscriptionKeys$,
} from "./storage.state"
import { JsonDisplay } from "@/components/JsonDisplay"
import { Trash2 } from "lucide-react"
export const StorageSubscriptions: FC = () => {
const keys = useStateObservable(storageSubscriptionKeys$)
if (!keys.length) return null
return (
<div className="p-2 w-full border-t border-slate-400">
<h2 className="text-lg text-polkadot-200 mb-2">Results</h2>
<ul className="flex flex-col gap-2">
{keys.map((key) => (
<StorageSubscriptionBox key={key} subscription={key} />
))}
</ul>
</div>
)
}
const StorageSubscriptionBox: FC<{ subscription: string }> = ({
subscription,
}) => {
const storageSubscription = useStateObservable(
storageSubscription$(subscription),
)
if (!storageSubscription) return null
return (
<li className="border rounded border-polkadot-200 p-2">
<div className="flex justify-between items-center pb-1 overflow-hidden">
<h3 className="text-polkadot-200 overflow-hidden text-ellipsis whitespace-nowrap">
{storageSubscription.name}
</h3>
<button>
<Trash2
size={20}
className="text-polkadot-400 cursor-pointer hover:text-polkadot-500"
onClick={() => removeStorageSubscription(subscription)}
/>
</button>
</div>
{"result" in storageSubscription ? (
<JsonDisplay src={storageSubscription.result} />
) : (
<div className="text-sm text-slate-400">Loading</div>
)}
</li>
)
}

View File

@@ -1,5 +1,12 @@
import { state } from "@react-rxjs/core"
import { createSignal } from "@react-rxjs/utils"
import {
createKeyedSignal,
createSignal,
partitionByKey,
toKeySet,
} from "@react-rxjs/utils"
import { map, Observable, startWith, switchMap, takeUntil } from "rxjs"
import { v4 as uuid } from "uuid"
export type StorageMetadataEntry = {
pallet: string
@@ -11,3 +18,49 @@ export type StorageMetadataEntry = {
export const [entryChange$, setSelectedEntry] =
createSignal<StorageMetadataEntry | null>()
export const selectedEntry$ = state(entryChange$, null)
export const [newStorageSubscription$, addStorageSubscription] = createSignal<{
name: string
type: number
single: boolean
stream: Observable<unknown>
}>()
export const [removeStorageSubscription$, removeStorageSubscription] =
createKeyedSignal<string>()
export type StorageSubscription = {
name: string
type: number
single: boolean
} & ({ result: unknown } | {})
const [getStorageSubscription$, storageSubscriptionKeyChange$] = partitionByKey(
newStorageSubscription$,
() => uuid(),
(src$, id) =>
src$.pipe(
switchMap(
({ stream, ...props }): Observable<StorageSubscription> =>
stream.pipe(
map((result) => ({
...props,
result,
})),
startWith(props),
),
),
takeUntil(removeStorageSubscription$(id)),
),
)
export const storageSubscriptionKeys$ = state(
storageSubscriptionKeyChange$.pipe(
toKeySet(),
map((keys) => [...keys].reverse()),
),
[],
)
export const storageSubscription$ = state(
(key: string) => getStorageSubscription$(key),
null,
)