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<{