diff --git a/client/src/components/dms-log-events/dms-log-events.component.jsx b/client/src/components/dms-log-events/dms-log-events.component.jsx index 5219ddd88..fef6c5e91 100644 --- a/client/src/components/dms-log-events/dms-log-events.component.jsx +++ b/client/src/components/dms-log-events/dms-log-events.component.jsx @@ -1,4 +1,5 @@ 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"; @@ -13,38 +14,116 @@ const mapDispatchToProps = (dispatch) => ({ export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents); -export function DmsLogEvents({ logs }) { - return ( - ({ - key: idx, - color: LogLevelHierarchy(log.level), - children: ( - - {log.level} - {dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")} - - {log.message} - - ) - }))} - /> +export function DmsLogEvents({ logs, detailsOpen, detailsNonce }) { + const [openSet, setOpenSet] = useState(() => new Set()); + + // 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; // prop optional for compatibility + const len = (logs || []).length; + if (detailsOpen) { + setOpenSet(new Set(Array.from({ length: len }, (_, i) => i))); // expand all + } else { + setOpenSet(new Set()); // collapse all + } + }, [detailsNonce, detailsOpen, logs?.length]); + + const items = useMemo( + () => + (logs || []).map((raw, idx) => { + const { level, message, timestamp, meta } = normalizeLog(raw); + + return { + key: idx, + color: logLevelColor(level), + children: ( + + {/* Row 1: summary */} + + {level} + {dayjs(timestamp).format("MM/DD/YYYY HH:mm:ss")} + + {message} + + + {/* Row 2: details on a new line */} + {!isEmpty(meta) && ( +
+
{ + const isOpen = e.currentTarget.open; + setOpenSet((prev) => { + const next = new Set(prev); + if (isOpen) next.add(idx); + else next.delete(idx); + return next; + }); + }} + > + Details +
{safeStringify(meta, 2)}
+
+
+ )} +
+ ) + }; + }), + [logs, openSet] ); + + return ; } -function LogLevelHierarchy(level) { - switch (level) { +/** Accepts both legacy shape and new "normalized" shape */ +function 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 }; +} + +function logLevelColor(level) { + switch ((level || "").toUpperCase()) { case "DEBUG": return "orange"; case "INFO": return "blue"; case "WARN": + case "WARNING": return "yellow"; case "ERROR": return "red"; default: - return 0; + return "default"; + } +} + +function 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; +} + +function safeStringify(obj, spaces = 2) { + try { + return JSON.stringify(obj, null, spaces); + } catch { + return String(obj); } } diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index 1d047eab8..ccb7629b3 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -1,4 +1,3 @@ -// DmsContainer updated import { useQuery } from "@apollo/client"; import { Button, Card, Col, Result, Row, Select, Space } from "antd"; import queryString from "query-string"; @@ -55,6 +54,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse const history = useNavigate(); const [logs, setLogs] = useState([]); const search = queryString.parse(useLocation().search); + const [detailsOpen, setDetailsOpen] = useState(false); // false => button shows "Expand All" + const [detailsNonce, setDetailsNonce] = useState(0); // forces child to react to toggles + const { jobId } = search; const notification = useNotification(); const { @@ -77,9 +79,14 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); + const logsRef = useRef(null); - // NEW: RR “open RO limit” UX hold + const toggleDetailsAll = () => { + setDetailsOpen((v) => !v); + setDetailsNonce((n) => n + 1); + }; + const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false); const clearRrOpenRoLimit = () => setRrOpenRoLimit(false); @@ -166,7 +173,16 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse notification.error({ message: err.message }); }; - const handleLogEvent = (payload) => setLogs((prev) => [...prev, payload]); + const handleLogEvent = (payload = {}) => { + const normalized = { + timestamp: payload.timestamp ? new Date(payload.timestamp) : payload.ts ? new Date(payload.ts) : new Date(), + level: (payload.level || "INFO").toUpperCase(), + message: payload.message || payload.msg || "", + // show details regardless of property name + meta: payload.meta ?? payload.ctx ?? payload.details ?? null + }; + setLogs((prev) => [...prev, normalized]); + }; // FINAL step only (emitted by server after rr-finalize-repair-order) const handleExportSuccess = (payload) => { @@ -379,7 +395,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse value={logLevel} onChange={(value) => { setLogLevel(value); - // Send to the active socket type if (dms === "rr" || Fortellis.treatment === "on") { wsssocket.emit("set-log-level", value); } else { @@ -392,6 +407,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse WARN ERROR +