feat(logs): filter improvements

This commit is contained in:
Nicholas Penree
2024-12-16 16:51:29 -05:00
parent 71fe6de9cb
commit ca4820940e
4 changed files with 529 additions and 363 deletions

View File

@@ -1,26 +1,16 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectSeparator
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import { Download as DownloadIcon, Loader2 } from "lucide-react";
Download as DownloadIcon,
Loader2
} from "lucide-react";
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { SinceLogsFilter } from "./since-logs-filter";
import { StatusLogsFilter } from "./status-logs-filter";
import { TerminalLine } from "./terminal-line"; import { TerminalLine } from "./terminal-line";
import { type LogLine, getLogType, parseLogs } from "./utils"; import { type LogLine, getLogType, parseLogs } from "./utils";
import { StatusLogsFilter } from "./status-logs-filter";
interface Props { interface Props {
containerId: string; containerId: string;
serverId?: string | null; serverId?: string | null;
} }
type TimeFilter = "all" | "timestamp" | "1h" | "6h" | "24h" | "168h" | "720h"; type TimeFilter = "all" | "timestamp" | "1h" | "6h" | "24h" | "168h" | "720h";
@@ -49,276 +39,264 @@ export const priorities = [
]; ];
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => { export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
const { data } = api.docker.getConfig.useQuery( const { data } = api.docker.getConfig.useQuery(
{ {
containerId, containerId,
serverId: serverId ?? undefined, serverId: serverId ?? undefined,
}, },
{ {
enabled: !!containerId, enabled: !!containerId,
} },
); );
const [rawLogs, setRawLogs] = React.useState(""); const [rawLogs, setRawLogs] = React.useState("");
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]); const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
const [autoScroll, setAutoScroll] = React.useState(true); const [autoScroll, setAutoScroll] = React.useState(true);
const [lines, setLines] = React.useState<number>(100); const [lines, setLines] = React.useState<number>(100);
const [search, setSearch] = React.useState<string>(""); const [search, setSearch] = React.useState<string>("");
const [showTimestamp, setShowTimestamp] = React.useState(true); const [showTimestamp, setShowTimestamp] = React.useState(true);
const [since, setSince] = React.useState<TimeFilter>("all"); const [since, setSince] = React.useState<TimeFilter>("all");
const [typeFilter, setTypeFilter] = React.useState<string[]>([]); const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const scrollToBottom = () => { const scrollToBottom = () => {
if (autoScroll && scrollRef.current) { if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
} }
}; };
const handleScroll = () => { const handleScroll = () => {
if (!scrollRef.current) return; if (!scrollRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
setAutoScroll(isAtBottom); setAutoScroll(isAtBottom);
}; };
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value || ""); setSearch(e.target.value || "");
}; };
const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => { const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
setLines(Number(e.target.value) || 1); setLines(Number(e.target.value) || 1);
}; };
const handleSince = (value: TimeFilter) => { const handleSince = (value: string) => {
if (value === "timestamp") { if (value === "timestamp") {
setShowTimestamp(!showTimestamp); setShowTimestamp(!showTimestamp);
} else { } else {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
setSince(value); setSince(value);
} }
}; };
useEffect(() => { useEffect(() => {
if (!containerId) return; if (!containerId) return;
let isCurrentConnection = true;
let noDataTimeout: NodeJS.Timeout;
setIsLoading(true);
setRawLogs("");
setFilteredLogs([]);
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; let isCurrentConnection = true;
const params = new globalThis.URLSearchParams({ let noDataTimeout: NodeJS.Timeout;
containerId, setIsLoading(true);
tail: lines.toString(), setRawLogs("");
since, setFilteredLogs([]);
search,
});
if (serverId) { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
params.append("serverId", serverId); const params = new globalThis.URLSearchParams({
} containerId,
tail: lines.toString(),
since,
search,
});
const wsUrl = `${protocol}//${ if (serverId) {
window.location.host params.append("serverId", serverId);
}/docker-container-logs?${params.toString()}`; }
console.log("Connecting to WebSocket:", wsUrl);
const ws = new WebSocket(wsUrl);
const resetNoDataTimeout = () => { const wsUrl = `${protocol}//${
if (noDataTimeout) clearTimeout(noDataTimeout); window.location.host
noDataTimeout = setTimeout(() => { }/docker-container-logs?${params.toString()}`;
if (isCurrentConnection) { console.log("Connecting to WebSocket:", wsUrl);
setIsLoading(false); const ws = new WebSocket(wsUrl);
}
}, 2000); // Wait 2 seconds for data before showing "No logs found"
};
ws.onopen = () => { const resetNoDataTimeout = () => {
if (!isCurrentConnection) { if (noDataTimeout) clearTimeout(noDataTimeout);
ws.close(); noDataTimeout = setTimeout(() => {
return; if (isCurrentConnection) {
} setIsLoading(false);
console.log("WebSocket connected"); }
resetNoDataTimeout(); }, 2000); // Wait 2 seconds for data before showing "No logs found"
}; };
ws.onmessage = (e) => { ws.onopen = () => {
if (!isCurrentConnection) return; if (!isCurrentConnection) {
setRawLogs((prev) => prev + e.data); ws.close();
setIsLoading(false); return;
if (noDataTimeout) clearTimeout(noDataTimeout); }
}; console.log("WebSocket connected");
resetNoDataTimeout();
};
ws.onerror = (error) => { ws.onmessage = (e) => {
if (!isCurrentConnection) return; if (!isCurrentConnection) return;
console.error("WebSocket error:", error); setRawLogs((prev) => prev + e.data);
setIsLoading(false); setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout); if (noDataTimeout) clearTimeout(noDataTimeout);
}; };
ws.onclose = (e) => { ws.onerror = (error) => {
if (!isCurrentConnection) return; if (!isCurrentConnection) return;
console.log("WebSocket closed:", e.reason); console.error("WebSocket error:", error);
setIsLoading(false); setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout); if (noDataTimeout) clearTimeout(noDataTimeout);
}; };
return () => { ws.onclose = (e) => {
isCurrentConnection = false; if (!isCurrentConnection) return;
if (noDataTimeout) clearTimeout(noDataTimeout); console.log("WebSocket closed:", e.reason);
if (ws.readyState === WebSocket.OPEN) { setIsLoading(false);
ws.close(); if (noDataTimeout) clearTimeout(noDataTimeout);
} };
};
}, [containerId, serverId, lines, search, since]);
const handleDownload = () => { return () => {
const logContent = filteredLogs isCurrentConnection = false;
.map( if (noDataTimeout) clearTimeout(noDataTimeout);
({ timestamp, message }: { timestamp: Date | null; message: string }) => if (ws.readyState === WebSocket.OPEN) {
`${timestamp?.toISOString() || "No timestamp"} ${message}` ws.close();
) }
.join("\n"); };
}, [containerId, serverId, lines, search, since]);
const blob = new Blob([logContent], { type: "text/plain" }); const handleDownload = () => {
const url = URL.createObjectURL(blob); const logContent = filteredLogs
const a = document.createElement("a"); .map(
const appName = data.Name.replace("/", "") || "app"; ({ timestamp, message }: { timestamp: Date | null; message: string }) =>
const isoDate = new Date().toISOString(); `${timestamp?.toISOString() || "No timestamp"} ${message}`,
a.href = url; )
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate .join("\n");
.slice(11, 19)
.replace(/:/g, "")}.log.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const handleFilter = (logs: LogLine[]) => { const blob = new Blob([logContent], { type: "text/plain" });
return logs.filter((log) => { const url = URL.createObjectURL(blob);
const logType = getLogType(log.message).type; const a = document.createElement("a");
const appName = data.Name.replace("/", "") || "app";
if (typeFilter.length === 0) { const isoDate = new Date().toISOString();
return true; a.href = url;
} a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
.slice(11, 19)
.replace(/:/g, "")}.log.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return typeFilter.includes(logType); const handleFilter = (logs: LogLine[]) => {
}); return logs.filter((log) => {
}; const logType = getLogType(log.message).type;
useEffect(() => { if (typeFilter.length === 0) {
setRawLogs(""); return true;
setFilteredLogs([]); }
}, [containerId]);
useEffect(() => { return typeFilter.includes(logType);
const logs = parseLogs(rawLogs); });
const filtered = handleFilter(logs); };
setFilteredLogs(filtered);
}, [rawLogs, search, lines, since, typeFilter]);
useEffect(() => { useEffect(() => {
scrollToBottom(); setRawLogs("");
setFilteredLogs([]);
}, [containerId]);
if (autoScroll && scrollRef.current) { useEffect(() => {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; const logs = parseLogs(rawLogs);
} const filtered = handleFilter(logs);
}, [filteredLogs, autoScroll]); setFilteredLogs(filtered);
}, [rawLogs, search, lines, since, typeFilter]);
return ( useEffect(() => {
<div className="flex flex-col gap-4"> scrollToBottom();
<div className="rounded-lg overflow-hidden">
<div className="space-y-4">
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
<div className="flex flex-wrap gap-4">
<Input
type="text"
placeholder="Number of lines to show"
value={lines}
onChange={handleLines}
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
/>
<Select value={since} onValueChange={handleSince}> if (autoScroll && scrollRef.current) {
<SelectTrigger className="sm:w-[180px] w-full h-9"> scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
<SelectValue placeholder="Time filter" /> }
</SelectTrigger> }, [filteredLogs, autoScroll]);
<SelectContent>
<SelectItem value="1h">Last hour</SelectItem>
<SelectItem value="6h">Last 6 hours</SelectItem>
<SelectItem value="24h">Last 24 hours</SelectItem>
<SelectItem value="168h">Last 7 days</SelectItem>
<SelectItem value="720h">Last 30 days</SelectItem>
<SelectItem value="all">All time</SelectItem>
<SelectSeparator/>
<SelectItem value="timestamp">
{showTimestamp ? "Hide timestamp" : "Show timestamp"}
</SelectItem>
</SelectContent>
</Select>
<StatusLogsFilter return (
<div className="flex flex-col gap-4">
<div className="rounded-lg overflow-hidden">
<div className="space-y-4">
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
<div className="flex flex-wrap gap-4">
<Input
type="text"
placeholder="Number of lines to show"
value={lines}
onChange={handleLines}
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
/>
<SinceLogsFilter
value={since}
onValueChange={handleSince}
showTimestamp={showTimestamp}
onTimestampChange={setShowTimestamp}
/>
<StatusLogsFilter
value={typeFilter} value={typeFilter}
setValue={setTypeFilter} setValue={setTypeFilter}
title="Log type" title="Log type"
options={priorities} options={priorities}
/> />
<Input <Input
type="search" type="search"
placeholder="Search logs..." placeholder="Search logs..."
value={search} value={search}
onChange={handleSearch} onChange={handleSearch}
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto" className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
/> />
</div>
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-9 sm:w-auto w-full" className="h-9 sm:w-auto w-full"
onClick={handleDownload} onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name} disabled={filteredLogs.length === 0 || !data?.Name}
> >
<DownloadIcon className="mr-2 h-4 w-4" /> <DownloadIcon className="mr-2 h-4 w-4" />
Download logs Download logs
</Button> </Button>
</div> </div>
<div <div
ref={scrollRef} ref={scrollRef}
onScroll={handleScroll} onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar" className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
> >
{filteredLogs.length > 0 ? ( {filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => ( filteredLogs.map((filteredLog: LogLine, index: number) => (
<TerminalLine <TerminalLine
key={index} key={index}
log={filteredLog} log={filteredLog}
searchTerm={search} searchTerm={search}
noTimestamp={!showTimestamp} noTimestamp={!showTimestamp}
/> />
)) ))
) : isLoading ? ( ) : isLoading ? (
<div className="flex justify-center items-center h-full text-muted-foreground"> <div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
</div> </div>
) : ( ) : (
<div className="flex justify-center items-center h-full text-muted-foreground"> <div className="flex justify-center items-center h-full text-muted-foreground">
No logs found No logs found
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,121 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandGroup,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { CheckIcon } from "lucide-react";
import React from "react";
type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
const timeRanges = [
{
label: "All time",
value: "all",
},
{
label: "Last hour",
value: "1h",
},
{
label: "Last 6 hours",
value: "6h",
},
{
label: "Last 24 hours",
value: "24h",
},
{
label: "Last 7 days",
value: "168h",
},
{
label: "Last 30 days",
value: "720h",
},
];
interface SinceLogsFilterProps {
value: string;
onValueChange: (value: TimeFilter) => void;
showTimestamp: boolean;
onTimestampChange: (show: boolean) => void;
title?: string;
}
export function SinceLogsFilter({
value,
onValueChange,
showTimestamp,
onTimestampChange,
title = "Time range",
}: SinceLogsFilterProps) {
const selectedLabel =
timeRanges.find((range) => range.value === value)?.label ??
"Select time range";
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
>
{title}
<Separator orientation="vertical" className="mx-2 h-4" />
<div className="space-x-1 flex">
<Badge variant="blank" className="rounded-sm px-1 font-normal">
{selectedLabel}
</Badge>
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandList>
<CommandGroup>
{timeRanges.map((range) => {
const isSelected = value === range.value;
return (
<CommandItem
key={range.value}
onSelect={() => onValueChange(range.value)}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
<span className="text-sm">{range.label}</span>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
<Separator className="my-2" />
<div className="p-2 flex items-center justify-between">
<span className="text-sm">Show timestamps</span>
<Switch checked={showTimestamp} onCheckedChange={onTimestampChange} />
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -1,107 +1,170 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Command, Command,
CommandEmpty, CommandGroup,
CommandGroup, CommandItem,
CommandInput, CommandList,
CommandItem, } from "@/components/ui/command";
CommandList, import {
} from "@/components/ui/command"; Popover,
import { PopoverContent,
Popover, PopoverTrigger,
PopoverContent, } from "@/components/ui/popover";
PopoverTrigger, import { Separator } from "@/components/ui/separator";
} from "@/components/ui/popover"; import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator"; import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import type React from "react";
import { CheckIcon } from "lucide-react";
import React from "react"; interface StatusLogsFilterProps {
value?: string[];
interface StatusLogsFilterProps { setValue?: (value: string[]) => void;
value?: string[]; title?: string;
setValue?: (value: string[]) => void; options: {
title?: string; label: string;
options: { value: string;
label: string; icon?: React.ComponentType<{ className?: string }>;
value: string; }[];
icon?: React.ComponentType<{ className?: string }>; }
}[];
} export function StatusLogsFilter({
value = [],
export function StatusLogsFilter({ setValue,
value = [], title,
setValue, options,
title, }: StatusLogsFilterProps) {
options, const selectedValues = new Set(value as string[]);
}: StatusLogsFilterProps) { const allSelected = selectedValues.size === 0;
const selectedValues = new Set(value as string[]);
const getSelectedBadges = () => {
return ( if (allSelected) {
<Popover> return (
<PopoverTrigger asChild> <Badge variant="blank" className="rounded-sm px-1 font-normal">
<Button variant="outline" size="sm" className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"> All
{title} </Badge>
<Separator orientation="vertical" className="mx-2 h-4" /> );
<div className="space-x-1 flex"> }
<Badge
variant="blank" if (selectedValues.size >= 1) {
className="rounded-sm px-1 font-normal" const selected = options.find((opt) => selectedValues.has(opt.value));
> return (
{selectedValues.size} selected <>
</Badge> <Badge
</div> variant={
</Button> selected?.value === "success"
</PopoverTrigger> ? "green"
<PopoverContent className="w-[200px] p-0" align="start"> : selected?.value === "error"
<Command> ? "red"
<CommandInput placeholder={title} /> : selected?.value === "warning"
<CommandList> ? "orange"
<CommandEmpty>No results found.</CommandEmpty> : selected?.value === "info"
<CommandGroup> ? "blue"
{options.map((option) => { : selected?.value === "debug"
const isSelected = selectedValues.has(option.value); ? "yellow"
return ( : "blank"
<CommandItem }
key={option.value} className="rounded-sm px-1 font-normal"
onSelect={() => { >
if (isSelected) { {selected?.label}
selectedValues.delete(option.value); </Badge>
} else { {selectedValues.size > 1 && (
selectedValues.add(option.value); <Badge variant="blank" className="rounded-sm px-1 font-normal">
} +{selectedValues.size - 1}
const filterValues = Array.from(selectedValues); </Badge>
setValue?.(filterValues.length ? filterValues : []); )}
}} </>
> );
<div }
className={cn(
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary", return null;
isSelected };
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible", return (
)} <Popover>
> <PopoverTrigger asChild>
<CheckIcon className={cn("h-4 w-4")} /> <Button
</div> variant="outline"
{option.icon && ( size="sm"
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
)} >
<Badge variant={ {title}
option.value === 'success' ? 'green' : <Separator orientation="vertical" className="mx-2 h-4" />
option.value === 'error' ? 'red' : <div className="space-x-1 flex">{getSelectedBadges()}</div>
option.value === 'warning' ? 'orange' : </Button>
option.value === 'info' ? 'blue' : </PopoverTrigger>
option.value === 'debug' ? 'yellow' : 'blank' <PopoverContent className="w-[200px] p-0" align="start">
} >{option.label}</Badge> <Command>
</CommandItem> <CommandList>
); <CommandGroup>
})} <CommandItem
</CommandGroup> onSelect={() => {
setValue?.([]); // Empty array means "All"
</CommandList> }}
</Command> >
</PopoverContent> <div
</Popover> className={cn(
); "mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
} allSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
<Badge variant="blank">All</Badge>
</CommandItem>
{options.map((option) => {
const isSelected = selectedValues.has(option.value);
return (
<CommandItem
key={option.value}
onSelect={() => {
const newValues = new Set(selectedValues);
if (isSelected) {
newValues.delete(option.value);
} else {
newValues.add(option.value);
}
setValue?.(Array.from(newValues));
}}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className={cn("h-4 w-4")} />
</div>
{option.icon && (
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
)}
<Badge
variant={
option.value === "success"
? "green"
: option.value === "error"
? "red"
: option.value === "warning"
? "orange"
: option.value === "info"
? "blue"
: option.value === "debug"
? "yellow"
: "blank"
}
>
{option.label}
</Badge>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -1,5 +1,5 @@
export type LogType = "error" | "warning" | "success" | "info" | "debug"; export type LogType = "error" | "warning" | "success" | "info" | "debug";
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
export interface LogLine { export interface LogLine {
rawTimestamp: string | null; rawTimestamp: string | null;
@@ -138,8 +138,12 @@ export const getLogType = (message: string): LogStyle => {
if ( if (
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) || /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) || /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage) lowerMessage,
) ||
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(
lowerMessage,
)
) { ) {
return LOG_STYLES.debug; return LOG_STYLES.debug;
} }