"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;