blob: a2bf1ba8b19508f42adbcf1a2abec74c6a7d264d [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";
4import { Card, CardContent, CardHeader } from "@/components/ui/card";
5import { useToast } from "@/hooks/use-toast";
6
7// ANSI escape sequence regex
gio880de162025-05-11 07:26:00 +00008// eslint-disable-next-line no-control-regex
gio3a921b82025-05-10 07:36:09 +00009const ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
10
11function cleanAnsiEscapeSequences(text: string): string {
12 return text.replace(ANSI_ESCAPE_REGEX, "");
13}
14
15export function Logs() {
16 const { toast } = useToast();
17 const projectId = useProjectId();
18 const env = useEnv();
19 const [selectedService, setSelectedService] = useState<string>("");
20 const [logs, setLogs] = useState<string>("");
21 const preRef = useRef<HTMLPreElement>(null);
22 const wasAtBottom = useRef(true);
23
24 const checkIfAtBottom = useCallback(() => {
25 if (!preRef.current) return;
26 const { scrollTop, scrollHeight, clientHeight } = preRef.current;
27 wasAtBottom.current = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
28 }, []);
29
30 const scrollToBottom = useCallback(() => {
31 if (!preRef.current) return;
32 preRef.current.scrollTop = preRef.current.scrollHeight;
33 }, []);
34
35 const fetchLogs = useCallback(
36 async (service: string) => {
37 if (!projectId || !service) return;
38
39 try {
40 const resp = await fetch(`/api/project/${projectId}/logs/${service}`);
41 if (!resp.ok) {
42 throw new Error("Failed to fetch logs");
43 }
44 const data = await resp.json();
45 setLogs(data.logs || "");
46 } catch (e) {
47 console.error(e);
48 toast({
49 variant: "destructive",
50 title: "Failed to fetch logs",
51 });
52 }
53 },
54 [projectId, toast],
55 );
56
57 useEffect(() => {
58 if (selectedService) {
59 // Initial fetch
60 fetchLogs(selectedService);
61
62 // Set up interval for periodic updates
63 const interval = setInterval(() => {
64 fetchLogs(selectedService);
65 }, 5000);
66
67 // Cleanup interval on unmount or when service changes
68 return () => clearInterval(interval);
69 }
70 }, [selectedService, fetchLogs]);
71
72 // Handle scroll events
73 useEffect(() => {
74 const pre = preRef.current;
75 if (!pre) return;
76
77 const handleScroll = () => {
78 checkIfAtBottom();
79 };
80
81 pre.addEventListener("scroll", handleScroll);
82 return () => pre.removeEventListener("scroll", handleScroll);
83 }, [checkIfAtBottom]);
84
85 // Auto-scroll when new logs arrive
86 useEffect(() => {
87 if (wasAtBottom.current) {
88 scrollToBottom();
89 }
90 }, [logs, scrollToBottom]);
91
92 const sortedServices = useMemo(() => (env?.services ? [...env.services].sort() : []), [env]);
93
94 // Auto-select first service when services are available
95 useEffect(() => {
96 if (sortedServices.length && !selectedService) {
97 setSelectedService(sortedServices[0]);
98 }
99 }, [sortedServices, selectedService]);
100
101 return (
gio880de162025-05-11 07:26:00 +0000102 <Card className="h-full flex flex-col">
gio3a921b82025-05-10 07:36:09 +0000103 <CardHeader>
104 <Select value={selectedService} onValueChange={setSelectedService}>
105 <SelectTrigger>
106 <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>
116 </CardHeader>
gio880de162025-05-11 07:26:00 +0000117 <CardContent className="flex-1 min-h-0">
gio3a921b82025-05-10 07:36:09 +0000118 {selectedService && (
119 <pre
120 ref={preRef}
gio880de162025-05-11 07:26:00 +0000121 className="h-full p-4 bg-muted rounded-lg overflow-auto font-['JetBrains_Mono'] whitespace-pre-wrap break-all"
gio3a921b82025-05-10 07:36:09 +0000122 >
123 {cleanAnsiEscapeSequences(logs) || "No logs available"}
124 </pre>
125 )}
126 </CardContent>
127 </Card>
128 );
129}