From 795e1caca4bb63bbec6f8293695659af811f9a38 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 4 Sep 2026 11:42:11 -0700 Subject: [PATCH] log display --- .../Components/HeaderLogs.tsx | 16 +++ .../ContainerDetails/Components/RowLog.tsx | 74 +++++++++++ .../Pages/Docker/ContainerDetails/TabLogs.tsx | 119 ++++++++---------- .../Pages/Docker/ContainerDetails/index.tsx | 2 +- 4 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 client/src/Pages/Docker/ContainerDetails/Components/HeaderLogs.tsx create mode 100644 client/src/Pages/Docker/ContainerDetails/Components/RowLog.tsx diff --git a/client/src/Pages/Docker/ContainerDetails/Components/HeaderLogs.tsx b/client/src/Pages/Docker/ContainerDetails/Components/HeaderLogs.tsx new file mode 100644 index 000000000..b5cdec704 --- /dev/null +++ b/client/src/Pages/Docker/ContainerDetails/Components/HeaderLogs.tsx @@ -0,0 +1,16 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +export const HeaderLogs = ({ + lines, + isValidating, +}: { + lines: number; + isValidating: boolean; +}) => { + return ( + + {`${lines} recent lines`} + {isValidating && "Refreshing..."} + + ); +}; diff --git a/client/src/Pages/Docker/ContainerDetails/Components/RowLog.tsx b/client/src/Pages/Docker/ContainerDetails/Components/RowLog.tsx new file mode 100644 index 000000000..82ba9f259 --- /dev/null +++ b/client/src/Pages/Docker/ContainerDetails/Components/RowLog.tsx @@ -0,0 +1,74 @@ +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Divider from "@mui/material/Divider"; +import { Fragment } from "react"; + +// Hooks +import { useTheme } from "@mui/material"; +import { useSelector } from "react-redux"; + +// Utils +import type { DockerLogRow } from "@/Utils/MonitorUtils"; +import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { RootState } from "@/store"; + +export const RowLog = ({ row }: { row: DockerLogRow }) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + return ( + + + + + {formatDateWithTz(row.ts, "MMM D HH:mm:ss", uiTimezone)} + + + + + + {row.stream} + + : {row.text} + + + + {row.gapBefore ? ( + + Some log entries were skipped + + + ) : ( + + )} + + ); +}; diff --git a/client/src/Pages/Docker/ContainerDetails/TabLogs.tsx b/client/src/Pages/Docker/ContainerDetails/TabLogs.tsx index 9bd03e9bc..6d4087c53 100644 --- a/client/src/Pages/Docker/ContainerDetails/TabLogs.tsx +++ b/client/src/Pages/Docker/ContainerDetails/TabLogs.tsx @@ -1,30 +1,41 @@ // Components import Stack from "@mui/material/Stack"; import { BaseBox } from "@/Components/design-elements"; -import Typography from "@mui/material/Typography"; +import { HeaderLogs } from "@/Pages/Docker/ContainerDetails/Components/HeaderLogs"; // Types import type { DockerContainerLogsResponse } from "@/Types/Monitor"; // Hooks import { useGet } from "@/Hooks/UseApi"; -import { useState, useEffect, useMemo } from "react"; +import { + useState, + useEffect, + useLayoutEffect, + useMemo, + Fragment, + useRef, + useCallback, +} from "react"; import type { DockerLog } from "@/Types/Check"; -import { useSelector } from "react-redux"; - +import { useTheme } from "@mui/material"; // Util import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; import { flattenDockerLogs } from "@/Utils/MonitorUtils"; -import { Box } from "@mui/material"; -import { formatDateWithTz } from "@/Utils/TimeUtils"; -import type { RootState } from "@/store"; +import { RowLog } from "@/Pages/Docker/ContainerDetails/Components/RowLog"; interface TabLogsProps { monitorId?: string; containerName?: string; } +const SCROLL_PIN_THRESHOLD_PX = 8; +const isScrolledToBottom = (el: HTMLElement) => + el.scrollHeight - el.scrollTop - el.clientHeight <= SCROLL_PIN_THRESHOLD_PX; + export const TabLogs = ({ monitorId, containerName }: TabLogsProps) => { + const theme = useTheme(); + // State const [olderLogs, setOlderLogs] = useState([]); const [olderCursor, setOlderCursor] = useState(undefined); // undefined -> no older requested, string -> cursor, null -> no more data @@ -53,76 +64,46 @@ export const TabLogs = ({ monitorId, containerName }: TabLogsProps) => { ); const rows = useMemo(() => flattenDockerLogs(head?.logs ?? []), [head?.logs]); - const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + const logBoxRef = useRef(null); + const pinnedToBottom = useRef(true); + + // Always pin to bottom when container/monitor changes + useLayoutEffect(() => { + pinnedToBottom.current = true; + }, [monitorId, containerName]); + + // New log rows + useLayoutEffect(() => { + const el = logBoxRef.current; + if (!el || !pinnedToBottom.current) return; + el.scrollTop = el.scrollHeight; + }, [rows]); + + const handleScroll = useCallback((event: React.UIEvent) => { + pinnedToBottom.current = isScrolledToBottom(event.currentTarget); + }, []); + return ( - - {`${rows.length} recent lines`} - {isValidating && "Refreshing..."} - + - {[...rows].reverse().map((row) => { + {rows.map((row) => { return ( - - - - {formatDateWithTz(row.ts, "MMM D HH:mm:ss", uiTimezone)} - - - - {row.stream} - - - - {row.text} - - - {row.gapBefore && ( - - - - {"Some lines were skipped between checks"} - - - - )} - + ); })} diff --git a/client/src/Pages/Docker/ContainerDetails/index.tsx b/client/src/Pages/Docker/ContainerDetails/index.tsx index 51f765785..eaf370318 100644 --- a/client/src/Pages/Docker/ContainerDetails/index.tsx +++ b/client/src/Pages/Docker/ContainerDetails/index.tsx @@ -21,7 +21,7 @@ const TAB_MAP = { logs: 1, } as const; -const DockerDetailsPage = ({ open = "overview" }: DockerContainerDetailsProps) => { +const DockerDetailsPage = ({ open = "logs" }: DockerContainerDetailsProps) => { const [activeTab, setActiveTab] = useState(TAB_MAP[open]); const [dateRange, setDateRange] = useState("recent"); const { monitorId, containerName } = useParams<{