Fixed Filters Not Working in Scheduled Jobs

This commit is contained in:
2025-11-23 15:48:37 -07:00
parent 225bd3e427
commit 9258d0fa7b
2 changed files with 157 additions and 63 deletions

View File

@@ -40,7 +40,8 @@ def register_filters(app: Flask, adapters: "EngineServiceAdapters") -> None:
except Exception: except Exception:
groups = [] groups = []
scope = (row["site_scope"] or "global").lower() 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 { return {
"id": row["id"], "id": row["id"],
"name": row["name"], "name": row["name"],
@@ -49,7 +50,8 @@ def register_filters(app: Flask, adapters: "EngineServiceAdapters") -> None:
"applyToAllSites": scope != "scoped", "applyToAllSites": scope != "scoped",
"site": site_value, "site": site_value,
"site_name": site_value, "site_name": site_value,
"site_scope": site_value, "site_scope": scope,
"site_scope_value": site_value,
"groups": groups, "groups": groups,
"last_edited_by": row["last_edited_by"], "last_edited_by": row["last_edited_by"],
"last_edited": row["last_edited"], "last_edited": row["last_edited"],

View File

@@ -450,8 +450,9 @@ const normalizeFilterCatalog = (raw) => {
const idValue = item?.id ?? item?.filter_id ?? idx; const idValue = item?.id ?? item?.filter_id ?? idx;
const id = Number(idValue); const id = Number(idValue);
if (!Number.isFinite(id)) return null; if (!Number.isFinite(id)) return null;
const scopeText = String(item?.site_scope || item?.scope || item?.type || "global").toLowerCase(); const scopeText = String(item?.site_scope || item?.scope || item?.type || "global").trim().toLowerCase();
const scope = scopeText === "scoped" ? "scoped" : "global"; 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 = const deviceCount =
typeof item?.matching_device_count === "number" && Number.isFinite(item.matching_device_count) typeof item?.matching_device_count === "number" && Number.isFinite(item.matching_device_count)
? item.matching_device_count ? item.matching_device_count
@@ -460,7 +461,9 @@ const normalizeFilterCatalog = (raw) => {
id, id,
name: item?.name || `Filter ${idx + 1}`, name: item?.name || `Filter ${idx + 1}`,
scope, scope,
site: item?.site || item?.site_name || item?.target_site || null, site_scope: scope,
site: siteName,
site_name: siteName,
deviceCount, deviceCount,
}; };
}) })
@@ -1033,6 +1036,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
const [availableDevices, setAvailableDevices] = useState([]); // [{hostname, display, online}] const [availableDevices, setAvailableDevices] = useState([]); // [{hostname, display, online}]
const [selectedDeviceTargets, setSelectedDeviceTargets] = useState({}); const [selectedDeviceTargets, setSelectedDeviceTargets] = useState({});
const [selectedFilterTargets, setSelectedFilterTargets] = useState({}); const [selectedFilterTargets, setSelectedFilterTargets] = useState({});
const [selectedFilterRows, setSelectedFilterRows] = useState([]);
const [deviceSearch, setDeviceSearch] = useState(""); const [deviceSearch, setDeviceSearch] = useState("");
const [filterSearch, setFilterSearch] = useState(""); const [filterSearch, setFilterSearch] = useState("");
const [targetPickerTab, setTargetPickerTab] = useState("devices"); const [targetPickerTab, setTargetPickerTab] = useState("devices");
@@ -1095,8 +1099,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
if (!Number.isFinite(filterId)) return null; if (!Number.isFinite(filterId)) return null;
const catalogEntry = const catalogEntry =
filterCatalogMapRef.current[filterId] || filterCatalogMapRef.current[String(filterId)] || {}; filterCatalogMapRef.current[filterId] || filterCatalogMapRef.current[String(filterId)] || {};
const scopeText = String(rawTarget.site_scope || rawTarget.scope || rawTarget.type || catalogEntry.scope || "global").toLowerCase(); const scopeText = String(
const scope = scopeText === "scoped" ? "scoped" : "global"; rawTarget.site_scope || rawTarget.scope || rawTarget.type || catalogEntry.scope || "global"
)
.trim()
.toLowerCase();
const scope = scopeText === "scoped" || scopeText === "site" ? "scoped" : "global";
const deviceCount = const deviceCount =
typeof rawTarget.deviceCount === "number" && Number.isFinite(rawTarget.deviceCount) typeof rawTarget.deviceCount === "number" && Number.isFinite(rawTarget.deviceCount)
? rawTarget.deviceCount ? rawTarget.deviceCount
@@ -1231,23 +1239,42 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
const query = filterSearch.trim().toLowerCase(); const query = filterSearch.trim().toLowerCase();
if (query.length < 3) return []; if (query.length < 3) return [];
return (filterCatalog || []) return (filterCatalog || [])
.filter((f) => String(f?.name || "").toLowerCase().includes(query)) .filter((f) => {
.map((f, index) => ({ 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), id: String(f.id ?? f.filter_id ?? index),
name: f.name || `Filter ${index + 1}`, name: f.name || `Filter ${index + 1}`,
deviceCount: typeof f.deviceCount === "number" ? f.deviceCount : f.devices_targeted ?? f.matching_device_count ?? null, deviceCount,
scope: (f.scope || f.site_scope || f.type) === "scoped" ? "Site" : "Global", scope: scoped ? "Site" : "Global",
site: f.site || f.site_name || "", scopeKey: scoped ? "scoped" : "global",
site: "",
raw: f, raw: f,
})); };
});
}, [filterCatalog, filterSearch]); }, [filterCatalog, filterSearch]);
const filterPickerOverlay = useMemo( const filterPickerRowMap = useMemo(() => {
() => const map = new Map();
filterSearch.trim().length < 3 filterPickerRows.forEach((row) => {
? "Type 3+ characters to search filters." map.set(String(row.id), row);
: "No filters match your search.", });
[filterSearch] 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 deviceRowsMap = useMemo(() => {
const map = new Map(); const map = new Map();
deviceRows.forEach((device) => { 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 key = targetKey(target) || `${target?.kind || "target"}-${Math.random().toString(36).slice(2, 8)}`;
const isFilter = target?.kind === "filter"; const isFilter = target?.kind === "filter";
const siteLabel = isFilter const siteLabel = isFilter
? "N/A" ? ""
: target?.site || : target?.site ||
target?.site_name || target?.site_name ||
(() => { (() => {
@@ -1300,14 +1327,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
const deviceCount = const deviceCount =
typeof target?.deviceCount === "number" && Number.isFinite(target.deviceCount) ? target.deviceCount : null; typeof target?.deviceCount === "number" && Number.isFinite(target.deviceCount) ? target.deviceCount : null;
const detailText = isFilter const detailText = isFilter
? `${deviceCount != null ? deviceCount.toLocaleString() : "—"} device${deviceCount === 1 ? "" : "s"}${ ? `${deviceCount != null ? deviceCount.toLocaleString() : "—"} device${deviceCount === 1 ? "" : "s"}`
target?.site_scope === "scoped" ? `${target?.site || "Specific site"}` : ""
}`
: "—"; : "—";
const osLabel = isFilter ? "—" : resolveOs(target) || "Unknown"; const osLabel = isFilter ? "—" : resolveOs(target) || "Unknown";
return { return {
id: key, id: key,
typeLabel: isFilter ? "Filter" : "Device", typeLabel: isFilter ? "Device Filter" : "Device",
siteLabel, siteLabel,
targetLabel: isFilter ? target?.name || `Filter #${target?.filter_id}` : target?.hostname, targetLabel: isFilter ? target?.name || `Filter #${target?.filter_id}` : target?.hostname,
detailText, detailText,
@@ -1325,14 +1350,11 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
minWidth: 160, minWidth: 160,
flex: 1, flex: 1,
filter: "agTextColumnFilter", filter: "agTextColumnFilter",
cellRenderer: (params) => { cellRenderer: (params) => (
const isFilter = params?.data?.typeLabel === "Filter"; <Box component="span" sx={{ color: "#e2e8f0" }}>
return (
<Box component="span" sx={{ color: isFilter ? "rgba(226,232,240,0.7)" : "#e2e8f0" }}>
{params.value || ""} {params.value || ""}
</Box> </Box>
); ),
},
cellClass: "auto-col-tight", cellClass: "auto-col-tight",
}, },
{ field: "targetLabel", headerName: "Target", minWidth: 200, flex: 1.2, filter: "agTextColumnFilter" }, { 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", cellClass: "auto-col-tight",
}, },
{ field: "scope", headerName: "Scope", minWidth: 140, flex: 0.9, 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; filterPickerGridApiRef.current = params.api;
requestAnimationFrame(() => { requestAnimationFrame(() => {
try { try {
params.api.autoSizeColumns(["name", "deviceCount", "scope", "site"], true); params.api.autoSizeColumns(["name", "deviceCount", "scope"], true);
} catch {} } catch {}
}); });
}, []); }, []);
@@ -1505,13 +1526,27 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
} }
}); });
}, [selectedFilterTargets, filterPickerRows]); }, [selectedFilterTargets, filterPickerRows]);
const handleFilterPickerSelectionChanged = useCallback(() => { const handleFilterPickerSelectionChanged = useCallback((params) => {
const api = filterPickerGridApiRef.current; const api = params?.api || filterPickerGridApiRef.current;
if (!api) return; if (!api) return;
const next = {}; const next = {};
api.getSelectedNodes().forEach((node) => { const rows = typeof api.getSelectedRows === "function" ? api.getSelectedRows() : [];
if (node?.data?.id) next[node.data.id] = true; 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); setSelectedFilterTargets(next);
}, []); }, []);
@@ -2618,6 +2653,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null, quic
setTargetPickerTab("devices"); setTargetPickerTab("devices");
setSelectedDeviceTargets({}); setSelectedDeviceTargets({});
setSelectedFilterTargets({}); setSelectedFilterTargets({});
setSelectedFilterRows([]);
loadFilterCatalog(); loadFilterCatalog();
try { try {
const resp = await fetch("/api/agents"); const resp = await fetch("/api/agents");
@@ -3742,36 +3778,92 @@ const heroTiles = useMemo(() => {
<Button <Button
onClick={() => { onClick={() => {
if (targetPickerTab === "filters") { if (targetPickerTab === "filters") {
const selectedIds = new Set( const gridNodes =
(filterPickerGridApiRef.current &&
filterPickerGridApiRef.current.getSelectedNodes()) ||
[];
const additions = [];
const api = filterPickerGridApiRef.current;
const fromStateIds = new Set(
Object.entries(selectedFilterTargets) Object.entries(selectedFilterTargets)
.filter(([, checked]) => checked) .filter(([, checked]) => checked)
.map(([id]) => String(id)) .map(([id]) => String(id))
); );
const additions = (filterCatalog || [])
.filter((f) => selectedIds.has(String(f.id ?? f.filter_id))) const rowsToUse =
.map((f) => ({ (selectedFilterRows && selectedFilterRows.length
kind: "filter", ? selectedFilterRows
filter_id: f.id ?? f.filter_id, : null) ||
name: f.name, (() => {
site_scope: f.scope || f.site_scope || f.type, if (api && typeof api.getSelectedRows === "function") {
site: f.site, const rows = api.getSelectedRows();
deviceCount: f.deviceCount ?? f.devices_targeted ?? f.matching_device_count, if (Array.isArray(rows) && rows.length) return rows;
})); }
// Fallback to visible picker rows if catalog didn't return anything yet if (gridNodes.length) {
if (!additions.length && selectedIds.size) { const rows = gridNodes.map((n) => n?.data).filter(Boolean);
filterPickerRows.forEach((row) => { if (rows.length) return rows;
if (!selectedIds.has(String(row.id))) return; }
if (api && typeof api.forEachNode === "function") {
const rows = [];
api.forEachNode((node) => {
if (node && node.isSelected && node.isSelected()) {
rows.push(node.data);
}
});
if (rows.length) return rows;
}
return [];
})();
rowsToUse.forEach((row) => {
const parsedId = Number(row?.id ?? row?.filter_id);
if (!Number.isFinite(parsedId)) return;
additions.push({ additions.push({
kind: "filter", kind: "filter",
filter_id: Number(row.id), filter_id: parsedId,
name: row.name, name: row?.name || `Filter #${parsedId}`,
site_scope: row.scope, site_scope: row?.scopeKey || row?.scope || "global",
site: row.site, site: null,
deviceCount: row.deviceCount, deviceCount: row?.deviceCount,
});
});
if (!additions.length && fromStateIds.size) {
fromStateIds.forEach((id) => {
const catalog =
filterCatalogMapRef.current[id] || filterCatalogMapRef.current[String(id)] || null;
const source = catalog || null;
const parsedId = Number(source?.id ?? source?.filter_id ?? id);
if (!Number.isFinite(parsedId)) return;
additions.push({
kind: "filter",
filter_id: parsedId,
name: source?.name || `Filter #${parsedId}`,
site_scope: source?.site_scope || source?.scope || source?.type || "global",
site: null,
deviceCount: source?.deviceCount ?? source?.devices_targeted ?? source?.matching_device_count,
}); });
}); });
} }
if (additions.length) addTargets(additions); if (!additions.length && filterPickerRows.length === 1) {
const row = filterPickerRows[0];
const parsedId = Number(row?.id ?? row?.filter_id);
if (Number.isFinite(parsedId)) {
additions.push({
kind: "filter",
filter_id: parsedId,
name: row?.name || `Filter #${parsedId}`,
site_scope: row?.scopeKey || row?.scope || "global",
site: null,
deviceCount: row?.deviceCount,
});
}
}
if (additions.length) {
addTargets(additions);
} else {
alert("Select at least one filter to add.");
}
} else { } else {
const chosenDevices = Object.keys(selectedDeviceTargets) const chosenDevices = Object.keys(selectedDeviceTargets)
.filter((hostname) => selectedDeviceTargets[hostname]) .filter((hostname) => selectedDeviceTargets[hostname])