diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a44b69b0..60ffba076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#502](https://github.com/alleslabs/celatone-frontend/pull/502) Display queried time and add json/schema output switch - [#500](https://github.com/alleslabs/celatone-frontend/pull/500) Disable estimated fee when input is invalid - [#498](https://github.com/alleslabs/celatone-frontend/pull/498) Automatically switch to schema tab and show 404 not found error - [#491](https://github.com/alleslabs/celatone-frontend/pull/491) Improve scrolling into view by delaying scroll function diff --git a/src/lib/components/fund/index.tsx b/src/lib/components/fund/index.tsx index a273f6499..b67caa303 100644 --- a/src/lib/components/fund/index.tsx +++ b/src/lib/components/fund/index.tsx @@ -64,13 +64,15 @@ const AttachFundContent = ({ control, setValue }: AttachFundContentProps) => { interface AttachFundProps { control: Control; attachFundsOption: AttachFundsType; + labelBgColor?: string; setValue: UseFormSetValue; } export const AttachFund = ({ control, - setValue, attachFundsOption, + labelBgColor, + setValue, }: AttachFundProps) => ( <> @@ -90,6 +92,7 @@ export const AttachFund = ({ option will be used. } + labelBgColor={labelBgColor} /> diff --git a/src/lib/components/json-schema/MessageInputSwitch.tsx b/src/lib/components/json-schema/MessageInputSwitch.tsx index 56272b86e..86c4a4b24 100644 --- a/src/lib/components/json-schema/MessageInputSwitch.tsx +++ b/src/lib/components/json-schema/MessageInputSwitch.tsx @@ -1,6 +1,6 @@ import { Flex } from "@chakra-ui/react"; -import type { Dispatch, SetStateAction } from "react"; -import { useRef } from "react"; +import { useMemo } from "react"; +import type { CSSProperties, Dispatch, SetStateAction } from "react"; import { Tooltip } from "../Tooltip"; import { MotionBox } from "lib/components/MotionBox"; @@ -11,29 +11,47 @@ export enum MessageTabs { YOUR_SCHEMA = "Your Schema", } +export enum OutputMessageTabs { + JSON_OUTPUT = "JSON Output", + YOUR_SCHEMA = "Your Schema", +} + export const jsonInputFormKey = MessageTabs.JSON_INPUT as "JSON Input"; export const yourSchemaInputFormKey = MessageTabs.YOUR_SCHEMA as "Your Schema"; -interface MessageInputSwitchProps { - currentTab: Option; +interface MessageInputSwitchProps< + T extends Option +> { + currentTab: T; disabled?: boolean; tooltipLabel?: string; - onTabChange: Dispatch>>; + ml?: CSSProperties["marginLeft"]; + isOutput?: boolean; + onTabChange: Dispatch>; } -const tabs = Object.values(MessageTabs); - -export const MessageInputSwitch = ({ +export const MessageInputSwitch = < + T extends Option +>({ currentTab, disabled = false, tooltipLabel = "Select or fill code id first", + ml, + isOutput = false, onTabChange, -}: MessageInputSwitchProps) => { - const tabRefs = useRef<(HTMLDivElement | null)[]>([]); - const activeIndex = currentTab ? tabs.indexOf(currentTab) : -1; +}: MessageInputSwitchProps) => { + const tabs = useMemo( + () => Object.values(isOutput ? OutputMessageTabs : MessageTabs), + [isOutput] + ); + const activeIndex = currentTab ? tabs.indexOf(currentTab) : 0; + + /** + * @todos current implementation of sliding box dimensions and position is hardcoded due to issues with ref, improve this later + */ return ( -
+
- {tabs.map((tab, idx) => ( + {tabs.map((tab) => ( { - tabRefs.current[idx] = el; - }} cursor="pointer" p="2px 10px" + w="96px" fontSize="12px" fontWeight={700} variants={{ @@ -69,13 +85,13 @@ export const MessageInputSwitch = ({ ))} import("lib/components/json/JsonEditor"), { interface JsonReadOnlyProps { topic?: string; + labelBgColor?: string; text: string; canCopy?: boolean; isExpandable?: boolean; @@ -26,6 +27,7 @@ const THRESHOLD_LINES = 16; const JsonReadOnly = ({ topic, + labelBgColor = "background.main", text, canCopy, isExpandable, @@ -80,7 +82,7 @@ const JsonReadOnly = ({ diff --git a/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx b/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx index ed85b1bb2..e52dbb2ca 100644 --- a/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx +++ b/src/lib/pages/execute/components/schema-execute/ExecuteBox.tsx @@ -292,6 +292,7 @@ export const ExecuteBox = ({ control={control} setValue={setValue} attachFundsOption={attachFundsOption} + labelBgColor="gray.900" /> diff --git a/src/lib/pages/query/components/SchemaQuery.tsx b/src/lib/pages/query/components/SchemaQuery.tsx index b933d3044..5ae34bbd4 100644 --- a/src/lib/pages/query/components/SchemaQuery.tsx +++ b/src/lib/pages/query/components/SchemaQuery.tsx @@ -1,251 +1,17 @@ -import { - Accordion, - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, - Alert, - AlertDescription, - Box, - Button, - Flex, - Grid, - GridItem, - Text, -} from "@chakra-ui/react"; -import type { RJSFSchema } from "@rjsf/utils"; -import { useQuery } from "@tanstack/react-query"; -import type { AxiosError } from "axios"; -import { encode } from "js-base64"; -import dynamic from "next/dynamic"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Accordion, Button, Flex } from "@chakra-ui/react"; +import { useEffect, useMemo, useRef, useState } from "react"; -import { - CELATONE_QUERY_KEYS, - useBaseApiRoute, - useCurrentChain, -} from "lib/app-provider"; -import { CopyButton } from "lib/components/copy"; +import { useBaseApiRoute, useCurrentChain } from "lib/app-provider"; import { CustomIcon } from "lib/components/icon"; import InputWithIcon from "lib/components/InputWithIcon"; -import { JsonSchemaForm } from "lib/components/json-schema"; import { EmptyState } from "lib/components/state"; -import { DEFAULT_RPC_ERROR } from "lib/data"; import { useContractStore } from "lib/providers/store"; -import { AmpTrack, AmpEvent, AmpTrackExpandAll } from "lib/services/amplitude"; -import { queryData } from "lib/services/contract"; -import type { Activity } from "lib/stores/contract"; +import { AmpTrackExpandAll } from "lib/services/amplitude"; import type { QueryExecuteSchema, QuerySchema } from "lib/stores/schema"; -import type { ContractAddr, HumanAddr, Option, RpcQueryError } from "lib/types"; -import { - getCurrentDate, - jsonValidate, - parseSchemaInitialData, -} from "lib/utils"; +import type { ContractAddr } from "lib/types"; +import { parseSchemaInitialData } from "lib/utils"; -const CodeSnippet = dynamic(() => import("lib/components/modal/CodeSnippet"), { - ssr: false, -}); - -interface QueryComponentInterface { - msgSchema: QueryExecuteSchema; - resSchema: RJSFSchema; - contractAddress: ContractAddr; - lcdEndpoint: string; - walletAddress: Option; - initialMsg: Record; - addActivity: (activity: Activity) => void; -} - -const QueryComponent = ({ - msgSchema, - resSchema, - contractAddress, - lcdEndpoint, - walletAddress, - initialMsg, - addActivity, -}: QueryComponentInterface) => { - const [msg, setMsg] = useState("{}"); - const [res, setRes] = useState("{}"); - const [queryError, setQueryError] = useState(""); - - useEffect(() => { - if (Object.keys(initialMsg).length) setMsg(JSON.stringify(initialMsg)); - }, [initialMsg]); - - // TODO: Abstract query - const { - refetch, - isFetching: queryFetching, - isRefetching: queryRefetching, - } = useQuery( - [ - CELATONE_QUERY_KEYS.CONTRACT_QUERY, - lcdEndpoint, - contractAddress, - msgSchema.title, - msg, - ], - async () => - queryData( - lcdEndpoint, - contractAddress, - msgSchema.inputRequired - ? msg - : JSON.stringify({ [msgSchema.title ?? ""]: {} }) - ), - { - enabled: false, - retry: false, - cacheTime: 0, - onSuccess(data) { - setQueryError(""); - setRes(JSON.stringify(data.data, null, 2)); - addActivity({ - type: "query", - action: msgSchema.title ?? "Unknown", - sender: walletAddress as HumanAddr, - contractAddress, - msg: encode(msg), - timestamp: getCurrentDate(), - }); - }, - onError(err: AxiosError) { - setQueryError(err.response?.data.message || DEFAULT_RPC_ERROR); - }, - } - ); - - const handleQuery = useCallback(() => { - AmpTrack(AmpEvent.ACTION_QUERY); - refetch(); - }, [refetch]); - - useEffect(() => { - if (!msgSchema.inputRequired) refetch(); - }, [msgSchema.inputRequired, refetch]); - - return ( - -
- - - - {msgSchema.title} - - {msgSchema.description} - - - -
- - - {msgSchema.inputRequired && ( - - setMsg(JSON.stringify(data))} - initialFormData={initialMsg} - /> - - - - - - - )} - - - - Return Output - - - Query response will display here - - - - {queryError && ( - - - {queryError} - - - )} - - - - {!msgSchema.inputRequired && ( - - - - - - - - - )} - - - -
- ); -}; +import { SchemaQueryComponent } from "./SchemaQueryComponent"; interface SchemaQueryProps { schema: QuerySchema; @@ -347,7 +113,7 @@ export const SchemaQuery = ({ sx={{ ".chakra-accordion__icon": { color: "gray.600" } }} > {filteredMsgs.map(([msg, res]) => ( - import("lib/components/modal/CodeSnippet"), { + ssr: false, +}); + +interface SchemaQueryComponentInterface { + msgSchema: QueryExecuteSchema; + resSchema: RJSFSchema; + contractAddress: ContractAddr; + lcdEndpoint: string; + walletAddress: Option; + initialMsg: Record; + addActivity: (activity: Activity) => void; +} + +const TimestampText = memo(({ timestamp }: { timestamp: Option }) => { + const [, setRenderCount] = useState(0); + let text = "Query response will display here"; + if (timestamp) + text = `Last Queried at ${formatUTC(timestamp)} (${dateFromNow( + timestamp + )})`; + + useEffect(() => { + const interval = setInterval(() => { + setRenderCount((prev) => prev + 1); + }, 60 * 1000); + return () => clearInterval(interval); + }, []); + + return ( + + {text} + + ); +}); + +interface ReturnWidgetsProps { + timestamp: Option; + inputRequired: Option; + res: string; + resTab: Option; + queryError: string; + setResTab: Dispatch>>; +} + +const ReturnWidgets = ({ + timestamp, + inputRequired, + res, + resTab, + queryError, + setResTab, +}: ReturnWidgetsProps) => { + return !inputRequired ? ( + + + Return Output + + + + + ) : ( + + + + Return Output + + + + + + + + + ); +}; + +export const SchemaQueryComponent = ({ + msgSchema, + resSchema, + contractAddress, + lcdEndpoint, + walletAddress, + initialMsg, + addActivity, +}: SchemaQueryComponentInterface) => { + const [resTab, setResTab] = useState(); + const [msg, setMsg] = useState("{}"); + const [res, setRes] = useState("{}"); + const [queryError, setQueryError] = useState(""); + const [timestamp, setTimestamp] = useState(); + + useEffect(() => { + if (Object.keys(initialMsg).length) setMsg(JSON.stringify(initialMsg)); + }, [initialMsg]); + + useEffect(() => { + if (msgSchema.inputRequired) setResTab(OutputMessageTabs.YOUR_SCHEMA); + else setResTab(OutputMessageTabs.JSON_OUTPUT); + }, [msgSchema.inputRequired, setResTab]); + + // TODO: Abstract query + const { + refetch, + isFetching: queryFetching, + isRefetching: queryRefetching, + } = useQuery( + [ + CELATONE_QUERY_KEYS.CONTRACT_QUERY, + lcdEndpoint, + contractAddress, + msgSchema.title, + msg, + ], + async () => + queryData( + lcdEndpoint, + contractAddress, + msgSchema.inputRequired + ? msg + : JSON.stringify({ [msgSchema.title ?? ""]: {} }) + ), + { + enabled: false, + retry: false, + cacheTime: 0, + onSuccess(data) { + const currentDate = getCurrentDate(); + setQueryError(""); + setRes(JSON.stringify(data.data, null, 2)); + setTimestamp(currentDate); + addActivity({ + type: "query", + action: msgSchema.title ?? "Unknown", + sender: walletAddress as HumanAddr, + contractAddress, + msg: encode(msg), + timestamp: currentDate, + }); + }, + onError(err: AxiosError) { + setQueryError(err.response?.data.message || DEFAULT_RPC_ERROR); + setTimestamp(undefined); + setRes("{}"); + }, + } + ); + + const handleQuery = useCallback(() => { + AmpTrack(AmpEvent.ACTION_QUERY); + refetch(); + }, [refetch]); + + useEffect(() => { + if (!msgSchema.inputRequired) refetch(); + }, [msgSchema.inputRequired, refetch]); + + return ( + +
+ + + + {msgSchema.title} + + {msgSchema.description} + + + +
+ + + {msgSchema.inputRequired && ( + + setMsg(JSON.stringify(data))} + initialFormData={initialMsg} + /> + + + + + + + )} + + + {queryError && ( + + + {queryError} + + + )} + {resTab === OutputMessageTabs.JSON_OUTPUT ? ( + + ) : ( + + + + )} + {!msgSchema.inputRequired && ( + + + + + + + + + )} + + + +
+ ); +};