From ba4626c7da5152eb100b395f8eec59b07af04c8b Mon Sep 17 00:00:00 2001 From: hl9020 <114366310+hl9020@users.noreply.github.com> Date: Fri, 24 Oct 2025 16:38:29 +0200 Subject: [PATCH 1/3] feat: add copy to clipboard functionality for deployment and runtime logs --- .../deployments/show-deployment.tsx | 45 +++++++++++++++++-- .../dashboard/docker/logs/docker-logs-id.tsx | 44 +++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 69c697721..b4f54f76d 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -1,6 +1,7 @@ -import { Loader2 } from "lucide-react"; +import { Check, Copy, Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, @@ -29,9 +30,10 @@ export const ShowDeployment = ({ const [data, setData] = useState(""); const [showExtraLogs, setShowExtraLogs] = useState(false); const [filteredLogs, setFilteredLogs] = useState([]); - const wsRef = useRef(null); // Ref to hold WebSocket instance + const wsRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); const scrollRef = useRef(null); + const [copied, setCopied] = useState(false); const scrollToBottom = () => { if (autoScroll && scrollRef.current) { @@ -106,6 +108,29 @@ export const ShowDeployment = ({ } }, [filteredLogs, autoScroll]); + const handleCopy = async () => { + const logContent = filteredLogs + .map(({ timestamp, message }: LogLine) => + `${timestamp?.toISOString() || ""} ${message}`.trim() + ) + .join("\n"); + + try { + await navigator.clipboard.writeText(logContent); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + const textarea = document.createElement("textarea"); + textarea.value = logContent; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + const optionalErrors = parseLogs(errorMessage || ""); return ( @@ -128,13 +153,27 @@ export const ShowDeployment = ({ Deployment - + See all the details of this deployment |{" "} {filteredLogs.length} lines + + {serverId && (
= ({ const isPausedRef = useRef(false); const scrollRef = useRef(null); const [isLoading, setIsLoading] = React.useState(false); + const [copied, setCopied] = React.useState(false); const scrollToBottom = () => { if (autoScroll && scrollRef.current) { @@ -237,6 +238,32 @@ export const DockerLogsId: React.FC = ({ URL.revokeObjectURL(url); }; + const handleCopy = async () => { + const logContent = filteredLogs + .map( + ({ timestamp, message }: { timestamp: Date | null; message: string }) => + showTimestamp + ? `${timestamp?.toISOString() || "No timestamp"} ${message}` + : message + ) + .join("\n"); + + try { + await navigator.clipboard.writeText(logContent); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + const textarea = document.createElement("textarea"); + textarea.value = logContent; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + const handleFilter = (logs: LogLine[]) => { return logs.filter((log) => { const logType = getLogType(log.message).type; @@ -320,6 +347,21 @@ export const DockerLogsId: React.FC = ({ )} {isPaused ? "Resume" : "Pause"} +