implement display of log head in logs tab

This commit is contained in:
Alex Holliday committed 2026-09-03 12:57:06 -07:00
1 parent f7e1793bac
commit bee3d4221e
5 files changed
+205 -7

No files matched your search

@@ -1,3 +1,131 @@
export const TabLogs = () => {
return "This is the logs tab";
// Components
import Stack from "@mui/material/Stack";
import { BaseBox } from "@/Components/design-elements";
import Typography from "@mui/material/Typography";
// Types
import type { DockerContainerLogsResponse } from "@/Types/Monitor";
// Hooks
import { useGet } from "@/Hooks/UseApi";
import { useState, useEffect, useMemo } from "react";
import type { DockerLog } from "@/Types/Check";
import { useSelector } from "react-redux";
// 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";
interface TabLogsProps {
monitorId?: string;
containerName?: string;
}
export const TabLogs = ({ monitorId, containerName }: TabLogsProps) => {
// State
const [olderLogs, setOlderLogs] = useState<DockerLog[]>([]);
const [olderCursor, setOlderCursor] = useState<string | null | undefined>(undefined); // undefined -> no older requested, string -> cursor, null -> no more data
const [loadingOlder, setLoadingOlder] = useState(false);
const [odlerError, setOlderError] = useState<unknown>(null);
const baseLogsUrl =
monitorId && containerName
? `/monitors/docker/details/${encodeURIComponent(monitorId)}/containers/${encodeURIComponent(containerName)}/logs`
: null;
const {
data: head,
isLoading,
isValidating,
error: headError,
refetch,
} = useGet<DockerContainerLogsResponse>(
baseLogsUrl,
{},
{
refreshInterval: 10_000,
keepPreviousData: true,
revalidateOnFocus: true,
}
);
const rows = useMemo(() => flattenDockerLogs(head?.logs ?? []), [head?.logs]);
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
return (
<Stack gap={LAYOUT.MD}>
<Stack direction="row">
<Typography>{`${rows.length} recent lines`}</Typography>
{isValidating && "Refreshing..."}
</Stack>
<BaseBox
minHeight={240}
maxHeight="60vh"
overflow={"auto"}
padding={LAYOUT.MD}
>
{[...rows].reverse().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>
);
})}
</BaseBox>
</Stack>
);
};
@@ -70,7 +70,12 @@ const DockerDetailsPage = ({ open = "overview" }: DockerContainerDetailsProps) =
<Tab label={t("pages.docker.container.tabs.logs")} />
</Tabs>
{activeTab === 0 && <TabOverview stats={stats} />}
{activeTab === 1 && <TabLogs />}
{activeTab === 1 && (
<TabLogs
monitorId={monitorId}
containerName={containerName}
/>
)}
</BasePage>
);
};
+25
View File
@@ -161,6 +161,31 @@ export interface DockerContainerMount {
rw: boolean;
}
export const DockerLogStreams = ["stdout", "stderr"] as const;
export type DockerLogStream = (typeof DockerLogStreams)[number];
export interface DockerLogLine {
ts: string;
stream: DockerLogStream;
text: string;
}
export interface DockerLog {
id: string;
metadata: {
monitorId: string;
teamId: string;
containerId: string;
containerName: string;
};
lines: DockerLogLine[];
gap: boolean;
checkedAt: string;
expiry: string;
createdAt: string;
updatedAt: string;
}
export interface DockerContainerInfo {
id: string;
name: string;
+5
View File
@@ -5,6 +5,7 @@ import type {
DailyCheckBucket,
DockerContainerInfo,
DockerContainerSummary,
DockerLog,
} from "@/Types/Check";
import type { PageSpeedGroupedCheck } from "@/Types/Check";
import type { GeoContinent } from "@/Types/GeoCheck";
@@ -322,6 +323,10 @@ export interface DockerContainerResponse {
stats: DockerContainerStats;
}
export interface DockerContainerLogsResponse {
logs: DockerLog[];
nextCursor: string | null;
}
export interface Game {
name: string;
options?: {
+39 -4
View File
@@ -1,10 +1,13 @@
import type { MonitorStatus, MonitorType } from "@/Types/Monitor";
import type { PaletteKey } from "@/Utils/Theme/Theme";
import type { ValueType } from "@/Components/design-elements/StatusLabel";
import type {
DockerContainerMount,
DockerContainerPort,
DockerContainerState,
import {
DockerLogStreams,
type DockerContainerMount,
type DockerContainerPort,
type DockerContainerState,
type DockerLog,
type DockerLogLine,
} from "@/Types/Check";
export const getMonitorPath = (type: MonitorType): string => {
@@ -144,3 +147,35 @@ export const getDockerMountLabel = (mount: DockerContainerMount) => {
if (mount.source) return mount.source;
return mount.type;
};
export const DockerLogStreamFilters = ["all", ...DockerLogStreams] as const;
export type DockerLogStreamFilter = (typeof DockerLogStreamFilters)[number];
export interface DockerLogRow extends DockerLogLine {
key: string;
gapBefore: boolean; // This signifies lines skipped
}
export const flattenDockerLogs = (logs: DockerLog[]): DockerLogRow[] => {
const rows: DockerLogRow[] = [];
for (const log of [...logs].reverse()) {
log.lines.forEach((line: DockerLogLine, idx: number) => {
rows.push({ ...line, key: `${log.id}:${idx}`, gapBefore: idx === 0 && log.gap });
});
}
return rows;
};
export const filterDockerLogRows = (
rows: DockerLogRow[],
stream: DockerLogStreamFilter,
query: string
): DockerLogRow[] => {
const normalizedQuery = query.trim().toLowerCase();
return rows.filter((row) => {
return (
(stream === "all" || row.stream === stream) &&
(normalizedQuery === "" || row.text.toLowerCase().includes(normalizedQuery))
);
});
};