forked from enderofwings/NexusOS
94 lines
3.0 KiB
React
94 lines
3.0 KiB
React
import { useEffect, useRef, useState } from "react";
|
|
import { API_BASE } from "./config";
|
|
|
|
export function Logs() {
|
|
const [tabs, setTabs] = useState([]);
|
|
const [active, setActive] = useState(null);
|
|
const [content, setContent] = useState("");
|
|
const [follow, setFollow] = useState(true);
|
|
const preRef = useRef(null);
|
|
|
|
// Load the available log names once.
|
|
useEffect(() => {
|
|
fetch(`${API_BASE}/logs`)
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
setTabs(d.logs || []);
|
|
setActive(prev => prev ?? (d.logs || [])[0] ?? null);
|
|
})
|
|
.catch(() => setTabs([]));
|
|
}, []);
|
|
|
|
// Poll the active log every 2s.
|
|
useEffect(() => {
|
|
if (!active) return;
|
|
let cancelled = false;
|
|
const load = () => {
|
|
fetch(`${API_BASE}/logs/${active}`)
|
|
.then(r => r.json())
|
|
.then(d => { if (!cancelled) setContent(d.content || (d.missing ? "(no log file yet)" : "")); })
|
|
.catch(() => { if (!cancelled) setContent("(failed to load log)"); });
|
|
};
|
|
load();
|
|
const id = setInterval(load, 2000);
|
|
return () => { cancelled = true; clearInterval(id); };
|
|
}, [active]);
|
|
|
|
// Auto-scroll to bottom when following.
|
|
useEffect(() => {
|
|
if (follow && preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight;
|
|
}, [content, follow]);
|
|
|
|
return (
|
|
<div style={{ display: "flex", flexDirection: "column", flexGrow: 1, minHeight: 0 }}>
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "0.75rem" }}>
|
|
<div style={{ display: "flex", gap: "0.4rem", flexWrap: "wrap" }}>
|
|
{tabs.map(name => (
|
|
<button
|
|
key={name}
|
|
onClick={() => setActive(name)}
|
|
style={{
|
|
padding: "0.4rem 0.8rem",
|
|
background: active === name ? "#007acc" : "#161616",
|
|
color: "#fff",
|
|
border: "1px solid " + (active === name ? "#0099ff" : "#2a2a2a"),
|
|
borderRadius: "6px",
|
|
cursor: "pointer",
|
|
fontSize: "0.8rem",
|
|
textTransform: "capitalize",
|
|
}}
|
|
>
|
|
{name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<label style={{ fontSize: "0.75rem", color: "#888", display: "flex", alignItems: "center", gap: "0.35rem", cursor: "pointer" }}>
|
|
<input type="checkbox" checked={follow} onChange={e => setFollow(e.target.checked)} />
|
|
Follow
|
|
</label>
|
|
</div>
|
|
<pre
|
|
ref={preRef}
|
|
style={{
|
|
flexGrow: 1,
|
|
minHeight: 0,
|
|
overflow: "auto",
|
|
background: "#0a0a0a",
|
|
border: "1px solid #2a2a2a",
|
|
borderRadius: "8px",
|
|
padding: "0.9rem",
|
|
margin: 0,
|
|
fontSize: "0.78rem",
|
|
lineHeight: 1.45,
|
|
color: "#cfcfcf",
|
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
whiteSpace: "pre-wrap",
|
|
wordBreak: "break-word",
|
|
}}
|
|
>
|
|
{content || "(empty)"}
|
|
</pre>
|
|
</div>
|
|
);
|
|
}
|