add storage subscriptions
This commit is contained in:
@@ -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
17
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
30
src/components/JsonDisplay.tsx
Normal file
30
src/components/JsonDisplay.tsx
Normal 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,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
9
src/components/jsonDisplay.css
Normal file
9
src/components/jsonDisplay.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
58
src/pages/Storage/StorageSubscriptions.tsx
Normal file
58
src/pages/Storage/StorageSubscriptions.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user