Skip to main content
Glama
ToolsTable.tsx13.5 kB
"use client"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { ToolsItem } from "@/types"; import { ColumnDef, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, CircleX, MoreHorizontal, Plus, Wrench, } from "lucide-react"; import { useMemo, useRef, useState } from "react"; import { Label } from "../ui/label"; import { ToolSelector } from "./ToolSelector"; export const columns: ColumnDef<ToolsItem>[] = [ { accessorKey: "serviceName", header: ({ column }) => ( <Button variant="link" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} className="px-3" > Server {column.getIsSorted() && (column.getIsSorted() === "asc" ? <ArrowUp /> : <ArrowDown />)} </Button> ), cell: ({ row }) => <div>{row.getValue("serviceName")}</div>, }, { accessorKey: "name", header: ({ column }) => ( <Button variant="link" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} className="px-3" > Name {column.getIsSorted() && (column.getIsSorted() === "asc" ? <ArrowUp /> : <ArrowDown />)} </Button> ), cell: ({ row }) => <div>{row.getValue("name")}</div>, }, { accessorKey: "originalToolName", header: ({ column }) => ( <Button variant="link" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} className="px-3" > Original Tool {column.getIsSorted() && (column.getIsSorted() === "asc" ? <ArrowUp /> : <ArrowDown />)} </Button> ), cell: ({ row }) => { const originalToolName = (row.getValue("originalToolName") as string) || ""; return ( <span className={cn({ "p-1.5 bg-[var(--color-bg-neutral)] text-[var(--color-fg-interactive)] rounded": Boolean(originalToolName), })} > {originalToolName} </span> ); }, }, ]; export function ToolsTable({ data, handleAddServerClick, handleCreateClick, handleDeleteTool, handleDetailsClick, handleDuplicateClick, handleEditClick, }: { data: ToolsItem[]; handleAddServerClick: () => void; handleCreateClick: (tool: ToolsItem) => void; handleDeleteTool: (tool: ToolsItem) => void; handleDetailsClick: (tool: ToolsItem) => void; handleDuplicateClick: (tool: ToolsItem) => void; handleEditClick: (tool: ToolsItem) => void; }) { const inputRef = useRef<HTMLInputElement>(null); const [showOnlyCustomTools, setShowOnlyCustomTools] = useState(false); const toggleCustomTools = () => setShowOnlyCustomTools((prev) => !prev); const originalTools = useMemo( () => data.filter((tool) => !tool.originalToolId), [data], ); const filteredData = useMemo(() => { return showOnlyCustomTools ? data.filter((tool) => tool.originalToolId) : data; }, [data, showOnlyCustomTools]); const table = useReactTable({ columns, data: filteredData, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { sorting: [ { id: "name", desc: false, }, ], }, }); const { pageSize, pageIndex } = table.getState().pagination; const clearSearch = () => { table.resetGlobalFilter(); inputRef.current?.focus(); }; const clearFilters = () => { setShowOnlyCustomTools(false); table.resetGlobalFilter(); inputRef.current?.focus(); }; if (!data.length) { return ( <div className="flex flex-col gap-1 items-center justify-center h-64 bg-[var(--color-bg-container-overlay)] rounded-lg p-6"> <p>You have no MCP servers connected.</p> <p>Connect a server to see all your tools.</p> <Button variant="secondary" size="sm" onClick={(e) => { e.stopPropagation(); handleAddServerClick(); }} className="mt-4 px-2 border-[var(--color-border-interactive)] text-[var(--color-fg-interactive)] hover:bg-[var(--color-bg-interactive-hover)] hover:text-[var(--color-fg-interactive-hover)] focus:text-[var(--color-fg-interactive-hover)] focus:bg-[var(--color-bg-interactive-hover)]" > <Plus className="w-2 h-2 mr-0.5" /> Add Server </Button> </div> ); } return ( <div className="w-full p-6 bg-[var(--color-bg-container)] rounded-xl border border-[var(--color-border-primary)] shadow-xl"> <div className="flex items-start justify-between"> <div className="flex items-center gap-4 mb-4 max-w-md grow"> <div className="flex items-center"> <Input className="max-w-40" placeholder="Filter tools..." value={table.getState().globalFilter || ""} onChange={(event) => table.setGlobalFilter(event.target.value)} ref={inputRef} /> <Tooltip open={inputRef.current?.value ? undefined : false}> <TooltipTrigger asChild> <Button onClick={clearSearch} variant="vanilla" className="background-transparent focus-visible:ring-0 h-7 w-4 rounded-none" disabled={!inputRef.current?.value} > <CircleX /> </Button> </TooltipTrigger> <TooltipContent align="center" className="shadow bg-[var(--color-bg-container)] text-[var(--color-text-primary)] text-xs" > Clear search </TooltipContent> </Tooltip> </div> <Label className="flex items-center justify-between capitalize cursor-pointer"> <Switch className="ml-2" checked={showOnlyCustomTools} onCheckedChange={toggleCustomTools} /> <span className="ml-2 text-sm">Show only custom tools</span> </Label> </div> <ToolSelector toolsList={originalTools.map(({ name, serviceName }) => ({ name, serviceName, }))} onSelectionChange={(value) => handleCreateClick( originalTools.find((tool) => tool.name === value) || ({} as ToolsItem), ) } /> </div> <div className="rounded-md border"> <Table className="rounded-md overflow-hidden"> <TableHeader className="bg-[var(--color-bg-neutral)] font-medium text-[var(--color-text-primary)]"> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id} className="h-12"> {headerGroup.headers.map((header) => ( <TableHead key={header.id} className="px-0"> {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} </TableHead> ))} <TableHead key="actions" className="px-0 text-right"> <span className="whitespace-nowrap text-sm font-medium text-primary h-9 py-2 px-3"> Actions </span> </TableHead> </TableRow> ))} </TableHeader> <TableBody> {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( <TableRow key={row.id} data-state={row.getIsSelected() && "selected"} className="h-12 hover:bg-[var(--color-bg-modal-overlay)] transition-colors" > {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> {flexRender( cell.column.columnDef.cell, cell.getContext(), )} </TableCell> ))} <TableCell key={"actions"} className="text-right"> <DropdownMenu> <DropdownMenuTrigger asChild className="inline-flex self-end" > <Button className="h-8 w-8 p-0"> <span className="sr-only">Open menu</span> <MoreHorizontal /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {!row.original.originalToolId ? ( <> <DropdownMenuItem onClick={() => handleDetailsClick(row.original)} > Details </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem onClick={() => handleCreateClick(row.original)} > Customize </DropdownMenuItem> </> ) : null} {row.original.originalToolId ? ( <> <DropdownMenuItem onClick={() => handleDetailsClick(row.original)} > Details </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem onClick={() => handleEditClick(row.original)} > Edit </DropdownMenuItem> <DropdownMenuItem onClick={() => handleDuplicateClick(row.original)} > Duplicate </DropdownMenuItem> <DropdownMenuItem onClick={() => handleDeleteTool(row.original)} > Delete </DropdownMenuItem> </> ) : null} </DropdownMenuContent> </DropdownMenu> </TableCell> </TableRow> )) ) : ( <TableRow> <TableCell colSpan={columns.length + 1} className="h-24 text-center" > <div className="flex flex-col justify-center items-center gap-2 p-4"> <Wrench className="w-8 mx-auto text-[var(--color-fg-info)]" /> <p className="text-sm text-[var(--color-fg-info)]"> No results found </p> <Button onClick={clearFilters} variant="secondary" className="px-2" > <CircleX className="w-4 h-4 mr-2" /> Clear Filters </Button> </div> </TableCell> </TableRow> )} </TableBody> </Table> </div> <div className="flex items-center justify-end space-x-2 py-4"> <div className="text-muted-foreground flex-1 text-sm"> Showing {pageIndex * pageSize + 1} to{" "} {Math.min( (pageIndex + 1) * pageSize, table.getFilteredRowModel().rows.length, )}{" "} of {table.getFilteredRowModel().rows.length} results </div> <div className="space-x-2 flex items-center select-none"> <Button variant="secondary" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > <ArrowLeft className="mr-2 h-4 w-4" /> Previous </Button> <Button variant="secondary" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > Next <ArrowRight className="mr-2 h-4 w-4" /> </Button> </div> </div> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server