Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(explorer): various fixes #3299

Merged
merged 15 commits into from
Oct 17, 2024
11 changes: 11 additions & 0 deletions .changeset/soft-bears-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@latticexyz/explorer": patch
---

- Not found page if invalid chain name.
- Only show selector for worlds if options exist.
- Remove "future time" from transactions table.
- Improved layout for Interact tab.
- Wrap long args in transactions table.
- New tables polling.
- Add logs (regression).
19 changes: 19 additions & 0 deletions packages/explorer/src/app/(explorer)/[chainName]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { notFound } from "next/navigation";
import { isValidChainName } from "../../../common";

type Props = {
params: {
chainName: string;
};
children: React.ReactNode;
};

export default function ChainLayout({ params: { chainName }, children }: Props) {
if (!isValidChainName(chainName)) {
return notFound();
}

return children;
}
4 changes: 2 additions & 2 deletions packages/explorer/src/app/(explorer)/[chainName]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ type Props = {
};
};

export default async function ChainPage({ params }: Props) {
return redirect(`/${params.chainName}/worlds`);
export default async function ChainPage({ params: { chainName } }: Props) {
return redirect(`/${chainName}/worlds`);
Copy link
Member

@holic holic Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on validating the chain name here and only redirect when valid?

this is what causes the /favicon.ico/worlds 404 error

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since chain names are known statically, I wonder if its helpful to use getStaticPaths on these routes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getStaticPaths only works with pages router while we are using app router which is the recommended way now and moving forward.

There is something similar in the app router called generateStaticParams which I think we can use the same way. However, I don't think we should make this page static given that the world addresses which we display in world entry page are dynamic.

Now getServerSideProps inside app router is essentially replaced with async server-side component with a fetcher function inside. So based on that, the validation also is moved to the component itself. What I think would be best to do here is to actually move the validation inside layout.tsx. I already did that for (explorer)/[chainName]/worlds/[worldAddress]/layout.tsx but actually thinking that same could be done for (explorer)/[chainName]/layout.tsx(which doesn't exist yet but could be created for the purpose of validation). This way there's less repeating logic at least.

}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function WorldsForm({ worlds }: { worlds: Address[] }) {

<div className="relative">
<CommandList>
{open ? (
{open && worlds.length > 0 ? (
<div className="absolute top-3 z-10 max-h-[200px] w-full overflow-y-auto rounded-md border bg-popover text-popover-foreground outline-none animate-in">
<CommandGroup>
{worlds?.map((world) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,22 @@ export function Form() {
);

return (
<div className="flex min-h-full">
<div className="w-[320px] flex-shrink-0">
<div className="sticky top-2 pr-4">
<h4 className="py-4 text-xs font-semibold uppercase opacity-70">Jump to:</h4>
<>
<div className="-mr-1g -ml-1 flex gap-x-4 overflow-y-hidden">
<div className="w-[320px] flex-shrink-0 overflow-y-auto border-r pl-1">
<div className="pr-4">
<h4 className="py-4 text-xs font-semibold uppercase opacity-70">Jump to:</h4>
<Input
type="text"
placeholder="Filter functions..."
value={deferredFilterValue}
onChange={(evt) => {
setFilterValue(evt.target.value);
}}
/>
</div>

<Input
type="text"
placeholder="Filter functions..."
value={deferredFilterValue}
onChange={(evt) => {
setFilterValue(evt.target.value);
}}
/>

<ul
className="mt-4 max-h-max space-y-2 overflow-y-auto pb-4"
style={{
maxHeight: "calc(100vh - 160px)",
}}
>
<ul className="mt-4 max-h-max space-y-2 overflow-y-auto pb-4">
{!isFetched &&
Array.from({ length: 10 }).map((_, index) => {
return (
Expand Down Expand Up @@ -77,25 +73,25 @@ export function Form() {
})}
</ul>
</div>
</div>

<div className="min-h-full w-full border-l pl-4">
{!isFetched && (
<>
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
</>
)}
<div className="w-full overflow-y-auto pl-1 pr-1">
{!isFetched && (
<>
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
<Separator className="my-4" />
<Skeleton className="h-[100px]" />
</>
)}

{filteredFunctions?.map((abi) => {
return <FunctionField key={JSON.stringify(abi)} abi={abi as AbiFunction} />;
})}
{filteredFunctions?.map((abi) => {
return <FunctionField key={JSON.stringify(abi)} abi={abi as AbiFunction} />;
})}
</div>
</div>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
"use client";

import { notFound } from "next/navigation";
import { Address } from "viem";
import { isValidChainName } from "../../../../../common";
import { Navigation } from "../../../../../components/Navigation";
import { Providers } from "./Providers";
import { TransactionsWatcher } from "./observe/TransactionsWatcher";

export default function WorldLayout({ children }: { children: React.ReactNode }) {
type Props = {
params: {
chainName: string;
worldAddress: Address;
};
children: React.ReactNode;
};

export default function WorldLayout({ params: { chainName }, children }: Props) {
if (!isValidChainName(chainName)) {
return notFound();
}

return (
<Providers>
<Navigation />
<div className="flex h-screen flex-col">
<Navigation />
{children}
</div>
<TransactionsWatcher />
{children}
</Providers>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
<div className="flex items-start gap-x-4">
<h3 className="w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Inputs</h3>
{Array.isArray(data.functionData?.args) && data.functionData?.args.length > 0 ? (
<div className="flex-grow border border-white/20 p-2">
<div className="min-w-0 flex-grow border border-white/20 p-2">
{data.functionData?.args?.map((arg, idx) => (
<div key={idx} className="flex">
<span className="flex-shrink-0 text-xs text-white/60">arg {idx + 1}:</span>
<span className="ml-2 whitespace-pre-wrap text-xs">
<span className="ml-2 break-all text-xs">
{typeof arg === "object" && arg !== null ? JSON.stringify(arg, null, 2) : String(arg)}
</span>
</div>
Expand All @@ -113,8 +113,8 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
<>
<Separator className="my-5" />
<div className="flex items-start gap-x-4">
<h3 className="inline-block w-[45px] text-2xs font-bold uppercase">Logs</h3>
{Array.isArray(logs) && logs.length > 10 ? (
<h3 className="inline-block w-[45px] flex-shrink-0 text-2xs font-bold uppercase">Logs</h3>
{Array.isArray(logs) && logs.length > 0 ? (
<div className="flex-grow break-all border border-white/20 p-2 pb-3">
<ul>
{logs.map((log, idx) => {
Expand All @@ -128,7 +128,7 @@ export function TransactionTableRow({ row }: { row: Row<ObservedTransaction> })
{Object.entries(args).map(([key, value]) => (
<li key={key} className="mt-1 flex">
<span className="flex-shrink-0 text-xs text-white/60">{key}: </span>
<span className="ml-2 text-xs">{value as never}</span>
<span className="ml-2 break-all text-xs">{value as never}</span>
</li>
))}
</ul>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { redirect } from "next/navigation";
import { supportedChainName } from "../../../../../common";

type Props = {
params: {
chainName: string;
chainName: supportedChainName;
worldAddress: string;
};
};

export default async function WorldPage({ params }: Props) {
const { chainName, worldAddress } = params;
export default async function WorldPage({ params: { chainName, worldAddress } }: Props) {
return redirect(`/${chainName}/worlds/${worldAddress}/explore`);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { Address } from "viem";
import { supportedChains, validateChainName } from "../../../../common";
import { supportedChainName, supportedChains } from "../../../../common";
import { indexerForChainId } from "../../utils/indexerForChainId";
import { WorldsForm } from "./WorldsForm";

Expand All @@ -13,9 +13,7 @@ type ApiResponse = {
}[];
};

async function fetchWorlds(chainName: string): Promise<Address[]> {
validateChainName(chainName);

async function fetchWorlds(chainName: supportedChainName): Promise<Address[]> {
const chain = supportedChains[chainName];
const indexer = indexerForChainId(chain.id);
let worldsApiUrl: string | null = null;
Expand Down Expand Up @@ -49,15 +47,14 @@ async function fetchWorlds(chainName: string): Promise<Address[]> {

type Props = {
params: {
chainName: string;
chainName: supportedChainName;
};
};

export default async function WorldsPage({ params }: Props) {
const worlds = await fetchWorlds(params.chainName);
export default async function WorldsPage({ params: { chainName } }: Props) {
const worlds = await fetchWorlds(chainName);
if (worlds.length === 1) {
return redirect(`/${params.chainName}/worlds/${worlds[0]}`);
return redirect(`/${chainName}/worlds/${worlds[0]}`);
}

return <WorldsForm worlds={worlds} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ export function useTablesQuery() {
})
.sort(({ namespace }) => (internalNamespaces.includes(namespace) ? 1 : -1));
},
refetchInterval: 5000,
});
}
4 changes: 0 additions & 4 deletions packages/explorer/src/app/(explorer)/utils/timeAgo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ export function timeAgo(timestamp: bigint) {
const currentTimestampSeconds = Math.floor(Date.now() / 1000);
const diff = currentTimestampSeconds - Number(timestamp);

if (diff < 0) {
return "in the future";
}

for (const unit of units) {
if (diff >= unit.limit) {
const unitsAgo = Math.floor(diff / unit.inSeconds);
Expand Down
12 changes: 10 additions & 2 deletions packages/explorer/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,22 @@ export const chainIdToName = Object.fromEntries(
Object.entries(supportedChains).map(([chainName, chain]) => [chain.id, chainName]),
) as Record<supportedChainId, supportedChainName>;

export function isValidChainId(chainId: unknown): chainId is supportedChainId {
return typeof chainId === "number" && chainId in chainIdToName;
}

export function isValidChainName(name: unknown): name is supportedChainName {
return typeof name === "string" && name in supportedChains;
}

export function validateChainId(chainId: unknown): asserts chainId is supportedChainId {
if (!(typeof chainId === "number" && chainId in chainIdToName)) {
if (!isValidChainId(chainId)) {
throw new Error(`Invalid chain ID. Supported chains are: ${Object.keys(chainIdToName).join(", ")}.`);
}
}

export function validateChainName(name: unknown): asserts name is supportedChainName {
if (!(typeof name === "string" && name in supportedChains)) {
if (!isValidChainName(name)) {
throw new Error(`Invalid chain name. Supported chains are: ${Object.keys(supportedChains).join(", ")}.`);
}
}
2 changes: 1 addition & 1 deletion packages/explorer/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function NavigationLink({ href, children }: { href: string; children: React.Reac
export function Navigation() {
const { data, isFetched } = useWorldAbiQuery();
return (
<div className="mb-8">
<div className="pb-4">
<div className="flex items-center justify-between">
<div className="-mx-3 flex">
<NavigationLink href="explore">Explore</NavigationLink>
Expand Down
Loading