mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-10 22:15:16 +00:00
feat(docker-logs): add pause/resume functionality for log streaming with message buffering
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
|
import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Download as DownloadIcon, Loader2 } from "lucide-react";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { LineCountFilter } from "./line-count-filter";
|
import { LineCountFilter } from "./line-count-filter";
|
||||||
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
|
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
|
||||||
import { StatusLogsFilter } from "./status-logs-filter";
|
import { StatusLogsFilter } from "./status-logs-filter";
|
||||||
import { TerminalLine } from "./terminal-line";
|
import { TerminalLine } from "./terminal-line";
|
||||||
import { type LogLine, getLogType, parseLogs } from "./utils";
|
import { getLogType, type LogLine, parseLogs } from "./utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
@@ -61,6 +62,9 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
const [showTimestamp, setShowTimestamp] = React.useState(true);
|
const [showTimestamp, setShowTimestamp] = React.useState(true);
|
||||||
const [since, setSince] = React.useState<TimeFilter>("all");
|
const [since, setSince] = React.useState<TimeFilter>("all");
|
||||||
const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
|
const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
|
||||||
|
const [isPaused, setIsPaused] = React.useState(false);
|
||||||
|
const [messageBuffer, setMessageBuffer] = React.useState<string[]>([]);
|
||||||
|
const isPausedRef = useRef(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
|
||||||
@@ -85,15 +89,38 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
const handleLines = (lines: number) => {
|
const handleLines = (lines: number) => {
|
||||||
setRawLogs("");
|
setRawLogs("");
|
||||||
setFilteredLogs([]);
|
setFilteredLogs([]);
|
||||||
|
setMessageBuffer([]);
|
||||||
setLines(lines);
|
setLines(lines);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSince = (value: TimeFilter) => {
|
const handleSince = (value: TimeFilter) => {
|
||||||
setRawLogs("");
|
setRawLogs("");
|
||||||
setFilteredLogs([]);
|
setFilteredLogs([]);
|
||||||
|
setMessageBuffer([]);
|
||||||
setSince(value);
|
setSince(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePauseResume = () => {
|
||||||
|
if (isPaused) {
|
||||||
|
// Resume: Apply all buffered messages
|
||||||
|
if (messageBuffer.length > 0) {
|
||||||
|
const bufferedContent = messageBuffer.join("");
|
||||||
|
setRawLogs((prev) => {
|
||||||
|
const updated = prev + bufferedContent;
|
||||||
|
const splitLines = updated.split("\n");
|
||||||
|
if (splitLines.length > lines) {
|
||||||
|
return splitLines.slice(-lines).join("\n");
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
setMessageBuffer([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newPausedState = !isPaused;
|
||||||
|
setIsPaused(newPausedState);
|
||||||
|
isPausedRef.current = newPausedState;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerId) return;
|
if (!containerId) return;
|
||||||
|
|
||||||
@@ -102,6 +129,10 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setRawLogs("");
|
setRawLogs("");
|
||||||
setFilteredLogs([]);
|
setFilteredLogs([]);
|
||||||
|
setMessageBuffer([]);
|
||||||
|
// Reset pause state when container changes
|
||||||
|
setIsPaused(false);
|
||||||
|
isPausedRef.current = false;
|
||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
const params = new globalThis.URLSearchParams({
|
const params = new globalThis.URLSearchParams({
|
||||||
@@ -140,14 +171,22 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
if (!isCurrentConnection) return;
|
if (!isCurrentConnection) return;
|
||||||
setRawLogs((prev) => {
|
|
||||||
const updated = prev + e.data;
|
if (isPausedRef.current) {
|
||||||
const splitLines = updated.split("\n");
|
// When paused, buffer the messages instead of displaying them
|
||||||
if (splitLines.length > lines) {
|
setMessageBuffer((prev) => [...prev, e.data]);
|
||||||
return splitLines.slice(-lines).join("\n");
|
} else {
|
||||||
}
|
// When not paused, display messages normally
|
||||||
return updated;
|
setRawLogs((prev) => {
|
||||||
});
|
const updated = prev + e.data;
|
||||||
|
const splitLines = updated.split("\n");
|
||||||
|
if (splitLines.length > lines) {
|
||||||
|
return splitLines.slice(-lines).join("\n");
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
};
|
};
|
||||||
@@ -210,9 +249,15 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sync isPausedRef with isPaused state
|
||||||
|
useEffect(() => {
|
||||||
|
isPausedRef.current = isPaused;
|
||||||
|
}, [isPaused]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRawLogs("");
|
setRawLogs("");
|
||||||
setFilteredLogs([]);
|
setFilteredLogs([]);
|
||||||
|
setMessageBuffer([]);
|
||||||
}, [containerId]);
|
}, [containerId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -260,17 +305,48 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
className="h-9 sm:w-auto w-full"
|
size="sm"
|
||||||
onClick={handleDownload}
|
className="h-9"
|
||||||
disabled={filteredLogs.length === 0 || !data?.Name}
|
onClick={handlePauseResume}
|
||||||
>
|
title={isPaused ? "Resume logs" : "Pause logs"}
|
||||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
>
|
||||||
Download logs
|
{isPaused ? (
|
||||||
</Button>
|
<Play className="mr-2 h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Pause className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{isPaused ? "Resume" : "Pause"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 sm:w-auto w-full"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={filteredLogs.length === 0 || !data?.Name}
|
||||||
|
>
|
||||||
|
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||||
|
Download logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{isPaused && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Pause className="h-4 w-4" />
|
||||||
|
<span>
|
||||||
|
Logs paused
|
||||||
|
{messageBuffer.length > 0 && (
|
||||||
|
<span className="ml-1 font-medium">
|
||||||
|
({messageBuffer.length} messages buffered)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
|||||||
Reference in New Issue
Block a user