Skip to main content
Glama
page.tsx15.7 kB
"use client" import { useIntegrations } from '@/src/app/integrations-context'; import { Button } from "@/src/components/ui/button"; import { Input } from "@/src/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/src/components/ui/table"; import { DeployButton } from '@/src/components/tools/deploy/DeployButton'; import { FolderSelector, useFolderFilter } from '@/src/components/tools/folders/FolderSelector'; import { InlineFolderPicker } from '@/src/components/tools/folders/InlineFolderPicker'; import { CopyButton } from '@/src/components/tools/shared/CopyButton'; import { ToolActionsMenu } from '@/src/components/tools/ToolActionsMenu'; import { ToolCreateStepper } from '@/src/components/tools/ToolCreateStepper'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/src/components/ui/tooltip"; import { getIntegrationIcon as getIntegrationIconName } from '@/src/lib/general-utils'; import { Integration, Tool } from '@superglue/shared'; import { ArrowDown, ArrowUp, ArrowUpDown, Globe, Hammer, Loader2, Plus, RotateCw, Search } from "lucide-react"; import { useRouter } from 'next/navigation'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { SimpleIcon } from 'simple-icons'; import * as simpleIcons from 'simple-icons'; import { useTools } from '../tools-context'; type SortColumn = 'id' | 'folder' | 'instruction' | 'updatedAt'; type SortDirection = 'asc' | 'desc'; const ConfigTable = () => { const router = useRouter(); const {tools, isInitiallyLoading, isRefreshing, refreshTools} = useTools(); const { integrations } = useIntegrations(); const [searchTerm, setSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [manuallyOpenedStepper, setManuallyOpenedStepper] = useState(false); const [hasCompletedInitialLoad, setHasCompletedInitialLoad] = useState(false); const [sortColumn, setSortColumn] = useState<SortColumn>('updatedAt'); const [sortDirection, setSortDirection] = useState<SortDirection>('desc'); const { selectedFolder, setSelectedFolder, filteredByFolder } = useFolderFilter(tools); // Debounce search term useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); }, 150); return () => clearTimeout(timer); }, [searchTerm]); // Memoize filtered and sorted configs const currentConfigs = useMemo(() => { let filtered = filteredByFolder.filter(config => { if (!config) return false; if (debouncedSearchTerm) { const searchLower = debouncedSearchTerm.toLowerCase(); // Only search relevant fields instead of entire object const searchableText = [ config.id, config.folder, config.instruction ].filter(Boolean).join(' ').toLowerCase(); if (!searchableText.includes(searchLower)) return false; } return true; }); filtered = [...filtered].sort((a, b) => { const dir = sortDirection === 'asc' ? 1 : -1; switch (sortColumn) { case 'id': return dir * a.id.localeCompare(b.id); case 'folder': return dir * (a.folder || '').localeCompare(b.folder || ''); case 'instruction': return dir * (a.instruction || '').localeCompare(b.instruction || ''); case 'updatedAt': return dir * (new Date(a.updatedAt || a.createdAt).getTime() - new Date(b.updatedAt || b.createdAt).getTime()); default: return 0; } }); return filtered; }, [filteredByFolder, debouncedSearchTerm, sortColumn, sortDirection]); const refreshConfigs = useCallback(() => { refreshTools(); }, [refreshTools]); const handleTool = () => { setManuallyOpenedStepper(true); }; const handlePlayTool = (e: React.MouseEvent, id: string) => { e.stopPropagation(); // Navigate to the tool page, passing the ID. The user can then run it. router.push(`/tools/${encodeURIComponent(id)}`); }; const handleSort = (column: SortColumn) => { if (sortColumn === column) { setSortDirection(d => d === 'asc' ? 'desc' : 'asc'); } else { setSortColumn(column); setSortDirection(column === 'updatedAt' ? 'desc' : 'asc'); } }; const SortIcon = ({ column }: { column: SortColumn }) => { if (sortColumn !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />; return sortDirection === 'asc' ? <ArrowUp className="ml-1 h-3 w-3" /> : <ArrowDown className="ml-1 h-3 w-3" />; }; const getSimpleIcon = (name: string): SimpleIcon | null => { if (!name || name === "default") return null; const formatted = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); const iconKey = `si${formatted}`; try { // @ts-ignore let icon = simpleIcons[iconKey]; return icon || null; } catch (e) { return null; } }; const getIntegrationIcon = (integration: Integration) => { const iconName = getIntegrationIconName(integration); return iconName ? getSimpleIcon(iconName) : null; }; useEffect(() => { if (!isInitiallyLoading && !hasCompletedInitialLoad) { setHasCompletedInitialLoad(true); } }, [isInitiallyLoading, hasCompletedInitialLoad]); const shouldShowStepper = manuallyOpenedStepper || ( hasCompletedInitialLoad && tools.length === 0 ); if (shouldShowStepper) { return ( <div className="max-w-none w-full min-h-full"> <ToolCreateStepper onComplete={() => { setManuallyOpenedStepper(false); refreshConfigs(); }} /> </div> ) } return ( <div className="p-8 max-w-none w-full h-full flex flex-col overflow-hidden"> <div className="flex flex-col lg:flex-row justify-between lg:items-center mb-6 gap-2 flex-shrink-0"> <h1 className="text-2xl font-bold">Tools</h1> <div className="flex gap-4"> <Button onClick={handleTool}> <Plus className="mr-2 h-4 w-4" /> Create </Button> </div> </div> <div className="flex flex-wrap gap-3 mb-4 flex-shrink-0"> <FolderSelector tools={tools} selectedFolder={selectedFolder} onFolderChange={setSelectedFolder} /> <div className="relative flex-1 min-w-[200px]"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Input placeholder="Search by ID or details..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="pl-10" /> </div> </div> <div className="border rounded-lg flex-1 overflow-auto"> <Table> <TableHeader className="sticky top-0 bg-background z-10"> <TableRow> <TableHead className="w-[60px]"></TableHead> <TableHead className="cursor-pointer hover:bg-muted/50 select-none" onClick={() => handleSort('id')} > <div className="flex items-center"> ID <SortIcon column="id" /> </div> </TableHead> <TableHead className="cursor-pointer hover:bg-muted/50 select-none" onClick={() => handleSort('folder')} > <div className="flex items-center"> Folder <SortIcon column="folder" /> </div> </TableHead> <TableHead className="cursor-pointer hover:bg-muted/50 select-none" onClick={() => handleSort('instruction')} > <div className="flex items-center"> Instructions <SortIcon column="instruction" /> </div> </TableHead> <TableHead className="cursor-pointer hover:bg-muted/50 select-none" onClick={() => handleSort('updatedAt')} > <div className="flex items-center"> Updated At <SortIcon column="updatedAt" /> </div> </TableHead> <TableHead className="text-right"> <TooltipProvider> <Tooltip> <TooltipTrigger asChild> <Button variant="ghost" size="icon" onClick={refreshConfigs} className="transition-transform" > <RotateCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} /> </Button> </TooltipTrigger> <TooltipContent> <p>Refresh Tools</p> </TooltipContent> </Tooltip> </TooltipProvider> </TableHead> </TableRow> </TableHeader> <TableBody> {isInitiallyLoading && tools.length === 0 ? ( <TableRow> <TableCell colSpan={6} className="h-24 text-center"> <Loader2 className="h-6 w-6 animate-spin text-foreground inline-block" /> </TableCell> </TableRow> ) : currentConfigs.length === 0 ? ( <TableRow> <TableCell colSpan={6} className="h-24 text-center"> <div className="flex flex-col items-center gap-2 text-muted-foreground"> <span>{selectedFolder !== 'all' ? 'No tools in this folder' : 'No results found'}</span> {selectedFolder !== 'all' && ( <Button variant="outline" size="sm" onClick={() => setSelectedFolder('all')} > Show all tools </Button> )} </div> </TableCell> </TableRow> ) : ( currentConfigs.map((tool) => { const allIntegrationIds = new Set<string>(); if (tool.integrationIds) { tool.integrationIds.forEach(id => allIntegrationIds.add(id)); } if (tool.steps) { tool.steps.forEach((step: any) => { if (step.integrationId) { allIntegrationIds.add(step.integrationId); } }); } const integrationIdsArray = Array.from(allIntegrationIds); return ( <TableRow key={tool.id} className="hover:bg-secondary" > <TableCell className="w-[60px]"> {integrationIdsArray.length > 0 ? ( <div className="flex items-center justify-center gap-1 flex-shrink-0"> {integrationIdsArray.map((integrationId: string) => { const integration = integrations.find(i => i.id === integrationId); if (!integration) return null; const icon = getIntegrationIcon(integration); return icon ? ( <TooltipProvider key={integrationId}> <Tooltip> <TooltipTrigger asChild> <svg width="14" height="14" viewBox="0 0 24 24" fill={`#${icon.hex}`} className="flex-shrink-0" > <path d={icon.path} /> </svg> </TooltipTrigger> <TooltipContent> <p>{integration.id}</p> </TooltipContent> </Tooltip> </TooltipProvider> ) : ( <TooltipProvider key={integrationId}> <Tooltip> <TooltipTrigger asChild> <Globe className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" /> </TooltipTrigger> <TooltipContent> <p>{integration.id}</p> </TooltipContent> </Tooltip> </TooltipProvider> ); })} </div> ) : null} </TableCell> <TableCell className="font-medium max-w-[200px] truncate relative group"> <div className="flex items-center space-x-1"> <span className="truncate">{tool.id}</span> <div className="opacity-0 group-hover:opacity-100 transition-opacity"> <CopyButton text={tool.id} /> </div> </div> </TableCell> <TableCell className="w-[200px] min-w-[200px] max-w-[400px]"> <InlineFolderPicker tool={tool} /> </TableCell> <TableCell className="max-w-[300px] truncate relative group"> <div className="flex items-center space-x-1"> <span className="truncate">{tool.instruction}</span> <div className="opacity-0 group-hover:opacity-100 transition-opacity"> <CopyButton text={tool.instruction || ''} /> </div> </div> </TableCell> <TableCell className="w-[150px]"> {tool.updatedAt ? new Date(tool.updatedAt).toLocaleDateString() : (tool.createdAt ? new Date(tool.createdAt).toLocaleDateString() : '')} </TableCell> <TableCell className="w-[140px]"> <div className="flex justify-end gap-2"> <Button variant="default" size="sm" onClick={(e) => handlePlayTool(e, tool.id)} className="gap-2" > <Hammer className="h-4 w-4" /> View </Button> {!tool.archived && ( <DeployButton tool={tool} className="gap-2" /> )} <ToolActionsMenu tool={tool} /> </div> </TableCell> </TableRow> ); }) )} </TableBody> </Table> </div> </div> ); }; export default ConfigTable;

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/superglue-ai/superglue'

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