mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(logs): new logs style and system
This commit is contained in:
@@ -90,7 +90,6 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ export const ShowDockerLogsCompose = ({
|
|||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,115 +1,247 @@
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Terminal } from "@xterm/xterm";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { FitAddon } from "xterm-addon-fit";
|
import { getLogType, LogLine, parseLogs } from "./utils";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import { TerminalLine } from "./terminal-line";
|
||||||
|
import { Download as DownloadIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
containerId: string;
|
||||||
containerId: string;
|
serverId?: string | null;
|
||||||
serverId?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerLogsId: React.FC<Props> = ({
|
type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
||||||
id,
|
type TypeFilter = "all" | "error" | "warning" | "success" | "info";
|
||||||
containerId,
|
|
||||||
serverId,
|
|
||||||
}) => {
|
|
||||||
const [term, setTerm] = React.useState<Terminal>();
|
|
||||||
const [lines, setLines] = React.useState<number>(40);
|
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
||||||
// if (containerId === "select-a-container") {
|
const [rawLogs, setRawLogs] = React.useState("");
|
||||||
// return;
|
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
|
||||||
// }
|
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||||
const container = document.getElementById(id);
|
const [lines, setLines] = React.useState<number>(100);
|
||||||
if (container) {
|
const [search, setSearch] = React.useState<string>("");
|
||||||
container.innerHTML = "";
|
const [since, setSince] = React.useState<TimeFilter>("all");
|
||||||
}
|
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
if (wsRef.current) {
|
const scrollToBottom = () => {
|
||||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
if (autoScroll && scrollRef.current) {
|
||||||
wsRef.current.close();
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
wsRef.current = null;
|
};
|
||||||
}
|
|
||||||
const termi = new Terminal({
|
|
||||||
cursorBlink: true,
|
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
lineHeight: 1.25,
|
|
||||||
fontWeight: 400,
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily:
|
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
||||||
|
|
||||||
convertEol: true,
|
const handleScroll = () => {
|
||||||
theme: {
|
if (!scrollRef.current) return;
|
||||||
cursor: "transparent",
|
|
||||||
background: "rgba(0, 0, 0, 0)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
|
setAutoScroll(isAtBottom);
|
||||||
|
};
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const ws = new WebSocket(wsUrl);
|
setRawLogs("");
|
||||||
wsRef.current = ws;
|
setFilteredLogs([]);
|
||||||
const fitAddon = new FitAddon();
|
setSearch(e.target.value || "");
|
||||||
termi.loadAddon(fitAddon);
|
};
|
||||||
// @ts-ignore
|
|
||||||
termi.open(container);
|
|
||||||
fitAddon.fit();
|
|
||||||
termi.focus();
|
|
||||||
setTerm(termi);
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
console.error("WebSocket error: ", error);
|
setRawLogs("");
|
||||||
};
|
setFilteredLogs([]);
|
||||||
|
setLines(Number(e.target.value) || 1);
|
||||||
|
};
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
const handleSince = (value: TimeFilter) => {
|
||||||
termi.write(e.data);
|
setRawLogs("");
|
||||||
};
|
setFilteredLogs([]);
|
||||||
|
setSince(value);
|
||||||
|
};
|
||||||
|
|
||||||
ws.onclose = (e) => {
|
const handleTypeFilter = (value: TypeFilter) => {
|
||||||
console.log(e.reason);
|
setTypeFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
termi.write(`Connection closed!\nReason: ${e.reason}\n`);
|
useEffect(() => {
|
||||||
wsRef.current = null;
|
setRawLogs("");
|
||||||
};
|
setFilteredLogs([]);
|
||||||
return () => {
|
}, [containerId]);
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
ws.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [lines, containerId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
term?.clear();
|
if (!containerId) return;
|
||||||
}, [lines, term]);
|
|
||||||
|
|
||||||
return (
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
<div className="flex flex-col gap-4">
|
const wsUrl = `${protocol}//${
|
||||||
<div className="flex flex-col gap-2">
|
window.location.host
|
||||||
<Label>
|
}/docker-container-logs?containerId=${containerId}&tail=${lines}&since=${since}&search=${search}${
|
||||||
<span>Number of lines to show</span>
|
serverId ? `&serverId=${serverId}` : ""
|
||||||
</Label>
|
}`;
|
||||||
<Input
|
console.log("Connecting to WebSocket:", wsUrl);
|
||||||
type="text"
|
const ws = new WebSocket(wsUrl);
|
||||||
placeholder="Number of lines to show (Defaults to 35)"
|
|
||||||
value={lines}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLines(Number(e.target.value) || 1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
ws.onopen = () => {
|
||||||
<div id={id} />
|
console.log("WebSocket connected");
|
||||||
</div>
|
};
|
||||||
</div>
|
|
||||||
);
|
ws.onmessage = (e) => {
|
||||||
|
// console.log("Received message:", e.data);
|
||||||
|
setRawLogs((prev) => prev + e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (e) => {
|
||||||
|
console.log("WebSocket closed:", e.reason);
|
||||||
|
setRawLogs(
|
||||||
|
(prev) =>
|
||||||
|
prev +
|
||||||
|
`Connection closed!\nReason: ${
|
||||||
|
e.reason || "WebSocket was closed try to refresh"
|
||||||
|
}\n`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [containerId, serverId, lines, search, since]);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
const logContent = filteredLogs
|
||||||
|
.map(
|
||||||
|
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
||||||
|
`${timestamp?.toISOString() || "No timestamp"} ${message}`
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const blob = new Blob([logContent], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `dokploy-logs-${new Date().toISOString()}.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilter = (logs: LogLine[]) => {
|
||||||
|
return logs.filter((log) => {
|
||||||
|
const logType = getLogType(log.message).type;
|
||||||
|
|
||||||
|
const matchesType = typeFilter === "all" || logType === typeFilter;
|
||||||
|
|
||||||
|
return matchesType;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRawLogs("");
|
||||||
|
setFilteredLogs([]);
|
||||||
|
}, [containerId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const logs = parseLogs(rawLogs);
|
||||||
|
const filtered = handleFilter(logs);
|
||||||
|
setFilteredLogs(filtered);
|
||||||
|
}, [rawLogs, search, lines, since, typeFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="shadow-md 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 text-white placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search logs..."
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
/>
|
||||||
|
<Select value={since} onValueChange={handleSince}>
|
||||||
|
<SelectTrigger className="w-[180px] h-9">
|
||||||
|
<SelectValue placeholder="Time filter" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1h">Last 1 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>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={typeFilter} onValueChange={handleTypeFilter}>
|
||||||
|
<SelectTrigger className="w-[180px] h-9">
|
||||||
|
<SelectValue placeholder="Type filter" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">
|
||||||
|
<Badge variant="blank">All</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="error">
|
||||||
|
<Badge variant="red">Error</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="warning">
|
||||||
|
<Badge variant="yellow">Warning</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="success">
|
||||||
|
<Badge variant="green">Success</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="info">
|
||||||
|
<Badge variant="blue">Info</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9"
|
||||||
|
onClick={handleDownload}
|
||||||
|
>
|
||||||
|
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||||
|
Download logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
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 key={index} log={filteredLog} searchTerm={search} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export const ShowDockerModalLogs = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerLogsId
|
<DockerLogsId
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || ""}
|
containerId={containerId || ""}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getLogType, type LogLine } from "./utils";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface LogLineProps {
|
||||||
|
log: LogLine;
|
||||||
|
searchTerm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TerminalLine({ log, searchTerm }: LogLineProps) {
|
||||||
|
const { timestamp, message } = log;
|
||||||
|
const { type, variant, color } = getLogType(message);
|
||||||
|
|
||||||
|
const formattedTime = timestamp
|
||||||
|
? timestamp.toLocaleString("en-GB", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
: "--- No time found ---";
|
||||||
|
|
||||||
|
const highlightMessage = (text: string, term: string) => {
|
||||||
|
if (!term) return text;
|
||||||
|
|
||||||
|
const parts = text.split(new RegExp(`(${term})`, "gi"));
|
||||||
|
return parts.map((part, index) =>
|
||||||
|
part.toLowerCase() === term.toLowerCase() ? (
|
||||||
|
<span key={index} className="bg-yellow-200 dark:bg-yellow-900">
|
||||||
|
{part}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
part
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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"
|
||||||
|
: 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" /> */}
|
||||||
|
<div className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)} />
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
apps/dokploy/components/dashboard/docker/logs/utils.ts
Normal file
132
apps/dokploy/components/dashboard/docker/logs/utils.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
export type LogType = "error" | "warning" | "success" | "info";
|
||||||
|
export type LogVariant = "red" | "yellow" | "green" | "blue";
|
||||||
|
|
||||||
|
export interface LogLine {
|
||||||
|
timestamp: Date | null;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogStyle {
|
||||||
|
type: LogType;
|
||||||
|
variant: LogVariant;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_STYLES: Record<LogType, LogStyle> = {
|
||||||
|
error: {
|
||||||
|
type: "error",
|
||||||
|
variant: "red",
|
||||||
|
color: "bg-red-500/40",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
type: "warning",
|
||||||
|
variant: "yellow",
|
||||||
|
color: "bg-yellow-500/40",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: "success",
|
||||||
|
variant: "green",
|
||||||
|
color: "bg-green-500/40",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
type: "info",
|
||||||
|
variant: "blue",
|
||||||
|
color: "bg-blue-600/40",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function parseLogs(logString: string): LogLine[] {
|
||||||
|
// Regex to match the log line format
|
||||||
|
// Exemple of return :
|
||||||
|
// 1 2024-12-10T10:00:00.000Z The server is running on port 8080
|
||||||
|
// Should return :
|
||||||
|
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),
|
||||||
|
// message: "The server is running on port 8080" }
|
||||||
|
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")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line !== "")
|
||||||
|
.map((line) => {
|
||||||
|
const match = line.match(logRegex);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const [, , timestamp, message] = match;
|
||||||
|
|
||||||
|
if (!message?.trim()) return null;
|
||||||
|
|
||||||
|
// Delete other timestamps and keep only the one from --timestamps
|
||||||
|
const cleanedMessage = message
|
||||||
|
?.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();
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
|
||||||
|
message: cleanedMessage,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((log) => log !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect log type based on message content
|
||||||
|
export const getLogType = (message: string): LogStyle => {
|
||||||
|
const lowerMessage = message.toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
|
||||||
|
/\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
|
||||||
|
/(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
|
||||||
|
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
|
||||||
|
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
|
||||||
|
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
|
||||||
|
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
|
||||||
|
/\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
|
||||||
|
/\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
|
||||||
|
/\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) ||
|
||||||
|
/(?:deprecated|obsolete)\s+(?:since|in|as\s+of)/i.test(lowerMessage) ||
|
||||||
|
/\b(?:caution|attention|notice):\s/i.test(lowerMessage) ||
|
||||||
|
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
|
||||||
|
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
|
||||||
|
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
|
||||||
|
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
|
||||||
|
) {
|
||||||
|
return LOG_STYLES.warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:successfully|complete[d]?)\s+(?:initialized|started|completed|done)/i.test(
|
||||||
|
lowerMessage
|
||||||
|
) ||
|
||||||
|
/\[(?:success|ok|done)\]/i.test(lowerMessage) ||
|
||||||
|
/(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) ||
|
||||||
|
/(?:connected|established|ready)\s+(?:to|for|on)/i.test(lowerMessage) ||
|
||||||
|
/\b(?:loaded|mounted|initialized)\s+successfully\b/i.test(lowerMessage) ||
|
||||||
|
/✓|√|\[ok\]|done!/i.test(lowerMessage) ||
|
||||||
|
/\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) ||
|
||||||
|
/\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) ||
|
||||||
|
/\b(?:version|config|start|import|load)\b:?/i.test(lowerMessage)
|
||||||
|
) {
|
||||||
|
return LOG_STYLES.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LOG_STYLES.info;
|
||||||
|
};
|
||||||
@@ -92,7 +92,6 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogsId
|
<DockerLogsId
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || ""}
|
containerId={containerId || ""}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ 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:
|
||||||
|
"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:
|
||||||
|
"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:
|
||||||
|
"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:
|
||||||
|
"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:
|
||||||
|
"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:
|
||||||
|
"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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
const containerId = url.searchParams.get("containerId");
|
const containerId = url.searchParams.get("containerId");
|
||||||
const tail = url.searchParams.get("tail");
|
const tail = url.searchParams.get("tail");
|
||||||
|
const search = url.searchParams.get("search");
|
||||||
|
const since = url.searchParams.get("since")
|
||||||
const serverId = url.searchParams.get("serverId");
|
const serverId = url.searchParams.get("serverId");
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
client
|
client
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
const command = `
|
const command = `
|
||||||
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'"
|
||||||
`;
|
`;
|
||||||
client.exec(command, (err, stream) => {
|
client.exec(command, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -95,7 +97,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
shell,
|
shell,
|
||||||
[
|
[
|
||||||
"-c",
|
"-c",
|
||||||
`docker container logs --tail ${tail} --follow ${containerId}`,
|
`docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'`,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
name: "xterm-256color",
|
name: "xterm-256color",
|
||||||
|
|||||||
@@ -173,3 +173,29 @@
|
|||||||
padding-top: 1rem !important;
|
padding-top: 1rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Docker Logs Scrollbar */
|
||||||
|
@layer utilities {
|
||||||
|
.custom-logs-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: hsl(var(--muted-foreground)) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logs-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logs-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logs-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: hsl(var(--muted-foreground) / 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logs-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: hsl(var(--muted-foreground) / 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,8 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
const containerId = url.searchParams.get("containerId");
|
const containerId = url.searchParams.get("containerId");
|
||||||
const tail = url.searchParams.get("tail");
|
const tail = url.searchParams.get("tail");
|
||||||
|
const since = url.searchParams.get("since");
|
||||||
|
const search = url.searchParams.get("search");
|
||||||
const serverId = url.searchParams.get("serverId");
|
const serverId = url.searchParams.get("serverId");
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
client
|
client
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
const command = `
|
const command = `
|
||||||
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'"
|
||||||
`;
|
`;
|
||||||
client.exec(command, (err, stream) => {
|
client.exec(command, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -89,7 +91,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
shell,
|
shell,
|
||||||
[
|
[
|
||||||
"-c",
|
"-c",
|
||||||
`docker container logs --tail ${tail} --follow ${containerId}`,
|
`docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'`,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
name: "xterm-256color",
|
name: "xterm-256color",
|
||||||
|
|||||||
Reference in New Issue
Block a user