diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 477f26416..3e490a1f6 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,26 +1,16 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - SelectSeparator -} from "@/components/ui/select"; import { api } from "@/utils/api"; -import { - Download as DownloadIcon, - Loader2 -} from "lucide-react"; +import { Download as DownloadIcon, Loader2 } from "lucide-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 { type LogLine, getLogType, parseLogs } from "./utils"; -import { StatusLogsFilter } from "./status-logs-filter"; interface Props { - containerId: string; - serverId?: string | null; + containerId: string; + serverId?: string | null; } type TimeFilter = "all" | "timestamp" | "1h" | "6h" | "24h" | "168h" | "720h"; @@ -49,276 +39,264 @@ export const priorities = [ ]; export const DockerLogsId: React.FC = ({ containerId, serverId }) => { - const { data } = api.docker.getConfig.useQuery( - { - containerId, - serverId: serverId ?? undefined, - }, - { - enabled: !!containerId, - } - ); + const { data } = api.docker.getConfig.useQuery( + { + containerId, + serverId: serverId ?? undefined, + }, + { + enabled: !!containerId, + }, + ); - const [rawLogs, setRawLogs] = React.useState(""); - const [filteredLogs, setFilteredLogs] = React.useState([]); - const [autoScroll, setAutoScroll] = React.useState(true); - const [lines, setLines] = React.useState(100); - const [search, setSearch] = React.useState(""); - const [showTimestamp, setShowTimestamp] = React.useState(true); - const [since, setSince] = React.useState("all"); + const [rawLogs, setRawLogs] = React.useState(""); + const [filteredLogs, setFilteredLogs] = React.useState([]); + const [autoScroll, setAutoScroll] = React.useState(true); + const [lines, setLines] = React.useState(100); + const [search, setSearch] = React.useState(""); + const [showTimestamp, setShowTimestamp] = React.useState(true); + const [since, setSince] = React.useState("all"); const [typeFilter, setTypeFilter] = React.useState([]); - const scrollRef = useRef(null); - const [isLoading, setIsLoading] = React.useState(false); + const scrollRef = useRef(null); + const [isLoading, setIsLoading] = React.useState(false); - const scrollToBottom = () => { - if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }; + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; - const handleScroll = () => { - if (!scrollRef.current) return; + const handleScroll = () => { + if (!scrollRef.current) return; - const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; - setAutoScroll(isAtBottom); - }; + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; - const handleSearch = (e: React.ChangeEvent) => { - setSearch(e.target.value || ""); - }; + const handleSearch = (e: React.ChangeEvent) => { + setSearch(e.target.value || ""); + }; - const handleLines = (e: React.ChangeEvent) => { - setRawLogs(""); - setFilteredLogs([]); - setLines(Number(e.target.value) || 1); - }; + const handleLines = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setLines(Number(e.target.value) || 1); + }; - const handleSince = (value: TimeFilter) => { - if (value === "timestamp") { - setShowTimestamp(!showTimestamp); - } else { - setRawLogs(""); - setFilteredLogs([]); - setSince(value); - } - }; + const handleSince = (value: string) => { + if (value === "timestamp") { + setShowTimestamp(!showTimestamp); + } else { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + } + }; - useEffect(() => { - if (!containerId) return; - - let isCurrentConnection = true; - let noDataTimeout: NodeJS.Timeout; - setIsLoading(true); - setRawLogs(""); - setFilteredLogs([]); + useEffect(() => { + if (!containerId) return; - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const params = new globalThis.URLSearchParams({ - containerId, - tail: lines.toString(), - since, - search, - }); + let isCurrentConnection = true; + let noDataTimeout: NodeJS.Timeout; + setIsLoading(true); + setRawLogs(""); + setFilteredLogs([]); - if (serverId) { - params.append("serverId", serverId); - } + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const params = new globalThis.URLSearchParams({ + containerId, + tail: lines.toString(), + since, + search, + }); - const wsUrl = `${protocol}//${ - window.location.host - }/docker-container-logs?${params.toString()}`; - console.log("Connecting to WebSocket:", wsUrl); - const ws = new WebSocket(wsUrl); + if (serverId) { + params.append("serverId", serverId); + } - const resetNoDataTimeout = () => { - if (noDataTimeout) clearTimeout(noDataTimeout); - noDataTimeout = setTimeout(() => { - if (isCurrentConnection) { - setIsLoading(false); - } - }, 2000); // Wait 2 seconds for data before showing "No logs found" - }; + const wsUrl = `${protocol}//${ + window.location.host + }/docker-container-logs?${params.toString()}`; + console.log("Connecting to WebSocket:", wsUrl); + const ws = new WebSocket(wsUrl); - ws.onopen = () => { - if (!isCurrentConnection) { - ws.close(); - return; - } - console.log("WebSocket connected"); - resetNoDataTimeout(); - }; + const resetNoDataTimeout = () => { + if (noDataTimeout) clearTimeout(noDataTimeout); + noDataTimeout = setTimeout(() => { + if (isCurrentConnection) { + setIsLoading(false); + } + }, 2000); // Wait 2 seconds for data before showing "No logs found" + }; - ws.onmessage = (e) => { - if (!isCurrentConnection) return; - setRawLogs((prev) => prev + e.data); - setIsLoading(false); - if (noDataTimeout) clearTimeout(noDataTimeout); - }; + ws.onopen = () => { + if (!isCurrentConnection) { + ws.close(); + return; + } + console.log("WebSocket connected"); + resetNoDataTimeout(); + }; - ws.onerror = (error) => { - if (!isCurrentConnection) return; - console.error("WebSocket error:", error); - setIsLoading(false); - if (noDataTimeout) clearTimeout(noDataTimeout); - }; + ws.onmessage = (e) => { + if (!isCurrentConnection) return; + setRawLogs((prev) => prev + e.data); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); + }; - ws.onclose = (e) => { - if (!isCurrentConnection) return; - console.log("WebSocket closed:", e.reason); - setIsLoading(false); - if (noDataTimeout) clearTimeout(noDataTimeout); - }; + ws.onerror = (error) => { + if (!isCurrentConnection) return; + console.error("WebSocket error:", error); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); + }; - return () => { - isCurrentConnection = false; - if (noDataTimeout) clearTimeout(noDataTimeout); - if (ws.readyState === WebSocket.OPEN) { - ws.close(); - } - }; - }, [containerId, serverId, lines, search, since]); + ws.onclose = (e) => { + if (!isCurrentConnection) return; + console.log("WebSocket closed:", e.reason); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); + }; - const handleDownload = () => { - const logContent = filteredLogs - .map( - ({ timestamp, message }: { timestamp: Date | null; message: string }) => - `${timestamp?.toISOString() || "No timestamp"} ${message}` - ) - .join("\n"); + return () => { + isCurrentConnection = false; + if (noDataTimeout) clearTimeout(noDataTimeout); + if (ws.readyState === WebSocket.OPEN) { + ws.close(); + } + }; + }, [containerId, serverId, lines, search, since]); - const blob = new Blob([logContent], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - const appName = data.Name.replace("/", "") || "app"; - const isoDate = new Date().toISOString(); - 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); - }; + const handleDownload = () => { + const logContent = filteredLogs + .map( + ({ timestamp, message }: { timestamp: Date | null; message: string }) => + `${timestamp?.toISOString() || "No timestamp"} ${message}`, + ) + .join("\n"); - const handleFilter = (logs: LogLine[]) => { - return logs.filter((log) => { - const logType = getLogType(log.message).type; - - if (typeFilter.length === 0) { - return true; - } + const blob = new Blob([logContent], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + const appName = data.Name.replace("/", "") || "app"; + const isoDate = new Date().toISOString(); + 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(() => { - setRawLogs(""); - setFilteredLogs([]); - }, [containerId]); + if (typeFilter.length === 0) { + return true; + } - useEffect(() => { - const logs = parseLogs(rawLogs); - const filtered = handleFilter(logs); - setFilteredLogs(filtered); - }, [rawLogs, search, lines, since, typeFilter]); + return typeFilter.includes(logType); + }); + }; - useEffect(() => { - scrollToBottom(); + useEffect(() => { + setRawLogs(""); + setFilteredLogs([]); + }, [containerId]); - if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [filteredLogs, autoScroll]); + useEffect(() => { + const logs = parseLogs(rawLogs); + const filtered = handleFilter(logs); + setFilteredLogs(filtered); + }, [rawLogs, search, lines, since, typeFilter]); - return ( -
-
-
-
-
- + useEffect(() => { + scrollToBottom(); - + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [filteredLogs, autoScroll]); - +
+
+
+
+ + + + + - - -
+ +
- -
-
- {filteredLogs.length > 0 ? ( - filteredLogs.map((filteredLog: LogLine, index: number) => ( - - )) - ) : isLoading ? ( -
- -
- ) : ( -
- No logs found -
- )} -
-
-
-
- ); -}; \ No newline at end of file + +
+
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : isLoading ? ( +
+ +
+ ) : ( +
+ No logs found +
+ )} +
+
+
+ + ); +}; diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx new file mode 100644 index 000000000..6cc8785b3 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -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 ( + + + + + + + + + {timeRanges.map((range) => { + const isSelected = value === range.value; + return ( + onValueChange(range.value)} + > +
+ +
+ {range.label} +
+ ); + })} +
+
+
+ +
+ Show timestamps + +
+
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx index 70a6dfc85..3ef11517a 100644 --- a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx @@ -1,107 +1,170 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; -import { CheckIcon } from "lucide-react"; -import React from "react"; - -interface StatusLogsFilterProps { - value?: string[]; - setValue?: (value: string[]) => void; - title?: string; - options: { - label: string; - value: string; - icon?: React.ComponentType<{ className?: string }>; - }[]; -} - -export function StatusLogsFilter({ - value = [], - setValue, - title, - options, -}: StatusLogsFilterProps) { - const selectedValues = new Set(value as string[]); - - return ( - - - - - - - - - No results found. - - {options.map((option) => { - const isSelected = selectedValues.has(option.value); - return ( - { - if (isSelected) { - selectedValues.delete(option.value); - } else { - selectedValues.add(option.value); - } - const filterValues = Array.from(selectedValues); - setValue?.(filterValues.length ? filterValues : []); - }} - > -
- -
- {option.icon && ( - - )} - {option.label} -
- ); - })} -
- -
-
-
-
- ); -} +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 { cn } from "@/lib/utils"; +import { CheckIcon } from "lucide-react"; +import type React from "react"; + +interface StatusLogsFilterProps { + value?: string[]; + setValue?: (value: string[]) => void; + title?: string; + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; +} + +export function StatusLogsFilter({ + value = [], + setValue, + title, + options, +}: StatusLogsFilterProps) { + const selectedValues = new Set(value as string[]); + const allSelected = selectedValues.size === 0; + + const getSelectedBadges = () => { + if (allSelected) { + return ( + + All + + ); + } + + if (selectedValues.size >= 1) { + const selected = options.find((opt) => selectedValues.has(opt.value)); + return ( + <> + + {selected?.label} + + {selectedValues.size > 1 && ( + + +{selectedValues.size - 1} + + )} + + ); + } + + return null; + }; + + return ( + + + + + + + + + { + setValue?.([]); // Empty array means "All" + }} + > +
+ +
+ All +
+ {options.map((option) => { + const isSelected = selectedValues.has(option.value); + return ( + { + const newValues = new Set(selectedValues); + if (isSelected) { + newValues.delete(option.value); + } else { + newValues.add(option.value); + } + setValue?.(Array.from(newValues)); + }} + > +
+ +
+ {option.icon && ( + + )} + + {option.label} + +
+ ); + })} +
+
+
+
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 409c69892..cf0b30bbd 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -1,5 +1,5 @@ -export type LogType = "error" | "warning" | "success" | "info" | "debug"; -export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; +export type LogType = "error" | "warning" | "success" | "info" | "debug"; +export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; export interface LogLine { rawTimestamp: string | null; @@ -138,8 +138,12 @@ export const getLogType = (message: string): LogStyle => { if ( /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) || - /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) || - /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage) + /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test( + lowerMessage, + ) || + /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test( + lowerMessage, + ) ) { return LOG_STYLES.debug; }