mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: remove is loading false
This commit is contained in:
@@ -1,289 +1,289 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
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 {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Download as DownloadIcon, Loader2 } from "lucide-react";
|
import { Download as DownloadIcon, Loader2 } from "lucide-react";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { TerminalLine } from "./terminal-line";
|
import { TerminalLine } from "./terminal-line";
|
||||||
import { type LogLine, getLogType, parseLogs } from "./utils";
|
import { type LogLine, getLogType, parseLogs } from "./utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
||||||
type TypeFilter = "all" | "error" | "warning" | "success" | "info" | "debug";
|
type TypeFilter = "all" | "error" | "warning" | "success" | "info" | "debug";
|
||||||
|
|
||||||
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
||||||
const { data } = api.docker.getConfig.useQuery(
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
{
|
{
|
||||||
containerId,
|
containerId,
|
||||||
serverId: serverId ?? undefined,
|
serverId: serverId ?? undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!containerId,
|
enabled: !!containerId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [rawLogs, setRawLogs] = React.useState("");
|
const [rawLogs, setRawLogs] = React.useState("");
|
||||||
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
|
||||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||||
const [lines, setLines] = React.useState<number>(100);
|
const [lines, setLines] = React.useState<number>(100);
|
||||||
const [search, setSearch] = React.useState<string>("");
|
const [search, setSearch] = React.useState<string>("");
|
||||||
const [since, setSince] = React.useState<TimeFilter>("all");
|
|
||||||
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
|
const [since, setSince] = React.useState<TimeFilter>("all");
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const scrollToBottom = () => {
|
|
||||||
if (autoScroll && scrollRef.current) {
|
const scrollToBottom = () => {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
if (autoScroll && scrollRef.current) {
|
||||||
}
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
const handleScroll = () => {
|
|
||||||
if (!scrollRef.current) return;
|
const handleScroll = () => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
||||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
setAutoScroll(isAtBottom);
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
};
|
setAutoScroll(isAtBottom);
|
||||||
|
};
|
||||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setRawLogs("");
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setFilteredLogs([]);
|
setRawLogs("");
|
||||||
setSearch(e.target.value || "");
|
setFilteredLogs([]);
|
||||||
};
|
setSearch(e.target.value || "");
|
||||||
|
};
|
||||||
const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setRawLogs("");
|
const handleLines = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setFilteredLogs([]);
|
setRawLogs("");
|
||||||
setLines(Number(e.target.value) || 1);
|
setFilteredLogs([]);
|
||||||
};
|
setLines(Number(e.target.value) || 1);
|
||||||
|
};
|
||||||
const handleSince = (value: TimeFilter) => {
|
|
||||||
setRawLogs("");
|
const handleSince = (value: TimeFilter) => {
|
||||||
setFilteredLogs([]);
|
setRawLogs("");
|
||||||
setSince(value);
|
setFilteredLogs([]);
|
||||||
};
|
setSince(value);
|
||||||
|
};
|
||||||
const handleTypeFilter = (value: TypeFilter) => {
|
|
||||||
setTypeFilter(value);
|
const handleTypeFilter = (value: TypeFilter) => {
|
||||||
};
|
setTypeFilter(value);
|
||||||
|
};
|
||||||
useEffect(() => {
|
|
||||||
setRawLogs("");
|
useEffect(() => {
|
||||||
setFilteredLogs([]);
|
setRawLogs("");
|
||||||
}, [containerId]);
|
setFilteredLogs([]);
|
||||||
|
}, [containerId]);
|
||||||
useEffect(() => {
|
|
||||||
if (!containerId) return;
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
if (!containerId) return;
|
||||||
|
setIsLoading(true);
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
||||||
const params = new globalThis.URLSearchParams({
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
containerId,
|
const params = new globalThis.URLSearchParams({
|
||||||
tail: lines.toString(),
|
containerId,
|
||||||
since,
|
tail: lines.toString(),
|
||||||
search,
|
since,
|
||||||
});
|
search,
|
||||||
|
});
|
||||||
if (serverId) {
|
|
||||||
params.append("serverId", serverId);
|
if (serverId) {
|
||||||
}
|
params.append("serverId", serverId);
|
||||||
|
}
|
||||||
const wsUrl = `${protocol}//${
|
|
||||||
window.location.host
|
const wsUrl = `${protocol}//${
|
||||||
}/docker-container-logs?${params.toString()}`;
|
window.location.host
|
||||||
console.log("Connecting to WebSocket:", wsUrl);
|
}/docker-container-logs?${params.toString()}`;
|
||||||
const ws = new WebSocket(wsUrl);
|
console.log("Connecting to WebSocket:", wsUrl);
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
ws.onopen = () => {
|
|
||||||
console.log("WebSocket connected");
|
ws.onopen = () => {
|
||||||
setIsLoading(false)
|
console.log("WebSocket connected");
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
setRawLogs((prev) => prev + e.data);
|
setRawLogs((prev) => prev + e.data);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error("WebSocket error:", error);
|
console.error("WebSocket error:", error);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (e) => {
|
ws.onclose = (e) => {
|
||||||
console.log("WebSocket closed:", e.reason);
|
console.log("WebSocket closed:", e.reason);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [containerId, serverId, lines, search, since]);
|
}, [containerId, serverId, lines, search, since]);
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
const logContent = filteredLogs
|
const logContent = filteredLogs
|
||||||
.map(
|
.map(
|
||||||
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
||||||
`${timestamp?.toISOString() || "No timestamp"} ${message}`
|
`${timestamp?.toISOString() || "No timestamp"} ${message}`
|
||||||
)
|
)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const blob = new Blob([logContent], { type: "text/plain" });
|
const blob = new Blob([logContent], { type: "text/plain" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
const appName = data.Name.replace("/", "") || "app";
|
const appName = data.Name.replace("/", "") || "app";
|
||||||
const isoDate = new Date().toISOString();
|
const isoDate = new Date().toISOString();
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
|
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
|
||||||
.slice(11, 19)
|
.slice(11, 19)
|
||||||
.replace(/:/g, "")}.log.txt`;
|
.replace(/:/g, "")}.log.txt`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilter = (logs: LogLine[]) => {
|
const handleFilter = (logs: LogLine[]) => {
|
||||||
return logs.filter((log) => {
|
return logs.filter((log) => {
|
||||||
const logType = getLogType(log.message).type;
|
const logType = getLogType(log.message).type;
|
||||||
|
|
||||||
const matchesType = typeFilter === "all" || logType === typeFilter;
|
const matchesType = typeFilter === "all" || logType === typeFilter;
|
||||||
|
|
||||||
return matchesType;
|
return matchesType;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRawLogs("");
|
setRawLogs("");
|
||||||
setFilteredLogs([]);
|
setFilteredLogs([]);
|
||||||
}, [containerId]);
|
}, [containerId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logs = parseLogs(rawLogs);
|
const logs = parseLogs(rawLogs);
|
||||||
const filtered = handleFilter(logs);
|
const filtered = handleFilter(logs);
|
||||||
setFilteredLogs(filtered);
|
setFilteredLogs(filtered);
|
||||||
}, [rawLogs, search, lines, since, typeFilter]);
|
}, [rawLogs, search, lines, since, typeFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [filteredLogs, autoScroll]);
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="shadow-md rounded-lg overflow-hidden">
|
<div className="shadow-md rounded-lg overflow-hidden">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
|
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Number of lines to show"
|
placeholder="Number of lines to show"
|
||||||
value={lines}
|
value={lines}
|
||||||
onChange={handleLines}
|
onChange={handleLines}
|
||||||
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select value={since} onValueChange={handleSince}>
|
<Select value={since} onValueChange={handleSince}>
|
||||||
<SelectTrigger className="sm:w-[180px] w-full h-9">
|
<SelectTrigger className="sm:w-[180px] w-full h-9">
|
||||||
<SelectValue placeholder="Time filter" />
|
<SelectValue placeholder="Time filter" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="1h">Last hour</SelectItem>
|
<SelectItem value="1h">Last hour</SelectItem>
|
||||||
<SelectItem value="6h">Last 6 hours</SelectItem>
|
<SelectItem value="6h">Last 6 hours</SelectItem>
|
||||||
<SelectItem value="24h">Last 24 hours</SelectItem>
|
<SelectItem value="24h">Last 24 hours</SelectItem>
|
||||||
<SelectItem value="168h">Last 7 days</SelectItem>
|
<SelectItem value="168h">Last 7 days</SelectItem>
|
||||||
<SelectItem value="720h">Last 30 days</SelectItem>
|
<SelectItem value="720h">Last 30 days</SelectItem>
|
||||||
<SelectItem value="all">All time</SelectItem>
|
<SelectItem value="all">All time</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select value={typeFilter} onValueChange={handleTypeFilter}>
|
<Select value={typeFilter} onValueChange={handleTypeFilter}>
|
||||||
<SelectTrigger className="sm:w-[180px] w-full h-9">
|
<SelectTrigger className="sm:w-[180px] w-full h-9">
|
||||||
<SelectValue placeholder="Type filter" />
|
<SelectValue placeholder="Type filter" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
<Badge variant="blank">All</Badge>
|
<Badge variant="blank">All</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="error">
|
<SelectItem value="error">
|
||||||
<Badge variant="red">Error</Badge>
|
<Badge variant="red">Error</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="warning">
|
<SelectItem value="warning">
|
||||||
<Badge variant="orange">Warning</Badge>
|
<Badge variant="orange">Warning</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="debug">
|
<SelectItem value="debug">
|
||||||
<Badge variant="yellow">Debug</Badge>
|
<Badge variant="yellow">Debug</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="success">
|
<SelectItem value="success">
|
||||||
<Badge variant="green">Success</Badge>
|
<Badge variant="green">Success</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="info">
|
<SelectItem value="info">
|
||||||
<Badge variant="blue">Info</Badge>
|
<Badge variant="blue">Info</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search logs..."
|
placeholder="Search logs..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
className="inline-flex h-9 text-sm text-white placeholder-gray-400 w-full sm:w-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-9"
|
className="h-9"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={filteredLogs.length === 0 || !data?.Name}
|
disabled={filteredLogs.length === 0 || !data?.Name}
|
||||||
>
|
>
|
||||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||||
Download logs
|
Download logs
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#d4d4d4] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
>
|
>
|
||||||
{filteredLogs.length > 0 ? (
|
{filteredLogs.length > 0 ? (
|
||||||
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
||||||
<TerminalLine
|
<TerminalLine
|
||||||
key={index}
|
key={index}
|
||||||
log={filteredLog}
|
log={filteredLog}
|
||||||
searchTerm={search}
|
searchTerm={search}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : isLoading ? (
|
) : isLoading ? (
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
No logs found
|
No logs found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user