log display

This commit is contained in:
Alex Holliday committed 2026-09-04 11:42:11 -07:00
1 parent a5bb186814
commit 795e1caca4
4 files changed
+141 -70

No files matched your search

@@ -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 (
<Stack direction="row">
<Typography>{`${lines} recent lines`}</Typography>
{isValidating && "Refreshing..."}
</Stack>
);
};
@@ -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 (
<Fragment>
<Grid
container
spacing={{ xs: 0, md: LAYOUT.MD }}
py={{ xs: LAYOUT.LG, md: 0 }}
>
<Grid size={{ xs: 12, md: "auto" }}>
<Typography
fontFamily="monospace"
color={theme.palette.text.secondary}
>
{formatDateWithTz(row.ts, "MMM D HH:mm:ss", uiTimezone)}
</Typography>
</Grid>
<Grid size={{ xs: 12, md: "grow" }}>
<Typography
fontFamily={"monospace"}
whiteSpace={{ xs: "pre-wrap", md: "nowrap" }}
sx={{
overflowWrap: { xs: "anywhere", md: "normal" },
}}
>
<Box
component={"span"}
color={
row.stream === "stderr"
? theme.palette.error.main
: theme.palette.success.main
}
>
{row.stream}
</Box>
: {row.text}
</Typography>
</Grid>
</Grid>
{row.gapBefore ? (
<Stack
width="100%"
direction="row"
alignItems={"center"}
py={SPACING.MD}
>
<Typography pr={SPACING.MD}>Some log entries were skipped</Typography>
<Divider sx={{ flex: 1 }} />
</Stack>
) : (
<Divider sx={{ display: { xs: "block", md: "none" } }} />
)}
</Fragment>
);
};
@@ -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<DockerLog[]>([]);
const [olderCursor, setOlderCursor] = useState<string | null | undefined>(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<HTMLDivElement>(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<HTMLDivElement>) => {
pinnedToBottom.current = isScrolledToBottom(event.currentTarget);
}, []);
return (
<Stack gap={LAYOUT.MD}>
<Stack direction="row">
<Typography>{`${rows.length} recent lines`}</Typography>
{isValidating && "Refreshing..."}
</Stack>
<HeaderLogs
lines={rows.length}
isValidating={isValidating}
/>
<BaseBox
ref={logBoxRef}
onScroll={handleScroll}
minHeight={240}
maxHeight="60vh"
maxHeight="50vh"
overflow={"auto"}
padding={LAYOUT.MD}
>
{[...rows].reverse().map((row) => {
{rows.map((row) => {
return (
<Box key={row.key}>
<Box
display="grid"
gridTemplateColumns={{
xs: "1fr",
sm: "150px 56px minmax(0, 1fr)",
}}
>
<Typography
component="span"
color="text.secondary"
>
{formatDateWithTz(row.ts, "MMM D HH:mm:ss", uiTimezone)}
</Typography>
<Typography
component="span"
color={row.stream === "stderr" ? "error.main" : "text.secondary"}
>
{row.stream}
</Typography>
<Typography
component="span"
color="text.primary"
whiteSpace="pre-wrap"
>
{row.text}
</Typography>
</Box>
{row.gapBefore && (
<Stack
role="separator"
direction="row"
alignItems="center"
>
<Box
flex={1}
borderTop={1}
borderColor="divider"
/>
<Typography
variant="caption"
color="warning.main"
>
{"Some lines were skipped between checks"}
</Typography>
<Box
flex={1}
borderTop={1}
borderColor="divider"
/>
</Stack>
)}
</Box>
<RowLog
key={row.key}
row={row}
/>
);
})}
</BaseBox>
@@ -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<number>(TAB_MAP[open]);
const [dateRange, setDateRange] = useState<DateRange>("recent");
const { monitorId, containerName } = useParams<{