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 fef6c5e91..34f6f7988 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
@@ -4,8 +4,11 @@ import dayjs from "../../utils/day";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
+import { selectDarkMode } from "../../redux/application/application.selectors.js";
-const mapStateToProps = createStructuredSelector({});
+const mapStateToProps = createStructuredSelector({
+ isDarkMode: selectDarkMode
+});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -14,9 +17,26 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
-export function DmsLogEvents({ logs, detailsOpen, detailsNonce }) {
+export function DmsLogEvents({ logs, detailsOpen, detailsNonce, isDarkMode, colorizeJson = false }) {
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;
@@ -29,65 +49,74 @@ export function DmsLogEvents({ logs, detailsOpen, detailsNonce }) {
// Respond to global toggle button
useEffect(() => {
- if (detailsNonce == null) return; // prop optional for compatibility
+ if (detailsNonce == null) return;
const len = (logs || []).length;
- if (detailsOpen) {
- setOpenSet(new Set(Array.from({ length: len }, (_, i) => i))); // expand all
- } else {
- setOpenSet(new Set()); // collapse all
- }
+ 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);
+ const hasMeta = !isEmpty(meta);
+ const isOpen = openSet.has(idx);
return {
key: idx,
color: logLevelColor(level),
children: (
Details
- {safeStringify(meta, 2)}
-
{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}`;
+ }
+ );
+};
diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx
index ccb7629b3..80c6a5e68 100644
--- a/client/src/pages/dms/dms.container.jsx
+++ b/client/src/pages/dms/dms.container.jsx
@@ -1,29 +1,35 @@
-import { useQuery } from "@apollo/client";
-import { Button, Card, Col, Result, Row, Select, Space } from "antd";
-import queryString from "query-string";
import { useEffect, useMemo, useRef, useState } from "react";
-import { useTranslation } from "react-i18next";
-import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
+import { connect } from "react-redux";
+import { useTranslation } from "react-i18next";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
-import AlertComponent from "../../components/alert/alert.component";
-import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
-import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
-import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
-import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
-import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
-import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
+import queryString from "query-string";
+import { useQuery } from "@apollo/client";
+import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd";
+import { useSplitTreatments } from "@splitsoftware/splitio-react";
+
import { auth } from "../../firebase/firebase.utils";
+
+import { useSocket } from "../../contexts/SocketIO/useSocket.js";
+import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
+
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
-import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
+
import { selectBodyshop } from "../../redux/user/user.selectors";
+import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
+
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
-import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
-import { useSplitTreatments } from "@splitsoftware/splitio-react";
-import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { determineDmsType } from "../../utils/determineDmsType";
+import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
+
+import AlertComponent from "../../components/alert/alert.component";
+import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
+import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
+import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
+import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
+import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -50,15 +56,19 @@ export const socket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_A
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
const dms = determineDmsType(bodyshop);
- const [logLevel, setLogLevel] = useState(dms === "pbs" ? "INFO" : "DEBUG");
+
const history = useNavigate();
- const [logs, setLogs] = useState([]);
const search = queryString.parse(useLocation().search);
+
+ const [logLevel, setLogLevel] = useState(dms === "pbs" ? "INFO" : "DEBUG");
+ const [logs, setLogs] = useState([]);
const [detailsOpen, setDetailsOpen] = useState(false); // false => button shows "Expand All"
const [detailsNonce, setDetailsNonce] = useState(0); // forces child to react to toggles
+ const [colorizeJson, setColorizeJson] = useState(false); // default: OFF
const { jobId } = search;
const notification = useNotification();
+
const {
treatments: { Fortellis }
} = useSplitTreatments({
@@ -66,9 +76,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
names: ["Fortellis"],
splitKey: bodyshop.imexshopid
});
+
// New unified wss socket (Fortellis, RR)
const { socket: wsssocket } = useSocket();
-
const activeSocket = useMemo(() => {
return dms === "rr" || (dms === "cdk" && Fortellis.treatment === "on") ? wsssocket : socket;
}, [dms, Fortellis.treatment, wsssocket, socket]);
@@ -162,6 +172,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
if (dms === "rr") {
// set log level on connect and immediately
wsssocket.emit("set-log-level", logLevel);
+
const handleConnect = () => wsssocket.emit("set-log-level", logLevel);
const handleReconnect = () =>
setLogs((prev) => [
@@ -371,6 +382,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
jobId={jobId}
/>
+