Skip to content

Commit

Permalink
feat(explorer): filterable tables selector (#3203)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
  • Loading branch information
karooolis and holic authored Sep 19, 2024
1 parent 512ddc4 commit 7ac2a0d
Show file tree
Hide file tree
Showing 8 changed files with 749 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-ducks-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/explorer": patch
---

Table selector of the Explore tab now has an input for searching/filtering tables by name.
3 changes: 3 additions & 0 deletions packages/explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@
"@latticexyz/store-sync": "workspace:*",
"@latticexyz/world": "workspace:*",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
Expand All @@ -56,6 +58,7 @@
"better-sqlite3": "^8.6.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"debug": "^4.3.4",
"lucide-react": "^0.408.0",
"next": "14.2.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function DataExplorer() {
throwOnError: true,
retry: false,
});
const selectedTable = searchParams.get("table") || (tables?.length > 0 ? tables[0] : null);
const selectedTable = searchParams.get("tableId") || (tables?.length > 0 ? tables[0] : null);

if (isLoading) {
return <Loader className="animate-spin" />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,72 @@
import { Lock } from "lucide-react";
import { Check, ChevronsUpDown, Lock } from "lucide-react";
import { useParams } from "next/navigation";
import { useState } from "react";
import { internalTableNames } from "@latticexyz/store-sync/sqlite";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../../../components/ui/Select";
import { Button } from "../../../../../../components/ui/Button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../../../../../../components/ui/Command";
import { Popover, PopoverContent, PopoverTrigger } from "../../../../../../components/ui/Popover";
import { cn } from "../../../../../../lib/utils";

type Props = {
value: string | undefined;
options: string[];
};

export function TableSelector({ value, options }: Props) {
const [open, setOpen] = useState(false);
const { worldAddress } = useParams();

return (
<div className="py-4">
<Select
value={value}
onValueChange={(value: string) => {
const url = new URL(window.location.href);
const searchParams = new URLSearchParams(url.search);
searchParams.set("table", value);
window.history.pushState({}, "", `${window.location.pathname}?${searchParams}`);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select a table ..." />
</SelectTrigger>
<SelectContent>
{options?.map((option) => {
return (
<SelectItem key={option} value={option} className="font-mono">
{(internalTableNames as string[]).includes(option) && (
<Lock className="mr-2 inline-block opacity-70" size={14} />
)}
{option.replace(`${worldAddress}__`, "")}
</SelectItem>
);
})}
</SelectContent>
</Select>
<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..."}
<ChevronsUpDown 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>
<CommandGroup>
{options.map((option) => {
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}`);

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) && (
<Lock className="mr-2 inline-block opacity-70" size={14} />
)}
{option.replace(`${worldAddress}__`, "")}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
);
}
166 changes: 166 additions & 0 deletions packages/explorer/src/components/ui/Command.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"use client";

import { Search } from "lucide-react";
import { Command as CommandPrimitive } from "cmdk";
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { cn } from "../../lib/utils";
import { Dialog, DialogContent } from "./Dialog";

const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;

interface CommandDialogProps extends DialogProps {}

const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command
className={cn(
"[&_[cmdk-group-heading]]:px-2",
"[&_[cmdk-group-heading]]:font-medium",
"[&_[cmdk-group-heading]]:text-muted-foreground",
"[&_[cmdk-group]]:px-2",
"[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0",
"[&_[cmdk-input-wrapper]_svg]:h-5",
"[&_[cmdk-input-wrapper]_svg]:w-5",
"[&_[cmdk-input]]:h-12",
"[&_[cmdk-item]]:px-2",
"[&_[cmdk-item]]:py-3",
"[&_[cmdk-item]_svg]:h-5",
"[&_[cmdk-item]_svg]:w-5",
)}
>
{children}
</Command>
</DialogContent>
</Dialog>
);
};

const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));

CommandInput.displayName = CommandPrimitive.Input.displayName;

const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));

CommandList.displayName = CommandPrimitive.List.displayName;

const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);

CommandEmpty.displayName = CommandPrimitive.Empty.displayName;

const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden",
"p-1",
"text-foreground",
"[&_[cmdk-group-heading]]:px-2",
"[&_[cmdk-group-heading]]:py-1.5",
"[&_[cmdk-group-heading]]:text-xs",
"[&_[cmdk-group-heading]]:font-medium",
"[&_[cmdk-group-heading]]:text-muted-foreground",

className,
)}
{...props}
/>
));

CommandGroup.displayName = CommandPrimitive.Group.displayName;

const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;

const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex",
"px-2 py-1.5",
"text-sm",
"cursor-default select-none",
"outline-none",
"rounded-sm",
"data-[disabled=true]:pointer-events-none",
"data-[disabled=true]:opacity-50",
"data-[selected='true']:bg-accent",
"data-[selected=true]:text-accent-foreground",

className,
)}
{...props}
/>
));

CommandItem.displayName = CommandPrimitive.Item.displayName;

const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
CommandShortcut.displayName = "CommandShortcut";

export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
Loading

0 comments on commit 7ac2a0d

Please sign in to comment.