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(() => {