mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Redesign logs page (#10853)
* Adjust outline and structure to match designs * More color changes to fit design * Properly parse go2rtc severity * Add ability to filter by clicking item * Implement sheet / drawer for viewing full log * Add toast and filtering * Add links to docs when specific log items are selected * Cleanup log seeking * Use header in layout * Fix mobile menus * Fix safari theme * Hide rings * Theme adjustment
This commit is contained in:
		
							parent
							
								
									b26ceff44d
								
							
						
					
					
						commit
						cf2dfd9a54
					
				
							
								
								
									
										126
									
								
								web/src/components/filter/LogLevelFilter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								web/src/components/filter/LogLevelFilter.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					import { Button } from "../ui/button";
 | 
				
			||||||
 | 
					import { FaFilter } 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 { Label } from "../ui/label";
 | 
				
			||||||
 | 
					import { Switch } from "../ui/switch";
 | 
				
			||||||
 | 
					import { DropdownMenuSeparator } from "../ui/dropdown-menu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type LogLevelFilterButtonProps = {
 | 
				
			||||||
 | 
					  selectedLabels?: LogSeverity[];
 | 
				
			||||||
 | 
					  updateLabelFilter: (labels: LogSeverity[] | undefined) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function LogLevelFilterButton({
 | 
				
			||||||
 | 
					  selectedLabels,
 | 
				
			||||||
 | 
					  updateLabelFilter,
 | 
				
			||||||
 | 
					}: LogLevelFilterButtonProps) {
 | 
				
			||||||
 | 
					  const trigger = (
 | 
				
			||||||
 | 
					    <Button size="sm" className="flex items-center gap-2" variant="secondary">
 | 
				
			||||||
 | 
					      <FaFilter className="text-secondary-foreground" />
 | 
				
			||||||
 | 
					      <div className="hidden md:block text-primary-foreground">Filter</div>
 | 
				
			||||||
 | 
					    </Button>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const content = (
 | 
				
			||||||
 | 
					    <GeneralFilterContent
 | 
				
			||||||
 | 
					      selectedLabels={selectedLabels}
 | 
				
			||||||
 | 
					      updateLabelFilter={updateLabelFilter}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isMobile) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Drawer>
 | 
				
			||||||
 | 
					        <DrawerTrigger asChild>{trigger}</DrawerTrigger>
 | 
				
			||||||
 | 
					        <DrawerContent className="max-h-[75dvh] p-3 mx-1 overflow-hidden">
 | 
				
			||||||
 | 
					          {content}
 | 
				
			||||||
 | 
					        </DrawerContent>
 | 
				
			||||||
 | 
					      </Drawer>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Popover>
 | 
				
			||||||
 | 
					      <PopoverTrigger asChild>{trigger}</PopoverTrigger>
 | 
				
			||||||
 | 
					      <PopoverContent>{content}</PopoverContent>
 | 
				
			||||||
 | 
					    </Popover>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type GeneralFilterContentProps = {
 | 
				
			||||||
 | 
					  selectedLabels: LogSeverity[] | undefined;
 | 
				
			||||||
 | 
					  updateLabelFilter: (labels: LogSeverity[] | undefined) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function GeneralFilterContent({
 | 
				
			||||||
 | 
					  selectedLabels,
 | 
				
			||||||
 | 
					  updateLabelFilter,
 | 
				
			||||||
 | 
					}: GeneralFilterContentProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <div className="h-auto overflow-y-auto overflow-x-hidden">
 | 
				
			||||||
 | 
					        <div className="flex justify-between items-center my-2.5">
 | 
				
			||||||
 | 
					          <Label
 | 
				
			||||||
 | 
					            className="mx-2 text-primary-foreground cursor-pointer"
 | 
				
			||||||
 | 
					            htmlFor="allLabels"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            All Logs
 | 
				
			||||||
 | 
					          </Label>
 | 
				
			||||||
 | 
					          <Switch
 | 
				
			||||||
 | 
					            className="ml-1"
 | 
				
			||||||
 | 
					            id="allLabels"
 | 
				
			||||||
 | 
					            checked={selectedLabels == undefined}
 | 
				
			||||||
 | 
					            onCheckedChange={(isChecked) => {
 | 
				
			||||||
 | 
					              if (isChecked) {
 | 
				
			||||||
 | 
					                updateLabelFilter(undefined);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					        <div className="my-2.5 flex flex-col gap-2.5">
 | 
				
			||||||
 | 
					          {["debug", "info", "warning", "error"].map((item) => (
 | 
				
			||||||
 | 
					            <div className="flex justify-between items-center">
 | 
				
			||||||
 | 
					              <Label
 | 
				
			||||||
 | 
					                className="w-full mx-2 text-primary-foreground capitalize cursor-pointer"
 | 
				
			||||||
 | 
					                htmlFor={item}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                {item.replaceAll("_", " ")}
 | 
				
			||||||
 | 
					              </Label>
 | 
				
			||||||
 | 
					              <Switch
 | 
				
			||||||
 | 
					                key={item}
 | 
				
			||||||
 | 
					                className="ml-1"
 | 
				
			||||||
 | 
					                id={item}
 | 
				
			||||||
 | 
					                checked={selectedLabels?.includes(item as LogSeverity) ?? false}
 | 
				
			||||||
 | 
					                onCheckedChange={(isChecked) => {
 | 
				
			||||||
 | 
					                  if (isChecked) {
 | 
				
			||||||
 | 
					                    const updatedLabels = selectedLabels
 | 
				
			||||||
 | 
					                      ? [...selectedLabels]
 | 
				
			||||||
 | 
					                      : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    updatedLabels.push(item as LogSeverity);
 | 
				
			||||||
 | 
					                    updateLabelFilter(updatedLabels);
 | 
				
			||||||
 | 
					                  } else {
 | 
				
			||||||
 | 
					                    const updatedLabels = selectedLabels
 | 
				
			||||||
 | 
					                      ? [...selectedLabels]
 | 
				
			||||||
 | 
					                      : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // can not deselect the last item
 | 
				
			||||||
 | 
					                    if (updatedLabels.length > 1) {
 | 
				
			||||||
 | 
					                      updatedLabels.splice(
 | 
				
			||||||
 | 
					                        updatedLabels.indexOf(item as LogSeverity),
 | 
				
			||||||
 | 
					                        1,
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                      updateLabelFilter(updatedLabels);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -567,7 +567,7 @@ export function GeneralFilterContent({
 | 
				
			|||||||
          {allLabels.map((item) => (
 | 
					          {allLabels.map((item) => (
 | 
				
			||||||
            <div className="flex justify-between items-center">
 | 
					            <div className="flex justify-between items-center">
 | 
				
			||||||
              <Label
 | 
					              <Label
 | 
				
			||||||
                className="w-full mx-2 text-secondary-foreground capitalize cursor-pointer"
 | 
					                className="w-full mx-2 text-primary-foreground capitalize cursor-pointer"
 | 
				
			||||||
                htmlFor={item}
 | 
					                htmlFor={item}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                {item.replaceAll("_", " ")}
 | 
					                {item.replaceAll("_", " ")}
 | 
				
			||||||
@ -645,7 +645,7 @@ function ShowMotionOnlyButton({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <div className="hidden md:inline-flex items-center justify-center whitespace-nowrap text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground h-9 rounded-md px-3 mx-1 cursor-pointer">
 | 
					      <div className="hidden md:inline-flex items-center justify-center whitespace-nowrap text-sm bg-secondary hover:bg-secondary/80 text-primary-foreground h-9 rounded-md px-3 mx-1 cursor-pointer">
 | 
				
			||||||
        <Switch
 | 
					        <Switch
 | 
				
			||||||
          className="ml-1"
 | 
					          className="ml-1"
 | 
				
			||||||
          id="collapse-motion"
 | 
					          id="collapse-motion"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { ReactNode, useRef } from "react";
 | 
					import { LogSeverity } from "@/types/log";
 | 
				
			||||||
 | 
					import { ReactNode, useMemo, useRef } from "react";
 | 
				
			||||||
import { CSSTransition } from "react-transition-group";
 | 
					import { CSSTransition } from "react-transition-group";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ChipProps = {
 | 
					type ChipProps = {
 | 
				
			||||||
@ -39,3 +40,35 @@ export default function Chip({
 | 
				
			|||||||
    </CSSTransition>
 | 
					    </CSSTransition>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type LogChipProps = {
 | 
				
			||||||
 | 
					  severity: LogSeverity;
 | 
				
			||||||
 | 
					  onClickSeverity?: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function LogChip({ severity, onClickSeverity }: LogChipProps) {
 | 
				
			||||||
 | 
					  const severityClassName = useMemo(() => {
 | 
				
			||||||
 | 
					    switch (severity) {
 | 
				
			||||||
 | 
					      case "info":
 | 
				
			||||||
 | 
					        return "text-primary-foreground/60 bg-secondary hover:bg-secondary/60";
 | 
				
			||||||
 | 
					      case "warning":
 | 
				
			||||||
 | 
					        return "text-warning-foreground bg-warning hover:bg-warning/80";
 | 
				
			||||||
 | 
					      case "error":
 | 
				
			||||||
 | 
					        return "text-destructive-foreground bg-destructive hover:bg-destructive/80";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [severity]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={`py-[1px] px-1 capitalize text-xs rounded-md ${onClickSeverity ? "cursor-pointer" : ""} ${severityClassName}`}
 | 
				
			||||||
 | 
					      onClick={(e) => {
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (onClickSeverity) {
 | 
				
			||||||
 | 
					          onClickSeverity();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {severity}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										129
									
								
								web/src/components/overlay/LogInfoDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								web/src/components/overlay/LogInfoDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					import { LogLine } from "@/types/log";
 | 
				
			||||||
 | 
					import { isDesktop } from "react-device-detect";
 | 
				
			||||||
 | 
					import { Sheet, SheetContent } from "../ui/sheet";
 | 
				
			||||||
 | 
					import { Drawer, DrawerContent } from "../ui/drawer";
 | 
				
			||||||
 | 
					import { LogChip } from "../indicators/Chip";
 | 
				
			||||||
 | 
					import { useMemo } from "react";
 | 
				
			||||||
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type LogInfoDialogProps = {
 | 
				
			||||||
 | 
					  logLine?: LogLine;
 | 
				
			||||||
 | 
					  setLogLine: (log: LogLine | undefined) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export default function LogInfoDialog({
 | 
				
			||||||
 | 
					  logLine,
 | 
				
			||||||
 | 
					  setLogLine,
 | 
				
			||||||
 | 
					}: LogInfoDialogProps) {
 | 
				
			||||||
 | 
					  const Overlay = isDesktop ? Sheet : Drawer;
 | 
				
			||||||
 | 
					  const Content = isDesktop ? SheetContent : DrawerContent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const helpfulLinks = useHelpfulLinks(logLine?.content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Overlay
 | 
				
			||||||
 | 
					      open={logLine != undefined}
 | 
				
			||||||
 | 
					      onOpenChange={(open) => {
 | 
				
			||||||
 | 
					        if (!open) {
 | 
				
			||||||
 | 
					          setLogLine(undefined);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Content className={isDesktop ? "" : "max-h-[75dvh] p-2 overflow-hidden"}>
 | 
				
			||||||
 | 
					        {logLine && (
 | 
				
			||||||
 | 
					          <div className="size-full flex flex-col gap-5">
 | 
				
			||||||
 | 
					            <div className="w-min flex flex-col gap-1.5">
 | 
				
			||||||
 | 
					              <div className="text-sm text-primary-foreground/40">Type</div>
 | 
				
			||||||
 | 
					              <LogChip severity={logLine.severity} />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="flex flex-col gap-1.5">
 | 
				
			||||||
 | 
					              <div className="text-sm text-primary-foreground/40">
 | 
				
			||||||
 | 
					                Timestamp
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <div className="text-sm">{logLine.dateStamp}</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="flex flex-col gap-1.5">
 | 
				
			||||||
 | 
					              <div className="text-sm text-primary-foreground/40">Tag</div>
 | 
				
			||||||
 | 
					              <div className="text-sm">{logLine.section}</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="flex flex-col gap-1.5">
 | 
				
			||||||
 | 
					              <div className="text-sm text-primary-foreground/40">Message</div>
 | 
				
			||||||
 | 
					              <div className="text-sm">{logLine.content}</div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {helpfulLinks.length > 0 && (
 | 
				
			||||||
 | 
					              <div className="flex flex-col gap-1.5">
 | 
				
			||||||
 | 
					                <div className="text-sm text-primary-foreground/40">
 | 
				
			||||||
 | 
					                  Helpful Links
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {helpfulLinks.map((tip) => (
 | 
				
			||||||
 | 
					                  <Link to={tip.link} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
 | 
					                    <div className="text-sm text-selected hover:underline">
 | 
				
			||||||
 | 
					                      {tip.text}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  </Link>
 | 
				
			||||||
 | 
					                ))}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </Content>
 | 
				
			||||||
 | 
					    </Overlay>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function useHelpfulLinks(content: string | undefined) {
 | 
				
			||||||
 | 
					  return useMemo(() => {
 | 
				
			||||||
 | 
					    if (!content) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const links = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (/Could not clear [\d.]* currently [\d.]*/.exec(content)) {
 | 
				
			||||||
 | 
					      links.push({
 | 
				
			||||||
 | 
					        link: "https://docs.frigate.video/configuration/record#will-frigate-delete-old-recordings-if-my-storage-runs-out",
 | 
				
			||||||
 | 
					        text: "Frigate Automatic Storage Cleanup",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (/Did not detect hwaccel/.exec(content)) {
 | 
				
			||||||
 | 
					      links.push({
 | 
				
			||||||
 | 
					        link: "https://docs.frigate.video/configuration/hardware_acceleration",
 | 
				
			||||||
 | 
					        text: "Setup Hardware Acceleration",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      content.includes(
 | 
				
			||||||
 | 
					        "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so init failed",
 | 
				
			||||||
 | 
					      ) ||
 | 
				
			||||||
 | 
					      content.includes(
 | 
				
			||||||
 | 
					        "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so init failed",
 | 
				
			||||||
 | 
					      ) ||
 | 
				
			||||||
 | 
					      content.includes(
 | 
				
			||||||
 | 
					        "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so init failed",
 | 
				
			||||||
 | 
					      ) ||
 | 
				
			||||||
 | 
					      content.includes("No VA display found for device /dev/dri/renderD128")
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      links.push({
 | 
				
			||||||
 | 
					        link: "https://docs.frigate.video/configuration/hardware_acceleration",
 | 
				
			||||||
 | 
					        text: "Verify Hardware Acceleration Setup",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (content.includes("No EdgeTPU was detected")) {
 | 
				
			||||||
 | 
					      links.push({
 | 
				
			||||||
 | 
					        link: "https://docs.frigate.video/troubleshooting/edgetpu",
 | 
				
			||||||
 | 
					        text: "Troubleshoot Coral",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (content.includes("The current SHM size of")) {
 | 
				
			||||||
 | 
					      links.push({
 | 
				
			||||||
 | 
					        link: "https://docs.frigate.video/frigate/installation/#calculating-required-shm-size",
 | 
				
			||||||
 | 
					        text: "Calculate Correct SHM Size",
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return links;
 | 
				
			||||||
 | 
					  }, [content]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -142,7 +142,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
 | 
				
			|||||||
                    className={
 | 
					                    className={
 | 
				
			||||||
                      isDesktop
 | 
					                      isDesktop
 | 
				
			||||||
                        ? "cursor-pointer"
 | 
					                        ? "cursor-pointer"
 | 
				
			||||||
                        : "p-2 flex items-center text-sm"
 | 
					                        : "w-full p-2 flex items-center text-sm"
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <LuActivity className="mr-2 size-4" />
 | 
					                    <LuActivity className="mr-2 size-4" />
 | 
				
			||||||
@ -154,7 +154,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
 | 
				
			|||||||
                    className={
 | 
					                    className={
 | 
				
			||||||
                      isDesktop
 | 
					                      isDesktop
 | 
				
			||||||
                        ? "cursor-pointer"
 | 
					                        ? "cursor-pointer"
 | 
				
			||||||
                        : "p-2 flex items-center text-sm"
 | 
					                        : "w-full p-2 flex items-center text-sm"
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <LuList className="mr-2 size-4" />
 | 
					                    <LuList className="mr-2 size-4" />
 | 
				
			||||||
@ -172,7 +172,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
 | 
				
			|||||||
                    className={
 | 
					                    className={
 | 
				
			||||||
                      isDesktop
 | 
					                      isDesktop
 | 
				
			||||||
                        ? "cursor-pointer"
 | 
					                        ? "cursor-pointer"
 | 
				
			||||||
                        : "p-2 flex items-center text-sm"
 | 
					                        : "w-full p-2 flex items-center text-sm"
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <LuSettings className="mr-2 size-4" />
 | 
					                    <LuSettings className="mr-2 size-4" />
 | 
				
			||||||
@ -184,7 +184,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
 | 
				
			|||||||
                    className={
 | 
					                    className={
 | 
				
			||||||
                      isDesktop
 | 
					                      isDesktop
 | 
				
			||||||
                        ? "cursor-pointer"
 | 
					                        ? "cursor-pointer"
 | 
				
			||||||
                        : "p-2 flex items-center text-sm"
 | 
					                        : "w-full p-2 flex items-center text-sm"
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <LuPenSquare className="mr-2 size-4" />
 | 
					                    <LuPenSquare className="mr-2 size-4" />
 | 
				
			||||||
 | 
				
			|||||||
@ -3,10 +3,14 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
 | 
				
			|||||||
import { LogData, LogLine, LogSeverity } from "@/types/log";
 | 
					import { LogData, LogLine, LogSeverity } from "@/types/log";
 | 
				
			||||||
import copy from "copy-to-clipboard";
 | 
					import copy from "copy-to-clipboard";
 | 
				
			||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 | 
					import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 | 
				
			||||||
import { IoIosAlert } from "react-icons/io";
 | 
					 | 
				
			||||||
import { GoAlertFill } from "react-icons/go";
 | 
					 | 
				
			||||||
import { LuCopy } from "react-icons/lu";
 | 
					 | 
				
			||||||
import axios from "axios";
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					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";
 | 
				
			||||||
 | 
					import { isDesktop } from "react-device-detect";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const logTypes = ["frigate", "go2rtc", "nginx"] as const;
 | 
					const logTypes = ["frigate", "go2rtc", "nginx"] as const;
 | 
				
			||||||
type LogType = (typeof logTypes)[number];
 | 
					type LogType = (typeof logTypes)[number];
 | 
				
			||||||
@ -17,7 +21,7 @@ const frigateDateStamp = /\[[\d\s-:]*]/;
 | 
				
			|||||||
const frigateSeverity = /(DEBUG)|(INFO)|(WARNING)|(ERROR)/;
 | 
					const frigateSeverity = /(DEBUG)|(INFO)|(WARNING)|(ERROR)/;
 | 
				
			||||||
const frigateSection = /[\w.]*/;
 | 
					const frigateSection = /[\w.]*/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const goSeverity = /(DEB )|(INF )|(WARN )|(ERR )/;
 | 
					const goSeverity = /(DEB )|(INF )|(WRN )|(ERR )/;
 | 
				
			||||||
const goSection = /\[[\w]*]/;
 | 
					const goSection = /\[[\w]*]/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ngSeverity = /(GET)|(POST)|(PUT)|(PATCH)|(DELETE)/;
 | 
					const ngSeverity = /(GET)|(POST)|(PUT)|(PATCH)|(DELETE)/;
 | 
				
			||||||
@ -154,9 +158,28 @@ function Logs() {
 | 
				
			|||||||
            contentStart = line.indexOf(section) + section.length + 2;
 | 
					            contentStart = line.indexOf(section) + section.length + 2;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          let severityCat: LogSeverity;
 | 
				
			||||||
 | 
					          switch (severity?.at(0)?.toString().trim()) {
 | 
				
			||||||
 | 
					            case "INF":
 | 
				
			||||||
 | 
					              severityCat = "info";
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
 | 
					            case "WRN":
 | 
				
			||||||
 | 
					              severityCat = "warning";
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
 | 
					            case "ERR":
 | 
				
			||||||
 | 
					              severityCat = "error";
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
 | 
					            case "DBG":
 | 
				
			||||||
 | 
					            case "TRC":
 | 
				
			||||||
 | 
					              severityCat = "debug";
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					              severityCat = "info";
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return {
 | 
					          return {
 | 
				
			||||||
            dateStamp: line.substring(0, 19),
 | 
					            dateStamp: line.substring(0, 19),
 | 
				
			||||||
            severity: "INFO",
 | 
					            severity: severityCat,
 | 
				
			||||||
            section: section,
 | 
					            section: section,
 | 
				
			||||||
            content: line.substring(contentStart).trim(),
 | 
					            content: line.substring(contentStart).trim(),
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
@ -171,7 +194,7 @@ function Logs() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          return {
 | 
					          return {
 | 
				
			||||||
            dateStamp: line.substring(0, 19),
 | 
					            dateStamp: line.substring(0, 19),
 | 
				
			||||||
            severity: "INFO",
 | 
					            severity: "info",
 | 
				
			||||||
            section: ngSeverity.exec(line)?.at(0)?.toString() ?? "META",
 | 
					            section: ngSeverity.exec(line)?.at(0)?.toString() ?? "META",
 | 
				
			||||||
            content: line.substring(line.indexOf(" ", 20)).trim(),
 | 
					            content: line.substring(line.indexOf(" ", 20)).trim(),
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
@ -185,8 +208,15 @@ function Logs() {
 | 
				
			|||||||
  const handleCopyLogs = useCallback(() => {
 | 
					  const handleCopyLogs = useCallback(() => {
 | 
				
			||||||
    if (logs) {
 | 
					    if (logs) {
 | 
				
			||||||
      copy(logs.join("\n"));
 | 
					      copy(logs.join("\n"));
 | 
				
			||||||
 | 
					      toast.success(
 | 
				
			||||||
 | 
					        logRange.start == 0
 | 
				
			||||||
 | 
					          ? "Coplied logs to clipboard"
 | 
				
			||||||
 | 
					          : "Copied visible logs to clipboard",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      toast.error("Could not copy logs to clipboard");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [logs]);
 | 
					  }, [logs, logRange]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // scroll to bottom
 | 
					  // scroll to bottom
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -279,8 +309,19 @@ function Logs() {
 | 
				
			|||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
				
			||||||
  }, [logLines, logService]);
 | 
					  }, [logLines, logService]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // log filtering
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [filterSeverity, setFilterSeverity] = useState<LogSeverity[]>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // log selection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [selectedLog, setSelectedLog] = useState<LogLine>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="size-full p-2 flex flex-col">
 | 
					    <div className="size-full p-2 flex flex-col">
 | 
				
			||||||
 | 
					      <Toaster position="top-center" />
 | 
				
			||||||
 | 
					      <LogInfoDialog logLine={selectedLog} setLogLine={setSelectedLog} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className="flex justify-between items-center">
 | 
					      <div className="flex justify-between items-center">
 | 
				
			||||||
        <ToggleGroup
 | 
					        <ToggleGroup
 | 
				
			||||||
          className="*:px-3 *:py-4 *:rounded-md"
 | 
					          className="*:px-3 *:py-4 *:rounded-md"
 | 
				
			||||||
@ -290,6 +331,7 @@ function Logs() {
 | 
				
			|||||||
          onValueChange={(value: LogType) => {
 | 
					          onValueChange={(value: LogType) => {
 | 
				
			||||||
            if (value) {
 | 
					            if (value) {
 | 
				
			||||||
              setLogs([]);
 | 
					              setLogs([]);
 | 
				
			||||||
 | 
					              setFilterSeverity(undefined);
 | 
				
			||||||
              setLogService(value);
 | 
					              setLogService(value);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }} // don't allow the severity to be unselected
 | 
					          }} // don't allow the severity to be unselected
 | 
				
			||||||
@ -301,25 +343,32 @@ function Logs() {
 | 
				
			|||||||
              value={item}
 | 
					              value={item}
 | 
				
			||||||
              aria-label={`Select ${item}`}
 | 
					              aria-label={`Select ${item}`}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <div className="capitalize">{`${item} Logs`}</div>
 | 
					              <div className="capitalize">{item}</div>
 | 
				
			||||||
            </ToggleGroupItem>
 | 
					            </ToggleGroupItem>
 | 
				
			||||||
          ))}
 | 
					          ))}
 | 
				
			||||||
        </ToggleGroup>
 | 
					        </ToggleGroup>
 | 
				
			||||||
        <div>
 | 
					        <div className="flex items-center gap-2">
 | 
				
			||||||
          <Button
 | 
					          <Button
 | 
				
			||||||
            className="flex justify-between items-center gap-2"
 | 
					            className="flex justify-between items-center gap-2"
 | 
				
			||||||
            size="sm"
 | 
					            size="sm"
 | 
				
			||||||
 | 
					            variant="secondary"
 | 
				
			||||||
            onClick={handleCopyLogs}
 | 
					            onClick={handleCopyLogs}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <LuCopy />
 | 
					            <FaCopy />
 | 
				
			||||||
            <div className="hidden md:block">Copy to Clipboard</div>
 | 
					            <div className="hidden md:block text-primary-foreground">
 | 
				
			||||||
 | 
					              Copy to Clipboard
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
 | 
					          <LogLevelFilterButton
 | 
				
			||||||
 | 
					            selectedLabels={filterSeverity}
 | 
				
			||||||
 | 
					            updateLabelFilter={setFilterSeverity}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {initialScroll && !endVisible && (
 | 
					      {initialScroll && !endVisible && (
 | 
				
			||||||
        <Button
 | 
					        <Button
 | 
				
			||||||
          className="absolute bottom-8 left-[50%] -translate-x-[50%] rounded-xl bg-accent-foreground text-white bg-gray-400 z-20 p-2"
 | 
					          className="absolute bottom-8 left-[50%] -translate-x-[50%] rounded-md text-primary-foreground bg-secondary-foreground z-20 p-2"
 | 
				
			||||||
          variant="secondary"
 | 
					          variant="secondary"
 | 
				
			||||||
          onClick={() =>
 | 
					          onClick={() =>
 | 
				
			||||||
            contentRef.current?.scrollTo({
 | 
					            contentRef.current?.scrollTo({
 | 
				
			||||||
@ -332,48 +381,61 @@ function Logs() {
 | 
				
			|||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div
 | 
					      <div className="size-full flex flex-col my-2 font-mono text-sm sm:p-2 whitespace-pre-wrap bg-primary border border-secondary rounded-md overflow-hidden">
 | 
				
			||||||
        ref={contentRef}
 | 
					        <div className="grid grid-cols-5 sm:grid-cols-8 md:grid-cols-12 *:px-2 *:py-3 *:text-sm *:text-primary-foreground/40">
 | 
				
			||||||
        className="w-full h-min my-2 font-mono text-sm rounded py-4 sm:py-2 whitespace-pre-wrap overflow-auto no-scrollbar"
 | 
					          <div className="p-1 flex items-center capitalize">Type</div>
 | 
				
			||||||
      >
 | 
					          <div className="col-span-2 sm:col-span-1 flex items-center">
 | 
				
			||||||
        <div className="py-2 sticky top-0 -translate-y-1/4 grid grid-cols-5 sm:grid-cols-8 md:grid-cols-12 bg-background *:p-2">
 | 
					 | 
				
			||||||
          <div className="p-1 flex items-center capitalize border-y border-l">
 | 
					 | 
				
			||||||
            Type
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div className="col-span-2 sm:col-span-1 flex items-center border-y border-l">
 | 
					 | 
				
			||||||
            Timestamp
 | 
					            Timestamp
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div className="col-span-2 flex items-center border-y border-l border-r sm:border-r-0">
 | 
					          <div className="col-span-2 flex items-center">Tag</div>
 | 
				
			||||||
            Tag
 | 
					          <div className="col-span-5 sm:col-span-4 md:col-span-8 flex items-center">
 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
          <div className="col-span-5 sm:col-span-4 md:col-span-8 flex items-center border">
 | 
					 | 
				
			||||||
            Message
 | 
					            Message
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {logLines.length > 0 &&
 | 
					        <div
 | 
				
			||||||
          [...Array(logRange.end).keys()].map((idx) => {
 | 
					          ref={contentRef}
 | 
				
			||||||
            const logLine =
 | 
					          className="w-full flex flex-col overflow-y-auto no-scrollbar"
 | 
				
			||||||
              idx >= logRange.start
 | 
					        >
 | 
				
			||||||
                ? logLines[idx - logRange.start]
 | 
					          {logLines.length > 0 &&
 | 
				
			||||||
                : undefined;
 | 
					            [...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)}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (logLine) {
 | 
					 | 
				
			||||||
              return (
 | 
					              return (
 | 
				
			||||||
                <LogLineData
 | 
					                <div
 | 
				
			||||||
                  key={`${idx}-${logService}`}
 | 
					                  key={`${idx}-${logService}`}
 | 
				
			||||||
                  startRef={
 | 
					                  className={isDesktop ? "h-12" : "h-16"}
 | 
				
			||||||
                    idx == logRange.start + 10 ? startLogRef : undefined
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                  className={initialScroll ? "" : "invisible"}
 | 
					 | 
				
			||||||
                  offset={idx}
 | 
					 | 
				
			||||||
                  line={logLines[idx - logRange.start]}
 | 
					 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            }
 | 
					            })}
 | 
				
			||||||
 | 
					          {logLines.length > 0 && <div id="page-bottom" ref={endLogRef} />}
 | 
				
			||||||
            return <div key={`${idx}-${logService}`} className="h-12" />;
 | 
					        </div>
 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
        {logLines.length > 0 && <div id="page-bottom" ref={endLogRef} />}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@ -383,70 +445,37 @@ type LogLineDataProps = {
 | 
				
			|||||||
  startRef?: (node: HTMLDivElement | null) => void;
 | 
					  startRef?: (node: HTMLDivElement | null) => void;
 | 
				
			||||||
  className: string;
 | 
					  className: string;
 | 
				
			||||||
  line: LogLine;
 | 
					  line: LogLine;
 | 
				
			||||||
  offset: number;
 | 
					  onClickSeverity: () => void;
 | 
				
			||||||
 | 
					  onSelect: () => void;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
function LogLineData({ startRef, className, line, offset }: LogLineDataProps) {
 | 
					function LogLineData({
 | 
				
			||||||
  // long log message
 | 
					  startRef,
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
  const contentRef = useRef<HTMLDivElement | null>(null);
 | 
					  line,
 | 
				
			||||||
  const [expanded, setExpanded] = useState(false);
 | 
					  onClickSeverity,
 | 
				
			||||||
 | 
					  onSelect,
 | 
				
			||||||
  const contentOverflows = useMemo(() => {
 | 
					}: LogLineDataProps) {
 | 
				
			||||||
    if (!contentRef.current) {
 | 
					 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return contentRef.current.scrollWidth > contentRef.current.clientWidth;
 | 
					 | 
				
			||||||
    // update on ref change
 | 
					 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
					 | 
				
			||||||
  }, [contentRef.current]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // severity coloring
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const severityClassName = useMemo(() => {
 | 
					 | 
				
			||||||
    switch (line.severity) {
 | 
					 | 
				
			||||||
      case "info":
 | 
					 | 
				
			||||||
        return "text-secondary-foreground rounded-md";
 | 
					 | 
				
			||||||
      case "warning":
 | 
					 | 
				
			||||||
        return "text-yellow-400 rounded-md";
 | 
					 | 
				
			||||||
      case "error":
 | 
					 | 
				
			||||||
        return "text-danger rounded-md";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [line]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      ref={startRef}
 | 
					      ref={startRef}
 | 
				
			||||||
      className={`py-2 grid grid-cols-5 sm:grid-cols-8 md:grid-cols-12 gap-2 ${offset % 2 == 0 ? "bg-secondary" : "bg-secondary/80"} border-t border-x ${className}`}
 | 
					      className={`w-full py-2 grid grid-cols-5 sm:grid-cols-8 md:grid-cols-12 gap-2 border-secondary border-t cursor-pointer hover:bg-muted ${className} *:text-sm`}
 | 
				
			||||||
 | 
					      onClick={onSelect}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div
 | 
					      <div className="h-full p-1 flex items-center gap-2">
 | 
				
			||||||
        className={`h-full p-1 flex items-center gap-2 capitalize ${severityClassName}`}
 | 
					        <LogChip severity={line.severity} onClickSeverity={onClickSeverity} />
 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        {line.severity == "error" ? (
 | 
					 | 
				
			||||||
          <GoAlertFill className="size-5" />
 | 
					 | 
				
			||||||
        ) : (
 | 
					 | 
				
			||||||
          <IoIosAlert className="size-5" />
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        {line.severity}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="h-full col-span-2 sm:col-span-1 flex items-center">
 | 
					      <div className="h-full col-span-2 sm:col-span-1 flex items-center">
 | 
				
			||||||
        {line.dateStamp}
 | 
					        {line.dateStamp}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="h-full col-span-2 flex items-center overflow-hidden text-ellipsis">
 | 
					      <div className="size-full pr-2 col-span-2 flex items-center">
 | 
				
			||||||
        {line.section}
 | 
					        <div className="w-full overflow-hidden whitespace-nowrap text-ellipsis">
 | 
				
			||||||
 | 
					          {line.section}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="w-full col-span-5 sm:col-span-4 md:col-span-8 flex justify-between items-center">
 | 
					      <div className="size-full pl-2 sm:pl-0 pr-2 col-span-5 sm:col-span-4 md:col-span-8 flex justify-between items-center">
 | 
				
			||||||
        <div
 | 
					        <div className="w-full overflow-hidden whitespace-nowrap text-ellipsis">
 | 
				
			||||||
          ref={contentRef}
 | 
					 | 
				
			||||||
          className={`w-[94%] flex items-center" ${expanded ? "" : "overflow-hidden whitespace-nowrap text-ellipsis"}`}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {line.content}
 | 
					          {line.content}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {contentOverflows && (
 | 
					 | 
				
			||||||
          <Button className="mr-4" onClick={() => setExpanded(!expanded)}>
 | 
					 | 
				
			||||||
            ...
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
				
			|||||||
@ -135,6 +135,7 @@ export default function SubmitPlus() {
 | 
				
			|||||||
                  This is a {upload?.label}
 | 
					                  This is a {upload?.label}
 | 
				
			||||||
                </Button>
 | 
					                </Button>
 | 
				
			||||||
                <Button
 | 
					                <Button
 | 
				
			||||||
 | 
					                  className="text-white"
 | 
				
			||||||
                  variant="destructive"
 | 
					                  variant="destructive"
 | 
				
			||||||
                  onClick={() => onSubmitToPlus(true)}
 | 
					                  onClick={() => onSubmitToPlus(true)}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
 | 
				
			|||||||
@ -43,10 +43,6 @@ module.exports = {
 | 
				
			|||||||
        ring: "hsl(var(--ring))",
 | 
					        ring: "hsl(var(--ring))",
 | 
				
			||||||
        danger: "#ef4444",
 | 
					        danger: "#ef4444",
 | 
				
			||||||
        success: "#22c55e",
 | 
					        success: "#22c55e",
 | 
				
			||||||
        // detection colors
 | 
					 | 
				
			||||||
        motion: "#991b1b",
 | 
					 | 
				
			||||||
        object: "#06b6d4",
 | 
					 | 
				
			||||||
        audio: "#ea580c",
 | 
					 | 
				
			||||||
        background: "hsl(var(--background))",
 | 
					        background: "hsl(var(--background))",
 | 
				
			||||||
        foreground: "hsl(var(--foreground))",
 | 
					        foreground: "hsl(var(--foreground))",
 | 
				
			||||||
        selected: "hsl(var(--selected))",
 | 
					        selected: "hsl(var(--selected))",
 | 
				
			||||||
@ -63,6 +59,10 @@ module.exports = {
 | 
				
			|||||||
          DEFAULT: "hsl(var(--destructive))",
 | 
					          DEFAULT: "hsl(var(--destructive))",
 | 
				
			||||||
          foreground: "hsl(var(--destructive-foreground))",
 | 
					          foreground: "hsl(var(--destructive-foreground))",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        warning: {
 | 
				
			||||||
 | 
					          DEFAULT: "hsl(var(--warning))",
 | 
				
			||||||
 | 
					          foreground: "hsl(var(--warning-foreground))",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        muted: {
 | 
					        muted: {
 | 
				
			||||||
          DEFAULT: "hsl(var(--muted))",
 | 
					          DEFAULT: "hsl(var(--muted))",
 | 
				
			||||||
          foreground: "hsl(var(--muted-foreground))",
 | 
					          foreground: "hsl(var(--muted-foreground))",
 | 
				
			||||||
 | 
				
			|||||||
@ -48,17 +48,23 @@
 | 
				
			|||||||
    --destructive: hsl(0 84.2% 60.2%);
 | 
					    --destructive: hsl(0 84.2% 60.2%);
 | 
				
			||||||
    --destructive: 0 84.2% 60.2%;
 | 
					    --destructive: 0 84.2% 60.2%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --destructive-foreground: hsl(210 40% 98%);
 | 
					    --destructive-foreground: hsl(0 100% 83%);
 | 
				
			||||||
    --destructive-foreground: 210 40% 98%;
 | 
					    --destructive-foreground: 0 100% 83%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --warning: hsl(17 87% 18%);
 | 
				
			||||||
 | 
					    --warning: 17 87% 18%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --warning-foreground: hsl(32 100% 74%);
 | 
				
			||||||
 | 
					    --warning-foreground: 32 100% 74%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --border: hsl(214.3 31.8% 91.4%);
 | 
					    --border: hsl(214.3 31.8% 91.4%);
 | 
				
			||||||
    --border: 214.3 31.8% 91.4%;
 | 
					    --border: 214.3 31.8% 91.4%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --input: hsl(214.3 31.8% 91.4%);
 | 
					    --input: hsl(0 0% 85%);
 | 
				
			||||||
    --input: 0 0 85%;
 | 
					    --input: 0 0% 85%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --ring: hsl(222.2 84% 4.9%);
 | 
					    --ring: hsla(0 0% 25% 0%);
 | 
				
			||||||
    --ring: 222.2 84% 4.9%;
 | 
					    --ring: 0 0% 25% 0%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --selected: hsl(228, 89%, 63%);
 | 
					    --selected: hsl(228, 89%, 63%);
 | 
				
			||||||
    --selected: 228 89% 63%;
 | 
					    --selected: 228 89% 63%;
 | 
				
			||||||
@ -133,17 +139,23 @@
 | 
				
			|||||||
    --destructive: hsl(0 62.8% 30.6%);
 | 
					    --destructive: hsl(0 62.8% 30.6%);
 | 
				
			||||||
    --destructive: 0 62.8% 30.6%;
 | 
					    --destructive: 0 62.8% 30.6%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --destructive-foreground: hsl(210 40% 98%);
 | 
					    --destructive-foreground: hsl(0 100% 83%);
 | 
				
			||||||
    --destructive-foreground: 210 40% 98%;
 | 
					    --destructive-foreground: 0 100% 83%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --warning: hsl(17 87% 18%);
 | 
				
			||||||
 | 
					    --warning: 17 87% 18%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --warning-foreground: hsl(32 100% 74%);
 | 
				
			||||||
 | 
					    --warning-foreground: 32 100% 74%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --border: hsl(0, 0%, 32%);
 | 
					    --border: hsl(0, 0%, 32%);
 | 
				
			||||||
    --border: 0 0% 32%;
 | 
					    --border: 0 0% 32%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --input: hsl(217.2 32.6% 17.5%);
 | 
					    --input: hsl(0 0% 5%);
 | 
				
			||||||
    --input: 0 0 25%;
 | 
					    --input: 0 0% 25%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --ring: hsl(212.7 26.8% 83.9%);
 | 
					    --ring: hsla(0 0% 25% 0%);
 | 
				
			||||||
    --ring: 212.7 26.8% 83.9%;
 | 
					    --ring: 0 0% 25% 0%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --selected: hsl(228, 89%, 63%);
 | 
					    --selected: hsl(228, 89%, 63%);
 | 
				
			||||||
    --selected: 228 89% 63%;
 | 
					    --selected: 228 89% 63%;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user