implement display of log head in logs tab
This commit is contained in:
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,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?: {
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user