diff --git a/apps/dave/.storybook/preview.tsx b/apps/dave/.storybook/preview.tsx index 83586414..663e954b 100644 --- a/apps/dave/.storybook/preview.tsx +++ b/apps/dave/.storybook/preview.tsx @@ -7,10 +7,15 @@ import { Providers } from '../src/providers/Providers'; import theme from "../src/providers/theme"; import './global.css'; -// @ts-expect-error JSON.stringify will try to call toJSON on bigints. ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json -BigInt.prototype.toJSON = function () { - return this.toString(); -}; +try { + // @ts-expect-error JSON.stringify will try to call toJSON on bigints. ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#use_within_json + BigInt.prototype.toJSON = function () { + return this.toString(); + }; + +} catch (error: unknown) { + console.info((error as Error).message) +} const withLayout = (StoryFn: StoryFn, context: StoryContext) => { const { title } = context; diff --git a/apps/dave/package.json b/apps/dave/package.json index 1f7ae38f..5571904f 100644 --- a/apps/dave/package.json +++ b/apps/dave/package.json @@ -17,8 +17,8 @@ "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "dependencies": { - "@cartesi/viem": "2.0.0-alpha.24", - "@cartesi/wagmi": "2.0.0-alpha.27", + "@cartesi/viem": "2.0.0-alpha.26", + "@cartesi/wagmi": "2.0.0-alpha.30", "@mantine/core": "^8.3.13", "@mantine/form": "^8.3.13", "@mantine/hooks": "^8.3.13", @@ -31,6 +31,7 @@ "date-fns": "^4.1.0", "humanize-duration": "^3.33.2", "next": "^16.1.5", + "pretty-ms": "^8", "ramda": "^0.32.0", "ramda-adjunct": "^5.1.0", "react": "catalog:", diff --git a/apps/dave/src/components/Address.tsx b/apps/dave/src/components/Address.tsx new file mode 100644 index 00000000..749ca52b --- /dev/null +++ b/apps/dave/src/components/Address.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { + Anchor, + Group, + type GroupProps, + type MantineStyleProp, + Text, + Tooltip, +} from "@mantine/core"; +import Link from "next/link"; +import { type FC } from "react"; +import { jsNumberForAddress } from "react-jazzicon"; +import Jazzicon from "react-jazzicon/dist/Jazzicon"; +import { type Address as AddressType, getAddress } from "viem"; +import CopyButton from "./CopyButton"; + +import RollupContractResolver from "../lib/rollupContractResolver"; +import { shortenHash } from "../lib/textUtils"; + +export interface AddressProps extends GroupProps { + value: AddressType; + href?: string; + hrefTarget?: "_self" | "_blank" | "_top" | "_parent"; + icon?: boolean; + iconSize?: number; + shorten?: boolean; + canCopy?: boolean; +} + +const Address: FC = ({ + href, + value, + icon, + iconSize, + shorten, + hrefTarget = "_self", + canCopy = true, + ...restProps +}) => { + value = getAddress(value); + const name = RollupContractResolver.resolveName(value); + const text = shorten ? shortenHash(value) : value; + const textStyle: MantineStyleProp = { wordBreak: "break-all" }; + + const label = name ? ( + + {name} + + ) : shorten ? ( + + {text} + + ) : ( + {text} + ); + return ( + + + {icon && ( + + )} + + {href ? ( + + {label} + + ) : ( + label + )} + + {canCopy && } + + ); +}; + +export default Address; diff --git a/apps/dave/src/components/CopyButton.tsx b/apps/dave/src/components/CopyButton.tsx new file mode 100644 index 00000000..d6c55122 --- /dev/null +++ b/apps/dave/src/components/CopyButton.tsx @@ -0,0 +1,47 @@ +import { + ActionIcon, + CopyButton as MantineCopyButton, + rem, + Tooltip, +} from "@mantine/core"; +import { type FC } from "react"; +import { TbCheck, TbCopy } from "react-icons/tb"; + +interface CopyButtonProps { + value: string; +} + +const CopyButton: FC = ({ value }) => { + return ( + + {({ copied, copy }) => ( + + + {copied ? ( + + ) : ( + + )} + + + )} + + ); +}; + +export default CopyButton; diff --git a/apps/dave/src/components/PrettyTime.tsx b/apps/dave/src/components/PrettyTime.tsx new file mode 100644 index 00000000..a9db24b7 --- /dev/null +++ b/apps/dave/src/components/PrettyTime.tsx @@ -0,0 +1,49 @@ +import { Button, Text, type TextProps } from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import prettyMillis, { type Options } from "pretty-ms"; +import { Activity, type FC } from "react"; + +interface PrettyTimeProps { + milliseconds: number; + options?: Options; + displayTimestampUTC?: boolean; + size?: TextProps["size"]; +} + +const defaultOpts = { + unitCount: 2, + secondsDecimalDigits: 0, + verbose: true, +}; + +export const PrettyTime: FC = ({ + milliseconds, + options, + displayTimestampUTC = false, + size, +}) => { + const opts: Options = Object.assign({ ...defaultOpts }, options); + const [asTimestamp, handlers] = useDisclosure(false); + const text = asTimestamp + ? new Date(milliseconds).toISOString() + : `${prettyMillis(Date.now() - milliseconds, opts)} ago`; + + return ( + <> + + + + + {text} + + + ); +}; diff --git a/apps/dave/src/components/QueryPagination.tsx b/apps/dave/src/components/QueryPagination.tsx new file mode 100644 index 00000000..44985289 --- /dev/null +++ b/apps/dave/src/components/QueryPagination.tsx @@ -0,0 +1,44 @@ +import type { Pagination as QPagination } from "@cartesi/viem"; +import { Group, Pagination, type GroupProps } from "@mantine/core"; +import type { FC } from "react"; + +const getActivePage = (offset: number, limit: number) => { + const safeLimit = limit === 0 ? 1 : limit; + return offset / safeLimit + 1; +}; + +type QueryPaginationProps = { + pagination: QPagination; + onPaginationChange?: (newOffset: number) => void; + groupProps?: GroupProps; + hideIfSinglePage?: boolean; +}; + +export const QueryPagination: FC = ({ + onPaginationChange, + pagination, + groupProps, + hideIfSinglePage = true, +}) => { + const totalPages = Math.ceil(pagination.totalCount / pagination.limit); + const activePage = getActivePage(pagination.offset, pagination.limit); + const displayPagination = totalPages > 1 && hideIfSinglePage; + + if (!displayPagination) return ""; + + return ( + + { + if (newPageNumber !== activePage) { + onPaginationChange?.( + newPageNumber * pagination.limit - pagination.limit, + ); + } + }} + /> + + ); +}; diff --git a/apps/dave/src/components/TransactionHash.tsx b/apps/dave/src/components/TransactionHash.tsx new file mode 100644 index 00000000..11518546 --- /dev/null +++ b/apps/dave/src/components/TransactionHash.tsx @@ -0,0 +1,33 @@ +import { Flex, Text } from "@mantine/core"; +import { type FC } from "react"; +import { useConfig } from "wagmi"; +import { shortenHash } from "../lib/textUtils"; +import { BlockExplorerLink } from "./BlockExplorerLink"; +import CopyButton from "./CopyButton"; + +interface TransactionHashProps { + transactionHash: string; +} + +const TransactionHash: FC = ({ transactionHash }) => { + const config = useConfig(); + + const Link = BlockExplorerLink({ + value: transactionHash, + type: "tx", + chain: config.chains[0], + }); + + return ( + + {Link === null ? ( + {shortenHash(transactionHash)} + ) : ( + <>{Link} + )}{" "} + + + ); +}; + +export default TransactionHash; diff --git a/apps/dave/src/components/input/InputCard.tsx b/apps/dave/src/components/input/InputCard.tsx index 1cddd6c8..a64a2edd 100644 --- a/apps/dave/src/components/input/InputCard.tsx +++ b/apps/dave/src/components/input/InputCard.tsx @@ -1,21 +1,27 @@ import type { Input, InputStatus } from "@cartesi/viem"; import { Badge, - Button, Card, - Collapse, Group, + ScrollArea, + SegmentedControl, + Select, + Spoiler, Stack, Text, - Textarea, + Tooltip, type MantineColor, } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import { type FC } from "react"; -import { TbEyeMinus, TbEyePlus } from "react-icons/tb"; +import { Activity, useMemo, useState, type FC } from "react"; +import { TbReceipt } from "react-icons/tb"; import useRightColorShade from "../../hooks/useRightColorShade"; -import theme from "../../providers/theme"; -import { LongText } from "../LongText"; +import { getDecoder } from "../../lib/decoders"; +import Address from "../Address"; +import { PrettyTime } from "../PrettyTime"; +import TransactionHash from "../TransactionHash"; +import { OutputContainer } from "../output/OutputContainer"; +import { ReportContainer } from "../report/ReportContainer"; +import { contentDisplayOptions, type DecoderType } from "../types"; interface Props { input: Input; @@ -32,45 +38,126 @@ const getStatusColor = (status: InputStatus): MantineColor => { } }; +type ViewControl = "payload" | "output" | "report"; + +const maxHeight = 450; +const iconSize = 21; // TODO: Define what else will be inside like payload (decoding etc) export const InputCard: FC = ({ input }) => { - const [displayMeta, { toggle: toggleDisplayMeta }] = useDisclosure(false); const statusColor = useRightColorShade(getStatusColor(input.status)); + const [viewControl, setViewControl] = useState("payload"); + const [decoderType, setDecoderType] = useState("raw"); + const decoderFn = useMemo(() => getDecoder(decoderType), [decoderType]); + const millis = Number(input.decodedData.blockTimestamp * 1000n); return ( - + - # {input.index} - {input.status !== "ACCEPTED" && ( - {input.status} - )} +
+ + # {input.index} + + {input.status} + + - + + - + data={[ + { value: "payload", label: "Payload" }, + { value: "output", label: "Output" }, + { value: "report", label: "Report" }, + ]} + /> +