"use client"; import { useEffect, useRef, useState } from "react"; import { Box, Group, Loader, ScrollArea, Stack, Text } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import { createModal } from "@homarr/modals"; import { useI18n } from "@homarr/translation/client"; interface DockerLogsModalInnerProps { id: string; name: string; tail?: number; } const SCROLL_THRESHOLD = 100; const isViewportAtBottom = (viewport: HTMLDivElement | null): boolean => { if (!viewport) return false; return viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < SCROLL_THRESHOLD; }; export const DockerLogsModal = createModal(({ innerProps }) => { const t = useI18n(); const viewportRef = useRef(null); const [logs, setLogs] = useState(""); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const shouldScrollRef = useRef(true); const hasInitializedRef = useRef(false); // Get initial logs const { data: initialData } = clientApi.docker.logs.useQuery( { id: innerProps.id, tail: innerProps.tail ?? 200 }, { refetchOnWindowFocus: false, retry: 0, }, ); // Maximum log size to prevent memory issues (500KB) const MAX_LOG_SIZE = 500 * 1024; // Subscribe to streaming logs clientApi.docker.subscribeLogs.useSubscription( { id: innerProps.id, tail: innerProps.tail ?? 200 }, { onData(data) { setLogs((prev) => { const newLogs = prev + data; // Truncate from the beginning if logs exceed maximum size if (newLogs.length > MAX_LOG_SIZE) { const truncated = newLogs.slice(-MAX_LOG_SIZE); // Try to start from a newline to avoid partial lines const firstNewline = truncated.indexOf("\n"); return firstNewline > 0 ? truncated.slice(firstNewline + 1) : truncated; } return newLogs; }); setIsLoading(false); }, onError(err) { setError(err.message); setIsLoading(false); }, }, ); // Initialize with static logs (only once) useEffect(() => { if (initialData?.logs && !hasInitializedRef.current) { setLogs(initialData.logs); setIsLoading(false); hasInitializedRef.current = true; } }, [initialData]); // Auto-scroll to bottom when new logs arrive useEffect(() => { if (!shouldScrollRef.current) return; const viewport = viewportRef.current; if (!viewport) return; if (isViewportAtBottom(viewport)) { viewport.scrollTop = viewport.scrollHeight; } }, [logs]); // Handle manual scroll to determine auto-scroll behavior const handleScroll = () => { const viewport = viewportRef.current; shouldScrollRef.current = isViewportAtBottom(viewport); }; return ( {isLoading ? ( ) : error ? ( {error} ) : ( {logs ? (
                {logs}
              
) : ( {t("common.state.empty")} )}
)}
); }).withOptions({ size: "95%", centered: true, defaultTitle: (t) => t("docker.action.logs.label"), });