diff --git a/.changeset/sixty-comics-love.md b/.changeset/sixty-comics-love.md
new file mode 100644
index 0000000000..ab04a02f26
--- /dev/null
+++ b/.changeset/sixty-comics-love.md
@@ -0,0 +1,5 @@
+---
+"@latticexyz/explorer": patch
+---
+
+Table filters are now included as part of the URL. This enables deep links and improves navigating between pages without losing search state.
diff --git a/packages/explorer/package.json b/packages/explorer/package.json
index 186764a1c6..9582c238d3 100644
--- a/packages/explorer/package.json
+++ b/packages/explorer/package.json
@@ -61,6 +61,7 @@
"debug": "^4.3.4",
"lucide-react": "^0.408.0",
"next": "14.2.5",
+ "nuqs": "^1.19.2",
"query-string": "^9.1.0",
"react": "^18",
"react-dom": "^18",
diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx
index 012ef410b5..f2082085c8 100644
--- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx
+++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/DataExplorer.tsx
@@ -1,13 +1,11 @@
"use client";
import { Loader } from "lucide-react";
-import { useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { TableSelector } from "./TableSelector";
import { TablesViewer } from "./TablesViewer";
export function DataExplorer() {
- const searchParams = useSearchParams();
const { data: tables, isLoading } = useQuery({
queryKey: ["tables"],
queryFn: async () => {
@@ -16,7 +14,6 @@ export function DataExplorer() {
if (!response.ok) {
throw new Error(json.error);
}
-
return json;
},
select: (data) => data.tables.map((table: { name: string }) => table.name),
@@ -24,7 +21,6 @@ export function DataExplorer() {
throwOnError: true,
retry: false,
});
- const selectedTable = searchParams.get("tableId") || (tables?.length > 0 ? tables[0] : null);
if (isLoading) {
return ;
@@ -32,8 +28,8 @@ export function DataExplorer() {
return (
<>
-
-
+
+
>
);
}
diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx
index c5390670c4..c3f0582dd4 100644
--- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx
+++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TableSelector.tsx
@@ -1,6 +1,8 @@
import { Check, ChevronsUpDown, Lock } from "lucide-react";
import { useParams } from "next/navigation";
-import { useState } from "react";
+import { useQueryState } from "nuqs";
+import { Hex } from "viem";
+import { useEffect, useState } from "react";
import { internalTableNames } from "@latticexyz/store-sync/sqlite";
import { Button } from "../../../../../../components/ui/Button";
import {
@@ -15,20 +17,28 @@ import { Popover, PopoverContent, PopoverTrigger } from "../../../../../../compo
import { cn } from "../../../../../../lib/utils";
type Props = {
- value: string | undefined;
- options: string[];
+ tables: string[];
};
-export function TableSelector({ value, options }: Props) {
+export function TableSelector({ tables }: Props) {
+ const [selectedTableId, setTableId] = useQueryState("tableId");
const [open, setOpen] = useState(false);
const { worldAddress } = useParams();
+ useEffect(() => {
+ if (!selectedTableId && tables.length > 0) {
+ setTableId(tables[0] as Hex);
+ }
+ }, [selectedTableId, setTableId, tables]);
+
return (
@@ -39,26 +49,24 @@ export function TableSelector({ value, options }: Props) {
No framework found.
- {options.map((option) => {
+ {tables.map((tableId) => {
return (
{
- const url = new URL(window.location.href);
- const searchParams = new URLSearchParams(url.search);
- searchParams.set("tableId", currentValue);
- window.history.pushState({}, "", `${window.location.pathname}?${searchParams}`);
-
+ key={tableId}
+ value={tableId}
+ onSelect={(newTableId) => {
+ setTableId(newTableId as Hex);
setOpen(false);
}}
className="font-mono"
>
-
- {(internalTableNames as string[]).includes(option) && (
+
+ {(internalTableNames as string[]).includes(tableId) && (
)}
- {option.replace(`${worldAddress}__`, "")}
+ {tableId.replace(`${worldAddress}__`, "")}
);
})}
diff --git a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx
index 3d992be635..bba5a1d79f 100644
--- a/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx
+++ b/packages/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/TablesViewer.tsx
@@ -1,12 +1,10 @@
import { ArrowUpDown, Loader } from "lucide-react";
-import { useState } from "react";
+import { parseAsBoolean, parseAsJson, parseAsString, useQueryState } from "nuqs";
import { internalTableNames } from "@latticexyz/store-sync/sqlite";
import { useQuery } from "@tanstack/react-query";
import {
ColumnDef,
- ColumnFiltersState,
SortingState,
- VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
@@ -21,22 +19,18 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
import { bufferToBigInt } from "../utils/bufferToBigInt";
import { EditableTableCell } from "./EditableTableCell";
-type Props = {
- table: string | undefined;
-};
+const initialSortingState: SortingState = [];
-export function TablesViewer({ table: selectedTable }: Props) {
- const [sorting, setSorting] = useState([]);
- const [columnFilters, setColumnFilters] = useState([]);
- const [columnVisibility, setColumnVisibility] = useState({});
- const [rowSelection, setRowSelection] = useState({});
- const [globalFilter, setGlobalFilter] = useState("");
- const [showAllColumns, setShowAllColumns] = useState(false);
+export function TablesViewer() {
+ const [selectedTableId] = useQueryState("tableId", parseAsString.withDefault(""));
+ const [globalFilter, setGlobalFilter] = useQueryState("filter", parseAsString.withDefault(""));
+ const [showAllColumns, setShowAllColumns] = useQueryState("showAllColumns", parseAsBoolean.withDefault(false));
+ const [sorting, setSorting] = useQueryState("sort", parseAsJson().withDefault(initialSortingState));
const { data: schema } = useQuery({
- queryKey: ["schema", { table: selectedTable }],
+ queryKey: ["schema", { table: selectedTableId }],
queryFn: async () => {
- const response = await fetch(`/api/schema?table=${selectedTable}`);
+ const response = await fetch(`/api/schema?${new URLSearchParams({ table: selectedTableId })}`);
return response.json();
},
select: (data) => {
@@ -47,12 +41,13 @@ export function TablesViewer({ table: selectedTable }: Props) {
return !column.name.startsWith("__");
});
},
+ enabled: Boolean(selectedTableId),
});
const { data: rows } = useQuery({
- queryKey: ["rows", { table: selectedTable }],
+ queryKey: ["rows", { table: selectedTableId }],
queryFn: async () => {
- const response = await fetch(`/api/rows?table=${selectedTable}`);
+ const response = await fetch(`/api/rows?${new URLSearchParams({ table: selectedTableId })}`);
return response.json();
},
select: (data) => {
@@ -67,14 +62,14 @@ export function TablesViewer({ table: selectedTable }: Props) {
);
});
},
- enabled: Boolean(selectedTable),
+ enabled: Boolean(selectedTableId),
refetchInterval: 1000,
});
const { data: mudTableConfig } = useQuery({
- queryKey: ["table", { selectedTable }],
+ queryKey: ["table", { selectedTableId }],
queryFn: async () => {
- const response = await fetch(`/api/table?table=${selectedTable}`);
+ const response = await fetch(`/api/table?${new URLSearchParams({ table: selectedTableId })}`);
return response.json();
},
select: (data) => {
@@ -84,7 +79,7 @@ export function TablesViewer({ table: selectedTable }: Props) {
value_schema: JSON.parse(data.table.value_schema).json,
};
},
- enabled: Boolean(selectedTable),
+ enabled: Boolean(selectedTableId),
});
const columns: ColumnDef<{}>[] = schema?.map(({ name, type }: { name: string; type: string }) => {
@@ -122,7 +117,10 @@ export function TablesViewer({ table: selectedTable }: Props) {
const keysSchema = Object.keys(mudTableConfig?.key_schema || {});
const keyTuple = keysSchema.map((key) => row.getValue(key));
const value = row.getValue(name);
- if ((selectedTable && (internalTableNames as string[]).includes(selectedTable)) || keysSchema.includes(name)) {
+ if (
+ (selectedTableId && (internalTableNames as string[]).includes(selectedTableId)) ||
+ keysSchema.includes(name)
+ ) {
return value?.toString();
}
@@ -140,20 +138,14 @@ export function TablesViewer({ table: selectedTable }: Props) {
},
},
onSortingChange: setSorting,
- onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
- onColumnVisibilityChange: setColumnVisibility,
- onRowSelectionChange: setRowSelection,
onGlobalFilterChange: setGlobalFilter,
globalFilterFn: "includesString",
state: {
sorting,
- columnFilters,
- columnVisibility,
- rowSelection,
globalFilter,
},
});
@@ -171,7 +163,7 @@ export function TablesViewer({ table: selectedTable }: Props) {
table.setGlobalFilter(event.target.value)}
className="max-w-sm rounded border px-2 py-1"
/>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4ad28c4a1d..83790d4fb0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -537,6 +537,9 @@ importers:
next:
specifier: 14.2.5
version: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ nuqs:
+ specifier: ^1.19.2
+ version: 1.19.2(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
query-string:
specifier: ^9.1.0
version: 9.1.0
@@ -8596,6 +8599,9 @@ packages:
typescript:
optional: true
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@@ -8798,6 +8804,11 @@ packages:
nullthrows@1.1.1:
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
+ nuqs@1.19.2:
+ resolution: {integrity: sha512-r6Hw44yMg8tuxr+zS4AmeB+Z++j1pT99DuCfy+XmouCBaXHHhBPOoWZQQuSUI0ja2s00v6o96HdSnvq2/5qOyw==}
+ peerDependencies:
+ next: '>=13.4 <14.0.2 || ^14.0.3'
+
nwsapi@2.2.7:
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
@@ -20875,6 +20886,8 @@ snapshots:
optionalDependencies:
typescript: 5.4.2
+ mitt@3.0.1: {}
+
mkdirp-classic@0.5.3: {}
mkdirp@0.5.6:
@@ -21061,6 +21074,11 @@ snapshots:
nullthrows@1.1.1: {}
+ nuqs@1.19.2(next@14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)):
+ dependencies:
+ mitt: 3.0.1
+ next: 14.2.5(@babel/core@7.25.2)(@opentelemetry/api@1.8.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+
nwsapi@2.2.7: {}
ob1@0.80.12: