2023-12-31 16:31:33 +03:00
|
|
|
import { Button } from "@/components/ui/button";
|
2024-03-24 20:23:39 +03:00
|
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
2025-02-10 18:38:56 +03:00
|
|
|
import {
|
|
|
|
|
LogLine,
|
|
|
|
|
LogSettingsType,
|
|
|
|
|
LogSeverity,
|
|
|
|
|
LogType,
|
|
|
|
|
logTypes,
|
|
|
|
|
} from "@/types/log";
|
2023-12-31 16:31:33 +03:00
|
|
|
import copy from "copy-to-clipboard";
|
2024-04-03 19:55:13 +03:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
|
import axios from "axios";
|
2024-04-07 23:36:08 +03:00
|
|
|
import LogInfoDialog from "@/components/overlay/LogInfoDialog";
|
|
|
|
|
import { LogChip } from "@/components/indicators/Chip";
|
2025-02-10 18:38:56 +03:00
|
|
|
import { LogSettingsButton } from "@/components/filter/LogSettingsButton";
|
|
|
|
|
import { FaCopy, FaDownload } from "react-icons/fa";
|
2024-04-07 23:36:08 +03:00
|
|
|
import { Toaster } from "@/components/ui/sonner";
|
|
|
|
|
import { toast } from "sonner";
|
2024-04-14 19:14:10 +03:00
|
|
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
2024-05-07 17:00:25 +03:00
|
|
|
import { cn } from "@/lib/utils";
|
2024-05-29 21:05:39 +03:00
|
|
|
import { parseLogLines } from "@/utils/logUtil";
|
2024-08-19 17:53:33 +03:00
|
|
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|
|
|
|
import scrollIntoView from "scroll-into-view-if-needed";
|
2025-02-10 18:38:56 +03:00
|
|
|
import { LazyLog } from "@melloware/react-logviewer";
|
|
|
|
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
|
|
|
|
import EnhancedScrollFollow from "@/components/dynamic/EnhancedScrollFollow";
|
|
|
|
|
import { MdCircle } from "react-icons/md";
|
|
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "@/components/ui/tooltip";
|
|
|
|
|
import { debounce } from "lodash";
|
2025-03-09 16:47:10 +03:00
|
|
|
import { isIOS, isMobile } from "react-device-detect";
|
|
|
|
|
import { isPWA } from "@/utils/isPWA";
|
|
|
|
|
import { isInIframe } from "@/utils/isIFrame";
|
2025-03-16 18:36:20 +03:00
|
|
|
import { useTranslation } from "react-i18next";
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2023-12-08 16:33:22 +03:00
|
|
|
function Logs() {
|
2025-03-16 18:36:20 +03:00
|
|
|
const { t } = useTranslation(["views/system"]);
|
2023-12-31 16:31:33 +03:00
|
|
|
const [logService, setLogService] = useState<LogType>("frigate");
|
2024-08-19 17:53:33 +03:00
|
|
|
const tabsRef = useRef<HTMLDivElement | null>(null);
|
2025-02-10 18:38:56 +03:00
|
|
|
const lazyLogWrapperRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
|
|
|
const [filterSeverity, setFilterSeverity] = useState<LogSeverity[]>();
|
|
|
|
|
const [selectedLog, setSelectedLog] = useState<LogLine>();
|
|
|
|
|
const lazyLogRef = useRef<LazyLog>(null);
|
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
const lastFetchedIndexRef = useRef(-1);
|
2023-12-31 16:31:33 +03:00
|
|
|
|
2024-04-12 15:31:30 +03:00
|
|
|
useEffect(() => {
|
2025-03-16 18:36:20 +03:00
|
|
|
document.title = t("documentTitle.logs." + logService);
|
|
|
|
|
}, [logService, t]);
|
2024-04-12 15:31:30 +03:00
|
|
|
|
2024-08-19 17:53:33 +03:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (tabsRef.current) {
|
|
|
|
|
const element = tabsRef.current.querySelector(
|
|
|
|
|
`[data-nav-item="${logService}"]`,
|
|
|
|
|
);
|
|
|
|
|
if (element instanceof HTMLElement) {
|
|
|
|
|
scrollIntoView(element, {
|
2025-03-09 16:47:10 +03:00
|
|
|
behavior:
|
|
|
|
|
isMobile && isIOS && !isPWA && isInIframe ? "auto" : "smooth",
|
2024-08-19 17:53:33 +03:00
|
|
|
inline: "start",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [tabsRef, logService]);
|
|
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
// log settings
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const [logSettings, setLogSettings] = useState<LogSettingsType>({
|
|
|
|
|
disableStreaming: false,
|
|
|
|
|
});
|
2024-05-29 21:05:39 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
// filter
|
2024-05-29 21:05:39 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const filterLines = useCallback(
|
|
|
|
|
(lines: string[]) => {
|
|
|
|
|
if (!filterSeverity?.length) return lines;
|
2024-05-29 21:05:39 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
return lines.filter((line) => {
|
|
|
|
|
const parsedLine = parseLogLines(logService, [line])[0];
|
|
|
|
|
return filterSeverity.includes(parsedLine.severity);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[filterSeverity, logService],
|
|
|
|
|
);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
// fetchers
|
|
|
|
|
|
|
|
|
|
const fetchLogRange = useCallback(
|
|
|
|
|
async (start: number, end: number) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get(`logs/${logService}`, {
|
|
|
|
|
params: { start, end },
|
|
|
|
|
});
|
|
|
|
|
if (
|
|
|
|
|
response.status === 200 &&
|
|
|
|
|
response.data &&
|
|
|
|
|
Array.isArray(response.data.lines)
|
|
|
|
|
) {
|
|
|
|
|
const filteredLines = filterLines(response.data.lines);
|
|
|
|
|
return filteredLines;
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
2025-02-10 18:38:56 +03:00
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error instanceof Error ? error.message : "An unknown error occurred";
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(
|
|
|
|
|
t("logs.toast.error.fetchingLogsFailed", { errorMessage }),
|
|
|
|
|
{
|
|
|
|
|
position: "top-center",
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-02-10 18:38:56 +03:00
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
},
|
2025-03-16 18:36:20 +03:00
|
|
|
[logService, filterLines, t],
|
2025-02-10 18:38:56 +03:00
|
|
|
);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const fetchInitialLogs = useCallback(async () => {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get(`logs/${logService}`, {
|
|
|
|
|
params: { start: filterSeverity ? 0 : -100 },
|
|
|
|
|
});
|
|
|
|
|
if (
|
|
|
|
|
response.status === 200 &&
|
|
|
|
|
response.data &&
|
|
|
|
|
Array.isArray(response.data.lines)
|
|
|
|
|
) {
|
|
|
|
|
const filteredLines = filterLines(response.data.lines);
|
|
|
|
|
setLogs(filteredLines);
|
|
|
|
|
lastFetchedIndexRef.current =
|
|
|
|
|
response.data.totalLines - filteredLines.length;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error instanceof Error ? error.message : "An unknown error occurred";
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(t("logs.toast.error.fetchingLogsFailed", { errorMessage }), {
|
2025-02-10 18:38:56 +03:00
|
|
|
position: "top-center",
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
2025-03-16 18:36:20 +03:00
|
|
|
}, [logService, filterLines, filterSeverity, t]);
|
2025-02-10 18:38:56 +03:00
|
|
|
|
|
|
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
|
|
|
|
|
|
|
|
const fetchLogsStream = useCallback(() => {
|
|
|
|
|
// Cancel any existing stream
|
|
|
|
|
abortControllerRef.current?.abort();
|
|
|
|
|
const abortController = new AbortController();
|
|
|
|
|
abortControllerRef.current = abortController;
|
|
|
|
|
let buffer = "";
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
|
|
|
|
|
const processStreamChunk = (
|
|
|
|
|
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
|
|
|
): Promise<void> => {
|
|
|
|
|
return reader.read().then(({ done, value }) => {
|
|
|
|
|
if (done) return;
|
|
|
|
|
|
|
|
|
|
// Decode the chunk and add it to our buffer
|
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
|
|
|
|
|
|
// Split on newlines, keeping any partial line in the buffer
|
|
|
|
|
const lines = buffer.split("\n");
|
|
|
|
|
|
|
|
|
|
// Keep the last partial line
|
|
|
|
|
buffer = lines.pop() || "";
|
|
|
|
|
|
|
|
|
|
// Filter and append complete lines
|
|
|
|
|
if (lines.length > 0) {
|
|
|
|
|
const filteredLines = filterSeverity?.length
|
|
|
|
|
? lines.filter((line) => {
|
|
|
|
|
const parsedLine = parseLogLines(logService, [line])[0];
|
|
|
|
|
return filterSeverity.includes(parsedLine.severity);
|
|
|
|
|
})
|
|
|
|
|
: lines;
|
|
|
|
|
if (filteredLines.length > 0) {
|
|
|
|
|
lazyLogRef.current?.appendLines(filteredLines);
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
2025-02-10 18:38:56 +03:00
|
|
|
}
|
|
|
|
|
// Process next chunk
|
|
|
|
|
return processStreamChunk(reader);
|
|
|
|
|
});
|
|
|
|
|
};
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
fetch(`api/logs/${logService}?stream=true`, {
|
|
|
|
|
signal: abortController.signal,
|
|
|
|
|
})
|
|
|
|
|
.then((response): Promise<void> => {
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Error while fetching log stream, status: ${response.status}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const reader = response.body?.getReader();
|
|
|
|
|
if (!reader) {
|
|
|
|
|
throw new Error("No reader available");
|
|
|
|
|
}
|
|
|
|
|
return processStreamChunk(reader);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
if (error.name !== "AbortError") {
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error instanceof Error
|
|
|
|
|
? error.message
|
|
|
|
|
: "An unknown error occurred";
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(
|
|
|
|
|
t("logs.toast.error.whileStreamingLogs", { errorMessage }),
|
|
|
|
|
);
|
2025-02-10 18:38:56 +03:00
|
|
|
}
|
|
|
|
|
});
|
2025-03-16 18:36:20 +03:00
|
|
|
}, [logService, filterSeverity, t]);
|
2025-02-10 18:38:56 +03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
setLogs([]);
|
|
|
|
|
lastFetchedIndexRef.current = -1;
|
|
|
|
|
fetchInitialLogs().then(() => {
|
|
|
|
|
// Start streaming after initial load
|
|
|
|
|
if (!logSettings.disableStreaming) {
|
|
|
|
|
fetchLogsStream();
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
2025-02-10 18:38:56 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
abortControllerRef.current?.abort();
|
2024-04-03 19:55:13 +03:00
|
|
|
};
|
2025-02-10 18:38:56 +03:00
|
|
|
// we know that these deps are correct
|
2024-05-29 21:05:39 +03:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2025-02-10 18:38:56 +03:00
|
|
|
}, [logService, filterSeverity]);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
// handlers
|
|
|
|
|
|
|
|
|
|
const prependLines = useCallback((newLines: string[]) => {
|
|
|
|
|
if (!lazyLogRef.current) return;
|
|
|
|
|
|
|
|
|
|
const newLinesArray = newLines.map(
|
|
|
|
|
(line) => new Uint8Array(new TextEncoder().encode(line + "\n")),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
lazyLogRef.current.setState((prevState) => ({
|
|
|
|
|
...prevState,
|
|
|
|
|
lines: prevState.lines.unshift(...newLinesArray),
|
|
|
|
|
count: prevState.count + newLines.length,
|
|
|
|
|
}));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// debounced
|
|
|
|
|
const handleScroll = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
debounce(() => {
|
|
|
|
|
const scrollThreshold =
|
|
|
|
|
lazyLogRef.current?.listRef.current?.findEndIndex() ?? 10;
|
|
|
|
|
const startIndex =
|
|
|
|
|
lazyLogRef.current?.listRef.current?.findStartIndex() ?? 0;
|
|
|
|
|
const endIndex =
|
|
|
|
|
lazyLogRef.current?.listRef.current?.findEndIndex() ?? 0;
|
|
|
|
|
const pageSize = endIndex - startIndex;
|
|
|
|
|
if (
|
|
|
|
|
scrollThreshold < pageSize + pageSize / 2 &&
|
|
|
|
|
lastFetchedIndexRef.current > 0 &&
|
|
|
|
|
!isLoading
|
|
|
|
|
) {
|
|
|
|
|
const nextEnd = lastFetchedIndexRef.current;
|
|
|
|
|
const nextStart = Math.max(0, nextEnd - (pageSize || 100));
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
|
|
|
|
fetchLogRange(nextStart, nextEnd).then((newLines) => {
|
|
|
|
|
if (newLines.length > 0) {
|
|
|
|
|
prependLines(newLines);
|
|
|
|
|
lastFetchedIndexRef.current = nextStart;
|
|
|
|
|
|
|
|
|
|
lazyLogRef.current?.listRef.current?.scrollTo(
|
|
|
|
|
newLines.length *
|
|
|
|
|
lazyLogRef.current?.listRef.current?.getItemSize(1),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, 50),
|
|
|
|
|
[fetchLogRange, isLoading, prependLines],
|
|
|
|
|
);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2023-12-31 16:31:33 +03:00
|
|
|
const handleCopyLogs = useCallback(() => {
|
2025-02-10 18:38:56 +03:00
|
|
|
if (logs.length) {
|
|
|
|
|
fetchInitialLogs()
|
|
|
|
|
.then(() => {
|
|
|
|
|
copy(logs.join("\n"));
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.success(t("logs.copy.success"));
|
2025-02-10 18:38:56 +03:00
|
|
|
})
|
|
|
|
|
.catch(() => {
|
2025-03-16 18:36:20 +03:00
|
|
|
toast.error(t("logs.copy.error"));
|
2025-02-10 18:38:56 +03:00
|
|
|
});
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
2025-03-16 18:36:20 +03:00
|
|
|
}, [logs, fetchInitialLogs, t]);
|
2023-12-31 16:31:33 +03:00
|
|
|
|
2024-08-19 17:53:33 +03:00
|
|
|
const handleDownloadLogs = useCallback(() => {
|
|
|
|
|
axios
|
|
|
|
|
.get(`logs/${logService}?download=true`)
|
|
|
|
|
.then((resp) => {
|
|
|
|
|
const element = document.createElement("a");
|
|
|
|
|
element.setAttribute(
|
|
|
|
|
"href",
|
|
|
|
|
"data:text/plain;charset=utf-8," + encodeURIComponent(resp.data),
|
|
|
|
|
);
|
|
|
|
|
element.setAttribute("download", `${logService}-logs.txt`);
|
|
|
|
|
|
|
|
|
|
element.style.display = "none";
|
|
|
|
|
document.body.appendChild(element);
|
|
|
|
|
|
|
|
|
|
element.click();
|
|
|
|
|
|
|
|
|
|
document.body.removeChild(element);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}, [logService]);
|
|
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const handleRowClick = useCallback(
|
|
|
|
|
(rowInfo: { lineNumber: number; rowIndex: number }) => {
|
|
|
|
|
const clickedLine = parseLogLines(logService, [
|
|
|
|
|
logs[rowInfo.rowIndex],
|
|
|
|
|
])[0];
|
|
|
|
|
setSelectedLog(clickedLine);
|
2024-02-24 03:25:00 +03:00
|
|
|
},
|
2025-02-10 18:38:56 +03:00
|
|
|
[logs, logService],
|
2024-02-24 03:25:00 +03:00
|
|
|
);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
// keyboard listener
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
useKeyboardListener(
|
|
|
|
|
["PageDown", "PageUp", "ArrowDown", "ArrowUp"],
|
|
|
|
|
(key, modifiers) => {
|
|
|
|
|
if (!key || !modifiers.down || !lazyLogWrapperRef.current) {
|
|
|
|
|
return;
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const container =
|
|
|
|
|
lazyLogWrapperRef.current.querySelector(".react-lazylog");
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const logLineHeight = container?.querySelector(".log-line")?.clientHeight;
|
2024-04-03 19:55:13 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
if (!logLineHeight) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-02-24 03:25:00 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const scrollAmount = key.includes("Page")
|
|
|
|
|
? logLineHeight * 10
|
|
|
|
|
: logLineHeight;
|
|
|
|
|
const direction = key.includes("Down") ? 1 : -1;
|
|
|
|
|
container?.scrollBy({ top: scrollAmount * direction });
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-04-07 23:36:08 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
// format lines
|
2024-04-07 23:36:08 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const lineBufferRef = useRef<string>("");
|
2024-04-07 23:36:08 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const formatPart = useCallback(
|
|
|
|
|
(text: string) => {
|
|
|
|
|
lineBufferRef.current += text;
|
2024-04-07 23:36:08 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
if (text.endsWith("\n")) {
|
|
|
|
|
const completeLine = lineBufferRef.current.trim();
|
|
|
|
|
lineBufferRef.current = "";
|
2024-05-29 21:05:39 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
if (completeLine) {
|
|
|
|
|
const parsedLine = parseLogLines(logService, [completeLine])[0];
|
|
|
|
|
return (
|
|
|
|
|
<LogLineData
|
|
|
|
|
line={parsedLine}
|
|
|
|
|
logService={logService}
|
|
|
|
|
onClickSeverity={() => setFilterSeverity([parsedLine.severity])}
|
|
|
|
|
onSelect={() => setSelectedLog(parsedLine)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-05-29 21:05:39 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
return null;
|
2024-05-29 21:05:39 +03:00
|
|
|
},
|
2025-02-10 18:38:56 +03:00
|
|
|
[logService, setFilterSeverity, setSelectedLog],
|
2024-05-29 21:05:39 +03:00
|
|
|
);
|
|
|
|
|
|
2024-10-31 22:48:26 +03:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleCopy = (e: ClipboardEvent) => {
|
|
|
|
|
e.preventDefault();
|
2025-02-10 18:38:56 +03:00
|
|
|
if (!lazyLogWrapperRef.current) return;
|
2024-10-31 22:48:26 +03:00
|
|
|
|
|
|
|
|
const selection = window.getSelection();
|
|
|
|
|
if (!selection) return;
|
|
|
|
|
|
|
|
|
|
const range = selection.getRangeAt(0);
|
|
|
|
|
const fragment = range.cloneContents();
|
|
|
|
|
|
|
|
|
|
const extractLogData = (element: Element) => {
|
|
|
|
|
const severity =
|
|
|
|
|
element.querySelector(".log-severity")?.textContent?.trim() || "";
|
|
|
|
|
const dateStamp =
|
|
|
|
|
element.querySelector(".log-timestamp")?.textContent?.trim() || "";
|
|
|
|
|
const section =
|
|
|
|
|
element.querySelector(".log-section")?.textContent?.trim() || "";
|
|
|
|
|
const content =
|
|
|
|
|
element.querySelector(".log-content")?.textContent?.trim() || "";
|
|
|
|
|
|
|
|
|
|
return { severity, dateStamp, section, content };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let copyData: {
|
|
|
|
|
severity: string;
|
|
|
|
|
dateStamp: string;
|
|
|
|
|
section: string;
|
|
|
|
|
content: string;
|
|
|
|
|
}[] = [];
|
|
|
|
|
|
|
|
|
|
if (fragment.querySelectorAll(".grid").length > 0) {
|
|
|
|
|
// Multiple grid elements
|
|
|
|
|
copyData = Array.from(fragment.querySelectorAll(".grid")).map(
|
|
|
|
|
extractLogData,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// Try to find the closest grid element or use the first child element
|
|
|
|
|
const gridElement =
|
|
|
|
|
fragment.querySelector(".grid") || (fragment.firstChild as Element);
|
|
|
|
|
|
|
|
|
|
if (gridElement) {
|
|
|
|
|
const data = extractLogData(gridElement);
|
|
|
|
|
if (data.severity || data.dateStamp || data.section || data.content) {
|
|
|
|
|
copyData.push(data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (copyData.length === 0) return; // No valid data to copy
|
|
|
|
|
|
|
|
|
|
// Calculate maximum widths for each column
|
|
|
|
|
const maxWidths = {
|
|
|
|
|
severity: Math.max(...copyData.map((d) => d.severity.length)),
|
|
|
|
|
dateStamp: Math.max(...copyData.map((d) => d.dateStamp.length)),
|
|
|
|
|
section: Math.max(...copyData.map((d) => d.section.length)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const pad = (str: string, length: number) => str.padEnd(length, " ");
|
|
|
|
|
|
|
|
|
|
// Create the formatted copy text
|
|
|
|
|
const copyText = copyData
|
|
|
|
|
.map(
|
|
|
|
|
(d) =>
|
|
|
|
|
`${pad(d.severity, maxWidths.severity)} | ${pad(d.dateStamp, maxWidths.dateStamp)} | ${pad(d.section, maxWidths.section)} | ${d.content}`,
|
|
|
|
|
)
|
|
|
|
|
.join("\n");
|
|
|
|
|
|
|
|
|
|
e.clipboardData?.setData("text/plain", copyText);
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
const content = lazyLogWrapperRef.current;
|
2024-10-31 22:48:26 +03:00
|
|
|
content?.addEventListener("copy", handleCopy);
|
|
|
|
|
return () => {
|
|
|
|
|
content?.removeEventListener("copy", handleCopy);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2023-12-08 16:33:22 +03:00
|
|
|
return (
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="flex size-full flex-col p-2">
|
2024-05-04 22:54:50 +03:00
|
|
|
<Toaster position="top-center" closeButton={true} />
|
2024-04-07 23:36:08 +03:00
|
|
|
<LogInfoDialog logLine={selectedLog} setLogLine={setSelectedLog} />
|
|
|
|
|
|
2024-08-19 17:53:33 +03:00
|
|
|
<div className="relative flex h-11 w-full items-center justify-between">
|
|
|
|
|
<ScrollArea className="w-full whitespace-nowrap">
|
|
|
|
|
<div ref={tabsRef} className="flex flex-row">
|
|
|
|
|
<ToggleGroup
|
|
|
|
|
type="single"
|
|
|
|
|
size="sm"
|
|
|
|
|
value={logService}
|
|
|
|
|
onValueChange={(value: LogType) => {
|
|
|
|
|
if (value) {
|
|
|
|
|
setLogs([]);
|
|
|
|
|
setFilterSeverity(undefined);
|
|
|
|
|
setLogService(value);
|
|
|
|
|
}
|
2025-02-10 18:38:56 +03:00
|
|
|
}}
|
2024-03-24 20:23:39 +03:00
|
|
|
>
|
2024-08-19 17:53:33 +03:00
|
|
|
{Object.values(logTypes).map((item) => (
|
|
|
|
|
<ToggleGroupItem
|
|
|
|
|
key={item}
|
|
|
|
|
className={`flex items-center justify-between gap-2 ${logService == item ? "" : "text-muted-foreground"}`}
|
|
|
|
|
value={item}
|
|
|
|
|
data-nav-item={item}
|
|
|
|
|
aria-label={`Select ${item}`}
|
|
|
|
|
>
|
2025-04-23 01:21:09 +03:00
|
|
|
<div className="smart-capitalize">{item}</div>
|
2024-08-19 17:53:33 +03:00
|
|
|
</ToggleGroupItem>
|
|
|
|
|
))}
|
|
|
|
|
</ToggleGroup>
|
|
|
|
|
<ScrollBar orientation="horizontal" className="h-0" />
|
|
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
2024-04-07 23:36:08 +03:00
|
|
|
<div className="flex items-center gap-2">
|
2024-03-24 20:23:39 +03:00
|
|
|
<Button
|
2024-05-14 18:06:44 +03:00
|
|
|
className="flex items-center justify-between gap-2"
|
2025-03-16 18:36:20 +03:00
|
|
|
aria-label={t("logs.copy.label")}
|
2024-03-24 20:23:39 +03:00
|
|
|
size="sm"
|
|
|
|
|
onClick={handleCopyLogs}
|
|
|
|
|
>
|
2024-04-16 23:55:24 +03:00
|
|
|
<FaCopy className="text-secondary-foreground" />
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="hidden text-primary md:block">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("logs.copy.label")}
|
2024-04-07 23:36:08 +03:00
|
|
|
</div>
|
2024-03-24 20:23:39 +03:00
|
|
|
</Button>
|
2024-08-19 17:53:33 +03:00
|
|
|
<Button
|
|
|
|
|
className="flex items-center justify-between gap-2"
|
2025-03-16 18:36:20 +03:00
|
|
|
aria-label={t("logs.download.label")}
|
2024-08-19 17:53:33 +03:00
|
|
|
size="sm"
|
|
|
|
|
onClick={handleDownloadLogs}
|
|
|
|
|
>
|
|
|
|
|
<FaDownload className="text-secondary-foreground" />
|
2025-03-16 18:36:20 +03:00
|
|
|
<div className="hidden text-primary md:block">
|
|
|
|
|
{t("button.download", { ns: "common" })}
|
|
|
|
|
</div>
|
2024-08-19 17:53:33 +03:00
|
|
|
</Button>
|
2025-02-10 18:38:56 +03:00
|
|
|
<LogSettingsButton
|
2024-04-07 23:36:08 +03:00
|
|
|
selectedLabels={filterSeverity}
|
|
|
|
|
updateLabelFilter={setFilterSeverity}
|
2025-02-10 18:38:56 +03:00
|
|
|
logSettings={logSettings}
|
|
|
|
|
setLogSettings={setLogSettings}
|
2024-04-07 23:36:08 +03:00
|
|
|
/>
|
2023-12-31 16:31:33 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
<div className="font-mono relative my-2 flex size-full flex-col overflow-hidden whitespace-pre-wrap rounded-md border border-secondary bg-background_alt text-xs sm:p-1">
|
|
|
|
|
<div className="grid grid-cols-5 *:px-0 *:py-3 *:text-sm *:text-primary/40 md:grid-cols-12">
|
|
|
|
|
<div className="col-span-3 lg:col-span-2">
|
|
|
|
|
<div className="flex w-full flex-row items-center">
|
2025-04-23 01:21:09 +03:00
|
|
|
<div className="ml-1 min-w-16 smart-capitalize lg:min-w-20">
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("logs.type.label")}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mr-3">{t("logs.type.timestamp")}</div>
|
2025-02-10 18:38:56 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center",
|
|
|
|
|
logService == "frigate" ? "col-span-2" : "col-span-1",
|
|
|
|
|
)}
|
|
|
|
|
>
|
2025-03-16 18:36:20 +03:00
|
|
|
{t("logs.type.tag")}
|
2024-04-03 19:55:13 +03:00
|
|
|
</div>
|
2025-02-10 18:38:56 +03:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"col-span-5 flex items-center",
|
|
|
|
|
logService == "frigate"
|
|
|
|
|
? "md:col-span-7 lg:col-span-8"
|
|
|
|
|
: "md:col-span-8 lg:col-span-9",
|
|
|
|
|
)}
|
|
|
|
|
>
|
2025-03-16 18:36:20 +03:00
|
|
|
<div className="flex flex-1">{t("logs.type.message")}</div>
|
2024-04-03 19:55:13 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-04-07 23:36:08 +03:00
|
|
|
|
2025-02-10 18:38:56 +03:00
|
|
|
<div ref={lazyLogWrapperRef} className="size-full">
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
|
|
|
) : (
|
|
|
|
|
<EnhancedScrollFollow
|
|
|
|
|
startFollowing={!isLoading}
|
|
|
|
|
onCustomScroll={handleScroll}
|
|
|
|
|
render={({ follow, onScroll }) => (
|
|
|
|
|
<>
|
|
|
|
|
{follow && !logSettings.disableStreaming && (
|
|
|
|
|
<div className="absolute right-1 top-3">
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger>
|
|
|
|
|
<MdCircle className="mr-2 size-2 animate-pulse cursor-default text-selected shadow-selected drop-shadow-md" />
|
|
|
|
|
</TooltipTrigger>
|
2025-03-16 18:36:20 +03:00
|
|
|
<TooltipContent>{t("logs.tips")}</TooltipContent>
|
2025-02-10 18:38:56 +03:00
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<LazyLog
|
|
|
|
|
ref={lazyLogRef}
|
|
|
|
|
enableLineNumbers={false}
|
|
|
|
|
selectableLines
|
|
|
|
|
lineClassName="text-primary bg-background"
|
|
|
|
|
highlightLineClassName="bg-primary/20"
|
|
|
|
|
onRowClick={handleRowClick}
|
|
|
|
|
formatPart={formatPart}
|
|
|
|
|
text={logs.join("\n")}
|
|
|
|
|
follow={follow}
|
|
|
|
|
onScroll={onScroll}
|
|
|
|
|
loadingComponent={
|
|
|
|
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
2024-04-07 23:36:08 +03:00
|
|
|
}
|
2025-02-10 18:38:56 +03:00
|
|
|
loading={isLoading}
|
2024-04-07 23:36:08 +03:00
|
|
|
/>
|
2025-02-10 18:38:56 +03:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2024-04-07 23:36:08 +03:00
|
|
|
</div>
|
2024-04-03 19:55:13 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LogLineDataProps = {
|
2025-02-10 18:38:56 +03:00
|
|
|
className?: string;
|
2024-04-03 19:55:13 +03:00
|
|
|
line: LogLine;
|
2025-02-10 18:38:56 +03:00
|
|
|
logService: string;
|
2024-04-07 23:36:08 +03:00
|
|
|
onClickSeverity: () => void;
|
|
|
|
|
onSelect: () => void;
|
2024-04-03 19:55:13 +03:00
|
|
|
};
|
2025-02-10 18:38:56 +03:00
|
|
|
|
2024-04-07 23:36:08 +03:00
|
|
|
function LogLineData({
|
|
|
|
|
className,
|
|
|
|
|
line,
|
2025-02-10 18:38:56 +03:00
|
|
|
logService,
|
2024-04-07 23:36:08 +03:00
|
|
|
onClickSeverity,
|
|
|
|
|
onSelect,
|
|
|
|
|
}: LogLineDataProps) {
|
2024-04-03 19:55:13 +03:00
|
|
|
return (
|
|
|
|
|
<div
|
2024-05-07 17:00:25 +03:00
|
|
|
className={cn(
|
2025-02-10 18:38:56 +03:00
|
|
|
"grid w-full cursor-pointer grid-cols-5 gap-2 border-t border-secondary bg-background_alt py-1 hover:bg-muted md:grid-cols-12 md:py-0",
|
2024-05-07 17:00:25 +03:00
|
|
|
className,
|
2025-02-10 18:38:56 +03:00
|
|
|
"text-xs lg:text-sm/5",
|
2024-05-07 17:00:25 +03:00
|
|
|
)}
|
2024-04-07 23:36:08 +03:00
|
|
|
onClick={onSelect}
|
2024-04-03 19:55:13 +03:00
|
|
|
>
|
2025-02-10 18:38:56 +03:00
|
|
|
<div className="col-span-3 flex h-full items-center gap-2 lg:col-span-2">
|
|
|
|
|
<div className="flex w-full flex-row items-center">
|
|
|
|
|
<div className="log-severity p-1">
|
|
|
|
|
<LogChip
|
|
|
|
|
severity={line.severity}
|
|
|
|
|
onClickSeverity={onClickSeverity}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="log-timestamp whitespace-normal">
|
|
|
|
|
{line.dateStamp}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-04-03 19:55:13 +03:00
|
|
|
</div>
|
2025-02-10 18:38:56 +03:00
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"log-section flex size-full items-center pr-2",
|
|
|
|
|
logService == "frigate" ? "col-span-2" : "col-span-1",
|
|
|
|
|
)}
|
|
|
|
|
>
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
2024-04-07 23:36:08 +03:00
|
|
|
{line.section}
|
|
|
|
|
</div>
|
2024-04-03 19:55:13 +03:00
|
|
|
</div>
|
2025-02-10 18:38:56 +03:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"log-content col-span-5 flex size-full items-center justify-between px-2 md:px-0 md:pr-2",
|
|
|
|
|
logService == "frigate"
|
|
|
|
|
? "md:col-span-7 lg:col-span-8"
|
|
|
|
|
: "md:col-span-8 lg:col-span-9",
|
|
|
|
|
)}
|
|
|
|
|
>
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
2024-04-03 19:55:13 +03:00
|
|
|
{line.content}
|
|
|
|
|
</div>
|
2023-12-31 16:31:33 +03:00
|
|
|
</div>
|
2024-02-21 23:07:32 +03:00
|
|
|
</div>
|
2023-12-08 16:33:22 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Logs;
|