Skip to content

Commit

Permalink
feat(explorer): dozer integration (#3185)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
Co-authored-by: alvarius <alvarius@lattice.xyz>
  • Loading branch information
3 people authored Sep 25, 2024
1 parent 6c056de commit 2f2e63a
Show file tree
Hide file tree
Showing 53 changed files with 606 additions and 486 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-chefs-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

Exploring worlds on Redstone and Garnet chains will now retrieve data from the hosted SQL indexer.
1 change: 1 addition & 0 deletions packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@latticexyz/common": "workspace:*",
"@latticexyz/config": "workspace:*",
"@latticexyz/protocol-parser": "workspace:*",
"@latticexyz/schema-type": "workspace:*",
"@latticexyz/store": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { RainbowKitProvider, darkTheme } from "@rainbow-me/rainbowkit";
import "@rainbow-me/rainbowkit/styles.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { getDefaultAnvilConnectors } from "../../../../../connectors/anvil";
import { useChain } from "../../../../../hooks/useChain";
import { useChain } from "../../../hooks/useChain";

const queryClient = new QueryClient();

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,48 @@ import { toast } from "sonner";
import { Hex } from "viem";
import { useAccount, useConfig } from "wagmi";
import { ChangeEvent, useState } from "react";
import { encodeField, getFieldIndex } from "@latticexyz/protocol-parser/internal";
import { SchemaAbiType } from "@latticexyz/schema-type/internal";
import { Table } from "@latticexyz/config";
import {
ValueSchema,
encodeField,
getFieldIndex,
getSchemaTypes,
getValueSchema,
} from "@latticexyz/protocol-parser/internal";
import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { waitForTransactionReceipt, writeContract } from "@wagmi/core";
import { Checkbox } from "../../../../../../components/ui/Checkbox";
import { useChain } from "../../../../../../hooks/useChain";
import { camelCase, cn } from "../../../../../../lib/utils";
import { TableConfig } from "../../../../../api/table/route";
import { cn } from "../../../../../../utils";
import { useChain } from "../../../../hooks/useChain";

type Props = {
name: string;
value: string;
keyTuple: string[];
config: TableConfig;
value: string | undefined;
table: Table;
keyTuple: readonly Hex[];
};

export function EditableTableCell({ name, config, keyTuple, value: defaultValue }: Props) {
export function EditableTableCell({ name, table, keyTuple, value: defaultValue }: Props) {
const [value, setValue] = useState<unknown>(defaultValue);
const wagmiConfig = useConfig();
const queryClient = useQueryClient();
const { worldAddress } = useParams();
const { id: chainId } = useChain();
const account = useAccount();

const [value, setValue] = useState<unknown>(defaultValue);

const tableId = config?.table_id;
const fieldType = config?.value_schema[camelCase(name)] as SchemaAbiType;
const valueSchema = getValueSchema(table);
const fieldType = valueSchema[name as never].type;

const { mutate, isPending } = useMutation({
mutationFn: async (newValue: unknown) => {
const fieldIndex = getFieldIndex(config?.value_schema, camelCase(name));
const encodedField = encodeField(fieldType, newValue);
const fieldIndex = getFieldIndex<ValueSchema>(getSchemaTypes(valueSchema), name);
const encodedFieldValue = encodeField(fieldType, newValue);
const txHash = await writeContract(wagmiConfig, {
abi: IBaseWorldAbi,
address: worldAddress as Hex,
functionName: "setField",
args: [tableId, keyTuple, fieldIndex, encodedField],
args: [table.tableId, keyTuple, fieldIndex, encodedFieldValue],
chainId,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { useParams } from "next/navigation";
import { parseAsString, useQueryState } from "nuqs";
import { Hex } from "viem";
import { useEffect } from "react";
import { useChain } from "../../../../hooks/useChain";
import { usePrevious } from "../../../../hooks/usePrevious";
import { useTableDataQuery } from "../../../../queries/useTableDataQuery";
import { useTablesQuery } from "../../../../queries/useTablesQuery";
import { constructTableName } from "../../../../utils/constructTableName";
import { indexerForChainId } from "../../../../utils/indexerForChainId";
import { SQLEditor } from "./SQLEditor";
import { TableSelector } from "./TableSelector";
import { TablesViewer } from "./TablesViewer";

export function Explorer() {
const { worldAddress } = useParams();
const { id: chainId } = useChain();
const indexer = indexerForChainId(chainId);
const [query, setQuery] = useQueryState("query", parseAsString.withDefault(""));
const [selectedTableId] = useQueryState("tableId");
const prevSelectedTableId = usePrevious(selectedTableId);

const { data: tables } = useTablesQuery();
const table = tables?.find(({ tableId }) => tableId === selectedTableId);
const { data: tableData, isLoading, isFetched } = useTableDataQuery({ table, query });

useEffect(() => {
if (table && (!query || prevSelectedTableId !== selectedTableId)) {
const tableName = constructTableName(table, worldAddress as Hex, chainId);

if (indexer.type === "sqlite") {
setQuery(`SELECT * FROM "${tableName}"`);
} else {
setQuery(`SELECT ${Object.keys(table.schema).join(", ")} FROM ${tableName}`);
}
}
}, [chainId, setQuery, selectedTableId, table, worldAddress, prevSelectedTableId, query, indexer.type]);

return (
<>
{indexer.type !== "sqlite" && <SQLEditor />}
<TableSelector tables={tables} />
<TablesViewer table={table} tableData={tableData} isLoading={isLoading || !isFetched} />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { PlayIcon } from "lucide-react";
import { useQueryState } from "nuqs";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { Button } from "../../../../../../components/ui/Button";
import { Form, FormControl, FormField, FormItem } from "../../../../../../components/ui/Form";
import { Input } from "../../../../../../components/ui/Input";

export function SQLEditor() {
const [query, setQuery] = useQueryState("query");
const form = useForm({
defaultValues: {
query: query || "",
},
});

const handleSubmit = form.handleSubmit((data) => {
setQuery(data.query);
});

useEffect(() => {
form.reset({ query: query || "" });
}, [query, form]);

return (
<Form {...form}>
<form onSubmit={handleSubmit}>
<div className="relative">
<FormField
name="query"
render={({ field }) => (
<FormItem>
<FormControl>
<Input {...field} className="pr-[90px]" />
</FormControl>
</FormItem>
)}
/>

<Button className="absolute right-1 top-1 h-8 px-4" type="submit">
<PlayIcon className="mr-1.5 h-3 w-3" /> Run
</Button>
</div>
</form>
</Form>
);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Check, ChevronsUpDown, Lock } from "lucide-react";
import { useParams } from "next/navigation";
import { CheckIcon, ChevronsUpDownIcon, Link2Icon, Link2OffIcon } from "lucide-react";
import { useQueryState } from "nuqs";
import { Hex } from "viem";
import { useEffect, useState } from "react";
import { internalTableNames } from "@latticexyz/store-sync/sqlite";
import { useState } from "react";
import { useEffect } from "react";
import { Table } from "@latticexyz/config";
import { Button } from "../../../../../../components/ui/Button";
import {
Command,
Expand All @@ -14,59 +14,71 @@ import {
CommandList,
} from "../../../../../../components/ui/Command";
import { Popover, PopoverContent, PopoverTrigger } from "../../../../../../components/ui/Popover";
import { cn } from "../../../../../../lib/utils";
import { cn } from "../../../../../../utils";

type Props = {
tables: string[];
};
function TableSelectorItem({ table, selected, asOption }: { table: Table; selected: boolean; asOption?: boolean }) {
const { type, name, namespace } = table;
return (
<div className="flex items-center">
{asOption && <CheckIcon className={cn("mr-2 h-4 w-4", selected ? "opacity-100" : "opacity-0")} />}
{type === "offchainTable" && <Link2OffIcon className="mr-2 inline-block opacity-70" size={14} />}
{type === "table" && <Link2Icon className="mr-2 inline-block opacity-70" size={14} />}
{name} {namespace && <span className="ml-2 opacity-70">({namespace})</span>}
</div>
);
}

export function TableSelector({ tables }: Props) {
export function TableSelector({ tables }: { tables?: Table[] }) {
const [selectedTableId, setTableId] = useQueryState("tableId");
const [open, setOpen] = useState(false);
const { worldAddress } = useParams();
const selectedTableConfig = tables?.find(({ tableId }) => tableId === selectedTableId);

useEffect(() => {
if (!selectedTableId && tables.length > 0) {
setTableId(tables[0] as Hex);
if (!selectedTableId && Array.isArray(tables) && tables.length > 0) {
setTableId(tables[0].tableId);
}
}, [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">
{selectedTableId
? tables.find((tableId) => tableId === selectedTableId)?.replace(`${worldAddress}__`, "")
: "Select table..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
disabled={!tables}
>
{selectedTableConfig && (
<TableSelectorItem
table={selectedTableConfig}
selected={selectedTableId === selectedTableConfig.tableId}
/>
)}
{!selectedTableConfig && <span className="opacity-50">Select table...</span>}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>

<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput placeholder="Search tables..." className="font-mono" />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandEmpty className="py-4 text-center font-mono text-sm">No table found.</CommandEmpty>
<CommandGroup>
{tables.map((tableId) => {
{tables?.map((table) => {
return (
<CommandItem
key={tableId}
value={tableId}
key={table.tableId}
value={table.tableId}
onSelect={(newTableId) => {
setTableId(newTableId as Hex);
setOpen(false);
}}
className="font-mono"
>
<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} />
)}
{tableId.replace(`${worldAddress}__`, "")}
<TableSelectorItem table={table} selected={selectedTableId === table.tableId} asOption />
</CommandItem>
);
})}
Expand Down
Loading

0 comments on commit 2f2e63a

Please sign in to comment.