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) => (
 | 
			
		||||
            <div className="flex justify-between items-center">
 | 
			
		||||
              <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}
 | 
			
		||||
              >
 | 
			
		||||
                {item.replaceAll("_", " ")}
 | 
			
		||||
@ -645,7 +645,7 @@ function ShowMotionOnlyButton({
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
          className="ml-1"
 | 
			
		||||
          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";
 | 
			
		||||
 | 
			
		||||
type ChipProps = {
 | 
			
		||||
@ -39,3 +40,35 @@ export default function Chip({
 | 
			
		||||
    </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={
 | 
			
		||||
                      isDesktop
 | 
			
		||||
                        ? "cursor-pointer"
 | 
			
		||||
                        : "p-2 flex items-center text-sm"
 | 
			
		||||
                        : "w-full p-2 flex items-center text-sm"
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <LuActivity className="mr-2 size-4" />
 | 
			
		||||
@ -154,7 +154,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
 | 
			
		||||
                    className={
 | 
			
		||||
                      isDesktop
 | 
			
		||||
                        ? "cursor-pointer"
 | 
			
		||||
                        : "p-2 flex items-center text-sm"
 | 
			
		||||
                        : "w-full p-2 flex items-center text-sm"
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <LuList className="mr-2 size-4" />
 | 
			
		||||
@ -172,7 +172,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
 | 
			
		||||
                    className={
 | 
			
		||||
                      isDesktop
 | 
			
		||||
                        ? "cursor-pointer"
 | 
			
		||||
                        : "p-2 flex items-center text-sm"
 | 
			
		||||
                        : "w-full p-2 flex items-center text-sm"
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <LuSettings className="mr-2 size-4" />
 | 
			
		||||
@ -184,7 +184,7 @@ export default function GeneralSettings({ className }: GeneralSettings) {
 | 
			
		||||
                    className={
 | 
			
		||||
                      isDesktop
 | 
			
		||||
                        ? "cursor-pointer"
 | 
			
		||||
                        : "p-2 flex items-center text-sm"
 | 
			
		||||
                        : "w-full p-2 flex items-center text-sm"
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <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 copy from "copy-to-clipboard";
 | 
			
		||||
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 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;
 | 
			
		||||
type LogType = (typeof logTypes)[number];
 | 
			
		||||
@ -17,7 +21,7 @@ const frigateDateStamp = /\[[\d\s-:]*]/;
 | 
			
		||||
const frigateSeverity = /(DEBUG)|(INFO)|(WARNING)|(ERROR)/;
 | 
			
		||||
const frigateSection = /[\w.]*/;
 | 
			
		||||
 | 
			
		||||
const goSeverity = /(DEB )|(INF )|(WARN )|(ERR )/;
 | 
			
		||||
const goSeverity = /(DEB )|(INF )|(WRN )|(ERR )/;
 | 
			
		||||
const goSection = /\[[\w]*]/;
 | 
			
		||||
 | 
			
		||||
const ngSeverity = /(GET)|(POST)|(PUT)|(PATCH)|(DELETE)/;
 | 
			
		||||
@ -154,9 +158,28 @@ function Logs() {
 | 
			
		||||
            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 {
 | 
			
		||||
            dateStamp: line.substring(0, 19),
 | 
			
		||||
            severity: "INFO",
 | 
			
		||||
            severity: severityCat,
 | 
			
		||||
            section: section,
 | 
			
		||||
            content: line.substring(contentStart).trim(),
 | 
			
		||||
          };
 | 
			
		||||
@ -171,7 +194,7 @@ function Logs() {
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            dateStamp: line.substring(0, 19),
 | 
			
		||||
            severity: "INFO",
 | 
			
		||||
            severity: "info",
 | 
			
		||||
            section: ngSeverity.exec(line)?.at(0)?.toString() ?? "META",
 | 
			
		||||
            content: line.substring(line.indexOf(" ", 20)).trim(),
 | 
			
		||||
          };
 | 
			
		||||
@ -185,8 +208,15 @@ function Logs() {
 | 
			
		||||
  const handleCopyLogs = useCallback(() => {
 | 
			
		||||
    if (logs) {
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
@ -279,8 +309,19 @@ function Logs() {
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [logLines, logService]);
 | 
			
		||||
 | 
			
		||||
  // log filtering
 | 
			
		||||
 | 
			
		||||
  const [filterSeverity, setFilterSeverity] = useState<LogSeverity[]>();
 | 
			
		||||
 | 
			
		||||
  // log selection
 | 
			
		||||
 | 
			
		||||
  const [selectedLog, setSelectedLog] = useState<LogLine>();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <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">
 | 
			
		||||
        <ToggleGroup
 | 
			
		||||
          className="*:px-3 *:py-4 *:rounded-md"
 | 
			
		||||
@ -290,6 +331,7 @@ function Logs() {
 | 
			
		||||
          onValueChange={(value: LogType) => {
 | 
			
		||||
            if (value) {
 | 
			
		||||
              setLogs([]);
 | 
			
		||||
              setFilterSeverity(undefined);
 | 
			
		||||
              setLogService(value);
 | 
			
		||||
            }
 | 
			
		||||
          }} // don't allow the severity to be unselected
 | 
			
		||||
@ -301,25 +343,32 @@ function Logs() {
 | 
			
		||||
              value={item}
 | 
			
		||||
              aria-label={`Select ${item}`}
 | 
			
		||||
            >
 | 
			
		||||
              <div className="capitalize">{`${item} Logs`}</div>
 | 
			
		||||
              <div className="capitalize">{item}</div>
 | 
			
		||||
            </ToggleGroupItem>
 | 
			
		||||
          ))}
 | 
			
		||||
        </ToggleGroup>
 | 
			
		||||
        <div>
 | 
			
		||||
        <div className="flex items-center gap-2">
 | 
			
		||||
          <Button
 | 
			
		||||
            className="flex justify-between items-center gap-2"
 | 
			
		||||
            size="sm"
 | 
			
		||||
            variant="secondary"
 | 
			
		||||
            onClick={handleCopyLogs}
 | 
			
		||||
          >
 | 
			
		||||
            <LuCopy />
 | 
			
		||||
            <div className="hidden md:block">Copy to Clipboard</div>
 | 
			
		||||
            <FaCopy />
 | 
			
		||||
            <div className="hidden md:block text-primary-foreground">
 | 
			
		||||
              Copy to Clipboard
 | 
			
		||||
            </div>
 | 
			
		||||
          </Button>
 | 
			
		||||
          <LogLevelFilterButton
 | 
			
		||||
            selectedLabels={filterSeverity}
 | 
			
		||||
            updateLabelFilter={setFilterSeverity}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {initialScroll && !endVisible && (
 | 
			
		||||
        <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"
 | 
			
		||||
          onClick={() =>
 | 
			
		||||
            contentRef.current?.scrollTo({
 | 
			
		||||
@ -332,48 +381,61 @@ function Logs() {
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        ref={contentRef}
 | 
			
		||||
        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="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">
 | 
			
		||||
      <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">
 | 
			
		||||
        <div className="grid grid-cols-5 sm:grid-cols-8 md:grid-cols-12 *:px-2 *:py-3 *:text-sm *:text-primary-foreground/40">
 | 
			
		||||
          <div className="p-1 flex items-center capitalize">Type</div>
 | 
			
		||||
          <div className="col-span-2 sm:col-span-1 flex items-center">
 | 
			
		||||
            Timestamp
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="col-span-2 flex items-center border-y border-l border-r sm:border-r-0">
 | 
			
		||||
            Tag
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="col-span-5 sm:col-span-4 md:col-span-8 flex items-center border">
 | 
			
		||||
          <div className="col-span-2 flex items-center">Tag</div>
 | 
			
		||||
          <div className="col-span-5 sm:col-span-4 md:col-span-8 flex items-center">
 | 
			
		||||
            Message
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {logLines.length > 0 &&
 | 
			
		||||
          [...Array(logRange.end).keys()].map((idx) => {
 | 
			
		||||
            const logLine =
 | 
			
		||||
              idx >= logRange.start
 | 
			
		||||
                ? logLines[idx - logRange.start]
 | 
			
		||||
                : undefined;
 | 
			
		||||
        <div
 | 
			
		||||
          ref={contentRef}
 | 
			
		||||
          className="w-full flex flex-col overflow-y-auto no-scrollbar"
 | 
			
		||||
        >
 | 
			
		||||
          {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)}
 | 
			
		||||
                  />
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
            if (logLine) {
 | 
			
		||||
              return (
 | 
			
		||||
                <LogLineData
 | 
			
		||||
                <div
 | 
			
		||||
                  key={`${idx}-${logService}`}
 | 
			
		||||
                  startRef={
 | 
			
		||||
                    idx == logRange.start + 10 ? startLogRef : undefined
 | 
			
		||||
                  }
 | 
			
		||||
                  className={initialScroll ? "" : "invisible"}
 | 
			
		||||
                  offset={idx}
 | 
			
		||||
                  line={logLines[idx - logRange.start]}
 | 
			
		||||
                  className={isDesktop ? "h-12" : "h-16"}
 | 
			
		||||
                />
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return <div key={`${idx}-${logService}`} className="h-12" />;
 | 
			
		||||
          })}
 | 
			
		||||
        {logLines.length > 0 && <div id="page-bottom" ref={endLogRef} />}
 | 
			
		||||
            })}
 | 
			
		||||
          {logLines.length > 0 && <div id="page-bottom" ref={endLogRef} />}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
@ -383,70 +445,37 @@ type LogLineDataProps = {
 | 
			
		||||
  startRef?: (node: HTMLDivElement | null) => void;
 | 
			
		||||
  className: string;
 | 
			
		||||
  line: LogLine;
 | 
			
		||||
  offset: number;
 | 
			
		||||
  onClickSeverity: () => void;
 | 
			
		||||
  onSelect: () => void;
 | 
			
		||||
};
 | 
			
		||||
function LogLineData({ startRef, className, line, offset }: LogLineDataProps) {
 | 
			
		||||
  // long log message
 | 
			
		||||
 | 
			
		||||
  const contentRef = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
  const [expanded, setExpanded] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const contentOverflows = useMemo(() => {
 | 
			
		||||
    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]);
 | 
			
		||||
 | 
			
		||||
function LogLineData({
 | 
			
		||||
  startRef,
 | 
			
		||||
  className,
 | 
			
		||||
  line,
 | 
			
		||||
  onClickSeverity,
 | 
			
		||||
  onSelect,
 | 
			
		||||
}: LogLineDataProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      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
 | 
			
		||||
        className={`h-full p-1 flex items-center gap-2 capitalize ${severityClassName}`}
 | 
			
		||||
      >
 | 
			
		||||
        {line.severity == "error" ? (
 | 
			
		||||
          <GoAlertFill className="size-5" />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <IoIosAlert className="size-5" />
 | 
			
		||||
        )}
 | 
			
		||||
        {line.severity}
 | 
			
		||||
      <div className="h-full p-1 flex items-center gap-2">
 | 
			
		||||
        <LogChip severity={line.severity} onClickSeverity={onClickSeverity} />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="h-full col-span-2 sm:col-span-1 flex items-center">
 | 
			
		||||
        {line.dateStamp}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="h-full col-span-2 flex items-center overflow-hidden text-ellipsis">
 | 
			
		||||
        {line.section}
 | 
			
		||||
      <div className="size-full pr-2 col-span-2 flex items-center">
 | 
			
		||||
        <div className="w-full overflow-hidden whitespace-nowrap text-ellipsis">
 | 
			
		||||
          {line.section}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="w-full col-span-5 sm:col-span-4 md:col-span-8 flex justify-between items-center">
 | 
			
		||||
        <div
 | 
			
		||||
          ref={contentRef}
 | 
			
		||||
          className={`w-[94%] flex items-center" ${expanded ? "" : "overflow-hidden whitespace-nowrap text-ellipsis"}`}
 | 
			
		||||
        >
 | 
			
		||||
      <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 className="w-full overflow-hidden whitespace-nowrap text-ellipsis">
 | 
			
		||||
          {line.content}
 | 
			
		||||
        </div>
 | 
			
		||||
        {contentOverflows && (
 | 
			
		||||
          <Button className="mr-4" onClick={() => setExpanded(!expanded)}>
 | 
			
		||||
            ...
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -135,6 +135,7 @@ export default function SubmitPlus() {
 | 
			
		||||
                  This is a {upload?.label}
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button
 | 
			
		||||
                  className="text-white"
 | 
			
		||||
                  variant="destructive"
 | 
			
		||||
                  onClick={() => onSubmitToPlus(true)}
 | 
			
		||||
                >
 | 
			
		||||
 | 
			
		||||
@ -43,10 +43,6 @@ module.exports = {
 | 
			
		||||
        ring: "hsl(var(--ring))",
 | 
			
		||||
        danger: "#ef4444",
 | 
			
		||||
        success: "#22c55e",
 | 
			
		||||
        // detection colors
 | 
			
		||||
        motion: "#991b1b",
 | 
			
		||||
        object: "#06b6d4",
 | 
			
		||||
        audio: "#ea580c",
 | 
			
		||||
        background: "hsl(var(--background))",
 | 
			
		||||
        foreground: "hsl(var(--foreground))",
 | 
			
		||||
        selected: "hsl(var(--selected))",
 | 
			
		||||
@ -63,6 +59,10 @@ module.exports = {
 | 
			
		||||
          DEFAULT: "hsl(var(--destructive))",
 | 
			
		||||
          foreground: "hsl(var(--destructive-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        warning: {
 | 
			
		||||
          DEFAULT: "hsl(var(--warning))",
 | 
			
		||||
          foreground: "hsl(var(--warning-foreground))",
 | 
			
		||||
        },
 | 
			
		||||
        muted: {
 | 
			
		||||
          DEFAULT: "hsl(var(--muted))",
 | 
			
		||||
          foreground: "hsl(var(--muted-foreground))",
 | 
			
		||||
 | 
			
		||||
@ -48,17 +48,23 @@
 | 
			
		||||
    --destructive: hsl(0 84.2% 60.2%);
 | 
			
		||||
    --destructive: 0 84.2% 60.2%;
 | 
			
		||||
 | 
			
		||||
    --destructive-foreground: hsl(210 40% 98%);
 | 
			
		||||
    --destructive-foreground: 210 40% 98%;
 | 
			
		||||
    --destructive-foreground: hsl(0 100% 83%);
 | 
			
		||||
    --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: 214.3 31.8% 91.4%;
 | 
			
		||||
 | 
			
		||||
    --input: hsl(214.3 31.8% 91.4%);
 | 
			
		||||
    --input: 0 0 85%;
 | 
			
		||||
    --input: hsl(0 0% 85%);
 | 
			
		||||
    --input: 0 0% 85%;
 | 
			
		||||
 | 
			
		||||
    --ring: hsl(222.2 84% 4.9%);
 | 
			
		||||
    --ring: 222.2 84% 4.9%;
 | 
			
		||||
    --ring: hsla(0 0% 25% 0%);
 | 
			
		||||
    --ring: 0 0% 25% 0%
 | 
			
		||||
 | 
			
		||||
    --selected: hsl(228, 89%, 63%);
 | 
			
		||||
    --selected: 228 89% 63%;
 | 
			
		||||
@ -133,17 +139,23 @@
 | 
			
		||||
    --destructive: hsl(0 62.8% 30.6%);
 | 
			
		||||
    --destructive: 0 62.8% 30.6%;
 | 
			
		||||
 | 
			
		||||
    --destructive-foreground: hsl(210 40% 98%);
 | 
			
		||||
    --destructive-foreground: 210 40% 98%;
 | 
			
		||||
    --destructive-foreground: hsl(0 100% 83%);
 | 
			
		||||
    --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: 0 0% 32%;
 | 
			
		||||
 | 
			
		||||
    --input: hsl(217.2 32.6% 17.5%);
 | 
			
		||||
    --input: 0 0 25%;
 | 
			
		||||
    --input: hsl(0 0% 5%);
 | 
			
		||||
    --input: 0 0% 25%;
 | 
			
		||||
 | 
			
		||||
    --ring: hsl(212.7 26.8% 83.9%);
 | 
			
		||||
    --ring: 212.7 26.8% 83.9%;
 | 
			
		||||
    --ring: hsla(0 0% 25% 0%);
 | 
			
		||||
    --ring: 0 0% 25% 0%
 | 
			
		||||
 | 
			
		||||
    --selected: hsl(228, 89%, 63%);
 | 
			
		||||
    --selected: 228 89% 63%;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user