feat(logs): improvements based on feedback

This commit is contained in:
Nicholas Penree
2024-12-11 19:13:53 -05:00
parent 50b1de9594
commit 42f3105f69
7 changed files with 511 additions and 491 deletions

View File

@@ -1,272 +1,274 @@
import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import React, { useEffect, useRef } from "react"; import { Input } from "@/components/ui/input";
import { getLogType, LogLine, parseLogs } from "./utils"; import {
import { TerminalLine } from "./terminal-line"; Select,
import { Download as DownloadIcon } from "lucide-react"; SelectContent,
import { SelectItem,
Select, SelectTrigger,
SelectContent, SelectValue,
SelectItem, } from "@/components/ui/select";
SelectTrigger, import { api } from "@/utils/api";
SelectValue, import { Download as DownloadIcon } from "lucide-react";
} from "@/components/ui/select"; import React, { useEffect, useRef } from "react";
import { Badge } from "@/components/ui/badge"; import { TerminalLine } from "./terminal-line";
import { api } from "@/utils/api"; import { type LogLine, getLogType, parseLogs } from "./utils";
interface Props { interface Props {
containerId: string; containerId: string;
serverId?: string | null; serverId?: string | null;
} }
type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
type TypeFilter = "all" | "error" | "warning" | "success" | "info"; type TypeFilter = "all" | "error" | "warning" | "success" | "info";
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,
}, },
{ {
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 [since, setSince] = React.useState<TimeFilter>("all"); const [since, setSince] = React.useState<TimeFilter>("all");
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all"); const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
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>) => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
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: TimeFilter) => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
setSince(value); setSince(value);
}; };
const handleTypeFilter = (value: TypeFilter) => { const handleTypeFilter = (value: TypeFilter) => {
setTypeFilter(value); setTypeFilter(value);
}; };
useEffect(() => { useEffect(() => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
}, [containerId]); }, [containerId]);
useEffect(() => { useEffect(() => {
if (!containerId) return; if (!containerId) return;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const params = new globalThis.URLSearchParams({ const params = new globalThis.URLSearchParams({
containerId, containerId,
tail: lines.toString(), tail: lines.toString(),
since, since,
search, search,
}); });
if (serverId) { if (serverId) {
params.append("serverId", serverId); params.append("serverId", serverId);
} }
const wsUrl = `${protocol}//${ const wsUrl = `${protocol}//${
window.location.host window.location.host
}/docker-container-logs?${params.toString()}`; }/docker-container-logs?${params.toString()}`;
console.log("Connecting to WebSocket:", wsUrl); console.log("Connecting to WebSocket:", wsUrl);
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
ws.onopen = () => { ws.onopen = () => {
console.log("WebSocket connected"); console.log("WebSocket connected");
}; };
ws.onmessage = (e) => { ws.onmessage = (e) => {
// console.log("Received message:", e.data); // console.log("Received message:", e.data);
setRawLogs((prev) => prev + e.data); setRawLogs((prev) => prev + e.data);
}; };
ws.onerror = (error) => { ws.onerror = (error) => {
console.error("WebSocket error:", error); console.error("WebSocket error:", error);
}; };
ws.onclose = (e) => { ws.onclose = (e) => {
console.log("WebSocket closed:", e.reason); console.log("WebSocket closed:", e.reason);
}; };
return () => { return () => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.close(); ws.close();
} }
}; };
}, [containerId, serverId, lines, search, since]); }, [containerId, serverId, lines, search, since]);
const handleDownload = () => { const handleDownload = () => {
const logContent = filteredLogs const logContent = filteredLogs
.map( .map(
({ timestamp, message }: { timestamp: Date | null; message: string }) => ({ timestamp, message }: { timestamp: Date | null; message: string }) =>
`${timestamp?.toISOString() || "No timestamp"} ${message}` `${timestamp?.toISOString() || "No timestamp"} ${message}`,
) )
.join("\n"); .join("\n");
const blob = new Blob([logContent], { type: "text/plain" }); const blob = new Blob([logContent], { type: "text/plain" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
const appName = data.Name.replace("/", "") || "app"; const appName = data.Name.replace("/", "") || "app";
a.href = url; a.href = url;
a.download = `logs-${appName}-${new Date().toISOString()}.txt`; a.download = `logs-${appName}-${new Date().toISOString()}.txt`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
const handleFilter = (logs: LogLine[]) => { const handleFilter = (logs: LogLine[]) => {
return logs.filter((log) => { return logs.filter((log) => {
const logType = getLogType(log.message).type; const logType = getLogType(log.message).type;
const matchesType = typeFilter === "all" || logType === typeFilter; const matchesType = typeFilter === "all" || logType === typeFilter;
return matchesType; return matchesType;
}); });
}; };
useEffect(() => { useEffect(() => {
setRawLogs(""); setRawLogs("");
setFilteredLogs([]); setFilteredLogs([]);
}, [containerId]); }, [containerId]);
useEffect(() => { useEffect(() => {
const logs = parseLogs(rawLogs); const logs = parseLogs(rawLogs);
const filtered = handleFilter(logs); const filtered = handleFilter(logs);
setFilteredLogs(filtered); setFilteredLogs(filtered);
}, [rawLogs, search, lines, since, typeFilter]); }, [rawLogs, search, lines, since, typeFilter]);
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
if (autoScroll && scrollRef.current) { if (autoScroll && scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight; scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
} }
}, [filteredLogs, autoScroll]); }, [filteredLogs, autoScroll]);
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="shadow-md rounded-lg overflow-hidden"> <div className="shadow-md rounded-lg overflow-hidden">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4"> <div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<Input <Input
type="text" type="text"
placeholder="Number of lines to show" placeholder="Number of lines to show"
value={lines} value={lines}
onChange={handleLines} onChange={handleLines}
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto" className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
/> />
<Input
type="search" <Select value={since} onValueChange={handleSince}>
placeholder="Search logs..." <SelectTrigger className="w-[180px] h-9">
value={search} <SelectValue placeholder="Time filter" />
onChange={handleSearch} </SelectTrigger>
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto" <SelectContent>
/> <SelectItem value="1h">Last hour</SelectItem>
<Select value={since} onValueChange={handleSince}> <SelectItem value="6h">Last 6 hours</SelectItem>
<SelectTrigger className="w-[180px] h-9"> <SelectItem value="24h">Last 24 hours</SelectItem>
<SelectValue placeholder="Time filter" /> <SelectItem value="168h">Last 7 days</SelectItem>
</SelectTrigger> <SelectItem value="720h">Last 30 days</SelectItem>
<SelectContent> <SelectItem value="all">All time</SelectItem>
<SelectItem value="1h">Last 1 hour</SelectItem> </SelectContent>
<SelectItem value="6h">Last 6 hours</SelectItem> </Select>
<SelectItem value="24h">Last 24 hours</SelectItem>
<SelectItem value="168h">Last 7 days</SelectItem> <Select value={typeFilter} onValueChange={handleTypeFilter}>
<SelectItem value="720h">Last 30 days</SelectItem> <SelectTrigger className="w-[180px] h-9">
<SelectItem value="all">All time</SelectItem> <SelectValue placeholder="Type filter" />
</SelectContent> </SelectTrigger>
</Select> <SelectContent>
<SelectItem value="all">
<Select value={typeFilter} onValueChange={handleTypeFilter}> <Badge variant="blank">All</Badge>
<SelectTrigger className="w-[180px] h-9"> </SelectItem>
<SelectValue placeholder="Type filter" /> <SelectItem value="error">
</SelectTrigger> <Badge variant="red">Error</Badge>
<SelectContent> </SelectItem>
<SelectItem value="all"> <SelectItem value="warning">
<Badge variant="blank">All</Badge> <Badge variant="yellow">Warning</Badge>
</SelectItem> </SelectItem>
<SelectItem value="error"> <SelectItem value="success">
<Badge variant="red">Error</Badge> <Badge variant="green">Success</Badge>
</SelectItem> </SelectItem>
<SelectItem value="warning"> <SelectItem value="info">
<Badge variant="yellow">Warning</Badge> <Badge variant="blue">Info</Badge>
</SelectItem> </SelectItem>
<SelectItem value="success"> </SelectContent>
<Badge variant="green">Success</Badge> </Select>
</SelectItem>
<SelectItem value="info"> <Input
<Badge variant="blue">Info</Badge> type="search"
</SelectItem> placeholder="Search logs..."
</SelectContent> value={search}
</Select> onChange={handleSearch}
</div> className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
/>
<Button </div>
variant="outline"
size="sm" <Button
className="h-9" variant="outline"
onClick={handleDownload} size="sm"
disabled={filteredLogs.length === 0 || !data?.Name} className="h-9"
> onClick={handleDownload}
<DownloadIcon className="mr-2 h-4 w-4" /> disabled={filteredLogs.length === 0 || !data?.Name}
Download logs >
</Button> <DownloadIcon className="mr-2 h-4 w-4" />
</div> Download logs
<div </Button>
ref={scrollRef} </div>
onScroll={handleScroll} <div
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar" ref={scrollRef}
> onScroll={handleScroll}
{filteredLogs.length > 0 ? ( className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
filteredLogs.map((filteredLog: LogLine, index: number) => ( >
<TerminalLine {filteredLogs.length > 0 ? (
key={index} filteredLogs.map((filteredLog: LogLine, index: number) => (
log={filteredLog} <TerminalLine
searchTerm={search} key={index}
/> log={filteredLog}
)) searchTerm={search}
) : ( />
<div className="flex justify-center items-center h-full text-muted-foreground"> ))
No logs found ) : (
</div> <div className="flex justify-center items-center h-full text-muted-foreground">
)} No logs found
</div> </div>
</div> )}
</div> </div>
</div> </div>
); </div>
}; </div>
);
};

View File

@@ -46,10 +46,7 @@ export const ShowDockerModalLogs = ({
<DialogDescription>View the logs for {containerId}</DialogDescription> <DialogDescription>View the logs for {containerId}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId <DockerLogsId containerId={containerId || ""} serverId={serverId} />
containerId={containerId || ""}
serverId={serverId}
/>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -1,74 +1,98 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils"; import {
import { getLogType, type LogLine } from "./utils"; Tooltip,
import React from "react"; TooltipContent,
import { escapeRegExp } from 'lodash'; TooltipProvider,
TooltipTrigger,
interface LogLineProps { } from "@/components/ui/tooltip";
log: LogLine; import { cn } from "@/lib/utils";
searchTerm?: string; import { escapeRegExp } from "lodash";
} import React from "react";
import { type LogLine, getLogType } from "./utils";
export function TerminalLine({ log, searchTerm }: LogLineProps) {
const { timestamp, message } = log; interface LogLineProps {
const { type, variant, color } = getLogType(message); log: LogLine;
searchTerm?: string;
const formattedTime = timestamp }
? timestamp.toLocaleString("en-GB", {
day: "2-digit", export function TerminalLine({ log, searchTerm }: LogLineProps) {
month: "short", const { timestamp, message, rawTimestamp } = log;
year: "numeric", const { type, variant, color } = getLogType(message);
hour: "2-digit",
minute: "2-digit", const formattedTime = timestamp
second: "2-digit", ? timestamp.toLocaleString([], {
hour12: false, month: "2-digit",
}) day: "2-digit",
: "--- No time found ---"; hour: "2-digit",
minute: "2-digit",
const highlightMessage = (text: string, term: string) => { year: "2-digit",
if (!term) return text; second: "2-digit",
})
const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi")); : "--- No time found ---";
return parts.map((part, index) =>
part.toLowerCase() === term.toLowerCase() ? ( const highlightMessage = (text: string, term: string) => {
<span key={index} className="bg-yellow-200 dark:bg-yellow-900"> if (!term) return text;
{part}
</span> const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi"));
) : ( return parts.map((part, index) =>
part part.toLowerCase() === term.toLowerCase() ? (
) <span key={index} className="bg-yellow-200 dark:bg-yellow-900">
); {part}
}; </span>
) : (
return ( part
<div ),
className={cn( );
"font-mono text-xs flex flex-row gap-3 py-2 sm:py-0.5 group", };
type === "error"
? "bg-red-500/10 hover:bg-red-500/15" const tooltip = (color: string, timestamp: string) => {
: type === "warning" return (
? "bg-yellow-500/10 hover:bg-yellow-500/15" <TooltipProvider>
: "hover:bg-gray-200/50 dark:hover:bg-gray-800/50" <Tooltip>
)} <TooltipTrigger asChild>
> <div
{" "} className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)}
<div className="flex items-start gap-x-2"> />
{/* Icon to expand the log item maybe implement a colapsible later */} </TooltipTrigger>
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */} <TooltipContent>
<div className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)} /> <p className="text text-xs text-muted-foreground break-all max-w-md">
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0"> {timestamp}
{formattedTime} </p>
</span> </TooltipContent>
<Badge </Tooltip>
variant={variant} </TooltipProvider>
className="w-14 justify-center text-[10px] px-1 py-0" );
> };
{type}
</Badge> return (
</div> <div
<span className="dark:text-gray-200 text-foreground "> className={cn(
{searchTerm ? highlightMessage(message, searchTerm) : message} "font-mono text-xs flex flex-row gap-3 py-2 sm:py-0.5 group",
</span> type === "error"
</div> ? "bg-red-500/10 hover:bg-red-500/15"
); : type === "warning"
} ? "bg-yellow-500/10 hover:bg-yellow-500/15"
: "hover:bg-gray-200/50 dark:hover:bg-gray-800/50",
)}
>
{" "}
<div className="flex items-start gap-x-2">
{/* Icon to expand the log item maybe implement a colapsible later */}
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
{rawTimestamp && tooltip(color, rawTimestamp)}
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0">
{formattedTime}
</span>
<Badge
variant={variant}
className="w-14 justify-center text-[10px] px-1 py-0"
>
{type}
</Badge>
</div>
<span className="dark:text-gray-200 text-foreground ">
{searchTerm ? highlightMessage(message, searchTerm) : message}
</span>
</div>
);
}

View File

@@ -1,132 +1,134 @@
export type LogType = "error" | "warning" | "success" | "info"; export type LogType = "error" | "warning" | "success" | "info";
export type LogVariant = "red" | "yellow" | "green" | "blue"; export type LogVariant = "red" | "yellow" | "green" | "blue";
export interface LogLine { export interface LogLine {
timestamp: Date | null; rawTimestamp: string | null;
message: string; timestamp: Date | null;
} message: string;
}
interface LogStyle {
type: LogType; interface LogStyle {
variant: LogVariant; type: LogType;
color: string; variant: LogVariant;
} color: string;
}
const LOG_STYLES: Record<LogType, LogStyle> = {
error: { const LOG_STYLES: Record<LogType, LogStyle> = {
type: "error", error: {
variant: "red", type: "error",
color: "bg-red-500/40", variant: "red",
}, color: "bg-red-500/40",
warning: { },
type: "warning", warning: {
variant: "yellow", type: "warning",
color: "bg-yellow-500/40", variant: "yellow",
}, color: "bg-yellow-500/40",
success: { },
type: "success", success: {
variant: "green", type: "success",
color: "bg-green-500/40", variant: "green",
}, color: "bg-green-500/40",
info: { },
type: "info", info: {
variant: "blue", type: "info",
color: "bg-blue-600/40", variant: "blue",
}, color: "bg-blue-600/40",
} as const; },
} as const;
export function parseLogs(logString: string): LogLine[] {
// Regex to match the log line format export function parseLogs(logString: string): LogLine[] {
// Exemple of return : // Regex to match the log line format
// 1 2024-12-10T10:00:00.000Z The server is running on port 8080 // Exemple of return :
// Should return : // 1 2024-12-10T10:00:00.000Z The server is running on port 8080
// { timestamp: new Date("2024-12-10T10:00:00.000Z"), // Should return :
// message: "The server is running on port 8080" } // { timestamp: new Date("2024-12-10T10:00:00.000Z"),
const logRegex = // message: "The server is running on port 8080" }
/^(?:(\d+)\s+)?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC)?\s*(.*)$/; const logRegex =
/^(?:(\d+)\s+)?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC)?\s*(.*)$/;
return logString
.split("\n") return logString
.map((line) => line.trim()) .split("\n")
.filter((line) => line !== "") .map((line) => line.trim())
.map((line) => { .filter((line) => line !== "")
const match = line.match(logRegex); .map((line) => {
if (!match) return null; const match = line.match(logRegex);
if (!match) return null;
const [, , timestamp, message] = match;
const [, , timestamp, message] = match;
if (!message?.trim()) return null;
if (!message?.trim()) return null;
// Delete other timestamps and keep only the one from --timestamps
const cleanedMessage = message // Delete other timestamps and keep only the one from --timestamps
?.replace( const cleanedMessage = message
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g, ?.replace(
"" /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g,
) "",
.trim(); )
.trim();
return {
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null, return {
message: cleanedMessage, rawTimestamp: timestamp,
}; timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
}) message: cleanedMessage,
.filter((log) => log !== null); };
} })
.filter((log) => log !== null);
// Detect log type based on message content }
export const getLogType = (message: string): LogStyle => {
const lowerMessage = message.toLowerCase(); // Detect log type based on message content
export const getLogType = (message: string): LogStyle => {
if ( const lowerMessage = message.toLowerCase();
/(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
/\b(?:exception|failed|failure)\b/i.test(lowerMessage) || if (
/(?:stack\s?trace):\s*$/i.test(lowerMessage) || /(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) || /\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) || /(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) || /^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) || /\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
/\[(?:error|err|fatal)\]/i.test(lowerMessage) || /Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
/\b(?:crash|critical|fatal)\b/i.test(lowerMessage) || /\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage) /\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
) { /\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
return LOG_STYLES.error; /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage)
} ) {
return LOG_STYLES.error;
if ( }
/(?:^|\s)(?:warning|warn):?\s/i.test(lowerMessage) ||
/\[(?:warn(?:ing)?|attention)\]/i.test(lowerMessage) || if (
/(?:deprecated|obsolete)\s+(?:since|in|as\s+of)/i.test(lowerMessage) || /(?:^|\s)(?:warning|warn):?\s/i.test(lowerMessage) ||
/\b(?:caution|attention|notice):\s/i.test(lowerMessage) || /\[(?:warn(?:ing)?|attention)\]/i.test(lowerMessage) ||
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) || /(?:deprecated|obsolete)\s+(?:since|in|as\s+of)/i.test(lowerMessage) ||
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) || /\b(?:caution|attention|notice):\s/i.test(lowerMessage) ||
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) || /(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
/\b(?:unstable|experimental)\b/i.test(lowerMessage) /(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
) { /\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
return LOG_STYLES.warning; /\b(?:unstable|experimental)\b/i.test(lowerMessage)
} ) {
return LOG_STYLES.warning;
if ( }
/(?:successfully|complete[d]?)\s+(?:initialized|started|completed|done)/i.test(
lowerMessage if (
) || /(?:successfully|complete[d]?)\s+(?:initialized|started|completed|done)/i.test(
/\[(?:success|ok|done)\]/i.test(lowerMessage) || lowerMessage,
/(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) || ) ||
/(?:connected|established|ready)\s+(?:to|for|on)/i.test(lowerMessage) || /\[(?:success|ok|done)\]/i.test(lowerMessage) ||
/\b(?:loaded|mounted|initialized)\s+successfully\b/i.test(lowerMessage) || /(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) ||
/✓|√|\[ok\]|done!/i.test(lowerMessage) || /(?:connected|established|ready)\s+(?:to|for|on)/i.test(lowerMessage) ||
/\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) || /\b(?:loaded|mounted|initialized)\s+successfully\b/i.test(lowerMessage) ||
/\b(?:started|running|active)\b/i.test(lowerMessage) /✓|√|\[ok\]|done!/i.test(lowerMessage) ||
) { /\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) ||
return LOG_STYLES.success; /\b(?:started|running|active)\b/i.test(lowerMessage)
} ) {
return LOG_STYLES.success;
if ( }
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
/\[(info|log|debug|trace|server|db|api)\]/i.test(lowerMessage) || if (
/\b(?:version|config|start|import|load)\b:?/i.test(lowerMessage) /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
) { /\[(info|log|debug|trace|server|db|api)\]/i.test(lowerMessage) ||
return LOG_STYLES.info; /\b(?:version|config|start|import|load)\b:?/i.test(lowerMessage)
} ) {
return LOG_STYLES.info;
return LOG_STYLES.info; }
};
return LOG_STYLES.info;
};

View File

@@ -91,10 +91,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
<DockerLogsId <DockerLogsId containerId={containerId || ""} serverId={serverId} />
containerId={containerId || ""}
serverId={serverId}
/>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -14,16 +14,14 @@ const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
red: red: "border-transparent select-none items-center whitespace-nowrap font-medium bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md",
"border-transparent select-none items-center whitespace-nowrap font-medium bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md",
yellow: yellow:
"border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-500/15 text-yellow-500 text-xs h-4 px-1 py-1 rounded-md", "border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-500/15 text-yellow-500 text-xs h-4 px-1 py-1 rounded-md",
orange: orange:
"border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-500/15 text-orange-500 text-xs h-4 px-1 py-1 rounded-md", "border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-500/15 text-orange-500 text-xs h-4 px-1 py-1 rounded-md",
green: green:
"border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-500/15 text-emerald-500 text-xs h-4 px-1 py-1 rounded-md", "border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-500/15 text-emerald-500 text-xs h-4 px-1 py-1 rounded-md",
blue: blue: "border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-500/15 text-blue-500 text-xs h-4 px-1 py-1 rounded-md",
"border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-500/15 text-blue-500 text-xs h-4 px-1 py-1 rounded-md",
blank: blank:
"border-transparent select-none items-center whitespace-nowrap font-medium dark:bg-white/15 bg-black/15 text-foreground text-xs h-4 px-1 py-1 rounded-md", "border-transparent select-none items-center whitespace-nowrap font-medium dark:bg-white/15 bg-black/15 text-foreground text-xs h-4 px-1 py-1 rounded-md",
outline: "text-foreground", outline: "text-foreground",

View File

@@ -198,4 +198,4 @@
.custom-logs-scrollbar::-webkit-scrollbar-thumb:hover { .custom-logs-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.5); background-color: hsl(var(--muted-foreground) / 0.5);
} }
} }