import { Divider, Space, Tag, Timeline } from "antd"; import { useEffect, useMemo, useState } from "react"; import dayjs from "../../utils/day"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectDarkMode } from "../../redux/application/application.selectors.js"; const mapStateToProps = createStructuredSelector({ isDarkMode: selectDarkMode }); const mapDispatchToProps = () => ({}); export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents); export function DmsLogEvents({ logs, detailsOpen, detailsNonce, isDarkMode, colorizeJson = false, showDetails = true }) { const [openSet, setOpenSet] = useState(() => new Set()); // Inject JSON highlight styles once (only when colorize is enabled) useEffect(() => { if (!colorizeJson) return; if (typeof document === "undefined") return; if (document.getElementById("json-highlight-styles")) return; const style = document.createElement("style"); style.id = "json-highlight-styles"; style.textContent = ` .json-key { color: #fa8c16; } .json-string { color: #52c41a; } .json-number { color: #722ed1; } .json-boolean { color: #1890ff; } .json-null { color: #faad14; } `; document.head.appendChild(style); }, [colorizeJson]); // Trim openSet if logs shrink useEffect(() => { const len = (logs || []).length; setOpenSet((prev) => { const next = new Set(); for (let i = 0; i < len; i++) if (prev.has(i)) next.add(i); return next; }); }, [logs?.length]); // Respond to global toggle button useEffect(() => { if (detailsNonce == null) return; const len = (logs || []).length; setOpenSet(detailsOpen ? new Set(Array.from({ length: len }, (_, i) => i)) : new Set()); }, [detailsNonce, detailsOpen, logs?.length]); const items = useMemo( () => (logs || []).map((raw, idx) => { const { level, message, timestamp, meta } = normalizeLog(raw); // Only treat meta as "present" when we are allowed to show details const hasMeta = !isEmpty(meta) && showDetails; const isOpen = hasMeta && openSet.has(idx); return { key: idx, color: logLevelColor(level), children: ( {/* Row 1: summary + inline "Details" toggle */} {level} {dayjs(timestamp).format("MM/DD/YYYY HH:mm:ss")} {message} {hasMeta && ( <> setOpenSet((prev) => { const next = new Set(prev); if (isOpen) next.delete(idx); else next.add(idx); return next; }) } style={{ cursor: "pointer", userSelect: "none" }} > {isOpen ? "Hide details" : "Details"} )} {/* Row 2: details body (only when open) */} {hasMeta && isOpen && (
)}
) }; }), [logs, openSet, colorizeJson, isDarkMode, showDetails] ); return ; } /** * Normalize various log input formats into a standard structure. * @param input * @returns {{level: string, message: *|string, timestamp: Date, meta: *}} */ const normalizeLog = (input) => { const n = input?.normalized || input || {}; const level = (n.level || input?.level || "INFO").toString().toUpperCase(); const message = n.message ?? input?.message ?? ""; const meta = input?.meta != null ? input.meta : n.meta != null ? n.meta : undefined; const tsRaw = input?.timestamp ?? n.timestamp ?? input?.ts ?? Date.now(); const timestamp = typeof tsRaw === "number" ? new Date(tsRaw) : new Date(tsRaw); return { level, message, timestamp, meta }; }; /** * Map log level to tag color. * @param level * @returns {string} */ const logLevelColor = (level) => { switch ((level || "").toUpperCase()) { case "SILLY": return "purple"; case "DEBUG": return "orange"; case "INFO": return "blue"; case "WARN": case "WARNING": return "yellow"; case "ERROR": return "red"; default: return "default"; } }; /** * Check if a value is "empty" (null/undefined, empty array, or empty object). * @param v */ const isEmpty = (v) => { if (v == null) return true; if (Array.isArray(v)) return v.length === 0; if (typeof v === "object") return Object.keys(v).length === 0; return false; }; /** * Safely stringify an object to JSON, falling back to String() on failure. * @param obj * @param spaces * @returns {string} */ const safeStringify = (obj, spaces = 2) => { try { return JSON.stringify(obj, null, spaces); } catch { return String(obj); } }; /** * JSON display block with optional syntax highlighting. * @param data * @param colorize * @param isDarkMode * @returns {JSX.Element} * @constructor */ const JsonBlock = ({ data, colorize, isDarkMode }) => { const jsonText = safeStringify(data, 2); const preStyle = { margin: "6px 0 0", maxWidth: 720, overflowX: "auto", fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', fontSize: 12, lineHeight: 1.45, padding: 8, borderRadius: 6, background: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.04)", border: isDarkMode ? "1px solid rgba(255,255,255,0.12)" : "1px solid rgba(0,0,0,0.08)", color: isDarkMode ? "var(--card-text-fallback)" : "#141414" }; if (colorize) { const html = syntaxHighlight(jsonText); return
;
  }
  return 
{jsonText}
; }; /** * Syntax highlight JSON text for HTML display. * @param jsonText * @returns {*} */ const syntaxHighlight = (jsonText) => { const esc = jsonText.replace(/&/g, "&").replace(//g, ">"); return esc.replace( /("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, (match) => { let cls = "json-number"; if (match.startsWith('"')) { cls = match.endsWith(":") ? "json-key" : "json-string"; } else if (match === "true" || match === "false") { cls = "json-boolean"; } else if (match === "null") { cls = "json-null"; } return `${match}`; } ); };