refactor: remove is loading false

This commit is contained in:
Mauricio Siu
2024-12-14 01:17:13 -06:00
parent a9c62b47ef
commit 115ed7e7bf

View File

@@ -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>
); );
}; };