Files
qapi/src/pages/ViewFns/ViewFnQuery.tsx
T
2025-11-12 17:35:08 -03:00

183 lines
4.8 KiB
TypeScript

import {
dynamicBuilder$,
lookup$,
unsafeApi$,
} from "@/state/chains/chain.state"
import {
InlineLookupTypeEdit,
LookupTypeEdit,
} from "@/codec-components/LookupTypeEdit"
import { ActionButton } from "@/components/ActionButton"
import { ExpandBtn } from "@/components/Expand"
import { getTypeComplexity } from "@/utils/shape"
import { state, useStateObservable, withDefault } from "@react-rxjs/core"
import { createSignal } from "@react-rxjs/utils"
import { Circle, Dot } from "lucide-react"
import { FC, useState } from "react"
import {
combineLatest,
filter,
firstValueFrom,
map,
scan,
startWith,
switchMap,
} from "rxjs"
import { twMerge } from "tailwind-merge"
import { addViewFnCall, selectedEntry$ } from "./viewFns.state"
export const ViewFnQuery: FC = () => {
const selectedEntry = useStateObservable(selectedEntry$)
const isReady = useStateObservable(isReady$)
if (!selectedEntry) return null
const submit = async () => {
const [entry, unsafeApi, inputValues, builder] = await firstValueFrom(
combineLatest([
selectedEntry$,
unsafeApi$,
inputValues$,
dynamicBuilder$,
]),
)
const decodedValues = inputValues.map((v, i) =>
builder
.buildDefinition(selectedEntry.inputs[i].type)
.dec(v as Uint8Array),
)
const promise = unsafeApi.view[entry!.pallet][entry!.name](...decodedValues)
addViewFnCall({
name: `${entry!.pallet}.${entry!.name}(…)`,
promise,
type: entry!.output,
})
}
return (
<div className="flex flex-col gap-4 items-start w-full">
<ViewFnInputValues />
<ActionButton disabled={!isReady} onClick={submit}>
Call
</ActionButton>
</div>
)
}
const [inputValueChange$, setInputValue] = createSignal<{
idx: number
value: Uint8Array | "partial" | null
}>()
const inputValues$ = selectedEntry$.pipeState(
filter((v) => !!v),
map((v) => v.inputs),
switchMap((inputs) => {
const values: Array<Uint8Array | "partial" | null> = inputs.map(() => null)
return inputValueChange$.pipe(
scan((acc, change) => {
const newValue = [...acc]
newValue[change.idx] = change.value
return newValue
}, values),
startWith(values),
)
}),
withDefault([] as Array<Uint8Array | "partial" | null>),
)
const isReady$ = inputValues$.pipeState(
map((inputValues) => inputValues.every((v) => v instanceof Uint8Array)),
withDefault(false),
)
const ViewFnInputValues: FC = () => {
const selectedEntry = useStateObservable(selectedEntry$)
if (!selectedEntry || !selectedEntry.inputs.length) return null
return (
<div className="w-full">
Inputs
<ol className="flex flex-col gap-2">
{selectedEntry.inputs.map((input, idx) => (
<RuntimeValueInput
key={idx}
idx={idx}
name={input.name}
type={input.type}
/>
))}
</ol>
</div>
)
}
const inputValue$ = state(
(idx: number) => inputValues$.pipe(map((v) => v[idx])),
null,
)
const lookupState$ = state(lookup$, null)
const RuntimeValueInput: FC<{ idx: number; name: string; type: number }> = ({
idx,
type,
name,
}) => {
const value = useStateObservable(inputValue$(idx))
const [expanded, setExpanded] = useState(false)
const lookup = useStateObservable(lookupState$)
if (!lookup) return null
const shape = lookup(type)
const complexity = getTypeComplexity(shape)
return (
<li key={idx} className="border rounded p-2 w-full">
<div
className={twMerge(
"flex items-center select-none",
complexity !== "inline" && "cursor-pointer",
)}
onClick={() => setExpanded((e) => !e)}
>
{complexity === "inline" ? (
<Dot size={16} />
) : (
<ExpandBtn expanded={expanded} />
)}
<Circle
size={8}
strokeWidth={4}
className={twMerge(
"mr-1",
value === null
? "text-red-600"
: value === "partial"
? "text-orange-600"
: "text-green-600",
)}
/>
<div className="text-foreground/80">{name}</div>
{complexity === "inline" ? (
<div className="px-2">
<InlineLookupTypeEdit
type={type}
value={value}
onValueChange={(value) => setInputValue({ idx, value })}
/>
</div>
) : null}
</div>
{expanded && complexity !== "inline" && (
<div className="py-2 max-h-[60svh] overflow-hidden flex flex-col justify-stretch">
<LookupTypeEdit
type={type}
value={value}
onValueChange={(value) => setInputValue({ idx, value })}
tree={complexity === "tree"}
/>
</div>
)}
</li>
)
}