diff --git a/.gitignore b/.gitignore index 368c5ed01..b96c9e37a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ yarn-debug.log* yarn-error.log* # Editor -.vscode .idea # Misc diff --git a/apps/dokploy/components/dashboard/application/delete-application.tsx b/apps/dokploy/components/dashboard/application/delete-application.tsx index 93173d637..f34d29a78 100644 --- a/apps/dokploy/components/dashboard/application/delete-application.tsx +++ b/apps/dokploy/components/dashboard/application/delete-application.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -19,7 +20,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; +import { Copy, TrashIcon } from "lucide-react"; import { useRouter } from "next/router"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -102,9 +103,26 @@ export const DeleteApplication = ({ applicationId }: Props) => { name="projectName" render={({ field }) => ( - - To confirm, type "{data?.name}/{data?.appName}" in the box - below + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + { const [data, setData] = useState(""); - const endOfLogsRef = useRef(null); + const [filteredLogs, setFilteredLogs] = useState([]); const wsRef = useRef(null); // Ref to hold WebSocket instance + const [autoScroll, setAutoScroll] = useState(true); + 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); + }; + useEffect(() => { if (!open || !logPath) return; @@ -48,14 +69,21 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { }; }, [logPath, open]); - const scrollToBottom = () => { - endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" }); - }; useEffect(() => { - scrollToBottom(); + const logs = parseLogs(data); + setFilteredLogs(logs); }, [data]); + useEffect(() => { + scrollToBottom(); + + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [filteredLogs, autoScroll]); + + return ( { Deployment - See all the details of this deployment + See all the details of this deployment | {filteredLogs.length} lines -
- -
-							{data || "Loading..."}
-						
-
- +
{ + filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( + + )) : + ( +
+ +
+ )}
diff --git a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx b/apps/dokploy/components/dashboard/application/general/deploy-application.tsx index f9115c769..252894dd9 100644 --- a/apps/dokploy/components/dashboard/application/general/deploy-application.tsx +++ b/apps/dokploy/components/dashboard/application/general/deploy-application.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; +import { useRouter } from "next/router"; import { toast } from "sonner"; interface Props { @@ -18,6 +19,7 @@ interface Props { } export const DeployApplication = ({ applicationId }: Props) => { + const router = useRouter(); const { data, refetch } = api.application.one.useQuery( { applicationId, @@ -51,6 +53,9 @@ export const DeployApplication = ({ applicationId }: Props) => { .then(async () => { toast.success("Application deployed succesfully"); await refetch(); + router.push( + `/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`, + ); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index 33beab120..dba3666c7 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/delete-compose.tsx b/apps/dokploy/components/dashboard/compose/delete-compose.tsx index 07f42448c..3bdcc6bfa 100644 --- a/apps/dokploy/components/dashboard/compose/delete-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/delete-compose.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -19,6 +20,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Copy } from "lucide-react"; import { TrashIcon } from "lucide-react"; import { useRouter } from "next/router"; import { useState } from "react"; @@ -100,10 +102,27 @@ export const DeleteCompose = ({ composeId }: Props) => { name="projectName" render={({ field }) => ( - - To confirm, type "{data?.name}/{data?.appName}" in the box - below - {" "} + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + + { const [data, setData] = useState(""); - const endOfLogsRef = useRef(null); + const [filteredLogs, setFilteredLogs] = useState([]); const wsRef = useRef(null); // Ref to hold WebSocket instance + const [autoScroll, setAutoScroll] = useState(true); + 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); + }; + + useEffect(() => { if (!open || !logPath) return; @@ -54,14 +76,20 @@ export const ShowDeploymentCompose = ({ }; }, [logPath, open]); - const scrollToBottom = () => { - endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" }); - }; useEffect(() => { - scrollToBottom(); + const logs = parseLogs(data); + setFilteredLogs(logs); }, [data]); + useEffect(() => { + scrollToBottom(); + + if (autoScroll && scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [filteredLogs, autoScroll]); + return ( - + Deployment - See all the details of this deployment + See all the details of this deployment | {filteredLogs.length} lines -
- -
-							{data || "Loading..."}
-						
-
- +
+ + + { + filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( + + )) : + ( +
+ +
+ ) + }
diff --git a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx b/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx index e9d5dfc19..c02a78028 100644 --- a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; +import { useRouter } from "next/router"; import { toast } from "sonner"; interface Props { @@ -18,6 +19,7 @@ interface Props { } export const DeployCompose = ({ composeId }: Props) => { + const router = useRouter(); const { data, refetch } = api.compose.one.useQuery( { composeId, @@ -48,9 +50,15 @@ export const DeployCompose = ({ composeId }: Props) => { await refetch(); await deploy({ composeId, - }).catch(() => { - toast.error("Error to deploy Compose"); - }); + }) + .then(async () => { + router.push( + `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments` + ); + }) + .catch(() => { + toast.error("Error to deploy Compose"); + }); await refetch(); }} diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 992086945..6b39f4137 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/config/show-container-config.tsx b/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx index 25d78dd7d..1f1591c9d 100644 --- a/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx +++ b/apps/dokploy/components/dashboard/docker/config/show-container-config.tsx @@ -1,3 +1,4 @@ +import { CodeEditor } from "@/components/shared/code-editor"; import { Dialog, DialogContent, @@ -34,7 +35,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => { View Config - + Container Config @@ -44,7 +45,13 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
-							{JSON.stringify(data, null, 2)}
+							
 						
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 6fc0ab48a..db1c774b9 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,309 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Terminal } from "@xterm/xterm"; +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 { FitAddon } from "xterm-addon-fit"; -import "@xterm/xterm/css/xterm.css"; +import { TerminalLine } from "./terminal-line"; +import { type LogLine, getLogType, parseLogs } from "./utils"; 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 - - useEffect(() => { - // if (containerId === "select-a-container") { - // return; - // } - const container = document.getElementById(id); - if (container) { - container.innerHTML = ""; - } - - 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', - - convertEol: true, - theme: { - cursor: "transparent", - background: "rgba(0, 0, 0, 0)", - }, - }); - - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - - 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); - - ws.onerror = (error) => { - console.error("WebSocket error: ", error); - }; - - ws.onmessage = (e) => { - termi.write(e.data); - }; - - ws.onclose = (e) => { - console.log(e.reason); - - 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(() => { - term?.clear(); - }, [lines, term]); - - return ( -
-
- - { - setLines(Number(e.target.value) || 1); - }} - /> -
- -
-
-
-
- ); -}; +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) => { + 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(() => { + if (!containerId) return; + + let isCurrentConnection = true; + let noDataTimeout: NodeJS.Timeout; + setIsLoading(true); + setRawLogs(""); + setFilteredLogs([]); + + 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); + + const resetNoDataTimeout = () => { + if (noDataTimeout) clearTimeout(noDataTimeout); + noDataTimeout = setTimeout(() => { + if (isCurrentConnection) { + setIsLoading(false); + } + }, 2000); // Wait 2 seconds for data before showing "No logs found" + }; + + ws.onopen = () => { + if (!isCurrentConnection) { + ws.close(); + return; + } + console.log("WebSocket connected"); + resetNoDataTimeout(); + }; + + ws.onmessage = (e) => { + if (!isCurrentConnection) return; + setRawLogs((prev) => prev + e.data); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); + }; + + ws.onerror = (error) => { + if (!isCurrentConnection) return; + console.error("WebSocket error:", error); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); + }; + + ws.onclose = (e) => { + if (!isCurrentConnection) return; + console.log("WebSocket closed:", e.reason); + setIsLoading(false); + if (noDataTimeout) clearTimeout(noDataTimeout); + }; + + return () => { + isCurrentConnection = false; + if (noDataTimeout) clearTimeout(noDataTimeout); + if (ws.readyState === WebSocket.OPEN) { + ws.close(); + } + }; + }, [containerId, serverId, lines, search, since]); + + 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 +
+ )} +
+
+
+
+ ); +}; \ No newline at end of file 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 c3d38d986..f8531d774 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,11 +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 new file mode 100644 index 000000000..cdbbb2c81 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -0,0 +1,111 @@ +import { Badge } from "@/components/ui/badge"; +import { + Tooltip, + TooltipContent, + TooltipPortal, + 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; + noTimestamp?: boolean; + searchTerm?: string; +} + +export function TerminalLine({ log, noTimestamp, 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 | null) => { + const square = ( +
+ ); + return timestamp ? ( + + + {square} + + +

+

{timestamp}
+

+
+
+
+
+ ) : ( + square + ); + }; + + return ( +
+ {" "} +
+ {/* Icon to expand the log item maybe implement a colapsible later */} + {/* */} + {tooltip(color, rawTimestamp)} + {!noTimestamp && ( + + {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 000000000..409c69892 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -0,0 +1,148 @@ +export type LogType = "error" | "warning" | "success" | "info" | "debug"; +export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; + +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: "orange", + color: "bg-orange-500/40", + }, + debug: { + type: "debug", + 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 ?? null, + 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)(?: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) || + /(?: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|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) || + /\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) || + /\b(?:started|starting|active)\b/i.test(lowerMessage) + ) { + return LOG_STYLES.success; + } + + if ( + /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) || + /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) || + /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage) + ) { + return LOG_STYLES.debug; + } + + return LOG_STYLES.info; +}; diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 4008d6fd5..42683887e 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/mariadb/delete-mariadb.tsx b/apps/dokploy/components/dashboard/mariadb/delete-mariadb.tsx index 26a6215f4..1956954a0 100644 --- a/apps/dokploy/components/dashboard/mariadb/delete-mariadb.tsx +++ b/apps/dokploy/components/dashboard/mariadb/delete-mariadb.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -19,7 +20,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; +import { Copy, TrashIcon } from "lucide-react"; import { useRouter } from "next/router"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -99,9 +100,26 @@ export const DeleteMariadb = ({ mariadbId }: Props) => { name="projectName" render={({ field }) => ( - - To confirm, type "{data?.name}/{data?.appName}" in the box - below + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + { name="projectName" render={({ field }) => ( - - To confirm, type "{data?.name}/{data?.appName}" in the box - below + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + { name="projectName" render={({ field }) => ( - - To confirm, type "{data?.name}/{data?.appName}" in the box - below + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + { name="projectName" render={({ field }) => ( - - To confirm, type "{data?.name}/{data?.appName}" in the box - below + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + { template.tags.some((tag) => selectedTags.includes(tag)); const matchesQuery = query === "" || - template.name.toLowerCase().includes(query.toLowerCase()); + template.name.toLowerCase().includes(query.toLowerCase()) || + template.description.toLowerCase().includes(query.toLowerCase()); return matchesTags && matchesQuery; }) || []; diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 92fc337f5..d05bbba2c 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -1,35 +1,35 @@ import { DateTooltip } from "@/components/shared/date-tooltip"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { - AlertTriangle, - BookIcon, - ExternalLink, - ExternalLinkIcon, - FolderInput, - MoreHorizontalIcon, - TrashIcon, + AlertTriangle, + BookIcon, + ExternalLink, + ExternalLinkIcon, + FolderInput, + MoreHorizontalIcon, + TrashIcon, } from "lucide-react"; import Link from "next/link"; import { Fragment } from "react"; @@ -38,253 +38,257 @@ import { ProjectEnviroment } from "./project-enviroment"; import { UpdateProject } from "./update"; export const ShowProjects = () => { - const utils = api.useUtils(); - const { data } = api.project.all.useQuery(); - const { data: auth } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - }, - ); - const { mutateAsync } = api.project.remove.useMutation(); + const utils = api.useUtils(); + const { data } = api.project.all.useQuery(); + const { data: auth } = api.auth.get.useQuery(); + const { data: user } = api.user.byAuthId.useQuery( + { + authId: auth?.id || "", + }, + { + enabled: !!auth?.id && auth?.rol === "user", + } + ); + const { mutateAsync } = api.project.remove.useMutation(); - return ( - <> - {data?.length === 0 && ( -
- - - No projects added yet. Click on Create project. - -
- )} -
- {data?.map((project) => { - const emptyServices = - project?.mariadb.length === 0 && - project?.mongo.length === 0 && - project?.mysql.length === 0 && - project?.postgres.length === 0 && - project?.redis.length === 0 && - project?.applications.length === 0 && - project?.compose.length === 0; + return ( + <> + {data?.length === 0 && ( +
+ + + No projects added yet. Click on Create project. + +
+ )} +
+ {data?.map((project) => { + const emptyServices = + project?.mariadb.length === 0 && + project?.mongo.length === 0 && + project?.mysql.length === 0 && + project?.postgres.length === 0 && + project?.redis.length === 0 && + project?.applications.length === 0 && + project?.compose.length === 0; - const totalServices = - project?.mariadb.length + - project?.mongo.length + - project?.mysql.length + - project?.postgres.length + - project?.redis.length + - project?.applications.length + - project?.compose.length; + const totalServices = + project?.mariadb.length + + project?.mongo.length + + project?.mysql.length + + project?.postgres.length + + project?.redis.length + + project?.applications.length + + project?.compose.length; - const flattedDomains = [ - ...project.applications.flatMap((a) => a.domains), - ...project.compose.flatMap((a) => a.domains), - ]; + const flattedDomains = [ + ...project.applications.flatMap((a) => a.domains), + ...project.compose.flatMap((a) => a.domains), + ]; - const renderDomainsDropdown = ( - item: typeof project.compose | typeof project.applications, - ) => - item[0] ? ( - - - {"applicationId" in item[0] ? "Applications" : "Compose"} - - {item.map((a) => ( - - - - - {a.name} - - - {a.domains.map((domain) => ( - - - {domain.host} - - - - ))} - - - ))} - - ) : null; + const renderDomainsDropdown = ( + item: typeof project.compose | typeof project.applications + ) => + item[0] ? ( + + + {"applicationId" in item[0] ? "Applications" : "Compose"} + + {item.map((a) => ( + + + + + {a.name} + + + {a.domains.map((domain) => ( + + + {domain.host} + + + + ))} + + + ))} + + ) : null; - return ( -
- - - {flattedDomains.length > 1 ? ( - - - - - e.stopPropagation()} - > - {renderDomainsDropdown(project.applications)} - {renderDomainsDropdown(project.compose)} - - - ) : flattedDomains[0] ? ( - - ) : null} + return ( +
+ + + {flattedDomains.length > 1 ? ( + + + + + e.stopPropagation()} + > + {renderDomainsDropdown(project.applications)} + {renderDomainsDropdown(project.compose)} + + + ) : flattedDomains[0] ? ( + + ) : null} - - - -
- - - {project.name} - -
+ + + +
+ + + {project.name} + +
- - {project.description} - -
-
- - - - - - - Actions - -
e.stopPropagation()}> - -
-
e.stopPropagation()}> - -
+ + {project.description} + + +
+ + + + + + + Actions + +
e.stopPropagation()}> + +
+
e.stopPropagation()}> + +
-
e.stopPropagation()}> - {(auth?.rol === "admin" || - user?.canDeleteProjects) && ( - - - e.preventDefault()} - > - - Delete - - - - - - Are you sure to delete this project? - - {!emptyServices ? ( -
- - - You have active services, please - delete them first - -
- ) : ( - - This action cannot be undone - - )} -
- - - Cancel - - { - await mutateAsync({ - projectId: project.projectId, - }) - .then(() => { - toast.success( - "Project delete succesfully", - ); - }) - .catch(() => { - toast.error( - "Error to delete this project", - ); - }) - .finally(() => { - utils.project.all.invalidate(); - }); - }} - > - Delete - - -
-
- )} -
-
-
-
- - - -
- - Created - - - {totalServices}{" "} - {totalServices === 1 ? "service" : "services"} - -
-
- - -
- ); - })} -
- - ); +
e.stopPropagation()}> + {(auth?.rol === "admin" || + user?.canDeleteProjects) && ( + + + e.preventDefault()} + > + + Delete + + + + + + Are you sure to delete this project? + + {!emptyServices ? ( +
+ + + You have active services, please + delete them first + +
+ ) : ( + + This action cannot be undone + + )} +
+ + + Cancel + + { + await mutateAsync({ + projectId: project.projectId, + }) + .then(() => { + toast.success( + "Project delete succesfully" + ); + }) + .catch(() => { + toast.error( + "Error to delete this project" + ); + }) + .finally(() => { + utils.project.all.invalidate(); + }); + }} + > + Delete + + +
+
+ )} +
+ + +
+ + + +
+ + Created + + + {totalServices}{" "} + {totalServices === 1 ? "service" : "services"} + +
+
+ + +
+ ); + })} +
+ + ); }; diff --git a/apps/dokploy/components/dashboard/redis/delete-redis.tsx b/apps/dokploy/components/dashboard/redis/delete-redis.tsx index af3a084c7..818cda9b5 100644 --- a/apps/dokploy/components/dashboard/redis/delete-redis.tsx +++ b/apps/dokploy/components/dashboard/redis/delete-redis.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -19,7 +20,7 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { TrashIcon } from "lucide-react"; +import { Copy, TrashIcon } from "lucide-react"; import { useRouter } from "next/router"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -97,9 +98,26 @@ export const DeleteRedis = ({ redisId }: Props) => { name="projectName" render={({ field }) => ( - - To confirm, type "{data?.name}/{data?.appName}" in the box - below + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + >; + +export const SearchCommand = () => { + const router = useRouter(); + const [open, setOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + const { data } = api.project.all.useQuery(); + const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); + + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "j" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + return ( +
+ + + + + No projects added yet. Click on Create project. + + + + {data?.map((project) => ( + { + router.push(`/dashboard/project/${project.projectId}`); + setOpen(false); + }} + > + + {project.name} + + ))} + + + + + + {data?.map((project) => { + const applications: Services[] = extractServices(project); + return applications.map((application) => ( + { + router.push( + `/dashboard/project/${project.projectId}/services/${application.type}/${application.id}` + ); + setOpen(false); + }} + > + {application.type === "postgres" && ( + + )} + {application.type === "redis" && ( + + )} + {application.type === "mariadb" && ( + + )} + {application.type === "mongo" && ( + + )} + {application.type === "mysql" && ( + + )} + {application.type === "application" && ( + + )} + {application.type === "compose" && ( + + )} + + {project.name} / {application.name}{" "} +
{application.id}
+
+
+ +
+
+ )); + })} +
+
+ + +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx index a04a166b0..0e3e56336 100644 --- a/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/github/add-github-provider.tsx @@ -107,7 +107,24 @@ export const AddGithubProvider = () => { />
-
+
+ + Unsure if you already have an app? + + +
+ + + ); +}; 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 000000000..faaecb1fa --- /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 recommend not modifying this script unless you know what you are doing. + + +
+
+ + ( + + Command + + + + + + )} + /> + + +
+ + + + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx b/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx index b398fe741..3cda7e806 100644 --- a/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx @@ -262,16 +262,16 @@ export function StatusRow({
{showIcon ? ( <> - {isEnabled ? ( - - ) : ( - - )} {description || (isEnabled ? "Installed" : "Not Installed")} + {isEnabled ? ( + + ) : ( + + )} ) : ( {value} diff --git a/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx new file mode 100644 index 000000000..475f2b8ff --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/security-audit.tsx @@ -0,0 +1,233 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { Loader2, LockKeyhole, RefreshCw } from "lucide-react"; +import { useState } from "react"; +import { StatusRow } from "./gpu-support"; + +interface Props { + serverId: string; +} + +export const SecurityAudit = ({ serverId }: Props) => { + const [isRefreshing, setIsRefreshing] = useState(false); + const { data, refetch, error, isLoading, isError } = + api.server.security.useQuery( + { serverId }, + { + enabled: !!serverId, + }, + ); + const utils = api.useUtils(); + return ( + +
+ + +
+
+
+ + + Setup Security Sugestions + +
+ Check the security sugestions +
+ +
+
+ {isError && ( + + {error.message} + + )} +
+
+ + + + Ubuntu/Debian OS support is currently supported (Experimental) + + {isLoading ? ( +
+ + Checking Server configuration +
+ ) : ( +
+
+

UFW

+

+ UFW (Uncomplicated Firewall) is a simple firewall that can + be used to block incoming and outgoing traffic from your + server. +

+
+ + + +
+
+ +
+

SSH

+

+ SSH (Secure Shell) is a protocol that allows you to securely + connect to a server and execute commands on it. +

+
+ + + + + +
+
+ +
+

Fail2Ban

+

+ Fail2Ban (Fail2Ban) is a service that can be used to prevent + brute force attacks on your server. +

+
+ + + + + + + + +
+
+
+ )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index eb0d22553..7c1814591 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -32,8 +32,10 @@ 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"; +import { SecurityAudit } from "./security-audit"; interface Props { serverId: string; @@ -89,12 +91,18 @@ export const SetupServer = ({ serverId }: Props) => {
) : ( -
+
+ + Using a root user is required to ensure everything works as + expected. + + - + SSH Keys Deployments Validate + Security GPU Setup { Automatic process @@ -198,6 +206,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 +244,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"); + }); + }} + > + + +
@@ -303,6 +338,14 @@ export const SetupServer = ({ serverId }: Props) => {
+ +
+ +
+
{ + const router = useRouter(); + const query = router.query; const { data, refetch } = api.server.all.useQuery(); const { mutateAsync } = api.server.remove.useMutation(); const { data: sshKeys } = api.sshKey.all.useQuery(); @@ -42,12 +46,26 @@ export const ShowServers = () => { return (
+ {query?.success && isCloud && }
-
-

Servers

-

- Add servers to deploy your applications remotely. -

+
+
+

Servers

+

+ Add servers to deploy your applications remotely. +

+
+ + {isCloud && ( + { + router.push("/dashboard/settings/servers?success=true"); + }} + > + Reset Onboarding + + )}
{sshKeys && sshKeys?.length > 0 && ( @@ -100,7 +118,9 @@ export const ShowServers = () => { {data && data?.length > 0 && (
- See all servers + +
See all servers
+
Name diff --git a/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx b/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx index 9a8f14c05..db4f17b76 100644 --- a/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/validate-server.tsx @@ -1,4 +1,5 @@ import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -8,9 +9,8 @@ import { } from "@/components/ui/card"; import { api } from "@/utils/api"; import { Loader2, PcCase, RefreshCw } from "lucide-react"; -import { StatusRow } from "./gpu-support"; -import { Button } from "@/components/ui/button"; import { useState } from "react"; +import { StatusRow } from "./gpu-support"; interface Props { serverId: string; @@ -66,7 +66,7 @@ export const ValidateServer = ({ serverId }: Props) => { {isLoading ? (
- Checking Server Configuration + Checking Server configuration
) : (
@@ -113,16 +113,31 @@ export const ValidateServer = ({ serverId }: Props) => { } />
diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx new file mode 100644 index 000000000..39edad718 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-server.tsx @@ -0,0 +1,284 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { DialogFooter } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { PlusIcon } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +const Schema = z.object({ + name: z.string().min(1, { + message: "Name is required", + }), + description: z.string().optional(), + ipAddress: z.string().min(1, { + message: "IP Address is required", + }), + port: z.number().optional(), + username: z.string().optional(), + sshKeyId: z.string().min(1, { + message: "SSH Key is required", + }), +}); + +type Schema = z.infer; + +interface Props { + stepper: any; +} + +export const CreateServer = ({ stepper }: Props) => { + const { data: sshKeys } = api.sshKey.all.useQuery(); + const [isOpen, setIsOpen] = useState(false); + const { data: canCreateMoreServers, refetch } = + api.stripe.canCreateMoreServers.useQuery(); + const { mutateAsync, error, isError } = api.server.create.useMutation(); + const cloudSSHKey = sshKeys?.find( + (sshKey) => sshKey.name === "dokploy-cloud-ssh-key", + ); + + const form = useForm({ + defaultValues: { + description: "Dokploy Cloud Server", + name: "My First Server", + ipAddress: "", + port: 22, + username: "root", + sshKeyId: cloudSSHKey?.sshKeyId || "", + }, + resolver: zodResolver(Schema), + }); + + useEffect(() => { + form.reset({ + description: "Dokploy Cloud Server", + name: "My First Server", + ipAddress: "", + port: 22, + username: "root", + sshKeyId: cloudSSHKey?.sshKeyId || "", + }); + }, [form, form.reset, form.formState.isSubmitSuccessful, sshKeys]); + + useEffect(() => { + refetch(); + }, [isOpen]); + + const onSubmit = async (data: Schema) => { + await mutateAsync({ + name: data.name, + description: data.description || "", + ipAddress: data.ipAddress || "", + port: data.port || 22, + username: data.username || "root", + sshKeyId: data.sshKeyId || "", + }) + .then(async (data) => { + toast.success("Server Created"); + stepper.next(); + }) + .catch(() => { + toast.error("Error to create a server"); + }); + }; + return ( + +
+ {!canCreateMoreServers && ( + + You cannot create more servers,{" "} + + Please upgrade your plan + + + )} +
+ + +
+ +
+ ( + + Name + + + + + + + )} + /> +
+ ( + + Description + +