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…
-
-
- )}
-
-
>
)}
-
-
-