"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 { RequestSource, Run, RunStatus, SuperglueClient } from "@superglue/shared";
import {
AlertTriangle,
Calendar,
CheckCircle,
ChevronDown,
ChevronRight,
Clock,
Code,
Cpu,
Link,
Loader2,
MousePointerClick,
Webhook,
XCircle,
} from "lucide-react";
import { CopyButton } from "@/src/components/tools/shared/CopyButton";
import React from "react";
const getRequestSourceLabel = (source?: RequestSource | string) => {
switch (source) {
case RequestSource.API:
return "API";
case RequestSource.FRONTEND:
return "Manual";
case RequestSource.SCHEDULER:
return "Scheduler";
case RequestSource.MCP:
return "MCP";
case RequestSource.TOOL_CHAIN:
return "Tool chain";
case RequestSource.WEBHOOK:
return "Webhook";
default:
return source ? String(source) : "-";
}
};
const getRequestSourceBadgeClassName = (_source?: RequestSource | string) => {
return "bg-muted-foreground/70 hover:bg-muted-foreground/70";
};
const RequestSourceIcon = ({ source }: { source?: RequestSource | string }) => {
const className = "h-3 w-3";
switch (source) {
case RequestSource.API:
return <Code className={className} />;
case RequestSource.FRONTEND:
return <MousePointerClick className={className} />;
case RequestSource.SCHEDULER:
return <Clock className={className} />;
case RequestSource.MCP:
return <Cpu className={className} />;
case RequestSource.TOOL_CHAIN:
return <Link className={className} />;
case RequestSource.WEBHOOK:
return <Webhook className={className} />;
default:
return null;
}
};
// 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-[120px]">Run trigger</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>
<Badge
variant="default"
className={`${getRequestSourceBadgeClassName(run.requestSource)} gap-1`}
title={run.requestSource ?? "unknown"}
>
<RequestSourceIcon source={run.requestSource} />
{getRequestSourceLabel(run.requestSource)}
</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={6} 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">Run trigger</h4>
<div className="flex items-center gap-2">
<Badge
variant="default"
className={`${getRequestSourceBadgeClassName(run.requestSource)} gap-1`}
title={run.requestSource ?? "unknown"}
>
<RequestSourceIcon source={run.requestSource} />
{getRequestSourceLabel(run.requestSource)}
</Badge>
</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>
<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>
{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 };