feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Expanded Logs / Formatting change

This commit is contained in:
Dave
2025-11-12 17:01:54 -05:00
parent 556cd993b9
commit 90f653c0b7
13 changed files with 444 additions and 254 deletions

View File

@@ -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 (
<Timeline
pending
reverse={true}
items={logs.map((log, idx) => ({
key: idx,
color: LogLevelHierarchy(log.level),
children: (
<Space wrap align="start" style={{}}>
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
<span>{dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
<Divider type="vertical" />
<span>{log.message}</span>
</Space>
)
}))}
/>
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: (
<Space direction="vertical" size={4} style={{ display: "flex" }}>
{/* Row 1: summary */}
<Space wrap align="start">
<Tag color={logLevelColor(level)}>{level}</Tag>
<span>{dayjs(timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
<Divider type="vertical" />
<span>{message}</span>
</Space>
{/* Row 2: details on a new line */}
{!isEmpty(meta) && (
<div style={{ marginLeft: 6 }}>
<details
open={openSet.has(idx)}
onToggle={(e) => {
const isOpen = e.currentTarget.open;
setOpenSet((prev) => {
const next = new Set(prev);
if (isOpen) next.add(idx);
else next.delete(idx);
return next;
});
}}
>
<summary>Details</summary>
<pre style={{ margin: "6px 0 0", maxWidth: 720, overflowX: "auto" }}>{safeStringify(meta, 2)}</pre>
</details>
</div>
)}
</Space>
)
};
}),
[logs, openSet]
);
return <Timeline pending reverse items={items} />;
}
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);
}
}