blob: c27d4209ece9113d0a2a64f8b09f0695cee8f724 [file] [log] [blame]
gio3a921b82025-05-10 07:36:09 +00001import { useCallback, useEffect, useState, useRef, useMemo } from "react";
2import { useProjectId, useEnv } from "@/lib/state";
3import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
gio3a921b82025-05-10 07:36:09 +00004import { useToast } from "@/hooks/use-toast";
5
6// ANSI escape sequence regex
gio880de162025-05-11 07:26:00 +00007// eslint-disable-next-line no-control-regex
gio3a921b82025-05-10 07:36:09 +00008const ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
9
10function cleanAnsiEscapeSequences(text: string): string {
11 return text.replace(ANSI_ESCAPE_REGEX, "");
12}
13
14export function Logs() {
15 const { toast } = useToast();
16 const projectId = useProjectId();
17 const env = useEnv();
18 const [selectedService, setSelectedService] = useState<string>("");
19 const [logs, setLogs] = useState<string>("");
20 const preRef = useRef<HTMLPreElement>(null);
21 const wasAtBottom = useRef(true);
22
23 const checkIfAtBottom = useCallback(() => {
24 if (!preRef.current) return;
25 const { scrollTop, scrollHeight, clientHeight } = preRef.current;
26 wasAtBottom.current = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
27 }, []);
28
29 const scrollToBottom = useCallback(() => {
30 if (!preRef.current) return;
31 preRef.current.scrollTop = preRef.current.scrollHeight;
32 }, []);
33
34 const fetchLogs = useCallback(
35 async (service: string) => {
36 if (!projectId || !service) return;
37
38 try {
39 const resp = await fetch(`/api/project/${projectId}/logs/${service}`);
40 if (!resp.ok) {
41 throw new Error("Failed to fetch logs");
42 }
43 const data = await resp.json();
44 setLogs(data.logs || "");
45 } catch (e) {
46 console.error(e);
47 toast({
48 variant: "destructive",
49 title: "Failed to fetch logs",
50 });
51 }
52 },
53 [projectId, toast],
54 );
55
56 useEffect(() => {
57 if (selectedService) {
58 // Initial fetch
59 fetchLogs(selectedService);
60
61 // Set up interval for periodic updates
62 const interval = setInterval(() => {
63 fetchLogs(selectedService);
64 }, 5000);
65
66 // Cleanup interval on unmount or when service changes
67 return () => clearInterval(interval);
68 }
69 }, [selectedService, fetchLogs]);
70
71 // Handle scroll events
72 useEffect(() => {
73 const pre = preRef.current;
74 if (!pre) return;
75
76 const handleScroll = () => {
77 checkIfAtBottom();
78 };
79
80 pre.addEventListener("scroll", handleScroll);
81 return () => pre.removeEventListener("scroll", handleScroll);
82 }, [checkIfAtBottom]);
83
84 // Auto-scroll when new logs arrive
85 useEffect(() => {
86 if (wasAtBottom.current) {
87 scrollToBottom();
88 }
89 }, [logs, scrollToBottom]);
90
91 const sortedServices = useMemo(() => (env?.services ? [...env.services].sort() : []), [env]);
92
93 // Auto-select first service when services are available
94 useEffect(() => {
95 if (sortedServices.length && !selectedService) {
96 setSelectedService(sortedServices[0]);
97 }
98 }, [sortedServices, selectedService]);
99
100 return (
giobc47f9f2025-05-12 08:31:07 +0000101 <div className="flex flex-col h-full">
102 <div className="flex-none w-full flex flex-row justify-start items-center gap-2 px-4 py-1">
103 <div>Service</div>
gio3a921b82025-05-10 07:36:09 +0000104 <Select value={selectedService} onValueChange={setSelectedService}>
giobc47f9f2025-05-12 08:31:07 +0000105 <SelectTrigger className="w-1/4">
gio3a921b82025-05-10 07:36:09 +0000106 <SelectValue placeholder="Select a service" />
107 </SelectTrigger>
108 <SelectContent>
109 {sortedServices.map((service) => (
110 <SelectItem key={service} value={service}>
111 {service}
112 </SelectItem>
113 ))}
114 </SelectContent>
115 </Select>
giobc47f9f2025-05-12 08:31:07 +0000116 </div>
117 {selectedService && (
118 <pre
119 ref={preRef}
gio8cadbc72025-05-16 07:51:02 +0000120 className="flex-1 h-full p-4 bg-muted overflow-auto font-['JetBrains_Mono'] text-xs whitespace-pre-wrap break-all"
giobc47f9f2025-05-12 08:31:07 +0000121 >
122 {cleanAnsiEscapeSequences(logs) || "No logs available"}
123 </pre>
124 )}
125 </div>
gio3a921b82025-05-10 07:36:09 +0000126 );
127}