createLogViewer function presentation exported ✓ 99.1%

Last updated: 2026-03-02T13:35:57.087Z

Metrics

LOC: 240 Complexity: 20 Params: 1 Coverage: 99.1% (107/108 lines, 31x executed)

Signature

createLogViewer(options: LogViewerOptions): : LogViewer

Architecture violations

View all

  • [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)

graph LR createLogViewer["createLogViewer"] onLogUpdate["onLogUpdate"] render["render"] createLogViewer -->|calls| onLogUpdate createLogViewer -->|calls| render style createLogViewer fill:#dbeafe,stroke:#2563eb,stroke-width:2px click createLogViewer "cc8b77ecd2282660.html" click onLogUpdate "86082b7c08e3acd5.html" click render "80ece8fdb820f2e0.html"
TargetType
onLogUpdate calls
render calls
click dynamic_call
input dynamic_call
change dynamic_call

Impact (Incoming)

graph LR createLogViewer["createLogViewer"] initLogTab["initLogTab"] makeEntry["makeEntry"] LogTabViewProps["LogTabViewProps"] initLogTab -->|uses| createLogViewer makeEntry -->|uses| createLogViewer LogTabViewProps -->|uses| createLogViewer style createLogViewer fill:#dbeafe,stroke:#2563eb,stroke-width:2px click createLogViewer "cc8b77ecd2282660.html" click initLogTab "f0e7324e99722ddb.html" click makeEntry "c949813d0d804166.html" click LogTabViewProps "7b7bdc48e67998ca.html"
SourceType
initLogTab uses
makeEntry uses
LogTabViewProps uses