-
Notifications
You must be signed in to change notification settings - Fork 193
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(explorer): export table data (#3380)
- Loading branch information
Showing
7 changed files
with
486 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@latticexyz/explorer": patch | ||
--- | ||
|
||
Added support for exporting table data in CSV, JSON, and TXT formats. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
...es/explorer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/ExportButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { DownloadIcon } from "lucide-react"; | ||
import { Button } from "../../../../../../components/ui/Button"; | ||
import { | ||
DropdownMenu, | ||
DropdownMenuContent, | ||
DropdownMenuItem, | ||
DropdownMenuTrigger, | ||
} from "../../../../../../components/ui/DropdownMenu"; | ||
import { TData } from "../../../../queries/useTableDataQuery"; | ||
import { exportTableData } from "./utils/exportTableData"; | ||
|
||
export function ExportButton({ tableData, isLoading }: { tableData?: TData; isLoading: boolean }) { | ||
return ( | ||
<DropdownMenu> | ||
<DropdownMenuTrigger asChild> | ||
<Button variant="outline" disabled={!tableData || isLoading}> | ||
<DownloadIcon className="mr-2 h-4 w-4" /> | ||
Export | ||
</Button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent> | ||
<DropdownMenuItem | ||
onClick={() => { | ||
const csv = tableData?.rows.map((row) => tableData.columns.map((col) => row[col]).join(",")).join("\n"); | ||
const header = tableData?.columns.join(",") + "\n"; | ||
exportTableData(header + csv, "data.csv", "text/csv"); | ||
}} | ||
> | ||
CSV | ||
</DropdownMenuItem> | ||
<DropdownMenuItem | ||
onClick={() => { | ||
const json = JSON.stringify(tableData?.rows, null, 2); | ||
exportTableData(json, "data.json", "application/json"); | ||
}} | ||
> | ||
JSON | ||
</DropdownMenuItem> | ||
<DropdownMenuItem | ||
onClick={() => { | ||
const txt = tableData?.rows.map((row) => tableData.columns.map((col) => row[col]).join("\t")).join("\n"); | ||
const header = tableData?.columns.join("\t") + "\n"; | ||
exportTableData(header + txt, "data.txt", "text/plain"); | ||
}} | ||
> | ||
TXT | ||
</DropdownMenuItem> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
...rer/src/app/(explorer)/[chainName]/worlds/[worldAddress]/explore/utils/exportTableData.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
export function exportTableData(content: string, filename: string, contentType: string) { | ||
const blob = new Blob([content], { type: contentType }); | ||
|
||
console.log("filename", filename); | ||
|
||
const link = document.createElement("a"); | ||
if (link.download === undefined) { | ||
console.warn("Browser does not support HTML5 download attribute"); | ||
return; | ||
} | ||
|
||
const url = URL.createObjectURL(blob); | ||
link.setAttribute("href", url); | ||
link.setAttribute("download", filename); | ||
link.style.visibility = "hidden"; | ||
document.body.appendChild(link); | ||
link.click(); | ||
document.body.removeChild(link); | ||
URL.revokeObjectURL(url); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
"use client"; | ||
|
||
import { Check, ChevronRight, Circle } from "lucide-react"; | ||
import * as React from "react"; | ||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; | ||
import { cn } from "../../utils"; | ||
|
||
const DropdownMenu = DropdownMenuPrimitive.Root; | ||
|
||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; | ||
|
||
const DropdownMenuGroup = DropdownMenuPrimitive.Group; | ||
|
||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal; | ||
|
||
const DropdownMenuSub = DropdownMenuPrimitive.Sub; | ||
|
||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; | ||
|
||
const DropdownMenuSubTrigger = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { | ||
inset?: boolean; | ||
} | ||
>(({ className, inset, children, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.SubTrigger | ||
ref={ref} | ||
className={cn( | ||
"flex items-center gap-2", | ||
"px-2 py-1.5", | ||
"text-sm", | ||
"cursor-default select-none", | ||
"rounded-sm", | ||
"outline-none focus:bg-accent data-[state=open]:bg-accent", | ||
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", | ||
inset && "pl-8", | ||
className, | ||
)} | ||
{...props} | ||
> | ||
{children} | ||
<ChevronRight className="ml-auto" /> | ||
</DropdownMenuPrimitive.SubTrigger> | ||
)); | ||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; | ||
|
||
const DropdownMenuSubContent = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> | ||
>(({ className, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.SubContent | ||
ref={ref} | ||
className={cn( | ||
"z-50 min-w-[8rem]", | ||
"rounded-md border bg-popover p-1 text-popover-foreground shadow-lg", | ||
"overflow-hidden", | ||
"data-[state=open]:animate-in data-[state=closed]:animate-out", | ||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", | ||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", | ||
"data-[side=bottom]:slide-in-from-top-2", | ||
"data-[side=left]:slide-in-from-right-2", | ||
"data-[side=right]:slide-in-from-left-2", | ||
"data-[side=top]:slide-in-from-bottom-2", | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; | ||
|
||
const DropdownMenuContent = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> | ||
>(({ className, sideOffset = 4, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.Portal> | ||
<DropdownMenuPrimitive.Content | ||
ref={ref} | ||
sideOffset={sideOffset} | ||
className={cn( | ||
"z-50 min-w-[8rem]", | ||
"rounded-md border bg-popover p-1 text-popover-foreground shadow-md", | ||
"overflow-hidden", | ||
"data-[state=open]:animate-in data-[state=closed]:animate-out", | ||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", | ||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", | ||
"data-[side=bottom]:slide-in-from-top-2", | ||
"data-[side=left]:slide-in-from-right-2", | ||
"data-[side=right]:slide-in-from-left-2", | ||
"data-[side=top]:slide-in-from-bottom-2", | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
</DropdownMenuPrimitive.Portal> | ||
)); | ||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; | ||
|
||
const DropdownMenuItem = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.Item>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { | ||
inset?: boolean; | ||
} | ||
>(({ className, inset, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.Item | ||
ref={ref} | ||
className={cn( | ||
"relative flex items-center gap-2", | ||
"px-2 py-1.5", | ||
"text-sm", | ||
"cursor-default select-none", | ||
"outline-none", | ||
"transition-colors", | ||
"focus:bg-accent focus:text-accent-foreground", | ||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||
"rounded-sm", | ||
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", | ||
inset && "pl-8", | ||
className, | ||
)} | ||
{...props} | ||
/> | ||
)); | ||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; | ||
|
||
const DropdownMenuCheckboxItem = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> | ||
>(({ className, children, checked, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.CheckboxItem | ||
ref={ref} | ||
className={cn( | ||
"relative flex items-center", | ||
"py-1.5 pl-8 pr-2", | ||
"text-sm", | ||
"cursor-default select-none", | ||
"outline-none", | ||
"transition-colors", | ||
"focus:bg-accent focus:text-accent-foreground", | ||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||
"rounded-sm", | ||
className, | ||
)} | ||
checked={checked} | ||
{...props} | ||
> | ||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||
<DropdownMenuPrimitive.ItemIndicator> | ||
<Check className="h-4 w-4" /> | ||
</DropdownMenuPrimitive.ItemIndicator> | ||
</span> | ||
{children} | ||
</DropdownMenuPrimitive.CheckboxItem> | ||
)); | ||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; | ||
|
||
const DropdownMenuRadioItem = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> | ||
>(({ className, children, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.RadioItem | ||
ref={ref} | ||
className={cn( | ||
"relative flex items-center", | ||
"py-1.5 pl-8 pr-2", | ||
"text-sm", | ||
"cursor-default select-none", | ||
"outline-none", | ||
"transition-colors", | ||
"focus:bg-accent focus:text-accent-foreground", | ||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50", | ||
"rounded-sm", | ||
className, | ||
)} | ||
{...props} | ||
> | ||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> | ||
<DropdownMenuPrimitive.ItemIndicator> | ||
<Circle className="h-2 w-2 fill-current" /> | ||
</DropdownMenuPrimitive.ItemIndicator> | ||
</span> | ||
{children} | ||
</DropdownMenuPrimitive.RadioItem> | ||
)); | ||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; | ||
|
||
const DropdownMenuLabel = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.Label>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { | ||
inset?: boolean; | ||
} | ||
>(({ className, inset, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.Label | ||
ref={ref} | ||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} | ||
{...props} | ||
/> | ||
)); | ||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; | ||
|
||
const DropdownMenuSeparator = React.forwardRef< | ||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, | ||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> | ||
>(({ className, ...props }, ref) => ( | ||
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> | ||
)); | ||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; | ||
|
||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { | ||
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />; | ||
}; | ||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; | ||
|
||
export { | ||
DropdownMenu, | ||
DropdownMenuTrigger, | ||
DropdownMenuContent, | ||
DropdownMenuItem, | ||
DropdownMenuCheckboxItem, | ||
DropdownMenuRadioItem, | ||
DropdownMenuLabel, | ||
DropdownMenuSeparator, | ||
DropdownMenuShortcut, | ||
DropdownMenuGroup, | ||
DropdownMenuPortal, | ||
DropdownMenuSub, | ||
DropdownMenuSubContent, | ||
DropdownMenuSubTrigger, | ||
DropdownMenuRadioGroup, | ||
}; |
Oops, something went wrong.