// Copyright 2017-2026 @pezkuwi/app-storage authors & contributors // SPDX-License-Identifier: Apache-2.0 import type { QueryableStorageEntry } from '@pezkuwi/api/types'; import type { ComponentRenderer, DefaultProps, RenderFn } from '@pezkuwi/react-api/hoc/types'; import type { ConstValue } from '@pezkuwi/react-components/InputConsts/types'; import type { Option, Raw } from '@pezkuwi/types'; import type { Registry } from '@pezkuwi/types/types'; import type { QueryTypes, StorageModuleQuery } from './types.js'; import React, { useCallback, useMemo } from 'react'; import { withCallDiv } from '@pezkuwi/react-api/hoc'; import { Button, Labelled, styled } from '@pezkuwi/react-components'; import { useApi } from '@pezkuwi/react-hooks'; import valueToText from '@pezkuwi/react-params/valueToText'; import { getSiName } from '@pezkuwi/types/metadata/util'; import { unwrapStorageType } from '@pezkuwi/types/util'; import { compactStripLength, isU8a, u8aToHex, u8aToString } from '@pezkuwi/util'; interface Props { className?: string; onRemove: (id: number) => void; value: QueryTypes; } interface CacheInstance { Component: React.ComponentType; render: RenderFn; refresh: (swallowErrors: boolean) => React.ComponentType; } const cache: CacheInstance[] = []; function keyToName (isConst: boolean, _key: Uint8Array | QueryableStorageEntry<'promise'> | ConstValue): string { if (isConst) { const key = _key as ConstValue; return `const ${key.section}.${key.method}`; } const key = _key as Uint8Array | QueryableStorageEntry<'promise'>; if (isU8a(key)) { const [, u8a] = compactStripLength(key); // If the string starts with `:`, handle it as a pure string return u8a[0] === 0x3a ? u8aToString(u8a) : u8aToHex(u8a); } return `${key.creator.section}.${key.creator.method}`; } function constTypeToString (registry: Registry, { meta }: ConstValue): string { return getSiName(registry.lookup, meta.type); } function queryTypeToString (registry: Registry, { creator: { meta: { modifier, type } } }: QueryableStorageEntry<'promise'>): string { const _type = unwrapStorageType(registry, type); return modifier.isOptional ? `Option<${_type}>` : _type; } function createComponent (type: string, Component: React.ComponentType, defaultProps: DefaultProps, renderHelper: ComponentRenderer): { Component: React.ComponentType; render: (createComponent: RenderFn) => React.ComponentType; refresh: (swallowErrors: boolean) => React.ComponentType } { return { Component, // In order to modify the parameters which are used to render the default component, we can use this method refresh: (): React.ComponentType => renderHelper( (value: unknown) =>
{valueToText(type, value as null)}
, defaultProps ), // In order to replace the default component during runtime we can provide a RenderFn to create a new 'plugged' component render: (createComponent: RenderFn): React.ComponentType => renderHelper(createComponent, defaultProps) }; } function getCachedComponent (registry: Registry, query: QueryTypes): CacheInstance { const { blockHash, id, isConst, key, params = [] } = query as StorageModuleQuery; if (!cache[id]) { let renderHelper; let type: string; if (isConst) { const { method, section } = key as unknown as ConstValue; renderHelper = withCallDiv(`consts.${section}.${method}`, { withIndicator: true }); type = constTypeToString(registry, key as unknown as ConstValue); } else { if (isU8a(key)) { // subscribe to the raw key here renderHelper = withCallDiv('rpc.state.subscribeStorage', { paramName: 'params', paramValid: true, params: [[key]], transform: ([data]: Option[]): Option => data, withIndicator: true }); } else { const values: unknown[] = params.map(({ value }) => value); const { creator: { meta: { type } } } = key; const allCount = type.isPlain ? 0 : type.asMap.hashers.length; const isEntries = values.length !== allCount; renderHelper = withCallDiv('subscribe', { paramName: 'params', paramValid: true, params: isEntries ? [key.entries, ...values] : blockHash // eslint-disable-next-line deprecation/deprecation ? [key.at, blockHash, ...values] : [key, ...values], withIndicator: true }); } type = key.creator?.meta ? queryTypeToString(registry, key) : 'Raw'; } const defaultProps = { className: 'ui--output' }; const Component = renderHelper( // By default we render a simple div node component with the query results in it (value: unknown) =>
{valueToText(type, value as null)}
, defaultProps ); cache[query.id] = createComponent(type, Component, defaultProps, renderHelper); } return cache[id]; } function Query ({ className = '', onRemove, value }: Props): React.ReactElement | null { const { api } = useApi(); const [{ Component }, callName, callType] = useMemo( () => [ getCachedComponent(api.registry, value), keyToName(value.isConst, value.key), value.isConst ? constTypeToString(api.registry, value.key as unknown as ConstValue) : isU8a(value.key) ? 'Raw' : queryTypeToString(api.registry, value.key as QueryableStorageEntry<'promise'>) ], [api, value] ); const _onRemove = useCallback( (): void => { delete cache[value.id]; onRemove(value.id); }, [onRemove, value] ); if (!Component) { return null; } return (
{callName}: {callType}
} >
); } const StyledDiv = styled.div` margin-bottom: 0.25em; label { text-transform: none !important; } .ui.disabled.dropdown.selection { color: #aaa; opacity: 1; } .ui--IdentityIcon { margin: -10px 0; vertical-align: middle; } pre { margin: 0.5; .ui--Param-text { overflow: hidden; text-overflow: ellipsis; } } .storage--actionrow-buttons { margin-top: -0.25rem; /* offset parent spacing for buttons */ } `; export default React.memo(Query);