diff --git a/Data/Engine/services/API/filters/management.py b/Data/Engine/services/API/filters/management.py index 5f4b7c9c..f005c6f5 100644 --- a/Data/Engine/services/API/filters/management.py +++ b/Data/Engine/services/API/filters/management.py @@ -40,7 +40,8 @@ def register_filters(app: Flask, adapters: "EngineServiceAdapters") -> None: except Exception: groups = [] scope = (row["site_scope"] or "global").lower() - site_value = row["site_scope"] or row["site_name"] + site_name = row["site_name"] or "" + site_value = site_name or row["site_scope"] or "" return { "id": row["id"], "name": row["name"], @@ -49,7 +50,8 @@ def register_filters(app: Flask, adapters: "EngineServiceAdapters") -> None: "applyToAllSites": scope != "scoped", "site": site_value, "site_name": site_value, - "site_scope": site_value, + "site_scope": scope, + "site_scope_value": site_value, "groups": groups, "last_edited_by": row["last_edited_by"], "last_edited": row["last_edited"], diff --git a/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx b/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx index 000d857f..370442a8 100644 --- a/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx +++ b/Data/Engine/web-interface/src/Scheduling/Create_Job.jsx @@ -450,8 +450,9 @@ const normalizeFilterCatalog = (raw) => { const idValue = item?.id ?? item?.filter_id ?? idx; const id = Number(idValue); if (!Number.isFinite(id)) return null; - const scopeText = String(item?.site_scope || item?.scope || item?.type || "global").toLowerCase(); - const scope = scopeText === "scoped" ? "scoped" : "global"; + const scopeText = String(item?.site_scope || item?.scope || item?.type || "global").trim().toLowerCase(); + const scope = scopeText === "scoped" || scopeText === "site" ? "scoped" : "global"; + const siteName = item?.site || item?.site_name || item?.target_site || item?.site_scope_value || null; const deviceCount = typeof item?.matching_device_count === "number" && Number.isFinite(item.matching_device_count) ? item.matching_device_count @@ -460,7 +461,9 @@ const normalizeFilterCatalog = (raw) => { id, name: item?.name || `Filter ${idx + 1}`, scope, - site: item?.site || item?.site_name || item?.target_site || null, + site_scope: scope, + site: siteName, + site_name: siteName, deviceCount, }; }) @@ -1033,6 +1036,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic const [availableDevices, setAvailableDevices] = useState([]); // [{hostname, display, online}] const [selectedDeviceTargets, setSelectedDeviceTargets] = useState({}); const [selectedFilterTargets, setSelectedFilterTargets] = useState({}); + const [selectedFilterRows, setSelectedFilterRows] = useState([]); const [deviceSearch, setDeviceSearch] = useState(""); const [filterSearch, setFilterSearch] = useState(""); const [targetPickerTab, setTargetPickerTab] = useState("devices"); @@ -1095,8 +1099,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic if (!Number.isFinite(filterId)) return null; const catalogEntry = filterCatalogMapRef.current[filterId] || filterCatalogMapRef.current[String(filterId)] || {}; - const scopeText = String(rawTarget.site_scope || rawTarget.scope || rawTarget.type || catalogEntry.scope || "global").toLowerCase(); - const scope = scopeText === "scoped" ? "scoped" : "global"; + const scopeText = String( + rawTarget.site_scope || rawTarget.scope || rawTarget.type || catalogEntry.scope || "global" + ) + .trim() + .toLowerCase(); + const scope = scopeText === "scoped" || scopeText === "site" ? "scoped" : "global"; const deviceCount = typeof rawTarget.deviceCount === "number" && Number.isFinite(rawTarget.deviceCount) ? rawTarget.deviceCount @@ -1231,23 +1239,42 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic 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, - })); + .filter((f) => { + if (!query) return true; + return String(f?.name || "").toLowerCase().includes(query); + }) + .map((f, index) => { + const scopeRaw = String(f.scope || f.site_scope || f.type || "").toLowerCase(); + const scoped = scopeRaw === "scoped" || scopeRaw === "site"; + const deviceCount = + typeof f.deviceCount === "number" + ? f.deviceCount + : f.devices_targeted ?? f.matching_device_count ?? null; + return { + id: String(f.id ?? f.filter_id ?? index), + name: f.name || `Filter ${index + 1}`, + deviceCount, + scope: scoped ? "Site" : "Global", + scopeKey: scoped ? "scoped" : "global", + site: "", + raw: f, + }; + }); }, [filterCatalog, filterSearch]); - const filterPickerOverlay = useMemo( - () => - filterSearch.trim().length < 3 - ? "Type 3+ characters to search filters." - : "No filters match your search.", - [filterSearch] - ); + const filterPickerRowMap = useMemo(() => { + const map = new Map(); + filterPickerRows.forEach((row) => { + map.set(String(row.id), row); + }); + return map; + }, [filterPickerRows]); + const filterPickerOverlay = useMemo(() => { + if (!filterCatalog?.length) return "No device filters available."; + const query = filterSearch.trim(); + if (query.length < 3) return "Type 3+ characters to search filters."; + if (query && filterPickerRows.length === 0) return "No filters match your search."; + return "Select filters to target."; + }, [filterCatalog?.length, filterPickerRows.length, filterSearch]); const deviceRowsMap = useMemo(() => { const map = new Map(); deviceRows.forEach((device) => { @@ -1290,7 +1317,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic 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 || (() => { @@ -1300,14 +1327,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic const deviceCount = typeof target?.deviceCount === "number" && Number.isFinite(target.deviceCount) ? target.deviceCount : null; const detailText = isFilter - ? `${deviceCount != null ? deviceCount.toLocaleString() : "—"} device${deviceCount === 1 ? "" : "s"}${ - target?.site_scope === "scoped" ? ` • ${target?.site || "Specific site"}` : "" - }` + ? `${deviceCount != null ? deviceCount.toLocaleString() : "—"} device${deviceCount === 1 ? "" : "s"}` : "—"; const osLabel = isFilter ? "—" : resolveOs(target) || "Unknown"; return { id: key, - typeLabel: isFilter ? "Filter" : "Device", + typeLabel: isFilter ? "Device Filter" : "Device", siteLabel, targetLabel: isFilter ? target?.name || `Filter #${target?.filter_id}` : target?.hostname, detailText, @@ -1325,14 +1350,11 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic minWidth: 160, flex: 1, filter: "agTextColumnFilter", - cellRenderer: (params) => { - const isFilter = params?.data?.typeLabel === "Filter"; - return ( - - {params.value || ""} - - ); - }, + cellRenderer: (params) => ( + + {params.value || ""} + + ), cellClass: "auto-col-tight", }, { field: "targetLabel", headerName: "Target", minWidth: 200, flex: 1.2, filter: "agTextColumnFilter" }, @@ -1482,7 +1504,6 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic 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" }, ], [] ); @@ -1491,7 +1512,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic filterPickerGridApiRef.current = params.api; requestAnimationFrame(() => { try { - params.api.autoSizeColumns(["name", "deviceCount", "scope", "site"], true); + params.api.autoSizeColumns(["name", "deviceCount", "scope"], true); } catch {} }); }, []); @@ -1505,13 +1526,27 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic } }); }, [selectedFilterTargets, filterPickerRows]); - const handleFilterPickerSelectionChanged = useCallback(() => { - const api = filterPickerGridApiRef.current; + const handleFilterPickerSelectionChanged = useCallback((params) => { + const api = params?.api || filterPickerGridApiRef.current; if (!api) return; const next = {}; - api.getSelectedNodes().forEach((node) => { - if (node?.data?.id) next[node.data.id] = true; - }); + const rows = typeof api.getSelectedRows === "function" ? api.getSelectedRows() : []; + if (rows.length) { + rows.forEach((row) => { + if (row?.id != null) next[row.id] = true; + }); + setSelectedFilterRows(rows); + } else { + const nodes = api.getSelectedNodes ? api.getSelectedNodes() : []; + const collectedRows = []; + nodes.forEach((node) => { + if (node?.data?.id != null) { + next[node.data.id] = true; + collectedRows.push(node.data); + } + }); + setSelectedFilterRows(collectedRows); + } setSelectedFilterTargets(next); }, []); @@ -2618,6 +2653,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic setTargetPickerTab("devices"); setSelectedDeviceTargets({}); setSelectedFilterTargets({}); + setSelectedFilterRows([]); loadFilterCatalog(); try { const resp = await fetch("/api/agents"); @@ -3742,36 +3778,92 @@ const heroTiles = useMemo(() => {