import { createEffect, createSignal, For, type JSX, Show } from "solid-js"; import { fmtTimestamp } from "../format"; import { logsResource } from "../state"; import { LEVEL_ORDER, type LogEntry } from "../types"; import { GlassCard } from "./GlassCard"; const RESERVED_LOG_FIELDS = new Set(["ts", "level", "logger", "msg", "exc"]); interface Extra { key: string; value: string; } interface FormattedRow { index: number; ts: string; level: string; logger: string; message: string; extras: Extra[]; exc: string | null; } function formatExtraValue(value: unknown): string { if (value == null) return ""; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") { return String(value); } try { return JSON.stringify(value); } catch { return String(value); } } function buildExtras(entry: LogEntry): Extra[] { const out: Extra[] = []; for (const [key, value] of Object.entries(entry)) { if (RESERVED_LOG_FIELDS.has(key)) continue; out.push({ key, value: formatExtraValue(value) }); } return out; } export function Logs(): JSX.Element { const [level, setLevel] = createSignal("INFO"); const [filter, setFilter] = createSignal(""); const [follow, setFollow] = createSignal(true); let scrollEl: HTMLDivElement | undefined; const allEntries = (): LogEntry[] => logsResource()?.entries ?? []; const rows = (): FormattedRow[] => { const wantLevel = level(); const minOrd = wantLevel ? (LEVEL_ORDER[wantLevel] ?? 0) : 0; const needle = filter().trim().toLowerCase(); const out: FormattedRow[] = []; let index = 0; for (const entry of allEntries()) { const lvl = entry.level ?? "INFO"; if ((LEVEL_ORDER[lvl] ?? 20) < minOrd) continue; const msg = entry.msg ?? ""; const extras = buildExtras(entry); if (needle) { const haystack = ( msg + " " + extras.map((e) => `${e.key}=${e.value}`).join(" ") ).toLowerCase(); if (!haystack.includes(needle)) continue; } out.push({ index: index++, ts: fmtTimestamp(entry.ts), level: lvl, logger: entry.logger ?? "", message: msg, extras, exc: entry.exc ?? null, }); } return out; }; createEffect(() => { // Touch dependencies so effect re-runs on new data / toggles. rows(); if (follow() && scrollEl) { scrollEl.scrollTop = scrollEl.scrollHeight; } }); return ( {rows().length} / {allEntries().length} } >
(scrollEl = el)}> no log entries match
}> {(row) => (
{row.ts} {row.level} {row.logger} {row.message} {(extra) => ( {" "} {extra.key}={extra.value} )} {row.exc}
)}
); }