mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-27 00:48:58 +03:00
Improve UI logs (#16434)
* use react-logviewer and backend streaming * layout adjustments * readd copy handler * reorder and fix key * add loading state * handle frigate log consolidation * handle newlines in sheet * update react-logviewer * fix scrolling and use chunked log download * don't combine frigate log lines with timestamp * basic deduplication * use react-logviewer and backend streaming * layout adjustments * readd copy handler * reorder and fix key * add loading state * handle frigate log consolidation * handle newlines in sheet * update react-logviewer * fix scrolling and use chunked log download * don't combine frigate log lines with timestamp * basic deduplication * move process logs function to services util * improve layout and scrolling behavior * clean up
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { useRef, useCallback, useEffect, type ReactNode } from "react";
|
||||
import { ScrollFollow } from "@melloware/react-logviewer";
|
||||
|
||||
export type ScrollFollowProps = {
|
||||
startFollowing?: boolean;
|
||||
render: (renderProps: ScrollFollowRenderProps) => ReactNode;
|
||||
onCustomScroll?: (
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
clientHeight: number,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type ScrollFollowRenderProps = {
|
||||
follow: boolean;
|
||||
onScroll: (args: {
|
||||
scrollTop: number;
|
||||
scrollHeight: number;
|
||||
clientHeight: number;
|
||||
}) => void;
|
||||
startFollowing: () => void;
|
||||
stopFollowing: () => void;
|
||||
onCustomScroll?: (
|
||||
scrollTop: number,
|
||||
scrollHeight: number,
|
||||
clientHeight: number,
|
||||
) => void;
|
||||
};
|
||||
|
||||
const SCROLL_BUFFER = 5;
|
||||
|
||||
export default function EnhancedScrollFollow(props: ScrollFollowProps) {
|
||||
const followRef = useRef(props.startFollowing || false);
|
||||
const prevScrollTopRef = useRef<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
prevScrollTopRef.current = undefined;
|
||||
}, []);
|
||||
|
||||
const wrappedRender = useCallback(
|
||||
(renderProps: ScrollFollowRenderProps) => {
|
||||
const wrappedOnScroll = (args: {
|
||||
scrollTop: number;
|
||||
scrollHeight: number;
|
||||
clientHeight: number;
|
||||
}) => {
|
||||
// Check if scrolling up and immediately stop following
|
||||
if (
|
||||
prevScrollTopRef.current !== undefined &&
|
||||
args.scrollTop < prevScrollTopRef.current
|
||||
) {
|
||||
if (followRef.current) {
|
||||
renderProps.stopFollowing();
|
||||
followRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
const bottomThreshold =
|
||||
args.scrollHeight - args.clientHeight - SCROLL_BUFFER;
|
||||
const isNearBottom = args.scrollTop >= bottomThreshold;
|
||||
|
||||
if (isNearBottom && !followRef.current) {
|
||||
renderProps.startFollowing();
|
||||
followRef.current = true;
|
||||
} else if (!isNearBottom && followRef.current) {
|
||||
renderProps.stopFollowing();
|
||||
followRef.current = false;
|
||||
}
|
||||
|
||||
prevScrollTopRef.current = args.scrollTop;
|
||||
renderProps.onScroll(args);
|
||||
if (props.onCustomScroll) {
|
||||
props.onCustomScroll(
|
||||
args.scrollTop,
|
||||
args.scrollHeight,
|
||||
args.clientHeight,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return props.render({
|
||||
...renderProps,
|
||||
onScroll: wrappedOnScroll,
|
||||
follow: followRef.current,
|
||||
});
|
||||
},
|
||||
[props],
|
||||
);
|
||||
|
||||
return <ScrollFollow {...props} render={wrappedRender} />;
|
||||
}
|
||||
+50
-14
@@ -1,36 +1,73 @@
|
||||
import { Button } from "../ui/button";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { FaCog } from "react-icons/fa";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { LogSeverity } from "@/types/log";
|
||||
import { LogSettingsType, LogSeverity } from "@/types/log";
|
||||
import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import FilterSwitch from "./FilterSwitch";
|
||||
|
||||
type LogLevelFilterButtonProps = {
|
||||
type LogSettingsButtonProps = {
|
||||
selectedLabels?: LogSeverity[];
|
||||
updateLabelFilter: (labels: LogSeverity[] | undefined) => void;
|
||||
logSettings?: LogSettingsType;
|
||||
setLogSettings: (logSettings: LogSettingsType) => void;
|
||||
};
|
||||
export function LogLevelFilterButton({
|
||||
export function LogSettingsButton({
|
||||
selectedLabels,
|
||||
updateLabelFilter,
|
||||
}: LogLevelFilterButtonProps) {
|
||||
logSettings,
|
||||
setLogSettings,
|
||||
}: LogSettingsButtonProps) {
|
||||
const trigger = (
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Filter log level"
|
||||
>
|
||||
<FaFilter className="text-secondary-foreground" />
|
||||
<div className="hidden text-primary md:block">Filter</div>
|
||||
<FaCog className="text-secondary-foreground" />
|
||||
<div className="hidden text-primary md:block">Settings</div>
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
<GeneralFilterContent
|
||||
selectedLabels={selectedLabels}
|
||||
updateLabelFilter={updateLabelFilter}
|
||||
/>
|
||||
<div className={cn("my-3 space-y-3 py-3 md:mt-0 md:py-0")}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Filter</div>
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
Filter logs by severity.
|
||||
</div>
|
||||
</div>
|
||||
<GeneralFilterContent
|
||||
selectedLabels={selectedLabels}
|
||||
updateLabelFilter={updateLabelFilter}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-md">Loading</div>
|
||||
<div className="mt-2.5 flex flex-col gap-2.5">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
When the log pane is scrolled to the bottom, new logs
|
||||
automatically stream as they are added.
|
||||
</div>
|
||||
<FilterSwitch
|
||||
label="Disable log streaming"
|
||||
isChecked={logSettings?.disableStreaming ?? false}
|
||||
onCheckedChange={(isChecked) => {
|
||||
setLogSettings({
|
||||
disableStreaming: isChecked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
@@ -63,7 +100,7 @@ export function GeneralFilterContent({
|
||||
return (
|
||||
<>
|
||||
<div className="scrollbar-container h-auto overflow-y-auto overflow-x-hidden">
|
||||
<div className="my-2.5 flex items-center justify-between">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<Label
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
htmlFor="allLabels"
|
||||
@@ -81,10 +118,9 @@ export function GeneralFilterContent({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="my-2.5 flex flex-col gap-2.5">
|
||||
{["debug", "info", "warning", "error"].map((item) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between" key={item}>
|
||||
<Label
|
||||
className="mx-2 w-full cursor-pointer capitalize text-primary"
|
||||
htmlFor={item}
|
||||
@@ -70,17 +70,19 @@ export function LogChip({ severity, onClickSeverity }: LogChipProps) {
|
||||
}, [severity]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-md px-1 py-[1px] text-xs capitalize ${onClickSeverity ? "cursor-pointer" : ""} ${severityClassName}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
<div className="min-w-16 lg:min-w-20">
|
||||
<span
|
||||
className={`rounded-md px-1 py-[1px] text-xs capitalize ${onClickSeverity ? "cursor-pointer" : ""} ${severityClassName}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (onClickSeverity) {
|
||||
onClickSeverity();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{severity}
|
||||
if (onClickSeverity) {
|
||||
onClickSeverity();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{severity}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,14 @@ export default function LogInfoDialog({
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">Message</div>
|
||||
<div className="text-sm">{logLine.content}</div>
|
||||
<div className="text-sm">
|
||||
{logLine.content.split("\n").map((line) => (
|
||||
<>
|
||||
{line}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{helpfulLinks.length > 0 && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user