From 6c056de6090a6f4a9633b96513ca1738dc0993c1 Mon Sep 17 00:00:00 2001 From: Karolis Ramanauskas Date: Tue, 24 Sep 2024 13:20:59 +0300 Subject: [PATCH] feat(explorer): move filter state to url (#3225) Co-authored-by: Kevin Ingersoll --- .changeset/sixty-comics-love.md | 5 ++ packages/explorer/package.json | 1 + .../[worldAddress]/explore/DataExplorer.tsx | 8 +-- .../[worldAddress]/explore/TableSelector.tsx | 42 +++++++++------- .../[worldAddress]/explore/TablesViewer.tsx | 50 ++++++++----------- pnpm-lock.yaml | 18 +++++++ 6 files changed, 72 insertions(+), 52 deletions(-) create mode 100644 .changeset/sixty-comics-love.md 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: