diff --git a/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx b/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx index b182ea39..2e5aedb3 100644 --- a/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx +++ b/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx @@ -22,6 +22,8 @@ import { Remove as RemoveIcon, Cached as CachedIcon, PlayArrow as PlayIcon, + CheckCircle as CheckCircleIcon, + HighlightOff as HighlightOffIcon, } from "@mui/icons-material"; import { AgGridReact } from "ag-grid-react"; import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community"; @@ -106,6 +108,20 @@ const buildEmptyGroup = (joinWith = null) => ({ conditions: [buildEmptyCondition()], }); +const statusTokenTheme = { + Online: { background: "rgba(74,222,128,0.18)", color: "#bbf7d0", icon: CheckCircleIcon }, + Offline: { background: "rgba(239,68,68,0.18)", color: "#fecdd3", icon: HighlightOffIcon }, + default: { background: "rgba(148,163,184,0.2)", color: "#e2e8f0", icon: HighlightOffIcon }, +}; + +const OS_ICON_MAP = { + windows: "fab fa-windows", + linux: "fab fa-linux", + mac: "fab fa-apple", +}; + +const AUTO_SIZE_COLUMNS = ["status", "site", "hostname", "description", "type", "os"]; + const resolveApplyAll = (filter) => Boolean(filter?.applyToAllSites ?? filter?.apply_to_all_sites); const resolveLastEdited = (filter) => @@ -117,10 +133,28 @@ const resolveSiteScope = (filter) => { return normalized === "scoped" ? "scoped" : "global"; }; -const resolveGroups = (filter) => { - const candidate = filter?.groups || filter?.raw?.groups; - if (candidate && Array.isArray(candidate) && candidate.length) return candidate; - return [buildEmptyGroup()]; +const normalizeGroupsForUI = (rawGroups) => { + if (!Array.isArray(rawGroups) || !rawGroups.length) { + return [buildEmptyGroup()]; + } + return rawGroups.map((g, gIdx) => { + const groupId = g.id || genId("group"); + const conditions = Array.isArray(g.conditions) ? g.conditions : []; + const normalizedConditions = conditions.length + ? conditions.map((c, cIdx) => ({ + id: c.id || genId("condition"), + field: c.field || DEVICE_FIELDS[0].value, + operator: c.operator || "contains", + value: c.value ?? "", + joinWith: cIdx === 0 ? null : c.joinWith || "AND", + })) + : [buildEmptyCondition()]; + return { + id: groupId, + joinWith: gIdx === 0 ? null : g.joinWith || "OR", + conditions: normalizedConditions, + }; + }); }; export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) { @@ -129,7 +163,7 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) const [scope, setScope] = useState(initialScope === "scoped" ? "site" : "global"); const [applyToAllSites, setApplyToAllSites] = useState(initialScope !== "scoped"); const [targetSite, setTargetSite] = useState(initialFilter?.site || initialFilter?.siteName || ""); - const [groups, setGroups] = useState(resolveGroups(initialFilter)); + const [groups, setGroups] = useState(normalizeGroupsForUI(initialFilter?.groups || initialFilter?.raw?.groups)); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [sites, setSites] = useState([]); @@ -164,7 +198,7 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) setScope(resolvedScope === "scoped" ? "site" : "global"); setApplyToAllSites(resolvedScope !== "scoped"); setTargetSite(filter?.site || filter?.site_scope || filter?.siteName || filter?.site_name || ""); - setGroups(resolveGroups(filter)); + setGroups(normalizeGroupsForUI(filter?.groups || filter?.raw?.groups)); setLastEditedTs(resolveLastEdited(filter)); }, []); @@ -205,6 +239,8 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) return device.hostname || summary.hostname || ""; case "description": return device.description || summary.description || ""; + case "os": + return device.os || summary.os || summary.operating_system || ""; case "type": return device.type || summary.type || summary.device_type || device.device_type || ""; default: @@ -292,6 +328,8 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) hostname: getDeviceField(d, "hostname"), description: getDeviceField(d, "description"), type: getDeviceField(d, "type"), + os: getDeviceField(d, "os"), + raw: d, })); setPreviewRows(rows); setPreviewAppliedAt(new Date()); @@ -306,11 +344,92 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) const previewColumns = useMemo( () => [ - { field: "status", headerName: "Status", minWidth: 110, cellClass: "auto-col-tight" }, + { + field: "status", + headerName: "Status", + minWidth: 120, + cellRenderer: (params) => { + const status = params.value || ""; + const theme = statusTokenTheme[status] || statusTokenTheme.default; + const Icon = theme.icon || CheckCircleIcon; + return ( + + + {status || "Unknown"} + + ); + }, + cellClass: "auto-col-tight", + }, { field: "site", headerName: "Site", minWidth: 140, cellClass: "auto-col-tight" }, - { field: "hostname", headerName: "Hostname", minWidth: 160, cellClass: "auto-col-tight" }, - { field: "description", headerName: "Description", minWidth: 200, cellClass: "auto-col-tight" }, + { + field: "hostname", + headerName: "Hostname", + minWidth: 180, + cellClass: "auto-col-tight", + cellRenderer: (params) => { + const name = params.value || ""; + const raw = params.data?.raw; + const device = raw || {}; + const deviceId = + device?.agent_guid || + device?.agent_id || + device?.id || + device?.hostname || + device?.summary?.agent_guid || + device?.summary?.hostname; + const href = deviceId ? `/device/${encodeURIComponent(deviceId)}` : "#"; + return ( + { + if (href === "#") return; + e.preventDefault(); + window.history.pushState({}, "", href); + window.dispatchEvent(new PopStateEvent("popstate")); + }} + style={{ color: "#7dd3fc", textDecoration: "none", fontWeight: 600 }} + > + {name} + + ); + }, + }, + { field: "description", headerName: "Description", minWidth: 220, cellClass: "auto-col-tight" }, { field: "type", headerName: "Device Type", minWidth: 140, cellClass: "auto-col-tight" }, + { + field: "os", + headerName: "OS", + minWidth: 170, + cellClass: "auto-col-tight", + cellRenderer: (params) => { + const value = params.value || ""; + const key = String(value || "").toLowerCase(); + const iconClass = + OS_ICON_MAP[key] || + (key.includes("mac") || key.includes("os x") ? OS_ICON_MAP.mac : key.includes("win") ? OS_ICON_MAP.windows : key.includes("linux") ? OS_ICON_MAP.linux : null); + return ( + + {iconClass ? : null} + {value || "Unknown"} + + ); + }, + }, ], [] ); @@ -955,12 +1074,12 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved })