diff --git a/services/explorer-ui/src/components/option-buttons/index.tsx b/services/explorer-ui/src/components/option-buttons/index.tsx new file mode 100644 index 000000000..a802f47b4 --- /dev/null +++ b/services/explorer-ui/src/components/option-buttons/index.tsx @@ -0,0 +1,98 @@ +import { CustomTooltip } from "~/components/custom-tooltip"; +import { + Button, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Separator, +} from "~/components/ui"; + +export type OptionItem = { + id: string; + label: string; +}; + +export type OptionItems = OptionItem[]; + +type ExtractOptionId = T[number]["id"]; + +type OptionButtonProps = { + options: T; + availableOptions: Record; // TODO: this should work: Record, boolean> + onOptionSelect: (option: ExtractOptionId) => void; + selectedItem: ExtractOptionId; +}; + +const withAvailableTooltip = ( + isAvailable: boolean, + key: number, + children: React.ReactNode +) => { + // TODO: refactor to use CustomTooltip component instead? + if (!isAvailable) { + return ( + + {children} + + ); + } + + return
{children}
; +}; + +export const OptionButtons = ({ + options, + availableOptions, + onOptionSelect, + selectedItem, +}: OptionButtonProps) => { + return ( + <> +
+ {options.map((option, key) => { + const isAvailable = availableOptions[option.id]; + return withAvailableTooltip( + isAvailable, + key, +
+ + {selectedItem === option.id && ( + + )} +
+ ); + })} +
+
+ +
+ + ); +}; diff --git a/services/explorer-ui/src/pages/block-details/constants.ts b/services/explorer-ui/src/pages/block-details/constants.ts new file mode 100644 index 000000000..9a439fa51 --- /dev/null +++ b/services/explorer-ui/src/pages/block-details/constants.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export type tabId = "txEffects" | "contracts"; + +export const tabIds = ["txEffects", "contracts"] as const; + +export const tabIdSchema = z.enum(tabIds); +export type TabId = z.infer; + +export const tabSchema = z.object({ + id: tabIdSchema, + label: z.string(), +}); +export type Tab = z.infer; + +export const blockDetailsTabs: Tab[] = [ + { id: "txEffects", label: "Transaction effects" }, + { id: "contracts", label: "Contracts" }, +]; diff --git a/services/explorer-ui/src/pages/block-details/index.tsx b/services/explorer-ui/src/pages/block-details/index.tsx index b23459959..ed5d8aa73 100644 --- a/services/explorer-ui/src/pages/block-details/index.tsx +++ b/services/explorer-ui/src/pages/block-details/index.tsx @@ -1,13 +1,14 @@ import { useParams } from "@tanstack/react-router"; -import { type FC } from "react"; +import { useState, type FC } from "react"; import { KeyValueDisplay } from "~/components/info-display/key-value-display"; +import { OptionButtons } from "~/components/option-buttons"; import { TxEffectsTable } from "~/components/tx-effects/tx-effects-table"; -import { Button } from "~/components/ui"; import { useGetBlockByIdentifier, useGetTxEffectsByBlockHeight, useSubTitle, } from "~/hooks"; +import { blockDetailsTabs, type TabId } from "./constants"; import { getBlockDetails, getTxEffects } from "./util"; export const BlockDetails: FC = () => { @@ -15,6 +16,10 @@ export const BlockDetails: FC = () => { from: "/blocks/$blockNumber", }); useSubTitle(`Block ${blockNumber}`); + const [selectedTab, setSelectedTab] = useState("txEffects"); + const onOptionSelect = (value: string) => { + setSelectedTab(value as TabId); + }; const { data: latestBlock, isLoading, @@ -28,7 +33,6 @@ export const BlockDetails: FC = () => { error: txEffectsError, } = useGetTxEffectsByBlockHeight(height); - //TODO: Check for better solution if (!latestBlock) return
No block hash
; return ( @@ -37,19 +41,22 @@ export const BlockDetails: FC = () => {

Block Details

-
+
-
- -
-
+
+ +
+ {selectedTab === "txEffects" && ( { } error={error ?? txEffectsError} /> -
+ )}
diff --git a/services/explorer-ui/src/pages/contract-class-details/index.tsx b/services/explorer-ui/src/pages/contract-class-details/index.tsx index cb5550502..d05768b17 100644 --- a/services/explorer-ui/src/pages/contract-class-details/index.tsx +++ b/services/explorer-ui/src/pages/contract-class-details/index.tsx @@ -3,6 +3,7 @@ import { useState, type FC } from "react"; import { ContractClassesTable } from "~/components/contracts/classes/table"; import { ContractInstancesTable } from "~/components/contracts/instances/table"; import { KeyValueDisplay } from "~/components/info-display/key-value-display"; +import { OptionButtons } from "~/components/option-buttons"; import { useContractClassPrivateFunctions, useContractClassUnconstrainedFunctions, @@ -12,15 +13,14 @@ import { } from "~/hooks"; import { mapContractClasses, mapContractInstances } from "../contract/util"; import { contractClassTabs, type TabId } from "./constants"; -import { OptionButtons } from "./tabs"; import { getContractClassKeyValueData } from "./util"; export const ContractClassDetails: FC = () => { - const [selectedTab, setSelectedTab] = useState("contractVersions"); const { id, version } = useParams({ from: "/contracts/classes/$id/versions/$version", }); useSubTitle(`Ctrct cls ${id}`); + const [selectedTab, setSelectedTab] = useState("contractVersions"); const onOptionSelect = (value: string) => { setSelectedTab(value as TabId); }; @@ -48,11 +48,13 @@ export const ContractClassDetails: FC = () => { privateFunctions: !contractClassPrivateFunctionsHookRes.isLoading && !contractClassPrivateFunctionsHookRes.error && - !!contractClassPrivateFunctionsHookRes.data, + !!contractClassPrivateFunctionsHookRes.data && + !!contractClassPrivateFunctionsHookRes.data.length, unconstrainedFunctions: !contractClassUnconstrainedFunctionsHookRes.isLoading && !contractClassUnconstrainedFunctionsHookRes.error && - !!contractClassUnconstrainedFunctionsHookRes.data, + !!contractClassUnconstrainedFunctionsHookRes.data && + !!contractClassUnconstrainedFunctionsHookRes.data.length, }; if (!id) return
No classId
; @@ -77,8 +79,8 @@ export const ContractClassDetails: FC = () => { diff --git a/services/explorer-ui/src/pages/contract-class-details/tabs.tsx b/services/explorer-ui/src/pages/contract-class-details/tabs.tsx deleted file mode 100644 index 108a4fe61..000000000 --- a/services/explorer-ui/src/pages/contract-class-details/tabs.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Tab, tabId } from "./constants"; -import { - Button, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Separator, -} from "~/components/ui"; -import { CustomTooltip } from "~/components/custom-tooltip"; - -export type OptionButtonProps = { - availableData: Record; - requiredOptions: Tab[]; - onOptionSelect: (option: string) => void; - selectedItem: tabId; -}; - -export const OptionButtons: React.FC = ({ - availableData, - requiredOptions, - onOptionSelect, - selectedItem, -}) => { - // Check if an option is available in the record - const isOptionAvailable = (option: string) => - option in availableData && - availableData[option] !== undefined && - availableData[option] !== null; - - return ( - <> -
- {requiredOptions.map((option, key) => { - const isAvailable = isOptionAvailable(option.id); - - if (!isAvailable) { - return ( - - - - ); - } - return ( -
- - {selectedItem === option.id && ( - - )} -
- ); - })} -
-
- -
- - ); -}; diff --git a/services/explorer-ui/src/pages/tx-effect-details/index.tsx b/services/explorer-ui/src/pages/tx-effect-details/index.tsx index 90a767a0c..b0e9d85de 100644 --- a/services/explorer-ui/src/pages/tx-effect-details/index.tsx +++ b/services/explorer-ui/src/pages/tx-effect-details/index.tsx @@ -1,9 +1,9 @@ import { useParams } from "@tanstack/react-router"; -import { useEffect, useState, type FC } from "react"; +import { useState, type FC } from "react"; import { KeyValueDisplay } from "~/components/info-display/key-value-display"; +import { OptionButtons } from "~/components/option-buttons"; import { useGetTxEffectByHash, useSubTitle } from "~/hooks"; import { txEffectTabs, type TabId } from "./constants"; -import { OptionButtons } from "./tabs"; import { getTxEffectData, mapTxEffectsData } from "./utils"; const naiveDecode = (data: Buffer): string => { @@ -27,7 +27,7 @@ const naiveDecode = (data: Buffer): string => { }; export const TxEffectDetails: FC = () => { - const [selectedTab, setSelectedTab] = useState("unencryptedLogs"); + const [selectedTab, setSelectedTab] = useState("nullifiers"); const { hash } = useParams({ from: "/tx-effects/$hash", }); @@ -35,26 +35,14 @@ export const TxEffectDetails: FC = () => { const { data: txEffects, isLoading, error } = useGetTxEffectByHash(hash); const txEffectData = mapTxEffectsData(txEffects); - useEffect(() => { - // check for the first avalible tab with data - if (txEffects) { - const firstAvailableTab = txEffectTabs.find( - (tab) => tab.id in txEffectData - ); - - if (firstAvailableTab) setSelectedTab(firstAvailableTab.id); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [txEffects]); - - const getSelectedItem = (value: string) => { + const onSelectChange = (value: string) => { setSelectedTab(value as TabId); }; if (!hash)
No txEffect hash
; if (isLoading) return
Loading...
; if (error) return
Error
; - if (!txEffects) return
No data
; + if (!txEffects || !selectedTab) return
No data
; return (
@@ -67,10 +55,9 @@ export const TxEffectDetails: FC = () => {
diff --git a/services/explorer-ui/src/pages/tx-effect-details/tabs.tsx b/services/explorer-ui/src/pages/tx-effect-details/tabs.tsx deleted file mode 100644 index 67afb888e..000000000 --- a/services/explorer-ui/src/pages/tx-effect-details/tabs.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { TxEffectDataType } from "./utils"; -import { Tab, tabId } from "./constants"; -import { - Button, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Separator, -} from "~/components/ui"; -import { CustomTooltip } from "~/components/custom-tooltip"; - -export type OptionButtonProps = { - availableData: Record; - requiredOptions: Tab[]; - onOptionSelect: (option: string) => void; - selectedItem: tabId; -}; - -export const OptionButtons: React.FC = ({ - availableData, - requiredOptions, - onOptionSelect, - selectedItem, -}) => { - // Check if an option is available in the record - const isOptionAvailable = (option: string) => - option in availableData && - availableData[option] !== undefined && - availableData[option] !== null; - - return ( - <> -
- {requiredOptions.map((option, key) => { - const isAvailable = isOptionAvailable(option.id); - - if (!isAvailable) { - return ( - - - - ); - } - return ( -
- - {selectedItem === option.id && ( - - )} -
- ); - })} -
-
- -
- - ); -}; diff --git a/services/explorer-ui/src/pages/tx-effect-details/utils.ts b/services/explorer-ui/src/pages/tx-effect-details/utils.ts index e51c0b09b..29b330d30 100644 --- a/services/explorer-ui/src/pages/tx-effect-details/utils.ts +++ b/services/explorer-ui/src/pages/tx-effect-details/utils.ts @@ -43,26 +43,15 @@ export const getTxEffectData = (data: ChicmozL2TxEffectDeluxe) => [ export const mapTxEffectsData = ( data?: ChicmozL2TxEffectDeluxe -): Record => { - if (!data) return {}; - - const effectsMap: Record = { - privateLogs: data.privateLogs.length ? data.privateLogs : undefined, - unencryptedLogs: data.unencryptedLogs?.functionLogs?.filter( +): Record => { + return { + privateLogs: !!data?.privateLogs?.length, + unencryptedLogs: !!data?.unencryptedLogs?.functionLogs?.filter( (log) => log.logs.length > 0 - ).length - ? data.unencryptedLogs.functionLogs - : undefined, - nullifiers: data.nullifiers?.length ? data.nullifiers : undefined, - noteHashes: data.noteHashes?.length ? data.noteHashes : undefined, - l2ToL1Msgs: data.l2ToL1Msgs?.length ? data.l2ToL1Msgs : undefined, - publicDataWrites: data.publicDataWrites?.length - ? data.publicDataWrites - : undefined, + ).length, + nullifiers: !!data?.nullifiers?.length, + noteHashes: !!data?.noteHashes?.length, + l2ToL1Msgs: !!data?.l2ToL1Msgs?.length, + publicDataWrites: !!data?.publicDataWrites?.length, }; - - // Filter out undefined values - return Object.fromEntries( - Object.entries(effectsMap).filter(([_, value]) => value !== undefined) - ); };