Skip to content

Commit d1130c4

Browse files
authored
Merge pull request #2893 from hl9020/feature/copy-logs-to-clipboard
feat: Add copy to clipboard functionality for deployment and runtime logs
2 parents 6fb3584 + fd2775e commit d1130c4

File tree

2 files changed

+82
-4
lines changed

2 files changed

+82
-4
lines changed

apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Loader2 } from "lucide-react";
1+
import copy from "copy-to-clipboard";
2+
import { Check, Copy, Loader2 } from "lucide-react";
23
import { useEffect, useRef, useState } from "react";
34
import { Badge } from "@/components/ui/badge";
5+
import { Button } from "@/components/ui/button";
46
import { Checkbox } from "@/components/ui/checkbox";
57
import {
68
Dialog,
@@ -29,9 +31,10 @@ export const ShowDeployment = ({
2931
const [data, setData] = useState("");
3032
const [showExtraLogs, setShowExtraLogs] = useState(false);
3133
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
32-
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
34+
const wsRef = useRef<WebSocket | null>(null);
3335
const [autoScroll, setAutoScroll] = useState(true);
3436
const scrollRef = useRef<HTMLDivElement>(null);
37+
const [copied, setCopied] = useState(false);
3538

3639
const scrollToBottom = () => {
3740
if (autoScroll && scrollRef.current) {
@@ -106,6 +109,20 @@ export const ShowDeployment = ({
106109
}
107110
}, [filteredLogs, autoScroll]);
108111

112+
const handleCopy = () => {
113+
const logContent = filteredLogs
114+
.map(({ timestamp, message }: LogLine) =>
115+
`${timestamp?.toISOString() || ""} ${message}`.trim(),
116+
)
117+
.join("\n");
118+
119+
const success = copy(logContent);
120+
if (success) {
121+
setCopied(true);
122+
setTimeout(() => setCopied(false), 2000);
123+
}
124+
};
125+
109126
const optionalErrors = parseLogs(errorMessage || "");
110127

111128
return (
@@ -128,13 +145,27 @@ export const ShowDeployment = ({
128145
<DialogHeader>
129146
<DialogTitle>Deployment</DialogTitle>
130147
<DialogDescription className="flex items-center gap-2">
131-
<span>
148+
<span className="flex items-center gap-2">
132149
See all the details of this deployment |{" "}
133150
<Badge variant="blank" className="text-xs">
134151
{filteredLogs.length} lines
135152
</Badge>
136153
</span>
137154

155+
<Button
156+
variant="outline"
157+
size="sm"
158+
className="h-7"
159+
onClick={handleCopy}
160+
disabled={filteredLogs.length === 0}
161+
>
162+
{copied ? (
163+
<Check className="h-3.5 w-3.5" />
164+
) : (
165+
<Copy className="h-3.5 w-3.5" />
166+
)}
167+
</Button>
168+
138169
{serverId && (
139170
<div className="flex items-center space-x-2">
140171
<Checkbox

apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react";
1+
import copy from "copy-to-clipboard";
2+
import {
3+
Check,
4+
Copy,
5+
Download as DownloadIcon,
6+
Loader2,
7+
Pause,
8+
Play,
9+
} from "lucide-react";
210
import React, { useEffect, useRef } from "react";
311
import { AlertBlock } from "@/components/shared/alert-block";
412
import { Button } from "@/components/ui/button";
@@ -67,6 +75,7 @@ export const DockerLogsId: React.FC<Props> = ({
6775
const isPausedRef = useRef(false);
6876
const scrollRef = useRef<HTMLDivElement>(null);
6977
const [isLoading, setIsLoading] = React.useState(false);
78+
const [copied, setCopied] = React.useState(false);
7079

7180
const scrollToBottom = () => {
7281
if (autoScroll && scrollRef.current) {
@@ -237,6 +246,29 @@ export const DockerLogsId: React.FC<Props> = ({
237246
URL.revokeObjectURL(url);
238247
};
239248

249+
const handleCopy = async () => {
250+
const logContent = filteredLogs
251+
.map(
252+
({
253+
timestamp,
254+
message,
255+
}: {
256+
timestamp: Date | null;
257+
message: string;
258+
}) =>
259+
showTimestamp
260+
? `${timestamp?.toISOString() || "No timestamp"} ${message}`
261+
: message,
262+
)
263+
.join("\n");
264+
265+
const success = copy(logContent);
266+
if (success) {
267+
setCopied(true);
268+
setTimeout(() => setCopied(false), 2000);
269+
}
270+
};
271+
240272
const handleFilter = (logs: LogLine[]) => {
241273
return logs.filter((log) => {
242274
const logType = getLogType(log.message).type;
@@ -320,6 +352,21 @@ export const DockerLogsId: React.FC<Props> = ({
320352
)}
321353
{isPaused ? "Resume" : "Pause"}
322354
</Button>
355+
<Button
356+
variant="outline"
357+
size="sm"
358+
className="h-9"
359+
onClick={handleCopy}
360+
disabled={filteredLogs.length === 0}
361+
title="Copy logs to clipboard"
362+
>
363+
{copied ? (
364+
<Check className="mr-2 h-4 w-4" />
365+
) : (
366+
<Copy className="mr-2 h-4 w-4" />
367+
)}
368+
Copy
369+
</Button>
323370
<Button
324371
variant="outline"
325372
size="sm"

0 commit comments

Comments
 (0)