diff --git a/Data/Engine/services/API/tokens/routes.py b/Data/Engine/services/API/tokens/routes.py index 41863101..7a78121e 100644 --- a/Data/Engine/services/API/tokens/routes.py +++ b/Data/Engine/services/API/tokens/routes.py @@ -33,6 +33,9 @@ def register( def _hash_token(token: str) -> str: return hashlib.sha256(token.encode("utf-8")).hexdigest() + def _iso(dt: datetime) -> str: + return dt.astimezone(timezone.utc).isoformat() + def _iso_now() -> str: return datetime.now(tz=timezone.utc).isoformat() 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 2e5aedb3..3be13e35 100644 --- a/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx +++ b/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx @@ -120,6 +120,8 @@ const OS_ICON_MAP = { mac: "fab fa-apple", }; +const TAB_HOVER_GRADIENT = "linear-gradient(120deg, rgba(125,211,252,0.18), rgba(192,132,252,0.22))"; + const AUTO_SIZE_COLUMNS = ["status", "site", "hostname", "description", "type", "os"]; const resolveApplyAll = (filter) => Boolean(filter?.applyToAllSites ?? filter?.apply_to_all_sites); @@ -127,6 +129,45 @@ const resolveApplyAll = (filter) => Boolean(filter?.applyToAllSites ?? filter?.a const resolveLastEdited = (filter) => filter?.lastEdited || filter?.last_edited || filter?.updated_at || filter?.updated || null; +const resolveLastEditedBy = (filter) => { + const candidate = + filter?.last_edited_by_username || + filter?.last_edited_by_name || + filter?.last_edited_by || + filter?.lastEditedBy || + filter?.last_editor || + filter?.lastEditor || + filter?.updated_by || + filter?.updatedBy || + filter?.owner || + filter?.user || + filter?.modified_by; + if (candidate && typeof candidate === "object") { + if (candidate.name) return candidate.name; + if (candidate.username) return candidate.username; + if (candidate.user) return candidate.user; + } + if (typeof candidate === "string" && candidate.trim()) return candidate.trim(); + return "Unknown"; +}; + +const formatLastEditedLabel = (ts, user) => { + if (!ts) return ""; + const date = new Date(ts); + if (Number.isNaN(date.getTime())) return ""; + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const year = date.getFullYear(); + let hours = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const suffix = hours >= 12 ? "PM" : "AM"; + hours = hours % 12 || 12; + const datePart = `${month}/${day}/${year}`; + const timePart = `${hours}:${minutes}${suffix}`; + const editor = user && typeof user === "string" && user.trim() ? user : "Unknown"; + return `Last edited by ${editor} @ ${datePart} @ ${timePart}`; +}; + const resolveSiteScope = (filter) => { const raw = filter?.site_scope || filter?.siteScope || filter?.scope || filter?.type; const normalized = String(raw || "").toLowerCase(); @@ -169,6 +210,7 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) const [sites, setSites] = useState([]); const [loadingSites, setLoadingSites] = useState(false); const [lastEditedTs, setLastEditedTs] = useState(resolveLastEdited(initialFilter)); + const [lastEditedBy, setLastEditedBy] = useState(resolveLastEditedBy(initialFilter)); const [loadingFilter, setLoadingFilter] = useState(false); const [loadError, setLoadError] = useState(null); const [previewRows, setPreviewRows] = useState([]); @@ -200,6 +242,7 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) setTargetSite(filter?.site || filter?.site_scope || filter?.siteName || filter?.site_name || ""); setGroups(normalizeGroupsForUI(filter?.groups || filter?.raw?.groups)); setLastEditedTs(resolveLastEdited(filter)); + setLastEditedBy(resolveLastEditedBy(filter)); }, []); useEffect(() => { @@ -597,17 +640,17 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) key={condition.id} sx={{ display: "grid", - gridTemplateColumns: "120px 220px 220px 1fr auto", - gap: 1, + gridTemplateColumns: "110px 220px 220px 1fr auto", + gap: 0.5, alignItems: "center", background: "rgba(12,18,35,0.7)", border: `1px solid ${AURORA_SHELL.border}`, borderRadius: 2, - px: 1.5, + px: 1.25, py: 1, }} > - + {!isFirst && ( {lastEditedTs && ( - Last edited {new Date(lastEditedTs).toLocaleString()} + {formatLastEditedLabel(lastEditedTs, lastEditedBy)} )} @@ -800,83 +847,70 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) ) : null} - - Name + + Name setName(e.target.value)} placeholder="Filter name or convention (e.g., RMM targeting)" sx={{ + width: { xs: "100%", md: "50%" }, + maxWidth: 420, "& .MuiInputBase-root": { backgroundColor: "rgba(4,7,17,0.65)" }, "& .MuiOutlinedInput-notchedOutline": { borderColor: AURORA_SHELL.border }, }} /> - - - - Scope - - Choose whether this filter is global or pinned to a specific site. - - - { - if (!val) return; - setScope(val); - }} - color="info" - sx={{ - background: "rgba(7,12,26,0.8)", - borderRadius: 2, - "& .MuiToggleButton-root": { - textTransform: "none", - color: AURORA_SHELL.text, - borderColor: "rgba(148,163,184,0.4)", - }, - "& .Mui-selected": { - background: "linear-gradient(135deg, rgba(125,211,252,0.24), rgba(192,132,252,0.22))", - color: "#0b1220", - }, - }} - > - Global - Site - - + + Scope + + Choose whether this filter is global or pinned to a specific site. + + { + if (!val) return; + setScope(val); + }} + color="info" + sx={{ + alignSelf: "flex-start", + background: "rgba(7,12,26,0.7)", + borderRadius: 2, + "& .MuiToggleButton-root": { + textTransform: "none", + color: AURORA_SHELL.text, + borderColor: "rgba(148,163,184,0.35)", + minHeight: 32, + paddingTop: 0.25, + paddingBottom: 0.25, + paddingLeft: 1.6, + paddingRight: 1.6, + fontWeight: 700, + }, + "& .Mui-selected": { + background: TAB_HOVER_GRADIENT, + color: "#0b1220", + boxShadow: "0 0 0 1px rgba(148,163,184,0.35) inset", + }, + }} + > + Global + Site + {scope === "site" && ( - + - + Criteria @@ -1034,24 +1058,12 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) - + Results - Apply criteria to preview matching devices (20 per page). + Apply criteria to preview matching devices. {previewAppliedAt && ( diff --git a/Data/Engine/web-interface/src/Devices/Filters/Filter_List.jsx b/Data/Engine/web-interface/src/Devices/Filters/Filter_List.jsx index d9e862e8..8f36d8ce 100644 --- a/Data/Engine/web-interface/src/Devices/Filters/Filter_List.jsx +++ b/Data/Engine/web-interface/src/Devices/Filters/Filter_List.jsx @@ -7,13 +7,11 @@ import { IconButton, Stack, Tooltip, - Chip, } from "@mui/material"; import { FilterAlt as HeaderIcon, Cached as CachedIcon, Add as AddIcon, - OpenInNew as DetailsIcon, } from "@mui/icons-material"; import { AgGridReact } from "ag-grid-react"; import { ModuleRegistry, AllCommunityModule, themeQuartz } from "ag-grid-community"; @@ -55,6 +53,20 @@ const gradientButtonSx = { }; const AUTO_SIZE_COLUMNS = ["name", "type", "deviceCount", "site", "lastEditedBy", "lastEdited"]; +const FILTER_TYPE_META = { + global: { + label: "Global", + textColor: "#8fdaa2", + backgroundColor: "rgba(56,161,105,0.16)", + borderColor: "rgba(56,161,105,0.4)", + }, + site: { + label: "Site-Scoped", + textColor: "#8ab4ff", + backgroundColor: "rgba(125,180,255,0.16)", + borderColor: "rgba(125,180,255,0.42)", + }, +}; const SAMPLE_ROWS = [ { @@ -84,6 +96,55 @@ function formatTimestamp(ts) { return date.toLocaleString(); } +function resolveLastEditor(filter) { + const candidate = + filter?.last_edited_by_username || + filter?.last_edited_by_name || + filter?.last_edited_by || + filter?.lastEditedBy || + filter?.last_editor || + filter?.lastEditor || + filter?.updated_by || + filter?.updatedBy || + filter?.owner || + filter?.user || + filter?.modified_by; + if (candidate && typeof candidate === "object") { + if (candidate.name) return candidate.name; + if (candidate.username) return candidate.username; + if (candidate.user) return candidate.user; + } + if (typeof candidate === "string" && candidate.trim()) return candidate.trim(); + return "Unknown"; +} + +function FilterTypePill({ type }) { + const key = String(type || "").toLowerCase() === "site" ? "site" : "global"; + const meta = FILTER_TYPE_META[key]; + return ( + + {meta.label} + + ); +} + function normalizeFilters(raw) { if (!Array.isArray(raw)) return []; return raw.map((f, idx) => ({ @@ -91,7 +152,7 @@ function normalizeFilters(raw) { name: f.name || f.title || "Unnamed Filter", type: (f.site_scope || f.scope || f.type || "global") === "scoped" ? "site" : "global", site: f.site || f.site_scope || f.site_name || f.target_site || null, - lastEditedBy: f.last_edited_by || f.owner || f.updated_by || "Unknown", + lastEditedBy: resolveLastEditor(f), lastEdited: f.last_edited || f.updated_at || f.updated || f.created_at || null, deviceCount: typeof f.matching_device_count === "number" @@ -196,11 +257,10 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh { headerName: "Type", field: "type", - width: 120, + minWidth: 150, cellRenderer: (params) => { const type = String(params.value || "").toLowerCase() === "site" ? "Site" : "Global"; - const color = type === "Global" ? "success" : "info"; - return ; + return ; }, cellClass: "auto-col-tight", }, @@ -236,31 +296,8 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh headerName: "Last Edited", field: "lastEdited", minWidth: 180, - valueFormatter: (params) => formatTimestamp(params.value), - cellClass: "auto-col-tight", - }, - { - headerName: "Details", - field: "details", - width: 120, - minWidth: 140, flex: 1, - cellRenderer: (params) => ( - onEditFilter?.(params.data)} - sx={{ - color: "#7dd3fc", - border: "1px solid rgba(148,163,184,0.4)", - borderRadius: 1.5, - backgroundColor: "rgba(255,255,255,0.03)", - "&:hover": { backgroundColor: "rgba(125,183,255,0.12)" }, - }} - > - - - ), + valueFormatter: (params) => formatTimestamp(params.value), cellClass: "auto-col-tight", }, ]; @@ -273,6 +310,12 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh resizable: true, cellClass: "auto-col-tight", suppressMenu: true, + cellStyle: { + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + textAlign: "left", + }, }), [] ); @@ -281,11 +324,15 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh @@ -341,76 +388,84 @@ export default function DeviceFilterList({ onCreateFilter, onEditFilter, refresh - - Filters - - {loading ? "Loading…" : `${rows.length} filter${rows.length === 1 ? "" : "s"}`} - - - - {error ? ( - - {error} - - ) : null} - - - - + justifyContent: "flex-start", + textAlign: "left", + paddingTop: "8px", + paddingBottom: "8px", + paddingLeft: "18px", + paddingRight: "12px", + }, + "& .ag-center-cols-container .ag-cell .ag-cell-wrapper, & .ag-pinned-left-cols-container .ag-cell .ag-cell-wrapper, & .ag-pinned-right-cols-container .ag-cell .ag-cell-wrapper": { + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + padding: 0, + }, + "& .ag-center-cols-container .ag-cell .ag-cell-value, & .ag-pinned-left-cols-container .ag-cell .ag-cell-value, & .ag-pinned-right-cols-container .ag-cell .ag-cell-value": { + width: "100%", + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + textAlign: "left", + }, + "& .ag-center-cols-container .ag-cell.auto-col-tight, & .ag-pinned-left-cols-container .ag-cell.auto-col-tight, & .ag-pinned-right-cols-container .ag-cell.auto-col-tight": { + paddingLeft: "12px", + paddingRight: "9px", + }, + "& .ag-center-cols-container .ag-cell.auto-col-tight .ag-cell-wrapper, & .ag-pinned-left-cols-container .ag-cell.auto-col-tight .ag-cell-wrapper, & .ag-pinned-right-cols-container .ag-cell.auto-col-tight .ag-cell-wrapper": { + justifyContent: "flex-start", + }, + "& .ag-center-cols-container .ag-cell.auto-col-tight .ag-cell-value, & .ag-pinned-left-cols-container .ag-cell.auto-col-tight .ag-cell-value, & .ag-pinned-right-cols-container .ag-cell.auto-col-tight .ag-cell-value": { + textAlign: "left", + justifyContent: "flex-start", + }, + }} + style={{ + "--ag-icon-font-family": iconFontFamily, + "--ag-background-color": "#070b1a", + "--ag-foreground-color": "#f4f7ff", + "--ag-header-background-color": "#0f172a", + "--ag-header-foreground-color": "#cfe0ff", + "--ag-odd-row-background-color": "rgba(255,255,255,0.02)", + "--ag-row-hover-color": "rgba(125,183,255,0.08)", + "--ag-selected-row-background-color": "rgba(64,164,255,0.18)", + "--ag-border-color": "rgba(125,183,255,0.18)", + "--ag-row-border-color": "rgba(125,183,255,0.14)", + "--ag-border-radius": "0px", + "--ag-checkbox-border-radius": "3px", + "--ag-checkbox-background-color": "rgba(255,255,255,0.06)", + "--ag-checkbox-border-color": "rgba(180,200,220,0.6)", + "--ag-checkbox-checked-color": "#7dd3fc", + }} + > + ); diff --git a/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx b/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx index 267f310e..000d857f 100644 --- a/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx +++ b/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx @@ -17,11 +17,6 @@ import { DialogTitle, DialogContent, DialogActions, - Table, - TableHead, - TableRow, - TableCell, - TableBody, GlobalStyles, CircularProgress } from "@mui/material"; @@ -422,21 +417,6 @@ const SCHEDULE_LABELS = { yearly: "Yearly cadence", }; -const TABLE_BASE_SX = { - "& .MuiTableCell-root": { - borderColor: "rgba(148,163,184,0.18)", - color: MAGIC_UI.textBright, - }, - "& .MuiTableHead-root .MuiTableCell-root": { - color: MAGIC_UI.textMuted, - fontWeight: 600, - backgroundColor: "rgba(8,12,24,0.7)", - }, - "& .MuiTableBody-root .MuiTableRow-root:hover": { - backgroundColor: "rgba(56,189,248,0.08)", - }, -}; - const hiddenHandleStyle = { width: 12, height: 12, @@ -1082,6 +1062,8 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic const [filterAnchorEl, setFilterAnchorEl] = useState(null); const [activeFilterColumn, setActiveFilterColumn] = useState(null); const [pendingFilterValue, setPendingFilterValue] = useState(""); + const devicePickerGridApiRef = useRef(null); + const filterPickerGridApiRef = useRef(null); const normalizeTarget = useCallback((rawTarget) => { if (!rawTarget) return null; @@ -1093,7 +1075,19 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic const rawKind = String(rawTarget.kind || "").toLowerCase(); if (rawKind === "device" || rawTarget.hostname) { const host = String(rawTarget.hostname || "").trim(); - return host ? { kind: "device", hostname: host } : null; + if (!host) return null; + const osValue = + rawTarget.os || + rawTarget.operating_system || + rawTarget.device_os || + rawTarget.platform || + rawTarget.agent_os || + rawTarget.system || + rawTarget.os_name || + (rawTarget.summary && (rawTarget.summary.os || rawTarget.summary.operating_system)) || + ""; + const siteValue = rawTarget.site || rawTarget.site_name || rawTarget.site_scope || rawTarget.siteScope || ""; + return { kind: "device", hostname: host, os: osValue, site: siteValue }; } if (rawKind === "filter" || rawTarget.filter_id != null || rawTarget.id != null) { const idValue = rawTarget.filter_id ?? rawTarget.id; @@ -1200,10 +1194,109 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic }, [targetKey] ); + const availableDeviceMap = useMemo(() => { + const map = new Map(); + availableDevices.forEach((device) => { + const key = String(device?.hostname || "").toLowerCase(); + if (!key) return; + map.set(key, device); + }); + return map; + }, [availableDevices]); + const devicePickerRows = useMemo(() => { + const query = deviceSearch.trim().toLowerCase(); + if (query.length < 3) return []; + return availableDevices + .filter((device) => { + const display = String(device?.display || device?.hostname || "").toLowerCase(); + return display.includes(query); + }) + .map((device, index) => ({ + id: String(device?.hostname || device?.display || `device-${index}`).toLowerCase(), + name: device?.display || device?.hostname || "Device", + status: device?.online ? "Online" : "Offline", + os: device?.os || "", + site: device?.site || "", + raw: device, + })); + }, [availableDevices, deviceSearch]); + const devicePickerOverlay = useMemo( + () => + deviceSearch.trim().length < 3 + ? "Type 3+ characters to search devices." + : "No devices match your search.", + [deviceSearch] + ); + const filterPickerRows = useMemo(() => { + const query = filterSearch.trim().toLowerCase(); + if (query.length < 3) return []; + return (filterCatalog || []) + .filter((f) => String(f?.name || "").toLowerCase().includes(query)) + .map((f, index) => ({ + id: String(f.id ?? f.filter_id ?? index), + name: f.name || `Filter ${index + 1}`, + deviceCount: typeof f.deviceCount === "number" ? f.deviceCount : f.devices_targeted ?? f.matching_device_count ?? null, + scope: (f.scope || f.site_scope || f.type) === "scoped" ? "Site" : "Global", + site: f.site || f.site_name || "", + raw: f, + })); + }, [filterCatalog, filterSearch]); + const filterPickerOverlay = useMemo( + () => + filterSearch.trim().length < 3 + ? "Type 3+ characters to search filters." + : "No filters match your search.", + [filterSearch] + ); + const deviceRowsMap = useMemo(() => { + const map = new Map(); + deviceRows.forEach((device) => { + const key = String(device?.hostname || device?.agent_hostname || device?.id || "").toLowerCase(); + if (!key) return; + const osValue = + device?.os || + device?.operating_system || + device?.agent_os || + device?.system || + device?.os_name || + (device?.summary && (device.summary.os || device.summary.operating_system)) || + ""; + const siteValue = device?.site || device?.site_name || device?.site_scope || device?.summary?.site || ""; + map.set(key, { ...device, os: osValue, site: siteValue }); + }); + return map; + }, [deviceRows]); const targetGridRows = useMemo(() => { + const resolveOs = (target) => { + if (!target || target.kind !== "device") return ""; + const explicit = + target.os || + target.operating_system || + target.device_os || + target.platform || + target.agent_os || + target.system || + target.os_name; + if (explicit) return explicit; + const key = String(target.hostname || "").toLowerCase(); + if (!key) return ""; + const fromAvailable = availableDeviceMap.get(key); + if (fromAvailable?.os) return fromAvailable.os; + const fromHistory = deviceRowsMap.get(key); + if (fromHistory?.os) return fromHistory.os; + return ""; + }; return targets.map((target) => { const key = targetKey(target) || `${target?.kind || "target"}-${Math.random().toString(36).slice(2, 8)}`; const isFilter = target?.kind === "filter"; + const siteLabel = isFilter + ? "N/A" + : target?.site || + target?.site_name || + (() => { + const key = String(target.hostname || "").toLowerCase(); + return availableDeviceMap.get(key)?.site || deviceRowsMap.get(key)?.site || ""; + })(); const deviceCount = typeof target?.deviceCount === "number" && Number.isFinite(target.deviceCount) ? target.deviceCount : null; const detailText = isFilter @@ -1211,25 +1304,45 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic target?.site_scope === "scoped" ? ` • ${target?.site || "Specific site"}` : "" }` : "—"; + const osLabel = isFilter ? "—" : resolveOs(target) || "Unknown"; return { id: key, typeLabel: isFilter ? "Filter" : "Device", + siteLabel, targetLabel: isFilter ? target?.name || `Filter #${target?.filter_id}` : target?.hostname, detailText, + osLabel, rawTarget: target, }; }); - }, [targets, targetKey]); + }, [targets, targetKey, availableDeviceMap, deviceRowsMap]); const targetGridColumnDefs = useMemo( () => [ - { field: "typeLabel", headerName: "Type", minWidth: 120, filter: "agTextColumnFilter" }, - { field: "targetLabel", headerName: "Target", minWidth: 200, flex: 1.1, filter: "agTextColumnFilter" }, - { field: "detailText", headerName: "Details", minWidth: 200, flex: 1.4, filter: "agTextColumnFilter" }, + { field: "typeLabel", headerName: "Type", minWidth: 120, flex: 0.9, filter: "agTextColumnFilter" }, + { + field: "siteLabel", + headerName: "Site", + minWidth: 160, + flex: 1, + filter: "agTextColumnFilter", + cellRenderer: (params) => { + const isFilter = params?.data?.typeLabel === "Filter"; + return ( + + {params.value || ""} + + ); + }, + cellClass: "auto-col-tight", + }, + { field: "targetLabel", headerName: "Target", minWidth: 200, flex: 1.2, filter: "agTextColumnFilter" }, + { field: "osLabel", headerName: "Operating System", minWidth: 170, flex: 1.1, filter: "agTextColumnFilter" }, + { field: "detailText", headerName: "Details", minWidth: 200, flex: 1, filter: "agTextColumnFilter" }, { field: "actions", headerName: "", - minWidth: 80, - maxWidth: 100, + minWidth: 100, + flex: 1.5, cellRenderer: "TargetActionsRenderer", sortable: false, suppressMenu: true, @@ -1241,19 +1354,21 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic const targetGridComponents = useMemo( () => ({ TargetActionsRenderer: (params) => ( - { - e.stopPropagation(); - params.context?.removeTarget?.(params.data?.rawTarget); - }} - sx={{ - color: "#fb7185", - "&:hover": { color: "#fecdd3" }, - }} - > - - + + { + e.stopPropagation(); + params.context?.removeTarget?.(params.data?.rawTarget); + }} + sx={{ + color: "#fb7185", + "&:hover": { color: "#fecdd3" }, + }} + > + + + ), }), [] @@ -1272,7 +1387,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic [] ); const targetGridApiRef = useRef(null); - const TARGET_AUTO_COLS = useRef(["typeLabel", "targetLabel", "detailText"]); + const TARGET_AUTO_COLS = useRef(["typeLabel", "targetLabel", "osLabel", "detailText"]); const handleTargetGridReady = useCallback((params) => { targetGridApiRef.current = params.api; requestAnimationFrame(() => { @@ -1290,6 +1405,116 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic }); }, [targetGridRows]); + const devicePickerSelectionCol = { + headerName: "", + field: "__select__", + width: 52, + maxWidth: 52, + checkboxSelection: true, + headerCheckboxSelection: true, + resizable: false, + sortable: false, + suppressMenu: true, + filter: false, + pinned: "left", + lockPosition: true, + }; + const devicePickerColumnDefs = useMemo( + () => [ + devicePickerSelectionCol, + { field: "site", headerName: "Site", minWidth: 160, flex: 1, cellClass: "auto-col-tight" }, + { field: "name", headerName: "Name", minWidth: 200, flex: 1.2, cellClass: "auto-col-tight" }, + { field: "status", headerName: "Status", minWidth: 140, flex: 0.9, cellClass: "auto-col-tight" }, + { field: "os", headerName: "OS", minWidth: 180, flex: 1.8, cellClass: "auto-col-tight" }, + ], + [] + ); + const devicePickerDefaultColDef = useMemo( + () => ({ + sortable: true, + filter: "agTextColumnFilter", + resizable: true, + flex: 1, + cellClass: "auto-col-tight", + cellStyle: LEFT_ALIGN_CELL_STYLE, + }), + [] + ); + const handleDevicePickerReady = useCallback((params) => { + devicePickerGridApiRef.current = params.api; + requestAnimationFrame(() => { + try { + params.api.autoSizeColumns(["name", "status", "os"], true); + } catch {} + }); + }, []); + useEffect(() => { + const api = devicePickerGridApiRef.current; + if (!api) return; + api.forEachNode((node) => { + const selected = !!selectedDeviceTargets[node?.data?.id]; + if (node.isSelected() !== selected) { + node.setSelected(selected); + } + }); + }, [selectedDeviceTargets, devicePickerRows]); + const handleDevicePickerSelectionChanged = useCallback(() => { + const api = devicePickerGridApiRef.current; + if (!api) return; + const next = {}; + api.getSelectedNodes().forEach((node) => { + if (node?.data?.id) next[node.data.id] = true; + }); + setSelectedDeviceTargets(next); + }, []); + + const filterPickerSelectionCol = { ...devicePickerSelectionCol }; + const filterPickerColumnDefs = useMemo( + () => [ + filterPickerSelectionCol, + { field: "name", headerName: "Filter", minWidth: 220, flex: 1.4, cellClass: "auto-col-tight" }, + { + field: "deviceCount", + headerName: "Devices", + minWidth: 140, + flex: 0.8, + valueFormatter: (params) => (typeof params.value === "number" ? params.value.toLocaleString() : "—"), + cellClass: "auto-col-tight", + }, + { field: "scope", headerName: "Scope", minWidth: 140, flex: 0.9, cellClass: "auto-col-tight" }, + { field: "site", headerName: "Site", minWidth: 160, flex: 1, cellClass: "auto-col-tight" }, + ], + [] + ); + const filterPickerDefaultColDef = devicePickerDefaultColDef; + const handleFilterPickerReady = useCallback((params) => { + filterPickerGridApiRef.current = params.api; + requestAnimationFrame(() => { + try { + params.api.autoSizeColumns(["name", "deviceCount", "scope", "site"], true); + } catch {} + }); + }, []); + useEffect(() => { + const api = filterPickerGridApiRef.current; + if (!api) return; + api.forEachNode((node) => { + const selected = !!selectedFilterTargets[node?.data?.id]; + if (node.isSelected() !== selected) { + node.setSelected(selected); + } + }); + }, [selectedFilterTargets, filterPickerRows]); + const handleFilterPickerSelectionChanged = useCallback(() => { + const api = filterPickerGridApiRef.current; + if (!api) return; + const next = {}; + api.getSelectedNodes().forEach((node) => { + if (node?.data?.id) next[node.data.id] = true; + }); + setSelectedFilterTargets(next); + }, []); + useEffect(() => { setTargets((prev) => { let changed = false; @@ -2401,7 +2626,17 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic const list = Object.values(data || {}).map((a) => ({ hostname: a.hostname || a.agent_hostname || a.id || "unknown", display: a.hostname || a.agent_hostname || a.id || "unknown", - online: !!a.collector_active + online: !!a.collector_active, + site: a.site || a.site_name || a.site_scope || a.group || "", + os: + a.os || + a.operating_system || + a.platform || + a.agent_os || + a.system || + a.os_name || + (a.summary && (a.summary.os || a.summary.operating_system)) || + "", })); list.sort((a, b) => a.display.localeCompare(b.display)); setAvailableDevices(list); @@ -2729,18 +2964,19 @@ const heroTiles = useMemo(() => { {tab === 0 && ( - handleJobNameInputChange(e.target.value)} - onBlur={(e) => setPageTitleJobName(e.target.value.trim())} - InputLabelProps={{ shrink: true }} - error={jobName.trim().length === 0} - helperText={jobName.trim().length === 0 ? "Job name is required" : ""} - /> - + handleJobNameInputChange(e.target.value)} + onBlur={(e) => setPageTitleJobName(e.target.value.trim())} + InputLabelProps={{ shrink: true }} + error={jobName.trim().length === 0} + helperText={jobName.trim().length === 0 ? "Job name is required" : ""} + inputProps={{ sx: { py: 0.9 } }} + /> + )} {tab === 1 && ( @@ -2782,7 +3018,7 @@ const heroTiles = useMemo(() => { )} {tab === 2 && ( - + { } /> - + { open={addCompOpen} onClose={() => setAddCompOpen(false)} fullWidth - maxWidth="md" + maxWidth={false} PaperProps={{ sx: { background: MAGIC_UI.panelBg, color: MAGIC_UI.textBright, border: `1px solid ${MAGIC_UI.panelBorder}`, boxShadow: MAGIC_UI.glow, + width: "95vw", + maxWidth: "95vw", + height: "95vh", + maxHeight: "95vh", }, }} > Select an Assembly - + { }} > - + - + setAssemblyFilterText(e.target.value)} - sx={{ minWidth: 220, ...INPUT_FIELD_SX }} + sx={{ minWidth: 352, maxWidth: 520, ...INPUT_FIELD_SX }} /> - loadAssemblies()} - sx={{ - color: MAGIC_UI.accentA, - border: `1px solid ${MAGIC_UI.panelBorder}`, - borderRadius: 2, - width: 38, - height: 38, - }} - > - - {assembliesError ? ( @@ -3304,7 +3540,16 @@ const heroTiles = useMemo(() => { Loading assemblies… )} - + { - {targetPickerTab === "devices" ? ( - <> - + {targetPickerTab === "devices" ? ( + <> + setDeviceSearch(e.target.value)} sx={{ flex: 1, ...INPUT_FIELD_SX }} + helperText={deviceSearch.trim().length < 3 ? "Type at least 3 characters to search devices." : ""} + FormHelperTextProps={{ sx: { color: MAGIC_UI.textMuted } }} /> - - - - - Name - Status - - - - {availableDevices - .filter((d) => d.display.toLowerCase().includes(deviceSearch.toLowerCase())) - .map((d) => ( - setSelectedDeviceTargets((prev) => ({ ...prev, [d.hostname]: !prev[d.hostname] }))}> - - { - e.stopPropagation(); - setSelectedDeviceTargets((prev) => ({ ...prev, [d.hostname]: e.target.checked })); - }} - sx={{ - color: MAGIC_UI.accentA, - "&.Mui-checked": { color: MAGIC_UI.accentB }, - }} - /> - - {d.display} - - - {d.online ? "Online" : "Offline"} - - - ))} - {availableDevices.length === 0 && ( - - - No devices available. - - - )} - -
+ + ${devicePickerOverlay}`} + getRowId={(params) => params.data?.id || params.rowIndex} + onGridReady={handleDevicePickerReady} + onSelectionChanged={handleDevicePickerSelectionChanged} + pagination + paginationPageSize={20} + paginationPageSizeSelector={[20, 50, 100]} + theme={gridTheme} + style={{ width: "100%", height: "100%", fontFamily: gridFontFamily, "--ag-icon-font-family": iconFontFamily }} + /> + - ) : ( - <> - + ) : ( + <> + setFilterSearch(e.target.value)} sx={{ flex: 1, ...INPUT_FIELD_SX }} + helperText={filterSearch.trim().length < 3 ? "Type at least 3 characters to search filters." : ""} + FormHelperTextProps={{ sx: { color: MAGIC_UI.textMuted } }} + /> + + + ${filterPickerOverlay}`} + getRowId={(params) => params.data?.id || params.rowIndex} + onGridReady={handleFilterPickerReady} + onSelectionChanged={handleFilterPickerSelectionChanged} + pagination + paginationPageSize={20} + paginationPageSizeSelector={[20, 50, 100]} + theme={gridTheme} + style={{ width: "100%", height: "100%", fontFamily: gridFontFamily, "--ag-icon-font-family": iconFontFamily }} /> - - - - - Filter - Devices - Scope - - - - {(filterCatalog || []) - .filter((f) => (f.name || "").toLowerCase().includes(filterSearch.toLowerCase())) - .map((f) => ( - setSelectedFilterTargets((prev) => ({ ...prev, [f.id]: !prev[f.id] }))}> - - { - e.stopPropagation(); - setSelectedFilterTargets((prev) => ({ ...prev, [f.id]: e.target.checked })); - }} - sx={{ - color: MAGIC_UI.accentA, - "&.Mui-checked": { color: MAGIC_UI.accentB }, - }} - /> - - {f.name} - {typeof f.deviceCount === "number" ? f.deviceCount.toLocaleString() : "—"} - {f.scope === "scoped" ? (f.site || "Specific Site") : "All Sites"} - - ))} - {!loadingFilterCatalog && (!filterCatalog || filterCatalog.length === 0) && ( - - - No filters available. - - - )} - {loadingFilterCatalog && ( - - - Loading filters… - - - )} - -
)}
- - - +