Skip to content

Commit e1c2958

Browse files
authored
feat(explorer): abi search and filter (#3773)
1 parent 4e8e67e commit e1c2958

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

‎.changeset/lazy-rules-wink.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/explorer": patch
3+
---
4+
5+
ABI in the "Decode" page can now be searched and filtered.

‎packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/decode/AbiExplorer.tsx‎

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,55 @@
11
"use client";
22

3+
import { XIcon } from "lucide-react";
4+
import { AbiItem } from "viem";
5+
import { useDeferredValue, useMemo, useState } from "react";
36
import JsonView from "react18-json-view";
47
import { CopyButton } from "../../../../../../components/CopyButton";
8+
import { Checkbox } from "../../../../../../components/ui/Checkbox";
9+
import { Input } from "../../../../../../components/ui/Input";
510
import { Skeleton } from "../../../../../../components/ui/Skeleton";
611
import { useWorldAbiQuery } from "../../../../queries/useWorldAbiQuery";
712

13+
const filterTypes = ["function", "event", "error"] as const;
14+
815
export function AbiExplorer() {
916
const { data, isLoading, isError } = useWorldAbiQuery();
17+
const [filterValue, setFilterValue] = useState("");
18+
const [selectedTypes, setSelectedTypes] = useState<Set<AbiItem["type"]>>(new Set(filterTypes));
19+
const deferredFilterValue = useDeferredValue(filterValue);
20+
21+
const hasActiveFilters = filterValue || selectedTypes.size !== filterTypes.length;
22+
23+
const filteredAbi = useMemo(() => {
24+
if (!data?.abi) return [];
25+
26+
const filtered = data.abi.filter((item: AbiItem) => {
27+
if (!selectedTypes.has(item.type)) return false;
28+
29+
if (deferredFilterValue) {
30+
const itemString = JSON.stringify(item).toLowerCase();
31+
return itemString.includes(deferredFilterValue.toLowerCase());
32+
}
33+
return true;
34+
});
35+
36+
return filtered;
37+
}, [data?.abi, deferredFilterValue, selectedTypes]);
38+
39+
const handleTypeToggle = (type: AbiItem["type"]) => {
40+
const newSelectedTypes = new Set(selectedTypes);
41+
if (newSelectedTypes.has(type)) {
42+
newSelectedTypes.delete(type);
43+
} else {
44+
newSelectedTypes.add(type);
45+
}
46+
setSelectedTypes(newSelectedTypes);
47+
};
48+
49+
const handleClearFilters = () => {
50+
setFilterValue("");
51+
setSelectedTypes(new Set(filterTypes));
52+
};
1053

1154
if (isLoading) {
1255
return <Skeleton className="h-[106px] w-full" />;
@@ -17,9 +60,38 @@ export function AbiExplorer() {
1760
return (
1861
<div className="space-y-4">
1962
<h4 className="font-semibold uppercase">World ABI</h4>
63+
64+
<div className="space-y-3">
65+
<Input
66+
type="search"
67+
placeholder="Filter ABI…"
68+
value={filterValue}
69+
onChange={(evt) => setFilterValue(evt.target.value)}
70+
/>
71+
72+
<div className="flex flex-wrap gap-4">
73+
{filterTypes.map((type) => (
74+
<label key={type} className="flex items-center gap-2 text-sm">
75+
<Checkbox checked={selectedTypes.has(type)} onCheckedChange={() => handleTypeToggle(type)} />
76+
<span className="capitalize">{`${type}s`}</span>
77+
</label>
78+
))}
79+
80+
{hasActiveFilters && (
81+
<button
82+
type="button"
83+
onClick={handleClearFilters}
84+
className="flex items-center gap-1 text-sm text-muted-foreground"
85+
>
86+
<XIcon className="h-4 w-4" /> Clear filters
87+
</button>
88+
)}
89+
</div>
90+
</div>
91+
2092
<pre className="text-md relative mb-4 rounded border border-white/20 p-3 text-sm">
21-
<JsonView src={data?.abi} theme="a11y" />
22-
<CopyButton value={JSON.stringify(data?.abi, null, 2)} className="absolute right-1.5 top-1.5" />
93+
<JsonView src={filteredAbi} theme="a11y" />
94+
<CopyButton value={JSON.stringify(filteredAbi, null, 2)} className="absolute right-1.5 top-1.5" />
2395
</pre>
2496
</div>
2597
);

0 commit comments

Comments
 (0)