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";
|
2024-05-29 21:05:39 +03:00
|
|
|
import { LogData, LogLine, 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";
|
|
|
|
|
import { LogLevelFilterButton } from "@/components/filter/LogLevelFilter";
|
|
|
|
|
import { FaCopy } from "react-icons/fa6";
|
|
|
|
|
import { Toaster } from "@/components/ui/sonner";
|
|
|
|
|
import { toast } from "sonner";
|
2024-05-29 21:05:39 +03:00
|
|
|
import {
|
|
|
|
|
isDesktop,
|
|
|
|
|
isMobile,
|
|
|
|
|
isMobileOnly,
|
|
|
|
|
isTablet,
|
|
|
|
|
} from "react-device-detect";
|
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-22 16:14:48 +03:00
|
|
|
import { MdVerticalAlignBottom } from "react-icons/md";
|
2024-05-29 21:05:39 +03:00
|
|
|
import { parseLogLines } from "@/utils/logUtil";
|
|
|
|
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
2024-08-19 17:53:33 +03:00
|
|
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|
|
|
|
import scrollIntoView from "scroll-into-view-if-needed";
|
|
|
|
|
import { FaDownload } from "react-icons/fa";
|
2023-12-08 16:33:22 +03:00
|
|
|
|
2024-04-03 19:55:13 +03:00
|
|
|
type LogRange = { start: number; end: number };
|
|
|
|
|
|
2023-12-08 16:33:22 +03:00
|
|
|
function Logs() {
|
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);
|
2023-12-31 16:31:33 +03:00
|
|
|
|
2024-04-12 15:31:30 +03:00
|
|
|
useEffect(() => {
|
2024-04-16 23:55:24 +03:00
|
|
|
document.title = `${logService[0].toUpperCase()}${logService.substring(1)} Logs - Frigate`;
|
2024-04-12 15:31:30 +03:00
|
|
|
}, [logService]);
|
|
|
|
|
|
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, {
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
inline: "start",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [tabsRef, logService]);
|
|
|
|
|
|
2024-04-03 19:55:13 +03:00
|
|
|
// log data handling
|
|
|
|
|
|
2024-05-29 21:05:39 +03:00
|
|
|
const logPageSize = useMemo(() => {
|
|
|
|
|
if (isMobileOnly) {
|
|
|
|
|
return 15;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isTablet) {
|
|
|
|
|
return 25;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 40;
|
|
|
|
|
}, []);
|
|
|
|
|
|
2024-04-03 19:55:13 +03:00
|
|
|
const [logRange, setLogRange] = useState<LogRange>({ start: 0, end: 0 });
|
|
|
|
|
const [logs, setLogs] = useState<string[]>([]);
|
2024-05-29 21:05:39 +03:00
|
|
|
const [logLines, setLogLines] = useState<LogLine[]>([]);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
axios
|
2024-05-29 21:05:39 +03:00
|
|
|
.get(`logs/${logService}?start=-${logPageSize}`)
|
2024-04-03 19:55:13 +03:00
|
|
|
.then((resp) => {
|
|
|
|
|
if (resp.status == 200) {
|
|
|
|
|
const data = resp.data as LogData;
|
|
|
|
|
setLogRange({
|
2024-05-29 21:05:39 +03:00
|
|
|
start: Math.max(0, data.totalLines - logPageSize),
|
2024-04-03 19:55:13 +03:00
|
|
|
end: data.totalLines,
|
|
|
|
|
});
|
|
|
|
|
setLogs(data.lines);
|
2024-05-29 21:05:39 +03:00
|
|
|
setLogLines(parseLogLines(logService, data.lines));
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
2024-05-29 21:05:39 +03:00
|
|
|
}, [logPageSize, logService]);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!logs || logs.length == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const id = setTimeout(() => {
|
|
|
|
|
axios
|
|
|
|
|
.get(`logs/${logService}?start=${logRange.end}`)
|
|
|
|
|
.then((resp) => {
|
|
|
|
|
if (resp.status == 200) {
|
|
|
|
|
const data = resp.data as LogData;
|
|
|
|
|
|
|
|
|
|
if (data.lines.length > 0) {
|
|
|
|
|
setLogRange({
|
|
|
|
|
start: logRange.start,
|
|
|
|
|
end: data.totalLines,
|
|
|
|
|
});
|
|
|
|
|
setLogs([...logs, ...data.lines]);
|
2024-05-29 21:05:39 +03:00
|
|
|
setLogLines([
|
|
|
|
|
...logLines,
|
|
|
|
|
...parseLogLines(logService, data.lines),
|
|
|
|
|
]);
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (id) {
|
|
|
|
|
clearTimeout(id);
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-05-29 21:05:39 +03:00
|
|
|
// we need to listen on the current range of visible items
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [logLines, logService, logRange]);
|
2024-04-03 19:55:13 +03:00
|
|
|
|
|
|
|
|
// convert to log data
|
|
|
|
|
|
2023-12-31 16:31:33 +03:00
|
|
|
const handleCopyLogs = useCallback(() => {
|
2024-04-03 19:55:13 +03:00
|
|
|
if (logs) {
|
|
|
|
|
copy(logs.join("\n"));
|
2024-04-07 23:36:08 +03:00
|
|
|
toast.success(
|
|
|
|
|
logRange.start == 0
|
2024-05-20 16:37:56 +03:00
|
|
|
? "Copied logs to clipboard"
|
2024-04-07 23:36:08 +03:00
|
|
|
: "Copied visible logs to clipboard",
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
toast.error("Could not copy logs to clipboard");
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
2024-04-07 23:36:08 +03:00
|
|
|
}, [logs, logRange]);
|
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]);
|
|
|
|
|
|
2024-04-03 19:55:13 +03:00
|
|
|
// scroll to bottom
|
|
|
|
|
|
|
|
|
|
const [initialScroll, setInitialScroll] = useState(false);
|
2024-02-24 03:25:00 +03:00
|
|
|
|
|
|
|
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
const [endVisible, setEndVisible] = useState(true);
|
2024-04-03 19:55:13 +03:00
|
|
|
const endObserver = useRef<IntersectionObserver | null>(null);
|
2024-02-24 03:25:00 +03:00
|
|
|
const endLogRef = useCallback(
|
|
|
|
|
(node: HTMLElement | null) => {
|
2024-04-03 19:55:13 +03:00
|
|
|
if (endObserver.current) endObserver.current.disconnect();
|
2024-02-24 03:25:00 +03:00
|
|
|
try {
|
2024-04-03 19:55:13 +03:00
|
|
|
endObserver.current = new IntersectionObserver((entries) => {
|
2024-02-24 03:25:00 +03:00
|
|
|
setEndVisible(entries[0].isIntersecting);
|
|
|
|
|
});
|
2024-04-03 19:55:13 +03:00
|
|
|
if (node) endObserver.current.observe(node);
|
2024-02-24 03:25:00 +03:00
|
|
|
} catch (e) {
|
|
|
|
|
// no op
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-02-29 01:23:56 +03:00
|
|
|
[setEndVisible],
|
2024-02-24 03:25:00 +03:00
|
|
|
);
|
2024-04-03 19:55:13 +03:00
|
|
|
const startObserver = useRef<IntersectionObserver | null>(null);
|
|
|
|
|
const startLogRef = useCallback(
|
|
|
|
|
(node: HTMLElement | null) => {
|
|
|
|
|
if (startObserver.current) startObserver.current.disconnect();
|
|
|
|
|
|
|
|
|
|
if (logs.length == 0 || !initialScroll) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2024-05-29 21:05:39 +03:00
|
|
|
startObserver.current = new IntersectionObserver(
|
|
|
|
|
(entries) => {
|
|
|
|
|
if (entries[0].isIntersecting && logRange.start > 0) {
|
|
|
|
|
const start = Math.max(0, logRange.start - logPageSize);
|
|
|
|
|
|
|
|
|
|
axios
|
|
|
|
|
.get(`logs/${logService}?start=${start}&end=${logRange.start}`)
|
|
|
|
|
.then((resp) => {
|
|
|
|
|
if (resp.status == 200) {
|
|
|
|
|
const data = resp.data as LogData;
|
|
|
|
|
|
|
|
|
|
if (data.lines.length > 0) {
|
|
|
|
|
setLogRange({
|
|
|
|
|
start: start,
|
|
|
|
|
end: logRange.end,
|
|
|
|
|
});
|
|
|
|
|
setLogs([...data.lines, ...logs]);
|
|
|
|
|
setLogLines([
|
|
|
|
|
...parseLogLines(logService, data.lines),
|
|
|
|
|
...logLines,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2024-04-03 19:55:13 +03:00
|
|
|
}
|
2024-05-29 21:05:39 +03:00
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
contentRef.current?.scrollBy({
|
|
|
|
|
top: 10,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ rootMargin: `${10 * (isMobile ? 64 : 48)}px 0px 0px 0px` },
|
|
|
|
|
);
|
2024-04-03 19:55:13 +03:00
|
|
|
if (node) startObserver.current.observe(node);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// no op
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// we need to listen on the current range of visible items
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
[logRange, initialScroll],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (logLines.length == 0) {
|
|
|
|
|
setInitialScroll(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (initialScroll) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!contentRef.current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contentRef.current.scrollHeight <= contentRef.current.clientHeight) {
|
|
|
|
|
setInitialScroll(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
contentRef.current?.scrollTo({
|
|
|
|
|
top: contentRef.current?.scrollHeight,
|
|
|
|
|
behavior: "instant",
|
|
|
|
|
});
|
|
|
|
|
setTimeout(() => setInitialScroll(true), 300);
|
|
|
|
|
// we need to listen on the current range of visible items
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [logLines, logService]);
|
2024-02-24 03:25:00 +03:00
|
|
|
|
2024-04-07 23:36:08 +03:00
|
|
|
// log filtering
|
|
|
|
|
|
|
|
|
|
const [filterSeverity, setFilterSeverity] = useState<LogSeverity[]>();
|
|
|
|
|
|
|
|
|
|
// log selection
|
|
|
|
|
|
|
|
|
|
const [selectedLog, setSelectedLog] = useState<LogLine>();
|
|
|
|
|
|
2024-05-29 21:05:39 +03:00
|
|
|
// interaction
|
|
|
|
|
|
|
|
|
|
useKeyboardListener(
|
|
|
|
|
["PageDown", "PageUp", "ArrowDown", "ArrowUp"],
|
2024-06-18 17:32:17 +03:00
|
|
|
(key, modifiers) => {
|
|
|
|
|
if (!modifiers.down) {
|
2024-05-29 21:05:39 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
|
case "PageDown":
|
|
|
|
|
contentRef.current?.scrollBy({
|
|
|
|
|
top: 480,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "PageUp":
|
|
|
|
|
contentRef.current?.scrollBy({
|
|
|
|
|
top: -480,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "ArrowDown":
|
|
|
|
|
contentRef.current?.scrollBy({
|
|
|
|
|
top: 48,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "ArrowUp":
|
|
|
|
|
contentRef.current?.scrollBy({
|
|
|
|
|
top: -48,
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-31 22:48:26 +03:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleCopy = (e: ClipboardEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!contentRef.current) return;
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const content = contentRef.current;
|
|
|
|
|
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([]);
|
|
|
|
|
setLogLines([]);
|
|
|
|
|
setFilterSeverity(undefined);
|
|
|
|
|
setLogService(value);
|
|
|
|
|
}
|
|
|
|
|
}} // don't allow the severity to be unselected
|
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}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="capitalize">{item}</div>
|
|
|
|
|
</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"
|
2024-10-23 01:07:42 +03:00
|
|
|
aria-label="Copy logs to clipboard"
|
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">
|
2024-04-07 23:36:08 +03:00
|
|
|
Copy to Clipboard
|
|
|
|
|
</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"
|
2024-10-23 01:07:42 +03:00
|
|
|
aria-label="Download logs"
|
2024-08-19 17:53:33 +03:00
|
|
|
size="sm"
|
|
|
|
|
onClick={handleDownloadLogs}
|
|
|
|
|
>
|
|
|
|
|
<FaDownload className="text-secondary-foreground" />
|
|
|
|
|
<div className="hidden text-primary md:block">Download</div>
|
|
|
|
|
</Button>
|
2024-04-07 23:36:08 +03:00
|
|
|
<LogLevelFilterButton
|
|
|
|
|
selectedLabels={filterSeverity}
|
|
|
|
|
updateLabelFilter={setFilterSeverity}
|
|
|
|
|
/>
|
2023-12-31 16:31:33 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2024-04-03 19:55:13 +03:00
|
|
|
{initialScroll && !endVisible && (
|
2024-02-28 17:16:32 +03:00
|
|
|
<Button
|
2024-05-22 16:14:48 +03:00
|
|
|
className="absolute bottom-8 left-[50%] z-20 flex -translate-x-[50%] items-center gap-1 rounded-md p-2"
|
2024-10-23 01:07:42 +03:00
|
|
|
aria-label="Jump to bottom of logs"
|
2024-02-24 03:25:00 +03:00
|
|
|
onClick={() =>
|
|
|
|
|
contentRef.current?.scrollTo({
|
|
|
|
|
top: contentRef.current?.scrollHeight,
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
>
|
2024-05-22 16:14:48 +03:00
|
|
|
<MdVerticalAlignBottom />
|
2024-02-24 03:25:00 +03:00
|
|
|
Jump to Bottom
|
2024-02-28 17:16:32 +03:00
|
|
|
</Button>
|
2024-02-24 03:25:00 +03:00
|
|
|
)}
|
|
|
|
|
|
2024-05-14 18:06:44 +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-sm sm:p-2">
|
|
|
|
|
<div className="grid grid-cols-5 *:px-2 *:py-3 *:text-sm *:text-primary/40 sm:grid-cols-8 md:grid-cols-12">
|
|
|
|
|
<div className="flex items-center p-1 capitalize">Type</div>
|
|
|
|
|
<div className="col-span-2 flex items-center sm:col-span-1">
|
2024-04-03 19:55:13 +03:00
|
|
|
Timestamp
|
|
|
|
|
</div>
|
2024-04-07 23:36:08 +03:00
|
|
|
<div className="col-span-2 flex items-center">Tag</div>
|
2024-05-14 18:06:44 +03:00
|
|
|
<div className="col-span-5 flex items-center sm:col-span-4 md:col-span-8">
|
2024-04-03 19:55:13 +03:00
|
|
|
Message
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-04-07 23:36:08 +03:00
|
|
|
<div
|
|
|
|
|
ref={contentRef}
|
2024-05-14 18:06:44 +03:00
|
|
|
className="no-scrollbar flex w-full flex-col overflow-y-auto overscroll-contain"
|
2024-04-07 23:36:08 +03:00
|
|
|
>
|
|
|
|
|
{logLines.length > 0 &&
|
|
|
|
|
[...Array(logRange.end).keys()].map((idx) => {
|
|
|
|
|
const logLine =
|
|
|
|
|
idx >= logRange.start
|
|
|
|
|
? logLines[idx - logRange.start]
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
if (logLine) {
|
|
|
|
|
const line = logLines[idx - logRange.start];
|
|
|
|
|
if (filterSeverity && !filterSeverity.includes(line.severity)) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={idx == logRange.start + 10 ? startLogRef : undefined}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<LogLineData
|
|
|
|
|
key={`${idx}-${logService}`}
|
|
|
|
|
startRef={
|
|
|
|
|
idx == logRange.start + 10 ? startLogRef : undefined
|
|
|
|
|
}
|
|
|
|
|
className={initialScroll ? "" : "invisible"}
|
|
|
|
|
line={line}
|
|
|
|
|
onClickSeverity={() => setFilterSeverity([line.severity])}
|
|
|
|
|
onSelect={() => setSelectedLog(line)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-03 19:55:13 +03:00
|
|
|
return (
|
2024-04-07 23:36:08 +03:00
|
|
|
<div
|
2024-04-03 19:55:13 +03:00
|
|
|
key={`${idx}-${logService}`}
|
2024-04-07 23:36:08 +03:00
|
|
|
className={isDesktop ? "h-12" : "h-16"}
|
2024-04-03 19:55:13 +03:00
|
|
|
/>
|
|
|
|
|
);
|
2024-04-07 23:36:08 +03:00
|
|
|
})}
|
|
|
|
|
{logLines.length > 0 && <div id="page-bottom" ref={endLogRef} />}
|
|
|
|
|
</div>
|
2024-04-14 19:14:10 +03:00
|
|
|
{logLines.length == 0 && (
|
2024-05-14 18:06:44 +03:00
|
|
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
2024-04-14 19:14:10 +03:00
|
|
|
)}
|
2024-04-03 19:55:13 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LogLineDataProps = {
|
|
|
|
|
startRef?: (node: HTMLDivElement | null) => void;
|
|
|
|
|
className: string;
|
|
|
|
|
line: LogLine;
|
2024-04-07 23:36:08 +03:00
|
|
|
onClickSeverity: () => void;
|
|
|
|
|
onSelect: () => void;
|
2024-04-03 19:55:13 +03:00
|
|
|
};
|
2024-04-07 23:36:08 +03:00
|
|
|
function LogLineData({
|
|
|
|
|
startRef,
|
|
|
|
|
className,
|
|
|
|
|
line,
|
|
|
|
|
onClickSeverity,
|
|
|
|
|
onSelect,
|
|
|
|
|
}: LogLineDataProps) {
|
2024-04-03 19:55:13 +03:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={startRef}
|
2024-05-07 17:00:25 +03:00
|
|
|
className={cn(
|
2024-05-14 18:06:44 +03:00
|
|
|
"grid w-full cursor-pointer grid-cols-5 gap-2 border-t border-secondary py-2 hover:bg-muted sm:grid-cols-8 md:grid-cols-12",
|
2024-05-07 17:00:25 +03:00
|
|
|
className,
|
|
|
|
|
"*:text-sm",
|
|
|
|
|
)}
|
2024-04-07 23:36:08 +03:00
|
|
|
onClick={onSelect}
|
2024-04-03 19:55:13 +03:00
|
|
|
>
|
2024-10-31 22:48:26 +03:00
|
|
|
<div className="log-severity flex h-full items-center gap-2 p-1">
|
2024-04-07 23:36:08 +03:00
|
|
|
<LogChip severity={line.severity} onClickSeverity={onClickSeverity} />
|
2024-04-03 19:55:13 +03:00
|
|
|
</div>
|
2024-10-31 22:48:26 +03:00
|
|
|
<div className="log-timestamp col-span-2 flex h-full items-center sm:col-span-1">
|
2024-04-03 19:55:13 +03:00
|
|
|
{line.dateStamp}
|
|
|
|
|
</div>
|
2024-10-31 22:48:26 +03:00
|
|
|
<div className="log-section col-span-2 flex size-full items-center pr-2">
|
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>
|
2024-10-31 22:48:26 +03:00
|
|
|
<div className="log-content col-span-5 flex size-full items-center justify-between pl-2 pr-2 sm:col-span-4 sm:pl-0 md:col-span-8">
|
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;
|