createLogViewer function presentation exported ✓ 99.1%
Last updated: 2026-03-02T13:35:57.087Z
Location
Metrics
LOC: 240
Complexity: 20
Params: 1
Coverage: 99.1% (107/108 lines, 31x executed)
Signature
createLogViewer(options: LogViewerOptions): : LogViewer
Architecture violations
- [warning] max-cyclomatic-complexity: 'createLogViewer' has cyclomatic complexity 20 (max 10)
- [warning] max-lines: 'createLogViewer' has 240 lines (max 80)
Source Code
export function createLogViewer(options: LogViewerOptions): LogViewer {
const { container, variant } = options;
let activeFilter: LogLevel | "all" = "all";
let searchQuery = "";
let timeFrom = "";
let timeTo = "";
let allEntries: LogEntry[] = [];
let unsubscribe: (() => void) | null = null;
function formatEntriesAsText(entries: LogEntry[]): string {
return entries
.map((e) => `[${e.ts}] [${e.level.toUpperCase()}] [${e.ns}] ${e.msg}`)
.join("\n");
}
async function copyToClipboard(text: string): Promise<void> {
try {
await navigator.clipboard.writeText(text);
} catch {
// Fallback for contexts where clipboard API is restricted
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
}
}
function downloadAsJson(entries: LogEntry[]): void {
const json = JSON.stringify(entries, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `fill-all-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`;
a.click();
URL.revokeObjectURL(url);
}
function filterEntries(): LogEntry[] {
let filtered = allEntries;
if (activeFilter !== "all") {
filtered = filtered.filter((e) => e.level === activeFilter);
}
if (searchQuery) {
const q = searchQuery.toLowerCase();
filtered = filtered.filter(
(e) =>
e.msg.toLowerCase().includes(q) || e.ns.toLowerCase().includes(q),
);
}
if (timeFrom) {
const fromMs = new Date(timeFrom).getTime();
filtered = filtered.filter((e) => new Date(e.ts).getTime() >= fromMs);
}
if (timeTo) {
// Add 59s 999ms to include the full minute selected
const toMs = new Date(timeTo).getTime() + 59999;
filtered = filtered.filter((e) => new Date(e.ts).getTime() <= toMs);
}
return filtered;
}
function formatTime(ts: string): string {
try {
const d = new Date(ts);
return d.toLocaleTimeString("pt-BR");
} catch {
return ts;
}
}
function renderEntries(entries: LogEntry[]): string {
if (entries.length === 0) {
return `<div class="lv-empty">Nenhum log encontrado.</div>`;
}
return entries
.map(
(entry, idx) => `
<div class="lv-entry lv-${LEVEL_CSS[entry.level] ?? "info"}">
<span class="lv-time">${formatTime(entry.ts)}</span>
<span class="lv-level">${entry.level.toUpperCase()}</span>
<span class="lv-ns">${escapeHtml(entry.ns)}</span>
<span class="lv-msg">${escapeHtml(entry.msg)}</span>
<button class="lv-copy-entry-btn" data-idx="${idx}" title="Copiar entrada">📋</button>
</div>`,
)
.join("");
}
function render(): void {
const filtered = filterEntries();
const filterBtns = (
["all", "debug", "info", "warn", "error", "audit"] as const
)
.map(
(level) =>
`<button class="lv-filter-btn${activeFilter === level ? " active" : ""}" data-level="${level}">${LEVEL_LABELS[level]}</button>`,
)
.join("");
container.innerHTML = `
<div class="lv-toolbar">
<div class="lv-filters">${filterBtns}</div>
<input class="lv-search" type="text" placeholder="Buscar logs..." value="${escapeHtml(searchQuery)}" />
<input class="lv-time-from" type="datetime-local" title="De:" value="${timeFrom}" />
<input class="lv-time-to" type="datetime-local" title="Até:" value="${timeTo}" />
<button class="lv-copy-all-btn" title="Copiar todos os logs visíveis">📋</button>
<button class="lv-download-json-btn" title="Baixar logs como JSON">⬇️</button>
<button class="lv-clear-btn" title="Limpar todos os logs">🗑️</button>
<span class="lv-count">${filtered.length}/${allEntries.length}</span>
</div>
<div class="lv-entries">${renderEntries(filtered)}</div>
`;
// Bind filter buttons
container
.querySelectorAll<HTMLButtonElement>(".lv-filter-btn")
.forEach((btn) => {
btn.addEventListener("click", () => {
activeFilter = btn.dataset.level as LogLevel | "all";
render();
});
});
// Bind search input
const searchInput = container.querySelector<HTMLInputElement>(".lv-search");
const actualLen = searchInput?.value.length ?? 0;
if (searchInput) {
searchInput.addEventListener(
"input",
debounce(() => {
searchQuery = searchInput.value;
render();
}, 300),
);
const newInput = container.querySelector<HTMLInputElement>(".lv-search");
if (newInput) {
const len =
newInput.value.length > actualLen ? newInput.value.length : actualLen;
newInput.focus();
newInput.setSelectionRange(len, len);
}
}
// Bind time-range inputs
const timeFromInput =
container.querySelector<HTMLInputElement>(".lv-time-from");
if (timeFromInput) {
timeFromInput.addEventListener("change", () => {
timeFrom = timeFromInput.value;
render();
});
}
const timeToInput =
container.querySelector<HTMLInputElement>(".lv-time-to");
if (timeToInput) {
timeToInput.addEventListener("change", () => {
timeTo = timeToInput.value;
render();
});
}
// Bind copy-all button
container
.querySelector(".lv-copy-all-btn")
?.addEventListener("click", () => {
const filtered = filterEntries();
const text = formatEntriesAsText(filtered);
void copyToClipboard(text);
});
// Bind download JSON button
container
.querySelector(".lv-download-json-btn")
?.addEventListener("click", () => {
downloadAsJson(filterEntries());
});
// Bind per-entry copy buttons
container
.querySelectorAll<HTMLButtonElement>(".lv-copy-entry-btn")
.forEach((btn) => {
btn.addEventListener("click", () => {
const idx = Number(btn.dataset.idx);
const filtered = filterEntries();
const entry = filtered[idx];
if (entry) {
const text = formatEntriesAsText([entry]);
void copyToClipboard(text);
}
});
});
// Bind clear button
container
.querySelector(".lv-clear-btn")
?.addEventListener("click", async () => {
await clearLogEntries();
allEntries = [];
render();
});
// Auto-scroll to bottom
const entriesEl = container.querySelector(".lv-entries");
if (entriesEl) {
entriesEl.scrollTop = entriesEl.scrollHeight;
}
}
async function refresh(): Promise<void> {
allEntries = await loadLogEntries();
render();
}
function dispose(): void {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
}
// Subscribe to real-time updates
unsubscribe = onLogUpdate((entries) => {
allEntries = entries;
render();
});
return { refresh, dispose };
}
Members
| Name | Kind | Visibility | Status | Signature |
|---|---|---|---|---|
| formatEntriesAsText | function | - | formatEntriesAsText(entries: LogEntry[]): : string | |
| copyToClipboard | function | - | copyToClipboard(text: string): : Promise<void> | |
| downloadAsJson | function | - | downloadAsJson(entries: LogEntry[]): : void | |
| filterEntries | function | - | filterEntries(): : LogEntry[] | |
| formatTime | function | - | formatTime(ts: string): : string | |
| renderEntries | function | - | renderEntries(entries: LogEntry[]): : string | |
| render | function | - | render(): : void | |
| refresh | function | - | refresh(): : Promise<void> | |
| dispose | function | - | dispose(): : void |
Dependencies (Outgoing)
| Target | Type |
|---|---|
| onLogUpdate | calls |
| render | calls |
| click | dynamic_call |
| input | dynamic_call |
| change | dynamic_call |
Impact (Incoming)
| Source | Type |
|---|---|
| initLogTab | uses |
| makeEntry | uses |
| LogTabViewProps | uses |