import { getErrorMessage } from "@/lib/error-message.js";
import React from "react";
import { Button } from "@/components/button.tsx";
import { Input } from "@/components/input.tsx";
import { Separator } from "@/components/separator.tsx";
import { Download, RefreshCw, Search, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import type { DockerLogOptions } from "@/types";
import { getContainerLogs, downloadContainerLogs } from "../docker-api";
import { useAdaptivePolling } from "@/hooks/use-adaptive-polling.ts";
import { Select2 } from "@/components/select2";
interface LogViewerProps {
sessionId: string;
containerId: string;
containerName: string;
}
function AdminToggle({
on,
onToggle,
label,
}: {
on: boolean;
onToggle: () => void;
label: string;
}) {
return (
{label}
);
}
function getLogColor(line: string): string {
if (line.includes(" WARN") || line.includes(" warn"))
return "text-yellow-400/90";
if (line.includes(" ERROR") || line.includes(" error"))
return "text-destructive";
if (line.includes(" DEBUG") || line.includes(" debug"))
return "text-muted-foreground/60";
return "text-foreground/90";
}
export function LogViewer({
sessionId,
containerId,
containerName,
}: LogViewerProps): React.ReactElement {
const { t } = useTranslation();
const [rawLogs, setRawLogs] = React.useState([]);
const [isLoading, setIsLoading] = React.useState(false);
const [isDownloading, setIsDownloading] = React.useState(false);
const [tailLines, setTailLines] = React.useState("100");
const [showTimestamps, setShowTimestamps] = React.useState(false);
const [autoRefresh, setAutoRefresh] = React.useState(false);
const [logSearch, setLogSearch] = React.useState("");
const logsEndRef = React.useRef(null);
const rawLogsRef = React.useRef(rawLogs);
rawLogsRef.current = rawLogs;
const fetchLogs = React.useCallback(async () => {
setIsLoading(true);
try {
const options: DockerLogOptions = {
tail: tailLines === "all" ? undefined : parseInt(tailLines, 10),
timestamps: showTimestamps,
};
const data = await getContainerLogs(sessionId, containerId, options);
const next = data.logs.split("\n").filter(Boolean);
const changed =
next.length !== rawLogsRef.current.length ||
next.some((line, index) => line !== rawLogsRef.current[index]);
if (changed) {
rawLogsRef.current = next;
setRawLogs(next);
}
return changed;
} catch (error) {
toast.error(`Failed to fetch logs: ${getErrorMessage(error)}`);
} finally {
setIsLoading(false);
}
}, [sessionId, containerId, tailLines, showTimestamps]);
React.useEffect(() => {
fetchLogs();
}, [fetchLogs]);
useAdaptivePolling(
fetchLogs,
{
minIntervalMs: 3_000,
maxIntervalMs: 18_000,
stablePollsPerStep: 2,
maxRequestDutyCycle: 0.2,
},
autoRefresh,
{ runImmediately: false },
);
React.useEffect(() => {
if (autoRefresh && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [rawLogs, autoRefresh]);
const handleDownload = async () => {
setIsDownloading(true);
try {
const blob = await downloadContainerLogs(sessionId, containerId, {
timestamps: showTimestamps,
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${containerName.replace(/[^a-z0-9]/gi, "_")}_logs.txt`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(t("docker.logsDownloaded"));
} catch (error) {
toast.error(`Failed to download logs: ${getErrorMessage(error)}`);
} finally {
setIsDownloading(false);
}
};
const filteredLogs = logSearch
? rawLogs.filter((l) => l.toLowerCase().includes(logSearch.toLowerCase()))
: rawLogs;
return (
setAutoRefresh(!autoRefresh)}
label={t("docker.autoRefresh")}
/>
setShowTimestamps(!showTimestamps)}
label={t("docker.timestamps")}
/>
setTailLines(e.target.value)}
className="h-7 px-2 text-[10px] bg-background border border-border text-foreground outline-none uppercase font-bold"
>
{filteredLogs.length}/{rawLogs.length} {t("docker.lines")}
{filteredLogs.length > 0 ? (
filteredLogs.map((line, i) => {
const tsEnd = line.indexOf(" ", 1);
const maybeTs = tsEnd > 10 ? line.substring(0, tsEnd) : null;
const rest = maybeTs ? line.substring(tsEnd) : line;
return (
{maybeTs && (
{maybeTs}
)}
{rest}
);
})
) : (
{logSearch
? t("docker.noLogsMatching", { query: logSearch })
: t("docker.noLogsAvailable")}
)}
);
}