Skip to content

Commit

Permalink
feat(explorer): move filter state to url (#3225)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <[email protected]>
  • Loading branch information
karooolis and holic authored Sep 24, 2024
1 parent 9d990b5 commit 6c056de
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-comics-love.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -16,24 +14,22 @@ export function DataExplorer() {
if (!response.ok) {
throw new Error(json.error);
}

return json;
},
select: (data) => data.tables.map((table: { name: string }) => table.name),
refetchInterval: 15000,
throwOnError: true,
retry: false,
});
const selectedTable = searchParams.get("tableId") || (tables?.length > 0 ? tables[0] : null);

if (isLoading) {
return <Loader className="animate-spin" />;
}

return (
<>
<TableSelector value={selectedTable} options={tables} />
<TablesViewer table={selectedTable} />
<TableSelector tables={tables} />
<TablesViewer />
</>
);
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 (
<div className="w-full py-4">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open} className="w-full justify-between">
{value ? options.find((option) => option === value)?.replace(`${worldAddress}__`, "") : "Select table..."}
{selectedTableId
? tables.find((tableId) => tableId === selectedTableId)?.replace(`${worldAddress}__`, "")
: "Select table..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
Expand All @@ -39,26 +49,24 @@ export function TableSelector({ value, options }: Props) {
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
{tables.map((tableId) => {
return (
<CommandItem
key={option}
value={option}
onSelect={(currentValue) => {
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"
>
<Check className={cn("mr-2 h-4 w-4", value === option ? "opacity-100" : "opacity-0")} />
{(internalTableNames as string[]).includes(option) && (
<Check
className={cn("mr-2 h-4 w-4", selectedTableId === tableId ? "opacity-100" : "opacity-0")}
/>
{(internalTableNames as string[]).includes(tableId) && (
<Lock className="mr-2 inline-block opacity-70" size={14} />
)}
{option.replace(`${worldAddress}__`, "")}
{tableId.replace(`${worldAddress}__`, "")}
</CommandItem>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
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<SortingState>().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) => {
Expand All @@ -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) => {
Expand All @@ -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) => {
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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();
}

Expand All @@ -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,
},
});
Expand All @@ -171,7 +163,7 @@ export function TablesViewer({ table: selectedTable }: Props) {
<div className="flex items-center justify-between gap-4 pb-4">
<Input
placeholder="Filter all columns..."
value={globalFilter ?? ""}
value={globalFilter}
onChange={(event) => table.setGlobalFilter(event.target.value)}
className="max-w-sm rounded border px-2 py-1"
/>
Expand Down
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6c056de

Please sign in to comment.