Skip to main content
Glama
RunsTable.tsx16.9 kB
"use client" import { useConfig } from '@/src/app/config-context'; import { tokenRegistry } from '@/src/lib/token-registry'; import { Badge } from "@/src/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/src/components/ui/table"; import { Run, RunStatus, SuperglueClient } from '@superglue/shared'; import { AlertTriangle, Calendar, CheckCircle, ChevronDown, ChevronRight, Clock, Loader2, XCircle } from 'lucide-react'; import { CopyButton } from '@/src/components/tools/shared/CopyButton'; import React from 'react'; // Helper function to recursively remove null values from objects const removeNullFields = (obj: any): any => { if (obj === null || obj === undefined) { return undefined; } if (Array.isArray(obj)) { return obj.map(removeNullFields).filter(item => item !== undefined); } if (typeof obj === 'object' && obj !== null) { const cleaned: any = {}; for (const [key, value] of Object.entries(obj)) { const cleanedValue = removeNullFields(value); if (cleanedValue !== undefined && cleanedValue !== null) { cleaned[key] = cleanedValue; } } return Object.keys(cleaned).length > 0 ? cleaned : undefined; } return obj; }; const RunsTable = ({ id }: { id?: string }) => { const [runs, setRuns] = React.useState<Run[]>([]); const [expandedRunId, setExpandedRunId] = React.useState<string | null>(null); const [runDetails, setRunDetails] = React.useState<Record<string, any>>({}); const [loadingDetails, setLoadingDetails] = React.useState<Record<string, boolean>>({}); const [loading, setLoading] = React.useState(true); const [currentPage, setCurrentPage] = React.useState(0); const pageSize = 50; const config = useConfig(); React.useEffect(() => { const getRuns = async () => { try { setLoading(true); const superglueClient = new SuperglueClient({ endpoint: config.superglueEndpoint, apiKey: tokenRegistry.getToken() }) const data = await superglueClient.listRuns(pageSize, currentPage * pageSize, id); setRuns(data.items); } catch (error) { console.error('Error fetching runs:', error); } finally { setLoading(false); } }; getRuns(); }, [currentPage]); const handleRunClick = async (run: Run) => { // Toggle expansion if (expandedRunId === run.id) { setExpandedRunId(null); return; } setExpandedRunId(run.id); // If we already have details, don't fetch again if (runDetails[run.id]) { return; } setLoadingDetails(prev => ({ ...prev, [run.id]: true })); try { const superglueClient = new SuperglueClient({ endpoint: config.superglueEndpoint, apiKey: tokenRegistry.getToken() }); // Get the detailed run data - try to get more details via getRun const detailedRun = await superglueClient.getRun(run.id); setRunDetails(prev => ({ ...prev, [run.id]: detailedRun || run })); } catch (error) { console.error('Error fetching run details:', error); // Fall back to the basic run data if we can't get details setRunDetails(prev => ({ ...prev, [run.id]: run })); } finally { setLoadingDetails(prev => ({ ...prev, [run.id]: false })); } }; if (loading) { return ( <div className="flex-1 flex items-center justify-center min-h-[400px]"> <div className="flex flex-col items-center gap-3"> <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> <p className="text-sm text-muted-foreground">Loading runs...</p> </div> </div> ); } return ( <div className="flex-1 overflow-auto"> <div className="flex justify-between items-center mb-6"> <h1 className="text-2xl font-bold">Tool Runs</h1> </div> <div className="border rounded-lg"> <Table className="table-fixed"> <TableHeader> <TableRow> <TableHead className="w-[400px]">Tool ID</TableHead> <TableHead className="w-[110px]">Status</TableHead> <TableHead className="w-[180px]">Started At</TableHead> <TableHead className="w-[180px]">Completed At</TableHead> <TableHead className="w-[100px]">Duration</TableHead> </TableRow> </TableHeader> <TableBody> {[...runs].sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()).map((run) => ( <React.Fragment key={run.id}> <TableRow className="cursor-pointer hover:bg-muted/50 transition-colors" onClick={() => handleRunClick(run)} > <TableCell className="font-medium"> <div className="flex items-center gap-2 max-w-[360px]"> <ChevronRight className={`h-4 w-4 flex-shrink-0 transition-transform ${expandedRunId === run.id ? 'rotate-90' : ''}`} /> <span className="truncate" title={run.toolId ?? "undefined"}> {run.toolId ?? "undefined"} </span> </div> </TableCell> <TableCell> {run.status === RunStatus.SUCCESS ? ( <Badge variant="default" className="bg-emerald-500 hover:bg-emerald-500 gap-1"> <CheckCircle className="h-3 w-3" /> Success </Badge> ) : run.status === RunStatus.RUNNING ? ( <Badge variant="default" className="bg-blue-500 hover:bg-blue-500 gap-1"> <Loader2 className="h-3 w-3 animate-spin" /> Running </Badge> ) : run.status === RunStatus.ABORTED ? ( <Badge variant="default" className="bg-amber-500 hover:bg-amber-500 gap-1"> <AlertTriangle className="h-3 w-3" /> Aborted </Badge> ) : ( <Badge variant="destructive" className="hover:bg-destructive gap-1"> <XCircle className="h-3 w-3" /> Failed </Badge> )} </TableCell> <TableCell className="whitespace-nowrap">{new Date(run.startedAt).toLocaleString()}</TableCell> <TableCell className="whitespace-nowrap">{run.completedAt ? new Date(run.completedAt).toLocaleString() : '-'}</TableCell> <TableCell className="whitespace-nowrap"> {run.completedAt ? (new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) + 'ms' : '-'} </TableCell> </TableRow> {/* Expanded Details Row */} {expandedRunId === run.id && ( <TableRow> <TableCell colSpan={5} className="bg-muted/10 p-0"> {loadingDetails[run.id] ? ( <div className="flex items-center justify-center py-8"> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> </div> ) : ( <RunDetails run={runDetails[run.id] || run} /> )} </TableCell> </TableRow> )} </React.Fragment> ))} </TableBody> </Table> </div> <div className="flex justify-center gap-2 mt-4"> <button onClick={() => setCurrentPage(p => Math.max(0, p - 1))} disabled={currentPage === 0} className="px-4 py-2 text-sm font-medium bg-secondary hover:bg-secondary/80 border border-input rounded-md transition-colors disabled:opacity-50" > Previous </button> <span className="px-4 py-2 text-sm font-medium bg-secondary rounded-md"> Page {currentPage + 1} </span> <button onClick={() => setCurrentPage(p => p + 1)} disabled={runs.length < pageSize} className="px-4 py-2 text-sm font-medium bg-secondary hover:bg-secondary/80 border border-input rounded-md transition-colors disabled:opacity-50" > Next </button> </div> </div> ); }; const CollapsibleSection = ({ title, children, defaultOpen = false, isFirst = false, isLast = false }: { title: string; children: React.ReactNode; defaultOpen?: boolean; isFirst?: boolean; isLast?: boolean; }) => { const [isOpen, setIsOpen] = React.useState(defaultOpen); return ( <div className={`border-x border-t ${isLast && !isOpen ? 'border-b' : ''} ${isFirst ? 'rounded-t-lg' : ''} ${isLast ? 'rounded-b-lg' : ''}`}> <button onClick={() => setIsOpen(!isOpen)} className="w-full flex items-center gap-2 px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted/30 transition-colors" > {isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />} {title} </button> {isOpen && ( <div className={`px-3 pb-3 ${isLast ? 'border-b rounded-b-lg' : 'border-b'}`}> {children} </div> )} </div> ); }; const RunDetails = ({ run }: { run: any }) => { if (!run) return null; const cleanedToolConfig = run.toolConfig ? removeNullFields(run.toolConfig) : null; const cleanedOptions = run.options ? removeNullFields(run.options) : null; const cleanedToolResult = run.toolResult ? removeNullFields(run.toolResult) : null; const cleanedToolPayload = run.toolPayload ? removeNullFields(run.toolPayload) : null; const hasToolConfig = cleanedToolConfig && Object.keys(cleanedToolConfig).length > 0; const hasOptions = cleanedOptions && Object.keys(cleanedOptions).length > 0; const hasToolResult = cleanedToolResult && (Array.isArray(cleanedToolResult) ? cleanedToolResult.length > 0 : Object.keys(cleanedToolResult).length > 0); const hasToolPayload = cleanedToolPayload && Object.keys(cleanedToolPayload).length > 0; const hasStepResults = run.stepResults && run.stepResults.length > 0; const isAborted = run.status === RunStatus.ABORTED; const isFailed = run.status === RunStatus.FAILED; return ( <div className="p-6 space-y-4 max-h-[600px] overflow-y-auto [scrollbar-gutter:stable]"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="space-y-2"> <h4 className="text-sm font-medium text-muted-foreground">Run ID</h4> <div className="flex items-center gap-2"> <span className="text-sm font-mono truncate" title={run.id}>{run.id}</span> <CopyButton text={run.id} /> </div> </div> <div className="space-y-2"> <h4 className="text-sm font-medium text-muted-foreground">Duration</h4> <div className="flex items-center gap-2"> <Clock className="h-4 w-4 text-muted-foreground" /> <span className="text-sm"> {run.completedAt ? `${new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()}ms` : '-'} </span> </div> </div> <div className="space-y-2"> <h4 className="text-sm font-medium text-muted-foreground">Timing</h4> <div className="space-y-1 text-sm"> <div className="flex items-center gap-2"> <Calendar className="h-4 w-4 text-muted-foreground" /> <span>Started: {new Date(run.startedAt).toLocaleString()}</span> </div> <div className="flex items-center gap-2"> <CheckCircle className="h-4 w-4 text-muted-foreground" /> <span>Completed: {run.completedAt ? new Date(run.completedAt).toLocaleString() : '-'}</span> </div> </div> </div> </div> {isFailed && run.error && ( <div className="space-y-1"> <h4 className="text-sm font-medium text-muted-foreground">Error Message</h4> <div className="p-3 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900 rounded-lg"> <pre className="text-sm text-red-700 dark:text-red-400 whitespace-pre-wrap font-mono"> {run.error} </pre> </div> </div> )} {(() => { const sections = [ hasToolPayload && { key: 'payload', title: 'Tool Payload', content: ( <div className="relative"> <div className="absolute top-2 right-2"> <CopyButton getData={() => JSON.stringify(cleanedToolPayload, null, 2)} /> </div> <pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto bg-muted/30 p-3 pr-10 rounded-md"> {JSON.stringify(cleanedToolPayload, null, 2)} </pre> </div> )}, hasOptions && { key: 'options', title: 'Execution Options', content: ( <div className="relative"> <div className="absolute top-2 right-2"> <CopyButton getData={() => JSON.stringify(cleanedOptions, null, 2)} /> </div> <pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto bg-muted/30 p-3 pr-10 rounded-md"> {JSON.stringify(cleanedOptions, null, 2)} </pre> </div> )}, hasToolConfig && { key: 'config', title: 'Tool Configuration', content: ( <div className="relative"> <div className="absolute top-2 right-2"> <CopyButton getData={() => JSON.stringify(cleanedToolConfig, null, 2)} /> </div> <pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto bg-muted/30 p-3 pr-10 rounded-md"> {JSON.stringify(cleanedToolConfig, null, 2)} </pre> </div> )}, hasStepResults && { key: 'steps', title: `Step Results (${run.stepResults.length})`, content: ( <div className="space-y-2"> {run.stepResults.map((step: any, index: number) => ( <div key={step.stepId} className="p-3 border rounded-lg space-y-2"> <div className="flex items-center justify-between"> <span className="font-medium text-sm">Step {index + 1}: {step.stepId}</span> <Badge variant={step.success ? "default" : "destructive"} className={step.success ? "bg-emerald-500 hover:bg-emerald-500" : "hover:bg-destructive"}> {step.success ? "Success" : "Failed"} </Badge> </div> {step.error && ( <div className="p-2 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900 rounded text-xs"> <pre className="text-red-600 dark:text-red-500 whitespace-pre-wrap font-mono">{step.error}</pre> </div> )} {step.data && ( <pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto bg-muted/30 p-2 rounded-md max-h-[200px] overflow-y-auto"> {JSON.stringify(removeNullFields(step.data), null, 2)} </pre> )} </div> ))} </div> )}, hasToolResult && { key: 'result', title: 'Tool Result', content: ( <div className="relative"> <div className="absolute top-2 right-2 z-10"> <CopyButton getData={() => JSON.stringify(cleanedToolResult, null, 2)} /> </div> <pre className="text-xs font-mono whitespace-pre-wrap overflow-x-auto bg-muted/30 p-3 pr-10 rounded-md max-h-[300px] overflow-y-auto"> {JSON.stringify(cleanedToolResult, null, 2)} </pre> </div> )}, ].filter(Boolean) as { key: string; title: string; content: React.ReactNode }[]; if (sections.length === 0) return null; return ( <div> {sections.map((section, idx) => ( <CollapsibleSection key={section.key} title={section.title} isFirst={idx === 0} isLast={idx === sections.length - 1} > {section.content} </CollapsibleSection> ))} </div> ); })()} </div> ); }; export { RunsTable };

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