feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Expanded Logs / Formatting change
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
<Select.Option key="WARN">WARN</Select.Option>
|
||||
<Select.Option key="ERROR">ERROR</Select.Option>
|
||||
</Select>
|
||||
<Button onClick={toggleDetailsAll}>{detailsOpen ? "Collapse All" : "Expand All"}</Button>
|
||||
<Button onClick={() => setLogs([])}>Clear Logs</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -409,7 +425,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<DmsLogEvents socket={socket} logs={logs} />
|
||||
<DmsLogEvents logs={logs} detailsOpen={detailsOpen} detailsNonce={detailsNonce} />
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
Reference in New Issue
Block a user