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