From 00f9e262a9c0140251df8e088b7a3a4399306f9a Mon Sep 17 00:00:00 2001 From: 190km Date: Wed, 11 Dec 2024 00:20:22 +0100 Subject: [PATCH 01/51] feat(logs): new logs style and system --- .../dashboard/application/logs/show.tsx | 1 - .../dashboard/compose/logs/show.tsx | 1 - .../dashboard/docker/logs/docker-logs-id.tsx | 324 ++++++++++++------ .../docker/logs/show-docker-modal-logs.tsx | 1 - .../dashboard/docker/logs/terminal-line.tsx | 73 ++++ .../components/dashboard/docker/logs/utils.ts | 132 +++++++ .../settings/web-server/show-modal-logs.tsx | 1 - apps/dokploy/components/ui/badge.tsx | 12 + .../server/wss/docker-container-logs.ts | 6 +- apps/dokploy/styles/globals.css | 26 ++ .../server/src/wss/docker-container-logs.ts | 6 +- 11 files changed, 479 insertions(+), 104 deletions(-) create mode 100644 apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx create mode 100644 apps/dokploy/components/dashboard/docker/logs/utils.ts diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index 33beab12..dba3666c 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -90,7 +90,6 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => { diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 99208694..6b39f413 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -96,7 +96,6 @@ export const ShowDockerLogsCompose = ({ 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 6fc0ab48..e9459849 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,115 +1,247 @@ import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Terminal } from "@xterm/xterm"; +import { Button } from "@/components/ui/button"; import React, { useEffect, useRef } from "react"; -import { FitAddon } from "xterm-addon-fit"; -import "@xterm/xterm/css/xterm.css"; +import { getLogType, LogLine, parseLogs } from "./utils"; +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 { - id: string; - containerId: string; - serverId?: string | null; + containerId: string; + serverId?: string | null; } -export const DockerLogsId: React.FC = ({ - id, - containerId, - serverId, -}) => { - const [term, setTerm] = React.useState(); - const [lines, setLines] = React.useState(40); - const wsRef = useRef(null); // Ref to hold WebSocket instance +type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; +type TypeFilter = "all" | "error" | "warning" | "success" | "info"; - useEffect(() => { - // if (containerId === "select-a-container") { - // return; - // } - const container = document.getElementById(id); - if (container) { - container.innerHTML = ""; - } +export const DockerLogsId: React.FC = ({ containerId, serverId }) => { + 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 [since, setSince] = React.useState("all"); + const [typeFilter, setTypeFilter] = React.useState("all"); + const scrollRef = useRef(null); - if (wsRef.current) { - if (wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.close(); - } - 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', + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; - convertEol: true, - theme: { - cursor: "transparent", - background: "rgba(0, 0, 0, 0)", - }, - }); + const handleScroll = () => { + if (!scrollRef.current) return; - 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 ws = new WebSocket(wsUrl); - wsRef.current = ws; - const fitAddon = new FitAddon(); - termi.loadAddon(fitAddon); - // @ts-ignore - termi.open(container); - fitAddon.fit(); - termi.focus(); - setTerm(termi); + const handleSearch = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setSearch(e.target.value || ""); + }; - ws.onerror = (error) => { - console.error("WebSocket error: ", error); - }; + const handleLines = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setLines(Number(e.target.value) || 1); + }; - ws.onmessage = (e) => { - termi.write(e.data); - }; + const handleSince = (value: TimeFilter) => { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + }; - ws.onclose = (e) => { - console.log(e.reason); + const handleTypeFilter = (value: TypeFilter) => { + setTypeFilter(value); + }; - termi.write(`Connection closed!\nReason: ${e.reason}\n`); - wsRef.current = null; - }; - return () => { - if (wsRef.current?.readyState === WebSocket.OPEN) { - ws.close(); - wsRef.current = null; - } - }; - }, [lines, containerId]); + useEffect(() => { + setRawLogs(""); + setFilteredLogs([]); + }, [containerId]); - useEffect(() => { - term?.clear(); - }, [lines, term]); + useEffect(() => { + if (!containerId) return; - return ( -
-
- - { - setLines(Number(e.target.value) || 1); - }} - /> -
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${protocol}//${ + window.location.host + }/docker-container-logs?containerId=${containerId}&tail=${lines}&since=${since}&search=${search}${ + serverId ? `&serverId=${serverId}` : "" + }`; + console.log("Connecting to WebSocket:", wsUrl); + const ws = new WebSocket(wsUrl); -
-
-
-
- ); + ws.onopen = () => { + console.log("WebSocket connected"); + }; + + 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 ( +
+
+
+
+
+ + + + + +
+ + +
+
+ {filteredLogs.map((filteredLog: LogLine, index: number) => ( + + ))} +
+
+
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx index c3d38d98..4aea3c7d 100644 --- a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx @@ -47,7 +47,6 @@ export const ShowDockerModalLogs = ({
diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx new file mode 100644 index 00000000..36499df9 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -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() ? ( + + {part} + + ) : ( + part + ) + ); + }; + + return ( +
+ {" "} +
+ {/* Icon to expand the log item maybe implement a colapsible later */} + {/* */} +
+ + {formattedTime} + + + {type} + +
+ + {searchTerm ? highlightMessage(message, searchTerm) : message} + +
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts new file mode 100644 index 00000000..1c010681 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -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 = { + 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; +}; diff --git a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx index 2693f79c..f15e475c 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx @@ -92,7 +92,6 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => { diff --git a/apps/dokploy/components/ui/badge.tsx b/apps/dokploy/components/ui/badge.tsx index f38976c0..fd190b8a 100644 --- a/apps/dokploy/components/ui/badge.tsx +++ b/apps/dokploy/components/ui/badge.tsx @@ -14,6 +14,18 @@ const badgeVariants = cva( "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "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", }, }, diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 1493f698..882b2b4e 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -31,6 +31,8 @@ export const setupDockerContainerLogsWebSocketServer = ( const url = new URL(req.url || "", `http://${req.headers.host}`); const containerId = url.searchParams.get("containerId"); 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 { user, session } = await validateWebSocketRequest(req); @@ -52,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = ( client .once("ready", () => { 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) => { if (err) { @@ -95,7 +97,7 @@ export const setupDockerContainerLogsWebSocketServer = ( shell, [ "-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", diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css index f7e2a71f..a116d66b 100644 --- a/apps/dokploy/styles/globals.css +++ b/apps/dokploy/styles/globals.css @@ -173,3 +173,29 @@ 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); + } +} \ No newline at end of file diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts index 75292018..ab3234b7 100644 --- a/packages/server/src/wss/docker-container-logs.ts +++ b/packages/server/src/wss/docker-container-logs.ts @@ -32,6 +32,8 @@ export const setupDockerContainerLogsWebSocketServer = ( const url = new URL(req.url || "", `http://${req.headers.host}`); const containerId = url.searchParams.get("containerId"); 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 { user, session } = await validateWebSocketRequest(req); @@ -54,7 +56,7 @@ export const setupDockerContainerLogsWebSocketServer = ( client .once("ready", () => { 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) => { if (err) { @@ -89,7 +91,7 @@ export const setupDockerContainerLogsWebSocketServer = ( shell, [ "-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", From 95cd410825e452f0b4f71928f283098347da366e Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 11 Dec 2024 10:29:09 -0500 Subject: [PATCH 02/51] feat(logs): improvements to searching --- .../dashboard/docker/logs/docker-logs-id.tsx | 22 ++++++++++++------- .../dashboard/docker/logs/terminal-line.tsx | 3 ++- .../server/wss/docker-container-logs.ts | 19 +++++++++++----- .../server/src/wss/docker-container-logs.ts | 4 ++-- 4 files changed, 32 insertions(+), 16 deletions(-) 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 e9459849..7c0edf05 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -76,11 +76,18 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { if (!containerId) return; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${ - window.location.host - }/docker-container-logs?containerId=${containerId}&tail=${lines}&since=${since}&search=${search}${ - serverId ? `&serverId=${serverId}` : "" - }`; + const params = new globalThis.URLSearchParams({ + containerId, + tail: lines.toString(), + since, + search + }); + + if (serverId) { + params.append('serverId', serverId); + } + + const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?${params.toString()}`; console.log("Connecting to WebSocket:", wsUrl); const ws = new WebSocket(wsUrl); @@ -101,8 +108,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { console.log("WebSocket closed:", e.reason); setRawLogs( (prev) => - prev + - `Connection closed!\nReason: ${ + `${prev}Connection closed!\nReason: ${ e.reason || "WebSocket was closed try to refresh" }\n` ); @@ -177,7 +183,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto" /> { if (!term) return text; - const parts = text.split(new RegExp(`(${term})`, "gi")); + const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi")); return parts.map((part, index) => part.toLowerCase() === term.toLowerCase() ? ( diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 882b2b4e..4630d8fe 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -32,7 +32,7 @@ export const setupDockerContainerLogsWebSocketServer = ( const containerId = url.searchParams.get("containerId"); const tail = url.searchParams.get("tail"); const search = url.searchParams.get("search"); - const since = url.searchParams.get("since") + const since = url.searchParams.get("since"); const serverId = url.searchParams.get("serverId"); const { user, session } = await validateWebSocketRequest(req); @@ -53,9 +53,12 @@ export const setupDockerContainerLogsWebSocketServer = ( const client = new Client(); client .once("ready", () => { - const command = ` - bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'" - `; + const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + since === "all" ? "" : `--since ${since}` + } --follow ${containerId}`; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${search}'` + : baseCommand; client.exec(command, (err, stream) => { if (err) { console.error("Execution error:", err); @@ -93,11 +96,17 @@ export const setupDockerContainerLogsWebSocketServer = ( }); } else { const shell = getShell(); + const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + since === "all" ? "" : `--since ${since}` + } --follow ${containerId}`; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${search}'` + : baseCommand; const ptyProcess = spawn( shell, [ "-c", - `docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'`, + command, ], { name: "xterm-256color", diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts index ab3234b7..db9ce90b 100644 --- a/packages/server/src/wss/docker-container-logs.ts +++ b/packages/server/src/wss/docker-container-logs.ts @@ -56,7 +56,7 @@ export const setupDockerContainerLogsWebSocketServer = ( client .once("ready", () => { const command = ` - bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'" + bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -iF '${search}'" `; client.exec(command, (err, stream) => { if (err) { @@ -91,7 +91,7 @@ export const setupDockerContainerLogsWebSocketServer = ( shell, [ "-c", - `docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'`, + `docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -iF '${search}'`, ], { name: "xterm-256color", From 7233667d496379f8730cdd52110e5a6e57855a7c Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 11 Dec 2024 10:35:12 -0500 Subject: [PATCH 03/51] feat(logs): improvements to searching --- .../server/wss/docker-container-logs.ts | 39 ++++++++----------- .../server/src/wss/docker-container-logs.ts | 38 +++++++++--------- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 4630d8fe..1b28c11b 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -55,10 +55,10 @@ export const setupDockerContainerLogsWebSocketServer = ( .once("ready", () => { const baseCommand = `docker container logs --timestamps --tail ${tail} ${ since === "all" ? "" : `--since ${since}` - } --follow ${containerId}`; - const command = search - ? `${baseCommand} 2>&1 | grep -iF '${search}'` - : baseCommand; + } --follow ${containerId}`; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${search}'` + : baseCommand; client.exec(command, (err, stream) => { if (err) { console.error("Execution error:", err); @@ -98,25 +98,18 @@ export const setupDockerContainerLogsWebSocketServer = ( const shell = getShell(); const baseCommand = `docker container logs --timestamps --tail ${tail} ${ since === "all" ? "" : `--since ${since}` - } --follow ${containerId}`; - const command = search - ? `${baseCommand} 2>&1 | grep -iF '${search}'` - : baseCommand; - const ptyProcess = spawn( - shell, - [ - "-c", - command, - ], - { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }, - ); + } --follow ${containerId}`; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${search}'` + : baseCommand; + const ptyProcess = spawn(shell, ["-c", command], { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }); ptyProcess.onData((data) => { ws.send(data); diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts index db9ce90b..7cfdb8d6 100644 --- a/packages/server/src/wss/docker-container-logs.ts +++ b/packages/server/src/wss/docker-container-logs.ts @@ -55,9 +55,12 @@ export const setupDockerContainerLogsWebSocketServer = ( new Promise((resolve, reject) => { client .once("ready", () => { - const command = ` - bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -iF '${search}'" - `; + const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + since === "all" ? "" : `--since ${since}` + } --follow ${containerId}`; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${search}'` + : baseCommand; client.exec(command, (err, stream) => { if (err) { console.error("Execution error:", err); @@ -87,21 +90,20 @@ export const setupDockerContainerLogsWebSocketServer = ( }); } else { const shell = getShell(); - const ptyProcess = spawn( - shell, - [ - "-c", - `docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -iF '${search}'`, - ], - { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }, - ); + const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + since === "all" ? "" : `--since ${since}` + } --follow ${containerId}`; + const command = search + ? `${baseCommand} 2>&1 | grep -iF '${search}'` + : baseCommand; + const ptyProcess = spawn(shell, ["-c", command], { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }); ptyProcess.onData((data) => { ws.send(data); From 9a51e0a00dc310cfce7585d203a3748c222a59cc Mon Sep 17 00:00:00 2001 From: 190km Date: Wed, 11 Dec 2024 17:58:35 +0100 Subject: [PATCH 04/51] show a message about no matches found --- .../dashboard/docker/logs/docker-logs-id.tsx | 504 +++++++++--------- 1 file changed, 251 insertions(+), 253 deletions(-) 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 7c0edf05..010a6265 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,253 +1,251 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import React, { useEffect, useRef } from "react"; -import { getLogType, LogLine, parseLogs } from "./utils"; -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 { - containerId: string; - serverId?: string | null; -} - -type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; -type TypeFilter = "all" | "error" | "warning" | "success" | "info"; - -export const DockerLogsId: React.FC = ({ containerId, serverId }) => { - 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 [since, setSince] = React.useState("all"); - const [typeFilter, setTypeFilter] = React.useState("all"); - const scrollRef = useRef(null); - - const scrollToBottom = () => { - if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }; - - const handleScroll = () => { - if (!scrollRef.current) return; - - const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; - setAutoScroll(isAtBottom); - }; - - const handleSearch = (e: React.ChangeEvent) => { - setRawLogs(""); - setFilteredLogs([]); - setSearch(e.target.value || ""); - }; - - const handleLines = (e: React.ChangeEvent) => { - setRawLogs(""); - setFilteredLogs([]); - setLines(Number(e.target.value) || 1); - }; - - const handleSince = (value: TimeFilter) => { - setRawLogs(""); - setFilteredLogs([]); - setSince(value); - }; - - const handleTypeFilter = (value: TypeFilter) => { - setTypeFilter(value); - }; - - useEffect(() => { - setRawLogs(""); - setFilteredLogs([]); - }, [containerId]); - - useEffect(() => { - if (!containerId) return; - - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const params = new globalThis.URLSearchParams({ - containerId, - tail: lines.toString(), - since, - search - }); - - if (serverId) { - params.append('serverId', serverId); - } - - const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?${params.toString()}`; - console.log("Connecting to WebSocket:", wsUrl); - const ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log("WebSocket connected"); - }; - - 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 ( -
-
-
-
-
- - - - - -
- - -
-
- {filteredLogs.map((filteredLog: LogLine, index: number) => ( - - ))} -
-
-
-
- ); -}; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import React, { useEffect, useRef } from "react"; +import { getLogType, LogLine, parseLogs } from "./utils"; +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 { + containerId: string; + serverId?: string | null; +} + +type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; +type TypeFilter = "all" | "error" | "warning" | "success" | "info"; + +export const DockerLogsId: React.FC = ({ containerId, serverId }) => { + 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 [since, setSince] = React.useState("all"); + const [typeFilter, setTypeFilter] = React.useState("all"); + const scrollRef = useRef(null); + const [errorMessage, setErrorMessage] = React.useState(null); + + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; + + const handleSearch = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setSearch(e.target.value || ""); + }; + + const handleLines = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setLines(Number(e.target.value) || 1); + }; + + const handleSince = (value: TimeFilter) => { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + }; + + const handleTypeFilter = (value: TypeFilter) => { + setTypeFilter(value); + }; + + useEffect(() => { + setRawLogs(""); + setFilteredLogs([]); + }, [containerId]); + + useEffect(() => { + if (!containerId) return; + + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const params = new globalThis.URLSearchParams({ + containerId, + tail: lines.toString(), + since, + search + }); + + if (serverId) { + params.append('serverId', serverId); + } + + const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?${params.toString()}`; + console.log("Connecting to WebSocket:", wsUrl); + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log("WebSocket connected"); + }; + + 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); + setErrorMessage(`Connection closed!\nReason: ${e.reason || "WebSocket was closed try to refresh"}`); + }; + + 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 ( +
+
+
+
+
+ + + + + +
+ + +
+
+ { + filteredLogs.length > 0 ? filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) :
No logs found
+ } +
+
+
+
+ ); +}; From 20b253e708edc5cd941e94556aed87c8f0234717 Mon Sep 17 00:00:00 2001 From: usopp Date: Wed, 11 Dec 2024 18:03:28 +0100 Subject: [PATCH 05/51] removed useless state --- .../dokploy/components/dashboard/docker/logs/docker-logs-id.tsx | 2 -- 1 file changed, 2 deletions(-) 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 010a6265..16a96314 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -30,7 +30,6 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const [since, setSince] = React.useState("all"); const [typeFilter, setTypeFilter] = React.useState("all"); const scrollRef = useRef(null); - const [errorMessage, setErrorMessage] = React.useState(null); const scrollToBottom = () => { if (autoScroll && scrollRef.current) { @@ -107,7 +106,6 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { ws.onclose = (e) => { console.log("WebSocket closed:", e.reason); - setErrorMessage(`Connection closed!\nReason: ${e.reason || "WebSocket was closed try to refresh"}`); }; return () => { From cb90281583597541d40605c270898390dc94458a Mon Sep 17 00:00:00 2001 From: 190km Date: Wed, 11 Dec 2024 20:25:49 +0100 Subject: [PATCH 06/51] feat: added appname as filename when export --- .../dashboard/docker/logs/docker-logs-id.tsx | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) 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 16a96314..03b82fe5 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -12,6 +12,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; +import { api } from "@/utils/api"; interface Props { containerId: string; @@ -22,6 +23,16 @@ type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; type TypeFilter = "all" | "error" | "warning" | "success" | "info"; export const DockerLogsId: React.FC = ({ containerId, serverId }) => { + const { data } = api.docker.getConfig.useQuery( + { + containerId, + serverId, + }, + { + enabled: !!containerId, + } + ); + const [rawLogs, setRawLogs] = React.useState(""); const [filteredLogs, setFilteredLogs] = React.useState([]); const [autoScroll, setAutoScroll] = React.useState(true); @@ -80,14 +91,16 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { containerId, tail: lines.toString(), since, - search + search, }); - + if (serverId) { - params.append('serverId', serverId); + params.append("serverId", serverId); } - const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?${params.toString()}`; + const wsUrl = `${protocol}//${ + window.location.host + }/docker-container-logs?${params.toString()}`; console.log("Connecting to WebSocket:", wsUrl); const ws = new WebSocket(wsUrl); @@ -126,8 +139,9 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const blob = new Blob([logContent], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); + const appName = data.Name.replace("/", "") || "app"; a.href = url; - a.download = `dokploy-logs-${new Date().toISOString()}.txt`; + a.download = `logs-${appName}-${new Date().toISOString()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -226,6 +240,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { size="sm" className="h-9" onClick={handleDownload} + disabled={filteredLogs.length === 0 || !data?.Name} > Download logs @@ -234,13 +249,21 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => {
- { - filteredLogs.length > 0 ? filteredLogs.map((filteredLog: LogLine, index: number) => ( - - )) :
No logs found
- } + {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : ( +
+ No logs found +
+ )}
From 50b1de959434018a615e0acf70e3f76fc07aa74d Mon Sep 17 00:00:00 2001 From: 190km Date: Wed, 11 Dec 2024 20:26:19 +0100 Subject: [PATCH 07/51] feat: added appname as filename when export --- .../dokploy/components/dashboard/docker/logs/docker-logs-id.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 03b82fe5..8cb9ff24 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -249,7 +249,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => {
{filteredLogs.length > 0 ? ( filteredLogs.map((filteredLog: LogLine, index: number) => ( From 42f3105f6949184133ab9c085863eaf6c0abc6dd Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 11 Dec 2024 19:13:53 -0500 Subject: [PATCH 08/51] feat(logs): improvements based on feedback --- .../dashboard/docker/logs/docker-logs-id.tsx | 546 +++++++++--------- .../docker/logs/show-docker-modal-logs.tsx | 5 +- .../dashboard/docker/logs/terminal-line.tsx | 172 +++--- .../components/dashboard/docker/logs/utils.ts | 266 ++++----- .../settings/web-server/show-modal-logs.tsx | 5 +- apps/dokploy/components/ui/badge.tsx | 6 +- apps/dokploy/styles/globals.css | 2 +- 7 files changed, 511 insertions(+), 491 deletions(-) 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 8cb9ff24..4d2dfe9c 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,272 +1,274 @@ -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import React, { useEffect, useRef } from "react"; -import { getLogType, LogLine, parseLogs } from "./utils"; -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"; -import { api } from "@/utils/api"; - -interface Props { - containerId: string; - serverId?: string | null; -} - -type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; -type TypeFilter = "all" | "error" | "warning" | "success" | "info"; - -export const DockerLogsId: React.FC = ({ containerId, serverId }) => { - const { data } = api.docker.getConfig.useQuery( - { - containerId, - serverId, - }, - { - 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 [since, setSince] = React.useState("all"); - const [typeFilter, setTypeFilter] = React.useState("all"); - const scrollRef = useRef(null); - - const scrollToBottom = () => { - if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }; - - const handleScroll = () => { - if (!scrollRef.current) return; - - const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; - setAutoScroll(isAtBottom); - }; - - const handleSearch = (e: React.ChangeEvent) => { - setRawLogs(""); - setFilteredLogs([]); - setSearch(e.target.value || ""); - }; - - const handleLines = (e: React.ChangeEvent) => { - setRawLogs(""); - setFilteredLogs([]); - setLines(Number(e.target.value) || 1); - }; - - const handleSince = (value: TimeFilter) => { - setRawLogs(""); - setFilteredLogs([]); - setSince(value); - }; - - const handleTypeFilter = (value: TypeFilter) => { - setTypeFilter(value); - }; - - useEffect(() => { - setRawLogs(""); - setFilteredLogs([]); - }, [containerId]); - - useEffect(() => { - if (!containerId) return; - - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const params = new globalThis.URLSearchParams({ - containerId, - tail: lines.toString(), - since, - search, - }); - - if (serverId) { - params.append("serverId", serverId); - } - - const wsUrl = `${protocol}//${ - window.location.host - }/docker-container-logs?${params.toString()}`; - console.log("Connecting to WebSocket:", wsUrl); - const ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log("WebSocket connected"); - }; - - 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); - }; - - 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"); - const appName = data.Name.replace("/", "") || "app"; - a.href = url; - a.download = `logs-${appName}-${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 ( -
-
-
-
-
- - - - - -
- - -
-
- {filteredLogs.length > 0 ? ( - filteredLogs.map((filteredLog: LogLine, index: number) => ( - - )) - ) : ( -
- No logs found -
- )} -
-
-
-
- ); -}; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; +import { Download as DownloadIcon } from "lucide-react"; +import React, { useEffect, useRef } from "react"; +import { TerminalLine } from "./terminal-line"; +import { type LogLine, getLogType, parseLogs } from "./utils"; + +interface Props { + containerId: string; + serverId?: string | null; +} + +type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; +type TypeFilter = "all" | "error" | "warning" | "success" | "info"; + +export const DockerLogsId: React.FC = ({ containerId, serverId }) => { + const { data } = api.docker.getConfig.useQuery( + { + containerId, + serverId, + }, + { + 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 [since, setSince] = React.useState("all"); + const [typeFilter, setTypeFilter] = React.useState("all"); + const scrollRef = useRef(null); + + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; + + const handleSearch = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setSearch(e.target.value || ""); + }; + + const handleLines = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setLines(Number(e.target.value) || 1); + }; + + const handleSince = (value: TimeFilter) => { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + }; + + const handleTypeFilter = (value: TypeFilter) => { + setTypeFilter(value); + }; + + useEffect(() => { + setRawLogs(""); + setFilteredLogs([]); + }, [containerId]); + + useEffect(() => { + if (!containerId) return; + + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const params = new globalThis.URLSearchParams({ + containerId, + tail: lines.toString(), + since, + search, + }); + + if (serverId) { + params.append("serverId", serverId); + } + + const wsUrl = `${protocol}//${ + window.location.host + }/docker-container-logs?${params.toString()}`; + console.log("Connecting to WebSocket:", wsUrl); + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log("WebSocket connected"); + }; + + 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); + }; + + 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"); + const appName = data.Name.replace("/", "") || "app"; + a.href = url; + a.download = `logs-${appName}-${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 ( +
+
+
+
+
+ + + + + + + +
+ + +
+
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : ( +
+ No logs found +
+ )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx index 4aea3c7d..f8531d77 100644 --- a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx @@ -46,10 +46,7 @@ export const ShowDockerModalLogs = ({ View the logs for {containerId}
- +
diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 6790e6d9..eb619277 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -1,74 +1,98 @@ -import { Badge } from "@/components/ui/badge"; -import { cn } from "@/lib/utils"; -import { getLogType, type LogLine } from "./utils"; -import React from "react"; -import { escapeRegExp } from 'lodash'; - -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(`(${escapeRegExp(term)})`, "gi")); - return parts.map((part, index) => - part.toLowerCase() === term.toLowerCase() ? ( - - {part} - - ) : ( - part - ) - ); - }; - - return ( -
- {" "} -
- {/* Icon to expand the log item maybe implement a colapsible later */} - {/* */} -
- - {formattedTime} - - - {type} - -
- - {searchTerm ? highlightMessage(message, searchTerm) : message} - -
- ); -} +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { escapeRegExp } from "lodash"; +import React from "react"; +import { type LogLine, getLogType } from "./utils"; + +interface LogLineProps { + log: LogLine; + searchTerm?: string; +} + +export function TerminalLine({ log, searchTerm }: LogLineProps) { + const { timestamp, message, rawTimestamp } = log; + const { type, variant, color } = getLogType(message); + + const formattedTime = timestamp + ? timestamp.toLocaleString([], { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + year: "2-digit", + second: "2-digit", + }) + : "--- No time found ---"; + + const highlightMessage = (text: string, term: string) => { + if (!term) return text; + + const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi")); + return parts.map((part, index) => + part.toLowerCase() === term.toLowerCase() ? ( + + {part} + + ) : ( + part + ), + ); + }; + + const tooltip = (color: string, timestamp: string) => { + return ( + + + +
+ + +

+ {timestamp} +

+
+ + + ); + }; + + return ( +
+ {" "} +
+ {/* Icon to expand the log item maybe implement a colapsible later */} + {/* */} + {rawTimestamp && tooltip(color, rawTimestamp)} + + {formattedTime} + + + {type} + +
+ + {searchTerm ? highlightMessage(message, searchTerm) : message} + +
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 1c010681..6e11fe0e 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -1,132 +1,134 @@ -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 = { - 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; -}; +export type LogType = "error" | "warning" | "success" | "info"; +export type LogVariant = "red" | "yellow" | "green" | "blue"; + +export interface LogLine { + rawTimestamp: string | null; + timestamp: Date | null; + message: string; +} + +interface LogStyle { + type: LogType; + variant: LogVariant; + color: string; +} + +const LOG_STYLES: Record = { + 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 { + rawTimestamp: timestamp, + 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; +}; diff --git a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx index f15e475c..92401dc3 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx @@ -91,10 +91,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => { - +
diff --git a/apps/dokploy/components/ui/badge.tsx b/apps/dokploy/components/ui/badge.tsx index fd190b8a..911b0071 100644 --- a/apps/dokploy/components/ui/badge.tsx +++ b/apps/dokploy/components/ui/badge.tsx @@ -14,16 +14,14 @@ const badgeVariants = cva( "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "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", + 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", + 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", diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css index a116d66b..ab6084e1 100644 --- a/apps/dokploy/styles/globals.css +++ b/apps/dokploy/styles/globals.css @@ -198,4 +198,4 @@ .custom-logs-scrollbar::-webkit-scrollbar-thumb:hover { background-color: hsl(var(--muted-foreground) / 0.5); } -} \ No newline at end of file +} From 2fa6f3bfa6e2cbcca9ebeec15252eb5db1720739 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 11 Dec 2024 19:20:30 -0500 Subject: [PATCH 09/51] feat(logs): lint --- .../dokploy/components/dashboard/docker/logs/docker-logs-id.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d2dfe9c..2781aff5 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -26,7 +26,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const { data } = api.docker.getConfig.useQuery( { containerId, - serverId, + serverId: serverId ?? undefined, }, { enabled: !!containerId, From 16ca198eb420e43acad3debfaebf3219234c63d9 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 11 Dec 2024 19:31:23 -0500 Subject: [PATCH 10/51] feat(logs): better download file names --- .../components/dashboard/docker/logs/docker-logs-id.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 2781aff5..63e24a20 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -140,8 +140,9 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { 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 = `logs-${appName}-${new Date().toISOString()}.txt`; + 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); From 8546031df005d9a3ce085c729f0d6d0ff92b064d Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 11 Dec 2024 19:32:34 -0500 Subject: [PATCH 11/51] feat(logs): lint --- apps/dokploy/components/dashboard/docker/logs/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 6e11fe0e..9f178d25 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -67,7 +67,7 @@ export function parseLogs(logString: string): LogLine[] { .trim(); return { - rawTimestamp: timestamp, + rawTimestamp: timestamp ?? null, timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null, message: cleanedMessage, }; From d374f5eedf04722696103821888143100e57c8d2 Mon Sep 17 00:00:00 2001 From: usopp Date: Thu, 12 Dec 2024 19:28:16 +0100 Subject: [PATCH 12/51] fix: no time found block same width as the timestamp ones --- .../components/dashboard/docker/logs/terminal-line.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index eb619277..bc04b26d 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -45,7 +45,7 @@ export function TerminalLine({ log, searchTerm }: LogLineProps) { ); }; - const tooltip = (color: string, timestamp: string) => { + const tooltip = (color: string, timestamp: string | null) => { return ( @@ -56,7 +56,7 @@ export function TerminalLine({ log, searchTerm }: LogLineProps) {

- {timestamp} + {timestamp || "--- No time found ---"}

@@ -79,7 +79,7 @@ export function TerminalLine({ log, searchTerm }: LogLineProps) {
{/* Icon to expand the log item maybe implement a colapsible later */} {/* */} - {rawTimestamp && tooltip(color, rawTimestamp)} + {tooltip(color, rawTimestamp)} {formattedTime} From fe088bad3bbc1cebeb640ead6f65908e51204f4a Mon Sep 17 00:00:00 2001 From: 190km Date: Thu, 12 Dec 2024 19:54:44 +0100 Subject: [PATCH 13/51] feat: add loading spinner when logs are being loaded --- .../dashboard/docker/logs/docker-logs-id.tsx | 561 +++++++++--------- 1 file changed, 286 insertions(+), 275 deletions(-) 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 63e24a20..6873ef9b 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,275 +1,286 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { api } from "@/utils/api"; -import { Download as DownloadIcon } from "lucide-react"; -import React, { useEffect, useRef } from "react"; -import { TerminalLine } from "./terminal-line"; -import { type LogLine, getLogType, parseLogs } from "./utils"; - -interface Props { - containerId: string; - serverId?: string | null; -} - -type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; -type TypeFilter = "all" | "error" | "warning" | "success" | "info"; - -export const DockerLogsId: React.FC = ({ containerId, serverId }) => { - 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 [since, setSince] = React.useState("all"); - const [typeFilter, setTypeFilter] = React.useState("all"); - const scrollRef = useRef(null); - - const scrollToBottom = () => { - if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }; - - const handleScroll = () => { - if (!scrollRef.current) return; - - const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; - setAutoScroll(isAtBottom); - }; - - const handleSearch = (e: React.ChangeEvent) => { - setRawLogs(""); - setFilteredLogs([]); - setSearch(e.target.value || ""); - }; - - const handleLines = (e: React.ChangeEvent) => { - setRawLogs(""); - setFilteredLogs([]); - setLines(Number(e.target.value) || 1); - }; - - const handleSince = (value: TimeFilter) => { - setRawLogs(""); - setFilteredLogs([]); - setSince(value); - }; - - const handleTypeFilter = (value: TypeFilter) => { - setTypeFilter(value); - }; - - useEffect(() => { - setRawLogs(""); - setFilteredLogs([]); - }, [containerId]); - - useEffect(() => { - if (!containerId) return; - - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const params = new globalThis.URLSearchParams({ - containerId, - tail: lines.toString(), - since, - search, - }); - - if (serverId) { - params.append("serverId", serverId); - } - - const wsUrl = `${protocol}//${ - window.location.host - }/docker-container-logs?${params.toString()}`; - console.log("Connecting to WebSocket:", wsUrl); - const ws = new WebSocket(wsUrl); - - ws.onopen = () => { - console.log("WebSocket connected"); - }; - - 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); - }; - - 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"); - 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 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 ( -
-
-
-
-
- - - - - - - -
- - -
-
- {filteredLogs.length > 0 ? ( - filteredLogs.map((filteredLog: LogLine, index: number) => ( - - )) - ) : ( -
- No logs found -
- )} -
-
-
-
- ); -}; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; +import { Download as DownloadIcon, Loader2 } from "lucide-react"; +import React, { useEffect, useRef } from "react"; +import { TerminalLine } from "./terminal-line"; +import { type LogLine, getLogType, parseLogs } from "./utils"; + +interface Props { + containerId: string; + serverId?: string | null; +} + +type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; +type TypeFilter = "all" | "error" | "warning" | "success" | "info"; + +export const DockerLogsId: React.FC = ({ containerId, serverId }) => { + 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 [since, setSince] = React.useState("all"); + const [typeFilter, setTypeFilter] = React.useState("all"); + const scrollRef = useRef(null); + const [isLoading, setIsLoading] = React.useState(false); + + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; + + const handleSearch = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setSearch(e.target.value || ""); + }; + + const handleLines = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setLines(Number(e.target.value) || 1); + }; + + const handleSince = (value: TimeFilter) => { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + }; + + const handleTypeFilter = (value: TypeFilter) => { + setTypeFilter(value); + }; + + useEffect(() => { + setRawLogs(""); + setFilteredLogs([]); + }, [containerId]); + + useEffect(() => { + if (!containerId) return; + setIsLoading(true); + + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const params = new globalThis.URLSearchParams({ + containerId, + tail: lines.toString(), + since, + search, + }); + + if (serverId) { + params.append("serverId", serverId); + } + + const wsUrl = `${protocol}//${ + window.location.host + }/docker-container-logs?${params.toString()}`; + console.log("Connecting to WebSocket:", wsUrl); + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log("WebSocket connected"); + setIsLoading(false) + }; + + ws.onmessage = (e) => { + setRawLogs((prev) => prev + e.data); + setIsLoading(false); + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + setIsLoading(false); + }; + + ws.onclose = (e) => { + console.log("WebSocket closed:", e.reason); + setIsLoading(false); + }; + + 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"); + 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 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 ( +
+
+
+
+
+ + + + + + + +
+ + +
+
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : isLoading ? ( +
+ +
+ ) : ( +
+ No logs found +
+ )} +
+
+
+
+ ); +}; From ee622b1ba066d4595726886db2146ff31f110f69 Mon Sep 17 00:00:00 2001 From: 190km Date: Thu, 12 Dec 2024 21:00:17 +0100 Subject: [PATCH 14/51] feat: added new logs styling in deployments views --- .../deployments/show-deployment.tsx | 21 ++++++++++++---- .../deployments/show-deployment-compose.tsx | 25 ++++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 8c15e2cd..39bff46a 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -6,6 +6,8 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { useEffect, useRef, useState } from "react"; +import { TerminalLine } from "../../docker/logs/terminal-line"; +import { LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; @@ -15,6 +17,7 @@ interface Props { } export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { const [data, setData] = useState(""); + const [filteredLogs, setFilteredLogs] = useState([]); const endOfLogsRef = useRef(null); const wsRef = useRef(null); // Ref to hold WebSocket instance @@ -52,6 +55,11 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" }); }; + useEffect(() => { + const logs = parseLogs(data); + setFilteredLogs(logs); + }, [data]); + useEffect(() => { scrollToBottom(); }, [data]); @@ -81,12 +89,15 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
- -
-							{data || "Loading..."}
-						
+
+ {filteredLogs.map((log: LogLine, index: number) => ( + + )) || "Loading..."}
- +
diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx index 14f3bcd7..4439a984 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx @@ -6,6 +6,8 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { useEffect, useRef, useState } from "react"; +import { TerminalLine } from "../../docker/logs/terminal-line"; +import { LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; @@ -21,8 +23,8 @@ export const ShowDeploymentCompose = ({ }: Props) => { const [data, setData] = useState(""); const endOfLogsRef = useRef(null); + const [filteredLogs, setFilteredLogs] = useState([]); const wsRef = useRef(null); // Ref to hold WebSocket instance - useEffect(() => { if (!open || !logPath) return; @@ -58,6 +60,13 @@ export const ShowDeploymentCompose = ({ endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" }); }; + useEffect(() => { + const logs = parseLogs(data); + console.log(data); + console.log(logs); + setFilteredLogs(logs); + }, [data]); + useEffect(() => { scrollToBottom(); }, [data]); @@ -87,12 +96,16 @@ export const ShowDeploymentCompose = ({
- -
-							{data || "Loading..."}
-						
+
+ + {filteredLogs.map((log: LogLine, index: number) => ( + + )) || "Loading..."}
- +
From 3bc1bd5b15d97015942f66609b7abf96053debff Mon Sep 17 00:00:00 2001 From: 190km Date: Thu, 12 Dec 2024 21:10:19 +0100 Subject: [PATCH 15/51] feat: added more log success filter --- apps/dokploy/components/dashboard/docker/logs/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 9f178d25..acae133e 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -88,7 +88,7 @@ export const getLogType = (message: string): LogStyle => { /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(?:&ash|critical|fatal)\b/i.test(lowerMessage) || /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage) ) { return LOG_STYLES.error; @@ -108,16 +108,16 @@ export const getLogType = (message: string): LogStyle => { } if ( - /(?:successfully|complete[d]?)\s+(?:initialized|started|completed|done)/i.test( + /(?:successfully|complete[d]?)\s+(?:initialized|started|completed|created|done|deployed)/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) || + /✓|√|✅|\[ok\]|done!/i.test(lowerMessage) || /\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) || - /\b(?:started|running|active)\b/i.test(lowerMessage) + /\b(?:started|starting|active)\b/i.test(lowerMessage) ) { return LOG_STYLES.success; } From cb487b8be05d6b4e458d2ffc863d1bc6e20c81f5 Mon Sep 17 00:00:00 2001 From: 190km Date: Thu, 12 Dec 2024 21:53:10 +0100 Subject: [PATCH 16/51] feat: added debug log type & noTimestamp props for TerminalLine --- .../application/deployments/show-deployment.tsx | 1 + .../deployments/show-deployment-compose.tsx | 1 + .../dashboard/docker/logs/docker-logs-id.tsx | 7 +++++-- .../dashboard/docker/logs/terminal-line.tsx | 17 ++++++++++++----- .../components/dashboard/docker/logs/utils.ts | 17 +++++++++++------ 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 39bff46a..982a7ad6 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -94,6 +94,7 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { )) || "Loading..."}
diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx index 4439a984..eb03f128 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx @@ -102,6 +102,7 @@ export const ShowDeploymentCompose = ({ )) || "Loading..."}
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 6873ef9b..78304ab3 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -20,7 +20,7 @@ interface Props { } type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; -type TypeFilter = "all" | "error" | "warning" | "success" | "info"; +type TypeFilter = "all" | "error" | "warning" | "success" | "info" | "debug"; export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const { data } = api.docker.getConfig.useQuery( @@ -225,7 +225,10 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { Error - Warning + Warning + + + Debug Success diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index bc04b26d..5077988d 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -12,10 +12,11 @@ import { type LogLine, getLogType } from "./utils"; interface LogLineProps { log: LogLine; + noTimestamp?: boolean; searchTerm?: string; } -export function TerminalLine({ log, searchTerm }: LogLineProps) { +export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { const { timestamp, message, rawTimestamp } = log; const { type, variant, color } = getLogType(message); @@ -72,7 +73,9 @@ export function TerminalLine({ log, searchTerm }: LogLineProps) { ? "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", + : type === "debug" + ? "bg-orange-500/10 hover:bg-orange-500/15" + : "hover:bg-gray-200/50 dark:hover:bg-gray-800/50", )} > {" "} @@ -80,9 +83,13 @@ export function TerminalLine({ log, searchTerm }: LogLineProps) { {/* Icon to expand the log item maybe implement a colapsible later */} {/* */} {tooltip(color, rawTimestamp)} - - {formattedTime} - + {!noTimestamp && ( + + {formattedTime} + + )} + + = { }, warning: { type: "warning", + variant: "orange", + color: "bg-orange-500/40", + }, + debug: { + type: "debug", variant: "yellow", color: "bg-yellow-500/40", }, @@ -88,7 +93,7 @@ export const getLogType = (message: string): LogStyle => { /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(?:&ash|critical|fatal)\b/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; @@ -124,10 +129,10 @@ export const getLogType = (message: string): LogStyle => { 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) + /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) || + /\b(?:version|config|import|load)\b:?/i.test(lowerMessage) ) { - return LOG_STYLES.info; + return LOG_STYLES.debug; } return LOG_STYLES.info; From 37ee89e6abfddac4f92ff83e8af85984badc29c2 Mon Sep 17 00:00:00 2001 From: 190km Date: Fri, 13 Dec 2024 00:53:58 +0100 Subject: [PATCH 17/51] fix: debug value in select --- .../dokploy/components/dashboard/docker/logs/docker-logs-id.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 78304ab3..aed2fe4c 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -227,7 +227,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { Warning - + Debug From 22a2e64563bdd5ca2e57765092f4e4dd7dcf10cd Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Thu, 12 Dec 2024 22:59:03 -0500 Subject: [PATCH 18/51] feat(logs): tooltip improvements (break out, no delay) --- .../dashboard/docker/logs/terminal-line.tsx | 34 +++++++++++-------- apps/dokploy/components/ui/tooltip.tsx | 10 +++++- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 5077988d..7024d253 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -2,6 +2,7 @@ import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, + TooltipPortal, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; @@ -46,22 +47,28 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { ); }; - const tooltip = (color: string, timestamp: string | null) => { - return ( - + const tooltip = (color: string, timestamp: string) => { + const square = ( +
+ ); + return timestamp ? ( + - -
- - -

- {timestamp || "--- No time found ---"} -

-
+ {square} + + +

+

{timestamp}
+

+
+
+ ) : ( + square ); }; @@ -89,7 +96,6 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { )} - , React.ComponentPropsWithoutRef @@ -25,4 +27,10 @@ const TooltipContent = React.forwardRef< )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; +export { + Tooltip, + TooltipTrigger, + TooltipContent, + TooltipProvider, + TooltipPortal, +}; From 4311ba93f31843f319cfd1e57d04a93b55f08a5e Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Fri, 13 Dec 2024 09:00:17 -0500 Subject: [PATCH 19/51] chore: lint/typecheck --- apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 7024d253..289165eb 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -47,7 +47,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { ); }; - const tooltip = (color: string, timestamp: string) => { + const tooltip = (color: string, timestamp: string | null) => { const square = (
); From e5d5a98bab575d3636d27504b8fa520feb7e93d8 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Fri, 13 Dec 2024 09:15:56 -0500 Subject: [PATCH 20/51] feat(logs): preserve whitespace in log line --- .../components/dashboard/docker/logs/terminal-line.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 289165eb..5aa374e6 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -103,9 +103,9 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { {type}
- +
 				{searchTerm ? highlightMessage(message, searchTerm) : message}
-			
+			
); } From 6773458da393d90d7121789f767e4b85eec9b40c Mon Sep 17 00:00:00 2001 From: 190km Date: Fri, 13 Dec 2024 18:29:18 +0100 Subject: [PATCH 21/51] fix: text came out of the parent div --- .../components/dashboard/docker/logs/terminal-line.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 5aa374e6..96b51d4a 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -103,9 +103,9 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { {type}
-
+			
 				{searchTerm ? highlightMessage(message, searchTerm) : message}
-			
+
); } From 3df3d187e4ad3ecde7e870945f02454616a814cf Mon Sep 17 00:00:00 2001 From: usopp Date: Fri, 13 Dec 2024 19:41:02 +0100 Subject: [PATCH 22/51] feat: added deployment loader & lines count --- .../deployments/show-deployment.tsx | 24 +++++++++++------ .../deployments/show-deployment-compose.tsx | 26 +++++++++++++------ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 982a7ad6..2d4cbe66 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -8,6 +8,8 @@ import { import { useEffect, useRef, useState } from "react"; import { TerminalLine } from "../../docker/logs/terminal-line"; import { LogLine, parseLogs } from "../../docker/logs/utils"; +import { Badge } from "@/components/ui/badge"; +import { Loader2 } from "lucide-react"; interface Props { logPath: string | null; @@ -84,19 +86,25 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { Deployment - See all the details of this deployment + See all the details of this deployment | {filteredLogs.length} lines
- {filteredLogs.map((log: LogLine, index: number) => ( - - )) || "Loading..."} + { + filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( + + )) : + ( +
+ +
+ )}
diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx index eb03f128..702394b6 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx @@ -8,6 +8,9 @@ import { import { useEffect, useRef, useState } from "react"; import { TerminalLine } from "../../docker/logs/terminal-line"; import { LogLine, parseLogs } from "../../docker/logs/utils"; +import { Badge } from "@/components/ui/badge"; +import { Loader2 } from "lucide-react"; + interface Props { logPath: string | null; @@ -91,20 +94,27 @@ export const ShowDeploymentCompose = ({ Deployment - See all the details of this deployment + See all the details of this deployment | {filteredLogs.length} lines
- {filteredLogs.map((log: LogLine, index: number) => ( - - )) || "Loading..."} + { + filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( + + )) : + ( +
+ +
+ ) + }
From c71d12fd0655a28d4a0b2b1710d03ef89d3c8a2c Mon Sep 17 00:00:00 2001 From: 190km Date: Fri, 13 Dec 2024 20:21:00 +0100 Subject: [PATCH 23/51] feat: added info possibilities & debug more debug possibilities --- .../dokploy/components/dashboard/docker/logs/utils.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index e47f1358..409c6989 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -84,6 +84,15 @@ export function parseLogs(logString: string): LogLine[] { export const getLogType = (message: string): LogStyle => { const lowerMessage = message.toLowerCase(); + if ( + /(?:^|\s)(?:info|inf|information):?\s/i.test(lowerMessage) || + /\[(?:info|information)\]/i.test(lowerMessage) || + /\b(?:status|state|current|progress)\b:?\s/i.test(lowerMessage) || + /\b(?:processing|executing|performing)\b/i.test(lowerMessage) + ) { + return LOG_STYLES.info; + } + if ( /(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) || /\b(?:exception|failed|failure)\b/i.test(lowerMessage) || @@ -130,7 +139,7 @@ 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)\b:?/i.test(lowerMessage) + /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage) ) { return LOG_STYLES.debug; } From 7726fa6112d72c05cfd3709b9d5846c59314b257 Mon Sep 17 00:00:00 2001 From: 190km Date: Fri, 13 Dec 2024 20:29:26 +0100 Subject: [PATCH 24/51] style: make selects responsive --- .../components/dashboard/docker/logs/docker-logs-id.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 aed2fe4c..76b040aa 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -200,7 +200,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { /> - + @@ -214,7 +214,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { - + @@ -214,7 +214,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { - + @@ -214,7 +214,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { - - - - - - -
- - -
-
- {filteredLogs.length > 0 ? ( - filteredLogs.map((filteredLog: LogLine, index: number) => ( - - )) - ) : isLoading ? ( -
- -
- ) : ( -
- No logs found -
- )} -
-
-
-
- ); -}; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; +import { Download as DownloadIcon, Loader2 } from "lucide-react"; +import React, { useEffect, useRef } from "react"; +import { TerminalLine } from "./terminal-line"; +import { type LogLine, getLogType, parseLogs } from "./utils"; + +interface Props { + containerId: string; + serverId?: string | null; +} + +type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; +type TypeFilter = "all" | "error" | "warning" | "success" | "info" | "debug"; + +export const DockerLogsId: React.FC = ({ containerId, serverId }) => { + 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 [since, setSince] = React.useState("all"); + const [typeFilter, setTypeFilter] = React.useState("all"); + const scrollRef = useRef(null); + const [isLoading, setIsLoading] = React.useState(false); + + const scrollToBottom = () => { + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }; + + const handleScroll = () => { + if (!scrollRef.current) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; + const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; + setAutoScroll(isAtBottom); + }; + + const handleSearch = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setSearch(e.target.value || ""); + }; + + const handleLines = (e: React.ChangeEvent) => { + setRawLogs(""); + setFilteredLogs([]); + setLines(Number(e.target.value) || 1); + }; + + const handleSince = (value: TimeFilter) => { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + }; + + const handleTypeFilter = (value: TypeFilter) => { + setTypeFilter(value); + }; + + useEffect(() => { + setRawLogs(""); + setFilteredLogs([]); + }, [containerId]); + + useEffect(() => { + if (!containerId) return; + setIsLoading(true); + + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const params = new globalThis.URLSearchParams({ + containerId, + tail: lines.toString(), + since, + search, + }); + + if (serverId) { + params.append("serverId", serverId); + } + + const wsUrl = `${protocol}//${ + window.location.host + }/docker-container-logs?${params.toString()}`; + console.log("Connecting to WebSocket:", wsUrl); + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log("WebSocket connected"); + }; + + ws.onmessage = (e) => { + setRawLogs((prev) => prev + e.data); + setIsLoading(false); + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + setIsLoading(false); + }; + + ws.onclose = (e) => { + console.log("WebSocket closed:", e.reason); + setIsLoading(false); + }; + + 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"); + 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 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 ( +
+
+
+
+
+ + + + + + + +
+ + +
+
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : isLoading ? ( +
+ +
+ ) : ( +
+ No logs found +
+ )} +
+
+
+
+ ); +}; From 1157e08aa101ef9832ffd2601fd708b391f21378 Mon Sep 17 00:00:00 2001 From: 190km Date: Sun, 15 Dec 2024 04:21:38 +0100 Subject: [PATCH 30/51] fix: log line came out of the div --- apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 96b51d4a..cdbbb2c8 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -103,7 +103,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { {type}
- + {searchTerm ? highlightMessage(message, searchTerm) : message}
From d20f86ffe13a4a285381d31c9be03f7c3e3a52b3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 14 Dec 2024 22:46:01 -0600 Subject: [PATCH 31/51] refactor: remove unsued files --- .../docker/terminal/docker-terminal.tsx | 12 +- .../settings/web-server/terminal-modal.tsx | 2 +- .../settings/web-server/terminal.tsx | 11 +- apps/dokploy/package.json | 8 +- .../server/wss/docker-container-terminal.ts | 12 +- apps/dokploy/server/wss/terminal.ts | 77 ++++----- packages/server/src/index.ts | 5 - packages/server/src/services/auth.ts | 2 +- .../server/src/wss/docker-container-logs.ts | 133 --------------- .../src/wss/docker-container-terminal.ts | 152 ------------------ packages/server/src/wss/docker-stats.ts | 96 ----------- packages/server/src/wss/listen-deployment.ts | 101 ------------ packages/server/src/wss/terminal.ts | 107 ------------ packages/server/src/wss/utils.ts | 29 ++-- pnpm-lock.yaml | 39 +++-- 15 files changed, 106 insertions(+), 680 deletions(-) delete mode 100644 packages/server/src/wss/docker-container-logs.ts delete mode 100644 packages/server/src/wss/docker-container-terminal.ts delete mode 100644 packages/server/src/wss/docker-stats.ts delete mode 100644 packages/server/src/wss/listen-deployment.ts delete mode 100644 packages/server/src/wss/terminal.ts diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 4008d6fd..098860cf 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -4,6 +4,7 @@ import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AttachAddon } from "@xterm/addon-attach"; +import { WebLinksAddon } from "@xterm/addon-web-links"; interface Props { id: string; @@ -25,13 +26,11 @@ export const DockerTerminal: React.FC = ({ } const term = new Terminal({ cursorBlink: true, - cols: 80, - rows: 30, lineHeight: 1.4, convertEol: true, theme: { cursor: "transparent", - background: "rgba(0, 0, 0, 0)", + background: "transparent", }, }); const addonFit = new FitAddon(); @@ -47,6 +46,7 @@ export const DockerTerminal: React.FC = ({ term.open(termRef.current); term.loadAddon(addonFit); term.loadAddon(addonAttach); + term.loadAddon(new WebLinksAddon()); addonFit.fit(); return () => { ws.readyState === WebSocket.OPEN && ws.close(); @@ -54,8 +54,8 @@ export const DockerTerminal: React.FC = ({ }, [containerId, activeWay, id]); return ( -
-
+
+
Select way to connect to {containerId} @@ -66,7 +66,7 @@ export const DockerTerminal: React.FC = ({
-
+
diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx index 5bdba8b8..19053879 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx @@ -11,7 +11,7 @@ import { api } from "@/utils/api"; import dynamic from "next/dynamic"; import type React from "react"; -const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), { +const Terminal = dynamic(async () => (await import("./terminal")).Terminal, { ssr: false, }); diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx index 2fe7f83c..f5febcda 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef } from "react"; import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { AttachAddon } from "@xterm/addon-attach"; +import { WebLinksAddon } from "@xterm/addon-web-links"; interface Props { id: string; @@ -20,13 +21,11 @@ export const Terminal: React.FC = ({ id, serverId }) => { } const term = new XTerm({ cursorBlink: true, - cols: 80, - rows: 30, lineHeight: 1.4, convertEol: true, theme: { cursor: "transparent", - background: "#19191A", + background: "transparent", }, }); const addonFit = new FitAddon(); @@ -42,15 +41,17 @@ export const Terminal: React.FC = ({ id, serverId }) => { term.open(termRef.current); term.loadAddon(addonFit); term.loadAddon(addonAttach); + term.loadAddon(new WebLinksAddon()); addonFit.fit(); + return () => { ws.readyState === WebSocket.OPEN && ws.close(); }; }, [id, serverId]); return ( -
-
+
+
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 292d3efb..14286dc1 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -35,6 +35,11 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { + "xterm-addon-fit": "0.8.0", + "@xterm/xterm": "^5.3.0", + "xterm": "5.2.1", + "@xterm/addon-attach": "^0.11.0", + "@xterm/addon-web-links": "^0.10.0", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.1", @@ -71,8 +76,6 @@ "@trpc/server": "^10.43.6", "@uiw/codemirror-theme-github": "^4.22.1", "@uiw/react-codemirror": "^4.22.1", - "@xterm/addon-attach": "0.10.0", - "@xterm/xterm": "^5.4.0", "adm-zip": "^0.5.14", "bcrypt": "5.1.1", "bullmq": "5.4.2", @@ -117,7 +120,6 @@ "undici": "^6.19.2", "use-resize-observer": "9.1.0", "ws": "8.16.0", - "xterm-addon-fit": "^0.8.0", "zod": "^3.23.4", "zod-form-data": "^2.0.2" }, diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index eeba72d5..4bf49bf3 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -110,12 +110,12 @@ export const setupDockerContainerTerminalWebSocketServer = ( shell, ["-c", `docker exec -it ${containerId} ${activeWay}`], { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, + // name: "xterm-256color", + // cwd: process.env.HOME, + // env: process.env, + // encoding: "utf8", + // cols: 80, + // rows: 30, }, ); diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index eb0bf2e2..a6338afe 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -70,53 +70,44 @@ export const setupTerminalWebSocketServer = ( let stderr = ""; conn .once("ready", () => { - conn.shell( - { - term: "terminal", - cols: 80, - rows: 30, - height: 30, - width: 80, - }, - (err, stream) => { - if (err) throw err; + conn.shell({}, (err, stream) => { + if (err) throw err; - stream - .on("close", (code: number, signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); - }); + stream + .on("close", (code: number, signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); + conn.end(); + }) + .on("data", (data: string) => { + stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; } - }); + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); - ws.on("close", () => { - stream.end(); - }); - }, - ); + ws.on("close", () => { + stream.end(); + }); + }); }) .on("error", (err) => { if (err.level === "client-authentication") { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f3f1e96f..41f2b0fd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -102,11 +102,6 @@ export * from "./utils/traefik/security"; export * from "./utils/traefik/types"; export * from "./utils/traefik/web-server"; -export * from "./wss/docker-container-logs"; -export * from "./wss/docker-container-terminal"; -export * from "./wss/docker-stats"; -export * from "./wss/listen-deployment"; -export * from "./wss/terminal"; export * from "./wss/utils"; export * from "./utils/access-log/handler"; diff --git a/packages/server/src/services/auth.ts b/packages/server/src/services/auth.ts index 11e2d24c..598e39e3 100644 --- a/packages/server/src/services/auth.ts +++ b/packages/server/src/services/auth.ts @@ -7,7 +7,7 @@ import { auth, users, } from "@dokploy/server/db/schema"; -import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal"; +import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { eq } from "drizzle-orm"; diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts deleted file mode 100644 index 75292018..00000000 --- a/packages/server/src/wss/docker-container-logs.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type http from "node:http"; -import { findServerById } from "@dokploy/server/services/server"; -import { spawn } from "node-pty"; -import { Client } from "ssh2"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { getShell } from "./utils"; - -export const setupDockerContainerLogsWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/docker-container-logs", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/docker-container-logs") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const containerId = url.searchParams.get("containerId"); - const tail = url.searchParams.get("tail"); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - - if (!containerId) { - ws.close(4000, "containerId no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - try { - if (serverId) { - const server = await findServerById(serverId); - - if (!server.sshKeyId) return; - const client = new Client(); - new Promise((resolve, reject) => { - client - .once("ready", () => { - const command = ` - bash -c "docker container logs --tail ${tail} --follow ${containerId}" - `; - client.exec(command, (err, stream) => { - if (err) { - console.error("Execution error:", err); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - ws.send(data.toString()); - }); - }); - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - timeout: 99999, - }); - }); - } else { - const shell = getShell(); - const ptyProcess = spawn( - shell, - [ - "-c", - `docker container logs --tail ${tail} --follow ${containerId}`, - ], - { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }, - ); - - ptyProcess.onData((data) => { - ws.send(data); - }); - ws.on("close", () => { - ptyProcess.kill(); - }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - ptyProcess.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - } - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - - ws.send(errorMessage); - } - }); -}; diff --git a/packages/server/src/wss/docker-container-terminal.ts b/packages/server/src/wss/docker-container-terminal.ts deleted file mode 100644 index 0cb174b6..00000000 --- a/packages/server/src/wss/docker-container-terminal.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type http from "node:http"; -import { findServerById } from "@dokploy/server/services/server"; -import { spawn } from "node-pty"; -import { Client } from "ssh2"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { getShell } from "./utils"; - -export const setupDockerContainerTerminalWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/docker-container-terminal", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/docker-container-terminal") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const containerId = url.searchParams.get("containerId"); - const activeWay = url.searchParams.get("activeWay"); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - - if (!containerId) { - ws.close(4000, "containerId no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - try { - if (serverId) { - const server = await findServerById(serverId); - if (!server.sshKeyId) - throw new Error("No SSH key available for this server"); - - const conn = new Client(); - let stdout = ""; - let stderr = ""; - conn - .once("ready", () => { - conn.exec( - `docker exec -it ${containerId} ${activeWay}`, - { pty: true }, - (err, stream) => { - if (err) throw err; - - stream - .on("close", (code: number, signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); - }); - - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - - ws.on("close", () => { - stream.end(); - }); - }, - ); - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - timeout: 99999, - }); - } else { - const shell = getShell(); - const ptyProcess = spawn( - shell, - ["-c", `docker exec -it ${containerId} ${activeWay}`], - { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }, - ); - - ptyProcess.onData((data) => { - ws.send(data); - }); - ws.on("close", () => { - ptyProcess.kill(); - }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - ptyProcess.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - } - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - - ws.send(errorMessage); - } - }); -}; diff --git a/packages/server/src/wss/docker-stats.ts b/packages/server/src/wss/docker-stats.ts deleted file mode 100644 index ed1dc46f..00000000 --- a/packages/server/src/wss/docker-stats.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type http from "node:http"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { docker } from "../constants"; -import { - getLastAdvancedStatsFile, - recordAdvancedStats, -} from "../monitoring/utilts"; - -export const setupDockerStatsMonitoringSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/listen-docker-stats-monitoring", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/listen-docker-stats-monitoring") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const appName = url.searchParams.get("appName"); - const appType = (url.searchParams.get("appType") || "application") as - | "application" - | "stack" - | "docker-compose"; - const { user, session } = await validateWebSocketRequest(req); - - if (!appName) { - ws.close(4000, "appName no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - const intervalId = setInterval(async () => { - try { - const filter = { - status: ["running"], - ...(appType === "application" && { - label: [`com.docker.swarm.service.name=${appName}`], - }), - ...(appType === "stack" && { - label: [`com.docker.swarm.task.name=${appName}`], - }), - ...(appType === "docker-compose" && { - name: [appName], - }), - }; - - const containers = await docker.listContainers({ - filters: JSON.stringify(filter), - }); - - const container = containers[0]; - if (!container || container?.State !== "running") { - ws.close(4000, "Container not running"); - return; - } - - const stats = await docker.getContainer(container.Id).stats({ - stream: false, - }); - - await recordAdvancedStats(stats, appName); - const data = await getLastAdvancedStatsFile(appName); - - ws.send( - JSON.stringify({ - data, - }), - ); - } catch (error) { - // @ts-ignore - ws.close(4000, `Error: ${error.message}`); - } - }, 1300); - - ws.on("close", () => { - clearInterval(intervalId); - }); - }); -}; diff --git a/packages/server/src/wss/listen-deployment.ts b/packages/server/src/wss/listen-deployment.ts deleted file mode 100644 index 363a3cc8..00000000 --- a/packages/server/src/wss/listen-deployment.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { spawn } from "node:child_process"; -import type http from "node:http"; -import { findServerById } from "@dokploy/server/services/server"; -import { Client } from "ssh2"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; - -export const setupDeploymentLogsWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/listen-deployment", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/listen-deployment") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const logPath = url.searchParams.get("logPath"); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - - if (!logPath) { - ws.close(4000, "logPath no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - - try { - if (serverId) { - const server = await findServerById(serverId); - - if (!server.sshKeyId) return; - const client = new Client(); - new Promise((resolve, reject) => { - client - .on("ready", () => { - const command = ` - tail -n +1 -f ${logPath}; - `; - client.exec(command, (err, stream) => { - if (err) { - console.error("Execution error:", err); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - ws.send(data.toString()); - }); - }); - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - timeout: 99999, - }); - }); - } else { - const tail = spawn("tail", ["-n", "+1", "-f", logPath]); - - tail.stdout.on("data", (data) => { - ws.send(data.toString()); - }); - - tail.stderr.on("data", (data) => { - ws.send(new Error(`tail error: ${data.toString()}`).message); - }); - } - } catch (error) { - // @ts-ignore - // const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); -}; diff --git a/packages/server/src/wss/terminal.ts b/packages/server/src/wss/terminal.ts deleted file mode 100644 index 562040d7..00000000 --- a/packages/server/src/wss/terminal.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type http from "node:http"; -import path from "node:path"; -import { findServerById } from "@dokploy/server/services/server"; -import { spawn } from "node-pty"; -import { publicIpv4, publicIpv6 } from "public-ip"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { paths } from "../constants"; - -export const getPublicIpWithFallback = async () => { - // @ts-ignore - let ip = null; - try { - ip = await publicIpv4(); - } catch (error) { - console.log( - "Error to obtain public IPv4 address, falling back to IPv6", - // @ts-ignore - error.message, - ); - try { - ip = await publicIpv6(); - } catch (error) { - // @ts-ignore - console.error("Error to obtain public IPv6 address", error.message); - ip = null; - } - } - return ip; -}; - -export const setupTerminalWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/terminal", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/terminal") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - if (!user || !session || !serverId) { - ws.close(); - return; - } - - const server = await findServerById(serverId); - - if (!server) { - ws.close(); - return; - } - const { SSH_PATH } = paths(); - const privateKey = path.join(SSH_PATH, `${server.sshKeyId}_rsa`); - const sshCommand = [ - "ssh", - "-o", - "StrictHostKeyChecking=no", - "-i", - privateKey, - `${server.username}@${server.ipAddress}`, - ]; - const ptyProcess = spawn("ssh", sshCommand.slice(1), { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }); - - ptyProcess.onData((data) => { - ws.send(data); - }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - ptyProcess.write(command.toString()); - } catch (error) { - console.log(error); - } - }); - - ws.on("close", () => { - ptyProcess.kill(); - }); - }); -}; diff --git a/packages/server/src/wss/utils.ts b/packages/server/src/wss/utils.ts index b5567127..d9190f3c 100644 --- a/packages/server/src/wss/utils.ts +++ b/packages/server/src/wss/utils.ts @@ -1,12 +1,23 @@ -import os from "node:os"; +import { publicIpv4, publicIpv6 } from "public-ip"; -export const getShell = () => { - switch (os.platform()) { - case "win32": - return "powershell.exe"; - case "darwin": - return "zsh"; - default: - return "bash"; +export const getPublicIpWithFallback = async () => { + // @ts-ignore + let ip = null; + try { + ip = await publicIpv4(); + } catch (error) { + console.log( + "Error to obtain public IPv4 address, falling back to IPv6", + // @ts-ignore + error.message, + ); + try { + ip = await publicIpv6(); + } catch (error) { + // @ts-ignore + console.error("Error to obtain public IPv6 address", error.message); + ip = null; + } } + return ip; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09d71bbf..6a3069f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,10 +206,13 @@ importers: specifier: ^4.22.1 version: 4.23.0(@babel/runtime@7.25.0)(@codemirror/autocomplete@6.17.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.29.0)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.29.0)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@xterm/addon-attach': - specifier: 0.10.0 + specifier: ^0.11.0 + version: 0.11.0(@xterm/xterm@5.5.0) + '@xterm/addon-web-links': + specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) '@xterm/xterm': - specifier: ^5.4.0 + specifier: ^5.3.0 version: 5.5.0 adm-zip: specifier: ^0.5.14 @@ -343,9 +346,12 @@ importers: ws: specifier: 8.16.0 version: 8.16.0 + xterm: + specifier: 5.2.1 + version: 5.2.1 xterm-addon-fit: - specifier: ^0.8.0 - version: 0.8.0(xterm@5.3.0) + specifier: 0.8.0 + version: 0.8.0(xterm@5.2.1) zod: specifier: ^3.23.4 version: 3.23.8 @@ -3400,8 +3406,13 @@ packages: '@webassemblyjs/wast-printer@1.12.1': resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} - '@xterm/addon-attach@0.10.0': - resolution: {integrity: sha512-ES/XO8pC1tPHSkh4j7qzM8ajFt++u8KMvfRc9vKIbjHTDOxjl9IUVo+vcQgLn3FTCM3w2czTvBss8nMWlD83Cg==} + '@xterm/addon-attach@0.11.0': + resolution: {integrity: sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/addon-web-links@0.10.0': + resolution: {integrity: sha512-QhrHCUr8w6ATGviyXwcAIM1qN3nD1hdxwMC8fsW7z/6aaQlb2nt7zmByJt4eOn7ZzrHOzczljqV5S2pkdQp2xw==} peerDependencies: '@xterm/xterm': ^5.0.0 @@ -6785,8 +6796,8 @@ packages: peerDependencies: xterm: ^5.0.0 - xterm@5.3.0: - resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} + xterm@5.2.1: + resolution: {integrity: sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA==} deprecated: This package is now deprecated. Move to @xterm/xterm instead. y18n@4.0.3: @@ -9635,7 +9646,11 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@xterm/addon-attach@0.10.0(@xterm/xterm@5.5.0)': + '@xterm/addon-attach@0.11.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/addon-web-links@0.10.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 @@ -13098,11 +13113,11 @@ snapshots: xtend@4.0.2: {} - xterm-addon-fit@0.8.0(xterm@5.3.0): + xterm-addon-fit@0.8.0(xterm@5.2.1): dependencies: - xterm: 5.3.0 + xterm: 5.2.1 - xterm@5.3.0: {} + xterm@5.2.1: {} y18n@4.0.3: {} From fc2b0abdb1d37979844f5fed5b1a26c36ffd7271 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:05:19 -0600 Subject: [PATCH 32/51] Revert "refactor: remove unsued files" This reverts commit d20f86ffe13a4a285381d31c9be03f7c3e3a52b3. --- .../docker/terminal/docker-terminal.tsx | 12 +- .../settings/web-server/terminal-modal.tsx | 2 +- .../settings/web-server/terminal.tsx | 11 +- apps/dokploy/package.json | 8 +- .../server/wss/docker-container-terminal.ts | 12 +- apps/dokploy/server/wss/terminal.ts | 79 +++++---- packages/server/src/index.ts | 5 + packages/server/src/services/auth.ts | 2 +- .../server/src/wss/docker-container-logs.ts | 133 +++++++++++++++ .../src/wss/docker-container-terminal.ts | 152 ++++++++++++++++++ packages/server/src/wss/docker-stats.ts | 96 +++++++++++ packages/server/src/wss/listen-deployment.ts | 101 ++++++++++++ packages/server/src/wss/terminal.ts | 107 ++++++++++++ packages/server/src/wss/utils.ts | 29 ++-- pnpm-lock.yaml | 39 ++--- 15 files changed, 681 insertions(+), 107 deletions(-) create mode 100644 packages/server/src/wss/docker-container-logs.ts create mode 100644 packages/server/src/wss/docker-container-terminal.ts create mode 100644 packages/server/src/wss/docker-stats.ts create mode 100644 packages/server/src/wss/listen-deployment.ts create mode 100644 packages/server/src/wss/terminal.ts diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 098860cf..4008d6fd 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -4,7 +4,6 @@ import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AttachAddon } from "@xterm/addon-attach"; -import { WebLinksAddon } from "@xterm/addon-web-links"; interface Props { id: string; @@ -26,11 +25,13 @@ export const DockerTerminal: React.FC = ({ } const term = new Terminal({ cursorBlink: true, + cols: 80, + rows: 30, lineHeight: 1.4, convertEol: true, theme: { cursor: "transparent", - background: "transparent", + background: "rgba(0, 0, 0, 0)", }, }); const addonFit = new FitAddon(); @@ -46,7 +47,6 @@ export const DockerTerminal: React.FC = ({ term.open(termRef.current); term.loadAddon(addonFit); term.loadAddon(addonAttach); - term.loadAddon(new WebLinksAddon()); addonFit.fit(); return () => { ws.readyState === WebSocket.OPEN && ws.close(); @@ -54,8 +54,8 @@ export const DockerTerminal: React.FC = ({ }, [containerId, activeWay, id]); return ( -
-
+
+
Select way to connect to {containerId} @@ -66,7 +66,7 @@ export const DockerTerminal: React.FC = ({
-
+
diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx index 19053879..5bdba8b8 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx @@ -11,7 +11,7 @@ import { api } from "@/utils/api"; import dynamic from "next/dynamic"; import type React from "react"; -const Terminal = dynamic(async () => (await import("./terminal")).Terminal, { +const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), { ssr: false, }); diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx index f5febcda..2fe7f83c 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx @@ -4,7 +4,6 @@ import { useEffect, useRef } from "react"; import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { AttachAddon } from "@xterm/addon-attach"; -import { WebLinksAddon } from "@xterm/addon-web-links"; interface Props { id: string; @@ -21,11 +20,13 @@ export const Terminal: React.FC = ({ id, serverId }) => { } const term = new XTerm({ cursorBlink: true, + cols: 80, + rows: 30, lineHeight: 1.4, convertEol: true, theme: { cursor: "transparent", - background: "transparent", + background: "#19191A", }, }); const addonFit = new FitAddon(); @@ -41,17 +42,15 @@ export const Terminal: React.FC = ({ id, serverId }) => { term.open(termRef.current); term.loadAddon(addonFit); term.loadAddon(addonAttach); - term.loadAddon(new WebLinksAddon()); addonFit.fit(); - return () => { ws.readyState === WebSocket.OPEN && ws.close(); }; }, [id, serverId]); return ( -
-
+
+
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 14286dc1..292d3efb 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -35,11 +35,6 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { - "xterm-addon-fit": "0.8.0", - "@xterm/xterm": "^5.3.0", - "xterm": "5.2.1", - "@xterm/addon-attach": "^0.11.0", - "@xterm/addon-web-links": "^0.10.0", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.1", @@ -76,6 +71,8 @@ "@trpc/server": "^10.43.6", "@uiw/codemirror-theme-github": "^4.22.1", "@uiw/react-codemirror": "^4.22.1", + "@xterm/addon-attach": "0.10.0", + "@xterm/xterm": "^5.4.0", "adm-zip": "^0.5.14", "bcrypt": "5.1.1", "bullmq": "5.4.2", @@ -120,6 +117,7 @@ "undici": "^6.19.2", "use-resize-observer": "9.1.0", "ws": "8.16.0", + "xterm-addon-fit": "^0.8.0", "zod": "^3.23.4", "zod-form-data": "^2.0.2" }, diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index 4bf49bf3..eeba72d5 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -110,12 +110,12 @@ export const setupDockerContainerTerminalWebSocketServer = ( shell, ["-c", `docker exec -it ${containerId} ${activeWay}`], { - // name: "xterm-256color", - // cwd: process.env.HOME, - // env: process.env, - // encoding: "utf8", - // cols: 80, - // rows: 30, + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, }, ); diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index a6338afe..eb0bf2e2 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -70,44 +70,53 @@ export const setupTerminalWebSocketServer = ( let stderr = ""; conn .once("ready", () => { - conn.shell({}, (err, stream) => { - if (err) throw err; + conn.shell( + { + term: "terminal", + cols: 80, + rows: 30, + height: 30, + width: 80, + }, + (err, stream) => { + if (err) throw err; - stream - .on("close", (code: number, signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); + stream + .on("close", (code: number, signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); + conn.end(); + }) + .on("data", (data: string) => { + stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); + + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - - ws.on("close", () => { - stream.end(); - }); - }); + ws.on("close", () => { + stream.end(); + }); + }, + ); }) .on("error", (err) => { if (err.level === "client-authentication") { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 41f2b0fd..f3f1e96f 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -102,6 +102,11 @@ export * from "./utils/traefik/security"; export * from "./utils/traefik/types"; export * from "./utils/traefik/web-server"; +export * from "./wss/docker-container-logs"; +export * from "./wss/docker-container-terminal"; +export * from "./wss/docker-stats"; +export * from "./wss/listen-deployment"; +export * from "./wss/terminal"; export * from "./wss/utils"; export * from "./utils/access-log/handler"; diff --git a/packages/server/src/services/auth.ts b/packages/server/src/services/auth.ts index 598e39e3..11e2d24c 100644 --- a/packages/server/src/services/auth.ts +++ b/packages/server/src/services/auth.ts @@ -7,7 +7,7 @@ import { auth, users, } from "@dokploy/server/db/schema"; -import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; +import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { eq } from "drizzle-orm"; diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts new file mode 100644 index 00000000..75292018 --- /dev/null +++ b/packages/server/src/wss/docker-container-logs.ts @@ -0,0 +1,133 @@ +import type http from "node:http"; +import { findServerById } from "@dokploy/server/services/server"; +import { spawn } from "node-pty"; +import { Client } from "ssh2"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { getShell } from "./utils"; + +export const setupDockerContainerLogsWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/docker-container-logs", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/docker-container-logs") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const containerId = url.searchParams.get("containerId"); + const tail = url.searchParams.get("tail"); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + + if (!containerId) { + ws.close(4000, "containerId no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + try { + if (serverId) { + const server = await findServerById(serverId); + + if (!server.sshKeyId) return; + const client = new Client(); + new Promise((resolve, reject) => { + client + .once("ready", () => { + const command = ` + bash -c "docker container logs --tail ${tail} --follow ${containerId}" + `; + client.exec(command, (err, stream) => { + if (err) { + console.error("Execution error:", err); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + ws.send(data.toString()); + }); + }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + timeout: 99999, + }); + }); + } else { + const shell = getShell(); + const ptyProcess = spawn( + shell, + [ + "-c", + `docker container logs --tail ${tail} --follow ${containerId}`, + ], + { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }, + ); + + ptyProcess.onData((data) => { + ws.send(data); + }); + ws.on("close", () => { + ptyProcess.kill(); + }); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + ptyProcess.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); + } + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + + ws.send(errorMessage); + } + }); +}; diff --git a/packages/server/src/wss/docker-container-terminal.ts b/packages/server/src/wss/docker-container-terminal.ts new file mode 100644 index 00000000..0cb174b6 --- /dev/null +++ b/packages/server/src/wss/docker-container-terminal.ts @@ -0,0 +1,152 @@ +import type http from "node:http"; +import { findServerById } from "@dokploy/server/services/server"; +import { spawn } from "node-pty"; +import { Client } from "ssh2"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { getShell } from "./utils"; + +export const setupDockerContainerTerminalWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/docker-container-terminal", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/docker-container-terminal") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const containerId = url.searchParams.get("containerId"); + const activeWay = url.searchParams.get("activeWay"); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + + if (!containerId) { + ws.close(4000, "containerId no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + try { + if (serverId) { + const server = await findServerById(serverId); + if (!server.sshKeyId) + throw new Error("No SSH key available for this server"); + + const conn = new Client(); + let stdout = ""; + let stderr = ""; + conn + .once("ready", () => { + conn.exec( + `docker exec -it ${containerId} ${activeWay}`, + { pty: true }, + (err, stream) => { + if (err) throw err; + + stream + .on("close", (code: number, signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); + conn.end(); + }) + .on("data", (data: string) => { + stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); + + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); + + ws.on("close", () => { + stream.end(); + }); + }, + ); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + timeout: 99999, + }); + } else { + const shell = getShell(); + const ptyProcess = spawn( + shell, + ["-c", `docker exec -it ${containerId} ${activeWay}`], + { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }, + ); + + ptyProcess.onData((data) => { + ws.send(data); + }); + ws.on("close", () => { + ptyProcess.kill(); + }); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + ptyProcess.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); + } + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + + ws.send(errorMessage); + } + }); +}; diff --git a/packages/server/src/wss/docker-stats.ts b/packages/server/src/wss/docker-stats.ts new file mode 100644 index 00000000..ed1dc46f --- /dev/null +++ b/packages/server/src/wss/docker-stats.ts @@ -0,0 +1,96 @@ +import type http from "node:http"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { docker } from "../constants"; +import { + getLastAdvancedStatsFile, + recordAdvancedStats, +} from "../monitoring/utilts"; + +export const setupDockerStatsMonitoringSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/listen-docker-stats-monitoring", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/listen-docker-stats-monitoring") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const appName = url.searchParams.get("appName"); + const appType = (url.searchParams.get("appType") || "application") as + | "application" + | "stack" + | "docker-compose"; + const { user, session } = await validateWebSocketRequest(req); + + if (!appName) { + ws.close(4000, "appName no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + const intervalId = setInterval(async () => { + try { + const filter = { + status: ["running"], + ...(appType === "application" && { + label: [`com.docker.swarm.service.name=${appName}`], + }), + ...(appType === "stack" && { + label: [`com.docker.swarm.task.name=${appName}`], + }), + ...(appType === "docker-compose" && { + name: [appName], + }), + }; + + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); + + const container = containers[0]; + if (!container || container?.State !== "running") { + ws.close(4000, "Container not running"); + return; + } + + const stats = await docker.getContainer(container.Id).stats({ + stream: false, + }); + + await recordAdvancedStats(stats, appName); + const data = await getLastAdvancedStatsFile(appName); + + ws.send( + JSON.stringify({ + data, + }), + ); + } catch (error) { + // @ts-ignore + ws.close(4000, `Error: ${error.message}`); + } + }, 1300); + + ws.on("close", () => { + clearInterval(intervalId); + }); + }); +}; diff --git a/packages/server/src/wss/listen-deployment.ts b/packages/server/src/wss/listen-deployment.ts new file mode 100644 index 00000000..363a3cc8 --- /dev/null +++ b/packages/server/src/wss/listen-deployment.ts @@ -0,0 +1,101 @@ +import { spawn } from "node:child_process"; +import type http from "node:http"; +import { findServerById } from "@dokploy/server/services/server"; +import { Client } from "ssh2"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; + +export const setupDeploymentLogsWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/listen-deployment", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/listen-deployment") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const logPath = url.searchParams.get("logPath"); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + + if (!logPath) { + ws.close(4000, "logPath no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + + try { + if (serverId) { + const server = await findServerById(serverId); + + if (!server.sshKeyId) return; + const client = new Client(); + new Promise((resolve, reject) => { + client + .on("ready", () => { + const command = ` + tail -n +1 -f ${logPath}; + `; + client.exec(command, (err, stream) => { + if (err) { + console.error("Execution error:", err); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + ws.send(data.toString()); + }); + }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + timeout: 99999, + }); + }); + } else { + const tail = spawn("tail", ["-n", "+1", "-f", logPath]); + + tail.stdout.on("data", (data) => { + ws.send(data.toString()); + }); + + tail.stderr.on("data", (data) => { + ws.send(new Error(`tail error: ${data.toString()}`).message); + }); + } + } catch (error) { + // @ts-ignore + // const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); +}; diff --git a/packages/server/src/wss/terminal.ts b/packages/server/src/wss/terminal.ts new file mode 100644 index 00000000..562040d7 --- /dev/null +++ b/packages/server/src/wss/terminal.ts @@ -0,0 +1,107 @@ +import type http from "node:http"; +import path from "node:path"; +import { findServerById } from "@dokploy/server/services/server"; +import { spawn } from "node-pty"; +import { publicIpv4, publicIpv6 } from "public-ip"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { paths } from "../constants"; + +export const getPublicIpWithFallback = async () => { + // @ts-ignore + let ip = null; + try { + ip = await publicIpv4(); + } catch (error) { + console.log( + "Error to obtain public IPv4 address, falling back to IPv6", + // @ts-ignore + error.message, + ); + try { + ip = await publicIpv6(); + } catch (error) { + // @ts-ignore + console.error("Error to obtain public IPv6 address", error.message); + ip = null; + } + } + return ip; +}; + +export const setupTerminalWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/terminal", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/terminal") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + if (!user || !session || !serverId) { + ws.close(); + return; + } + + const server = await findServerById(serverId); + + if (!server) { + ws.close(); + return; + } + const { SSH_PATH } = paths(); + const privateKey = path.join(SSH_PATH, `${server.sshKeyId}_rsa`); + const sshCommand = [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-i", + privateKey, + `${server.username}@${server.ipAddress}`, + ]; + const ptyProcess = spawn("ssh", sshCommand.slice(1), { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }); + + ptyProcess.onData((data) => { + ws.send(data); + }); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + ptyProcess.write(command.toString()); + } catch (error) { + console.log(error); + } + }); + + ws.on("close", () => { + ptyProcess.kill(); + }); + }); +}; diff --git a/packages/server/src/wss/utils.ts b/packages/server/src/wss/utils.ts index d9190f3c..b5567127 100644 --- a/packages/server/src/wss/utils.ts +++ b/packages/server/src/wss/utils.ts @@ -1,23 +1,12 @@ -import { publicIpv4, publicIpv6 } from "public-ip"; +import os from "node:os"; -export const getPublicIpWithFallback = async () => { - // @ts-ignore - let ip = null; - try { - ip = await publicIpv4(); - } catch (error) { - console.log( - "Error to obtain public IPv4 address, falling back to IPv6", - // @ts-ignore - error.message, - ); - try { - ip = await publicIpv6(); - } catch (error) { - // @ts-ignore - console.error("Error to obtain public IPv6 address", error.message); - ip = null; - } +export const getShell = () => { + switch (os.platform()) { + case "win32": + return "powershell.exe"; + case "darwin": + return "zsh"; + default: + return "bash"; } - return ip; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a3069f2..09d71bbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,13 +206,10 @@ importers: specifier: ^4.22.1 version: 4.23.0(@babel/runtime@7.25.0)(@codemirror/autocomplete@6.17.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.29.0)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.29.0)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@xterm/addon-attach': - specifier: ^0.11.0 - version: 0.11.0(@xterm/xterm@5.5.0) - '@xterm/addon-web-links': - specifier: ^0.10.0 + specifier: 0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) '@xterm/xterm': - specifier: ^5.3.0 + specifier: ^5.4.0 version: 5.5.0 adm-zip: specifier: ^0.5.14 @@ -346,12 +343,9 @@ importers: ws: specifier: 8.16.0 version: 8.16.0 - xterm: - specifier: 5.2.1 - version: 5.2.1 xterm-addon-fit: - specifier: 0.8.0 - version: 0.8.0(xterm@5.2.1) + specifier: ^0.8.0 + version: 0.8.0(xterm@5.3.0) zod: specifier: ^3.23.4 version: 3.23.8 @@ -3406,13 +3400,8 @@ packages: '@webassemblyjs/wast-printer@1.12.1': resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} - '@xterm/addon-attach@0.11.0': - resolution: {integrity: sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==} - peerDependencies: - '@xterm/xterm': ^5.0.0 - - '@xterm/addon-web-links@0.10.0': - resolution: {integrity: sha512-QhrHCUr8w6ATGviyXwcAIM1qN3nD1hdxwMC8fsW7z/6aaQlb2nt7zmByJt4eOn7ZzrHOzczljqV5S2pkdQp2xw==} + '@xterm/addon-attach@0.10.0': + resolution: {integrity: sha512-ES/XO8pC1tPHSkh4j7qzM8ajFt++u8KMvfRc9vKIbjHTDOxjl9IUVo+vcQgLn3FTCM3w2czTvBss8nMWlD83Cg==} peerDependencies: '@xterm/xterm': ^5.0.0 @@ -6796,8 +6785,8 @@ packages: peerDependencies: xterm: ^5.0.0 - xterm@5.2.1: - resolution: {integrity: sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA==} + xterm@5.3.0: + resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} deprecated: This package is now deprecated. Move to @xterm/xterm instead. y18n@4.0.3: @@ -9646,11 +9635,7 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@xterm/addon-attach@0.11.0(@xterm/xterm@5.5.0)': - dependencies: - '@xterm/xterm': 5.5.0 - - '@xterm/addon-web-links@0.10.0(@xterm/xterm@5.5.0)': + '@xterm/addon-attach@0.10.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 @@ -13113,11 +13098,11 @@ snapshots: xtend@4.0.2: {} - xterm-addon-fit@0.8.0(xterm@5.2.1): + xterm-addon-fit@0.8.0(xterm@5.3.0): dependencies: - xterm: 5.2.1 + xterm: 5.3.0 - xterm@5.2.1: {} + xterm@5.3.0: {} y18n@4.0.3: {} From c6e512bec1156dcb11237f61ce481ed1e3879a3d Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:06:54 -0600 Subject: [PATCH 33/51] =?UTF-8?q?ref=C3=A5ctor:=20remove=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/server/src/index.ts | 5 - packages/server/src/services/auth.ts | 2 +- .../server/src/wss/docker-container-logs.ts | 133 --------------- .../src/wss/docker-container-terminal.ts | 152 ------------------ packages/server/src/wss/docker-stats.ts | 96 ----------- packages/server/src/wss/listen-deployment.ts | 101 ------------ packages/server/src/wss/terminal.ts | 107 ------------ packages/server/src/wss/utils.ts | 23 +++ 8 files changed, 24 insertions(+), 595 deletions(-) delete mode 100644 packages/server/src/wss/docker-container-logs.ts delete mode 100644 packages/server/src/wss/docker-container-terminal.ts delete mode 100644 packages/server/src/wss/docker-stats.ts delete mode 100644 packages/server/src/wss/listen-deployment.ts delete mode 100644 packages/server/src/wss/terminal.ts diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f3f1e96f..41f2b0fd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -102,11 +102,6 @@ export * from "./utils/traefik/security"; export * from "./utils/traefik/types"; export * from "./utils/traefik/web-server"; -export * from "./wss/docker-container-logs"; -export * from "./wss/docker-container-terminal"; -export * from "./wss/docker-stats"; -export * from "./wss/listen-deployment"; -export * from "./wss/terminal"; export * from "./wss/utils"; export * from "./utils/access-log/handler"; diff --git a/packages/server/src/services/auth.ts b/packages/server/src/services/auth.ts index 11e2d24c..598e39e3 100644 --- a/packages/server/src/services/auth.ts +++ b/packages/server/src/services/auth.ts @@ -7,7 +7,7 @@ import { auth, users, } from "@dokploy/server/db/schema"; -import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal"; +import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { eq } from "drizzle-orm"; diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts deleted file mode 100644 index 75292018..00000000 --- a/packages/server/src/wss/docker-container-logs.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type http from "node:http"; -import { findServerById } from "@dokploy/server/services/server"; -import { spawn } from "node-pty"; -import { Client } from "ssh2"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { getShell } from "./utils"; - -export const setupDockerContainerLogsWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/docker-container-logs", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/docker-container-logs") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const containerId = url.searchParams.get("containerId"); - const tail = url.searchParams.get("tail"); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - - if (!containerId) { - ws.close(4000, "containerId no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - try { - if (serverId) { - const server = await findServerById(serverId); - - if (!server.sshKeyId) return; - const client = new Client(); - new Promise((resolve, reject) => { - client - .once("ready", () => { - const command = ` - bash -c "docker container logs --tail ${tail} --follow ${containerId}" - `; - client.exec(command, (err, stream) => { - if (err) { - console.error("Execution error:", err); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - ws.send(data.toString()); - }); - }); - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - timeout: 99999, - }); - }); - } else { - const shell = getShell(); - const ptyProcess = spawn( - shell, - [ - "-c", - `docker container logs --tail ${tail} --follow ${containerId}`, - ], - { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }, - ); - - ptyProcess.onData((data) => { - ws.send(data); - }); - ws.on("close", () => { - ptyProcess.kill(); - }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - ptyProcess.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - } - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - - ws.send(errorMessage); - } - }); -}; diff --git a/packages/server/src/wss/docker-container-terminal.ts b/packages/server/src/wss/docker-container-terminal.ts deleted file mode 100644 index 0cb174b6..00000000 --- a/packages/server/src/wss/docker-container-terminal.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type http from "node:http"; -import { findServerById } from "@dokploy/server/services/server"; -import { spawn } from "node-pty"; -import { Client } from "ssh2"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { getShell } from "./utils"; - -export const setupDockerContainerTerminalWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/docker-container-terminal", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/docker-container-terminal") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const containerId = url.searchParams.get("containerId"); - const activeWay = url.searchParams.get("activeWay"); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - - if (!containerId) { - ws.close(4000, "containerId no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - try { - if (serverId) { - const server = await findServerById(serverId); - if (!server.sshKeyId) - throw new Error("No SSH key available for this server"); - - const conn = new Client(); - let stdout = ""; - let stderr = ""; - conn - .once("ready", () => { - conn.exec( - `docker exec -it ${containerId} ${activeWay}`, - { pty: true }, - (err, stream) => { - if (err) throw err; - - stream - .on("close", (code: number, signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); - }); - - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - - ws.on("close", () => { - stream.end(); - }); - }, - ); - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - timeout: 99999, - }); - } else { - const shell = getShell(); - const ptyProcess = spawn( - shell, - ["-c", `docker exec -it ${containerId} ${activeWay}`], - { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }, - ); - - ptyProcess.onData((data) => { - ws.send(data); - }); - ws.on("close", () => { - ptyProcess.kill(); - }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - ptyProcess.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - } - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - - ws.send(errorMessage); - } - }); -}; diff --git a/packages/server/src/wss/docker-stats.ts b/packages/server/src/wss/docker-stats.ts deleted file mode 100644 index ed1dc46f..00000000 --- a/packages/server/src/wss/docker-stats.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type http from "node:http"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { docker } from "../constants"; -import { - getLastAdvancedStatsFile, - recordAdvancedStats, -} from "../monitoring/utilts"; - -export const setupDockerStatsMonitoringSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/listen-docker-stats-monitoring", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/listen-docker-stats-monitoring") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const appName = url.searchParams.get("appName"); - const appType = (url.searchParams.get("appType") || "application") as - | "application" - | "stack" - | "docker-compose"; - const { user, session } = await validateWebSocketRequest(req); - - if (!appName) { - ws.close(4000, "appName no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - const intervalId = setInterval(async () => { - try { - const filter = { - status: ["running"], - ...(appType === "application" && { - label: [`com.docker.swarm.service.name=${appName}`], - }), - ...(appType === "stack" && { - label: [`com.docker.swarm.task.name=${appName}`], - }), - ...(appType === "docker-compose" && { - name: [appName], - }), - }; - - const containers = await docker.listContainers({ - filters: JSON.stringify(filter), - }); - - const container = containers[0]; - if (!container || container?.State !== "running") { - ws.close(4000, "Container not running"); - return; - } - - const stats = await docker.getContainer(container.Id).stats({ - stream: false, - }); - - await recordAdvancedStats(stats, appName); - const data = await getLastAdvancedStatsFile(appName); - - ws.send( - JSON.stringify({ - data, - }), - ); - } catch (error) { - // @ts-ignore - ws.close(4000, `Error: ${error.message}`); - } - }, 1300); - - ws.on("close", () => { - clearInterval(intervalId); - }); - }); -}; diff --git a/packages/server/src/wss/listen-deployment.ts b/packages/server/src/wss/listen-deployment.ts deleted file mode 100644 index 363a3cc8..00000000 --- a/packages/server/src/wss/listen-deployment.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { spawn } from "node:child_process"; -import type http from "node:http"; -import { findServerById } from "@dokploy/server/services/server"; -import { Client } from "ssh2"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; - -export const setupDeploymentLogsWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/listen-deployment", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/listen-deployment") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const logPath = url.searchParams.get("logPath"); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - - if (!logPath) { - ws.close(4000, "logPath no provided"); - return; - } - - if (!user || !session) { - ws.close(); - return; - } - - try { - if (serverId) { - const server = await findServerById(serverId); - - if (!server.sshKeyId) return; - const client = new Client(); - new Promise((resolve, reject) => { - client - .on("ready", () => { - const command = ` - tail -n +1 -f ${logPath}; - `; - client.exec(command, (err, stream) => { - if (err) { - console.error("Execution error:", err); - reject(err); - return; - } - stream - .on("close", () => { - client.end(); - resolve(); - }) - .on("data", (data: string) => { - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - ws.send(data.toString()); - }); - }); - }) - .connect({ - host: server.ipAddress, - port: server.port, - username: server.username, - privateKey: server.sshKey?.privateKey, - timeout: 99999, - }); - }); - } else { - const tail = spawn("tail", ["-n", "+1", "-f", logPath]); - - tail.stdout.on("data", (data) => { - ws.send(data.toString()); - }); - - tail.stderr.on("data", (data) => { - ws.send(new Error(`tail error: ${data.toString()}`).message); - }); - } - } catch (error) { - // @ts-ignore - // const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); -}; diff --git a/packages/server/src/wss/terminal.ts b/packages/server/src/wss/terminal.ts deleted file mode 100644 index 562040d7..00000000 --- a/packages/server/src/wss/terminal.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type http from "node:http"; -import path from "node:path"; -import { findServerById } from "@dokploy/server/services/server"; -import { spawn } from "node-pty"; -import { publicIpv4, publicIpv6 } from "public-ip"; -import { WebSocketServer } from "ws"; -import { validateWebSocketRequest } from "../auth/auth"; -import { paths } from "../constants"; - -export const getPublicIpWithFallback = async () => { - // @ts-ignore - let ip = null; - try { - ip = await publicIpv4(); - } catch (error) { - console.log( - "Error to obtain public IPv4 address, falling back to IPv6", - // @ts-ignore - error.message, - ); - try { - ip = await publicIpv6(); - } catch (error) { - // @ts-ignore - console.error("Error to obtain public IPv6 address", error.message); - ip = null; - } - } - return ip; -}; - -export const setupTerminalWebSocketServer = ( - server: http.Server, -) => { - const wssTerm = new WebSocketServer({ - noServer: true, - path: "/terminal", - }); - - server.on("upgrade", (req, socket, head) => { - const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); - if (pathname === "/_next/webpack-hmr") { - return; - } - if (pathname === "/terminal") { - wssTerm.handleUpgrade(req, socket, head, function done(ws) { - wssTerm.emit("connection", ws, req); - }); - } - }); - - wssTerm.on("connection", async (ws, req) => { - const url = new URL(req.url || "", `http://${req.headers.host}`); - const serverId = url.searchParams.get("serverId"); - const { user, session } = await validateWebSocketRequest(req); - if (!user || !session || !serverId) { - ws.close(); - return; - } - - const server = await findServerById(serverId); - - if (!server) { - ws.close(); - return; - } - const { SSH_PATH } = paths(); - const privateKey = path.join(SSH_PATH, `${server.sshKeyId}_rsa`); - const sshCommand = [ - "ssh", - "-o", - "StrictHostKeyChecking=no", - "-i", - privateKey, - `${server.username}@${server.ipAddress}`, - ]; - const ptyProcess = spawn("ssh", sshCommand.slice(1), { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }); - - ptyProcess.onData((data) => { - ws.send(data); - }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - ptyProcess.write(command.toString()); - } catch (error) { - console.log(error); - } - }); - - ws.on("close", () => { - ptyProcess.kill(); - }); - }); -}; diff --git a/packages/server/src/wss/utils.ts b/packages/server/src/wss/utils.ts index b5567127..8c6217a2 100644 --- a/packages/server/src/wss/utils.ts +++ b/packages/server/src/wss/utils.ts @@ -1,4 +1,5 @@ import os from "node:os"; +import { publicIpv4, publicIpv6 } from "public-ip"; export const getShell = () => { switch (os.platform()) { @@ -10,3 +11,25 @@ export const getShell = () => { return "bash"; } }; + +export const getPublicIpWithFallback = async () => { + // @ts-ignore + let ip = null; + try { + ip = await publicIpv4(); + } catch (error) { + console.log( + "Error to obtain public IPv4 address, falling back to IPv6", + // @ts-ignore + error.message, + ); + try { + ip = await publicIpv6(); + } catch (error) { + // @ts-ignore + console.error("Error to obtain public IPv6 address", error.message); + ip = null; + } + } + return ip; +}; From 86aba9ce3e955a79467461c92f4eb2e87d186e01 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:12:21 -0600 Subject: [PATCH 34/51] refactor: remove cols --- .../docker/terminal/docker-terminal.tsx | 5 +- .../settings/web-server/terminal.tsx | 7 +- .../server/wss/docker-container-terminal.ts | 9 +-- apps/dokploy/server/wss/terminal.ts | 77 ++++++++----------- 4 files changed, 40 insertions(+), 58 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 4008d6fd..42683887 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -25,8 +25,6 @@ export const DockerTerminal: React.FC = ({ } const term = new Terminal({ cursorBlink: true, - cols: 80, - rows: 30, lineHeight: 1.4, convertEol: true, theme: { @@ -45,6 +43,7 @@ export const DockerTerminal: React.FC = ({ const addonAttach = new AttachAddon(ws); // @ts-ignore term.open(termRef.current); + // @ts-ignore term.loadAddon(addonFit); term.loadAddon(addonAttach); addonFit.fit(); @@ -66,7 +65,7 @@ export const DockerTerminal: React.FC = ({
-
+
diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx index 2fe7f83c..366784fc 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx @@ -20,13 +20,11 @@ export const Terminal: React.FC = ({ id, serverId }) => { } const term = new XTerm({ cursorBlink: true, - cols: 80, - rows: 30, lineHeight: 1.4, convertEol: true, theme: { cursor: "transparent", - background: "#19191A", + background: "transparent", }, }); const addonFit = new FitAddon(); @@ -40,6 +38,7 @@ export const Terminal: React.FC = ({ id, serverId }) => { // @ts-ignore term.open(termRef.current); + // @ts-ignore term.loadAddon(addonFit); term.loadAddon(addonAttach); addonFit.fit(); @@ -50,7 +49,7 @@ export const Terminal: React.FC = ({ id, serverId }) => { return (
-
+
diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index eeba72d5..d527b37c 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -109,14 +109,7 @@ export const setupDockerContainerTerminalWebSocketServer = ( const ptyProcess = spawn( shell, ["-c", `docker exec -it ${containerId} ${activeWay}`], - { - name: "xterm-256color", - cwd: process.env.HOME, - env: process.env, - encoding: "utf8", - cols: 80, - rows: 30, - }, + {}, ); ptyProcess.onData((data) => { diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index eb0bf2e2..a6338afe 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -70,53 +70,44 @@ export const setupTerminalWebSocketServer = ( let stderr = ""; conn .once("ready", () => { - conn.shell( - { - term: "terminal", - cols: 80, - rows: 30, - height: 30, - width: 80, - }, - (err, stream) => { - if (err) throw err; + conn.shell({}, (err, stream) => { + if (err) throw err; - stream - .on("close", (code: number, signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); - }); + stream + .on("close", (code: number, signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); + conn.end(); + }) + .on("data", (data: string) => { + stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; } - }); + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); - ws.on("close", () => { - stream.end(); - }); - }, - ); + ws.on("close", () => { + stream.end(); + }); + }); }) .on("error", (err) => { if (err.level === "client-authentication") { From 5f297fd984d4373ca78aa6c8d3f782ed6b44caf5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 15 Dec 2024 02:14:43 -0600 Subject: [PATCH 35/51] feat: add react tour --- .vscode/extensions.json | 3 - .vscode/settings.json | 26 - .../settings/servers/edit-script.tsx | 167 + .../settings/servers/setup-server.tsx | 69 +- .../dokploy/components/shared/code-editor.tsx | 8 +- apps/dokploy/drizzle/0051_hard_gorgon.sql | 1 + apps/dokploy/drizzle/meta/0051_snapshot.json | 4240 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + apps/dokploy/package.json | 1 + apps/dokploy/pages/_app.tsx | 111 +- apps/dokploy/server/api/routers/server.ts | 6 + .../server/wss/docker-container-terminal.ts | 1 - apps/dokploy/server/wss/terminal.ts | 1 - packages/server/src/db/schema/server.ts | 7 +- packages/server/src/setup/server-setup.ts | 217 +- packages/server/src/setup/server-validate.ts | 1 - pnpm-lock.yaml | 60 + 17 files changed, 4717 insertions(+), 209 deletions(-) delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/settings.json create mode 100644 apps/dokploy/components/dashboard/settings/servers/edit-script.tsx create mode 100644 apps/dokploy/drizzle/0051_hard_gorgon.sql create mode 100644 apps/dokploy/drizzle/meta/0051_snapshot.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 16e8e666..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["biomejs.biome"] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index d17ed236..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "editor.codeActionsOnSave": { - "quickfix.biome": "explicit", - "source.organizeImports.biome": "explicit" - }, - "editor.defaultFormatter": "biomejs.biome", - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - "emmet.showExpandedAbbreviation": "never", - "prettier.enable": false -} diff --git a/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx b/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx new file mode 100644 index 00000000..ccdd8d31 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx @@ -0,0 +1,167 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FileTerminal } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +interface Props { + serverId: string; +} + +const schema = z.object({ + command: z.string().min(1, { + message: "Command is required", + }), +}); + +type Schema = z.infer; + +export const EditScript = ({ serverId }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const { data: server } = api.server.one.useQuery( + { + serverId, + }, + { + enabled: !!serverId, + }, + ); + + const { mutateAsync, isLoading } = api.server.update.useMutation(); + + const { data: defaultCommand } = api.server.getDefaultCommand.useQuery( + { + serverId, + }, + { + enabled: !!serverId, + }, + ); + + const form = useForm({ + defaultValues: { + command: "", + }, + resolver: zodResolver(schema), + }); + + useEffect(() => { + if (server) { + form.reset({ + command: server.command || defaultCommand, + }); + } + }, [server, defaultCommand]); + + const onSubmit = async (formData: Schema) => { + if (server) { + await mutateAsync({ + ...server, + command: formData.command || "", + serverId, + }) + .then((data) => { + toast.success("Script modified successfully"); + }) + .catch(() => { + toast.error("Error modifying the script"); + }); + } + }; + + return ( + + + + + + + Modify Script + + Modify the script which install everything necessary to deploy + applications on your server, + + + + We suggest to don't modify the script if you don't know what you are + doing + + +
+
+ + ( + + Command + + + + + + )} + /> + + +
+ + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index eb0d2255..13326b64 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -32,6 +32,7 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { ShowDeployment } from "../../application/deployments/show-deployment"; +import { EditScript } from "./edit-script"; import { GPUSupport } from "./gpu-support"; import { ValidateServer } from "./validate-server"; @@ -89,7 +90,12 @@ export const SetupServer = ({ serverId }: Props) => {
) : ( -
+
+ + Using a root user is required to ensure everything works as + expected. + + SSH Keys @@ -198,6 +204,28 @@ export const SetupServer = ({ serverId }: Props) => {
+
+ + Supported Distros: + +

+ We strongly recommend to use the following distros to + ensure the best experience: +

+
    +
  • 1. Ubuntu 24.04 LTS
  • +
  • 2. Ubuntu 23.10 LTS
  • +
  • 3. Ubuntu 22.04 LTS
  • +
  • 4. Ubuntu 20.04 LTS
  • +
  • 5. Ubuntu 18.04 LTS
  • +
  • 6. Debian 12
  • +
  • 7. Debian 11
  • +
  • 8. Debian 10
  • +
  • 9. Fedora 40
  • +
  • 10. Centos 9
  • +
  • 11. Centos 8
  • +
+
@@ -214,24 +242,29 @@ export const SetupServer = ({ serverId }: Props) => { See all the 5 Server Setup
- { - await mutateAsync({ - serverId: server?.serverId || "", - }) - .then(async () => { - refetch(); - toast.success("Server setup successfully"); +
+ + { + await mutateAsync({ + serverId: server?.serverId || "", }) - .catch(() => { - toast.error("Error configuring server"); - }); - }} - > - - + .then(async () => { + refetch(); + toast.success("Server setup successfully"); + }) + .catch(() => { + toast.error("Error configuring server"); + }); + }} + > + + +
diff --git a/apps/dokploy/components/shared/code-editor.tsx b/apps/dokploy/components/shared/code-editor.tsx index 3dcf5267..5dcce77f 100644 --- a/apps/dokploy/components/shared/code-editor.tsx +++ b/apps/dokploy/components/shared/code-editor.tsx @@ -2,7 +2,9 @@ import { cn } from "@/lib/utils"; import { json } from "@codemirror/lang-json"; import { yaml } from "@codemirror/lang-yaml"; import { StreamLanguage } from "@codemirror/language"; + import { properties } from "@codemirror/legacy-modes/mode/properties"; +import { shell } from "@codemirror/legacy-modes/mode/shell"; import { EditorView } from "@codemirror/view"; import { githubDark, githubLight } from "@uiw/codemirror-theme-github"; import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror"; @@ -10,7 +12,7 @@ import { useTheme } from "next-themes"; interface Props extends ReactCodeMirrorProps { wrapperClassName?: string; disabled?: boolean; - language?: "yaml" | "json" | "properties"; + language?: "yaml" | "json" | "properties" | "shell"; lineWrapping?: boolean; lineNumbers?: boolean; } @@ -39,7 +41,9 @@ export const CodeEditor = ({ ? yaml() : language === "json" ? json() - : StreamLanguage.define(properties), + : language === "shell" + ? StreamLanguage.define(shell) + : StreamLanguage.define(properties), props.lineWrapping ? EditorView.lineWrapping : [], ]} {...props} diff --git a/apps/dokploy/drizzle/0051_hard_gorgon.sql b/apps/dokploy/drizzle/0051_hard_gorgon.sql new file mode 100644 index 00000000..f2e61090 --- /dev/null +++ b/apps/dokploy/drizzle/0051_hard_gorgon.sql @@ -0,0 +1 @@ +ALTER TABLE "server" ADD COLUMN "command" text DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0051_snapshot.json b/apps/dokploy/drizzle/meta/0051_snapshot.json new file mode 100644 index 00000000..037eb34e --- /dev/null +++ b/apps/dokploy/drizzle/meta/0051_snapshot.json @@ -0,0 +1,4240 @@ +{ + "id": "0f21aab4-69a8-4ca9-91fa-7a819774e5ea", + "prevId": "89b9d2ac-25d4-46ea-8050-74a96a330cd4", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_projectId_project_projectId_fk": { + "name": "application_projectId_project_projectId_fk", + "tableFrom": "application", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_projectId_project_projectId_fk": { + "name": "postgres_projectId_project_projectId_fk", + "tableFrom": "postgres", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_adminId_admin_adminId_fk": { + "name": "user_adminId_admin_adminId_fk", + "tableFrom": "user", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_authId_auth_id_fk": { + "name": "user_authId_auth_id_fk", + "tableFrom": "user", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.admin": { + "name": "admin", + "schema": "", + "columns": { + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableLogRotation": { + "name": "enableLogRotation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "admin_authId_auth_id_fk": { + "name": "admin_authId_auth_id_fk", + "tableFrom": "admin", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.auth": { + "name": "auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rol": { + "name": "rol", + "type": "Roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_email_unique": { + "name": "auth_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_adminId_admin_adminId_fk": { + "name": "project_adminId_admin_adminId_fk", + "tableFrom": "project", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_projectId_project_projectId_fk": { + "name": "mariadb_projectId_project_projectId_fk", + "tableFrom": "mariadb", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_projectId_project_projectId_fk": { + "name": "mongo_projectId_project_projectId_fk", + "tableFrom": "mongo", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_projectId_project_projectId_fk": { + "name": "mysql_projectId_project_projectId_fk", + "tableFrom": "mysql", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "destination_adminId_admin_adminId_fk": { + "name": "destination_adminId_admin_adminId_fk", + "tableFrom": "destination", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_adminId_admin_adminId_fk": { + "name": "certificate_adminId_admin_adminId_fk", + "tableFrom": "certificate", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + } + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_auth_id_fk": { + "name": "session_user_id_auth_id_fk", + "tableFrom": "session", + "tableTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + } + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_projectId_project_projectId_fk": { + "name": "redis_projectId_project_projectId_fk", + "tableFrom": "redis", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_projectId_project_projectId_fk": { + "name": "compose_projectId_project_projectId_fk", + "tableFrom": "compose", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_adminId_admin_adminId_fk": { + "name": "registry_adminId_admin_adminId_fk", + "tableFrom": "registry", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_adminId_admin_adminId_fk": { + "name": "notification_adminId_admin_adminId_fk", + "tableFrom": "notification", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_adminId_admin_adminId_fk": { + "name": "ssh-key_adminId_admin_adminId_fk", + "tableFrom": "ssh-key", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_adminId_admin_adminId_fk": { + "name": "git_provider_adminId_admin_adminId_fk", + "tableFrom": "git_provider", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "server_adminId_admin_adminId_fk": { + "name": "server_adminId_admin_adminId_fk", + "tableFrom": "server", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "drop" + ] + }, + "public.Roles": { + "name": "Roles", + "schema": "public", + "values": [ + "admin", + "user" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "raw" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index a5d282b0..eeb10257 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -358,6 +358,13 @@ "when": 1733889104203, "tag": "0050_nappy_wrecker", "breakpoints": true + }, + { + "idx": 51, + "version": "6", + "when": 1734241482851, + "tag": "0051_hard_gorgon", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 292d3efb..ba725bdc 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -35,6 +35,7 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { + "@reactour/tour": "3.7.0", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.1", diff --git a/apps/dokploy/pages/_app.tsx b/apps/dokploy/pages/_app.tsx index f91f378a..d5a5090a 100644 --- a/apps/dokploy/pages/_app.tsx +++ b/apps/dokploy/pages/_app.tsx @@ -1,8 +1,10 @@ import "@/styles/globals.css"; +import { SearchCommand } from "@/components/dashboard/search-command"; import { Toaster } from "@/components/ui/sonner"; import { Languages } from "@/lib/languages"; import { api } from "@/utils/api"; +import { TourProvider } from "@reactour/tour"; import type { NextPage } from "next"; import { appWithTranslation } from "next-i18next"; import { ThemeProvider } from "next-themes"; @@ -11,74 +13,83 @@ import { Inter } from "next/font/google"; import Head from "next/head"; import Script from "next/script"; import type { ReactElement, ReactNode } from "react"; -import { SearchCommand } from "@/components/dashboard/search-command"; const inter = Inter({ subsets: ["latin"] }); +export const steps = [ + { + selector: ".first-step", + content: "This is the first Step", + }, + { + selector: ".second-step", + content: "This is the second Step", + }, + { + selector: ".third-step", + content: "This is the third Step", + }, +]; + export type NextPageWithLayout

= NextPage & { - getLayout?: (page: ReactElement) => ReactNode; - // session: Session | null; - theme?: string; + getLayout?: (page: ReactElement) => ReactNode; + // session: Session | null; + theme?: string; }; type AppPropsWithLayout = AppProps & { - Component: NextPageWithLayout; + Component: NextPageWithLayout; }; const MyApp = ({ - Component, - pageProps: { ...pageProps }, + Component, + pageProps: { ...pageProps }, }: AppPropsWithLayout) => { - const getLayout = Component.getLayout ?? ((page) => page); + const getLayout = Component.getLayout ?? ((page) => page); - return ( - <> - - - Dokploy - - {process.env.NEXT_PUBLIC_UMAMI_HOST && - process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && ( -