mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-12-15 23:25:48 -07:00
Fixed Filters Not Working in Scheduled Jobs
This commit is contained in:
@@ -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"],
|
||||||
|
|||||||
@@ -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;
|
||||||
id: String(f.id ?? f.filter_id ?? index),
|
return String(f?.name || "").toLowerCase().includes(query);
|
||||||
name: f.name || `Filter ${index + 1}`,
|
})
|
||||||
deviceCount: typeof f.deviceCount === "number" ? f.deviceCount : f.devices_targeted ?? f.matching_device_count ?? null,
|
.map((f, index) => {
|
||||||
scope: (f.scope || f.site_scope || f.type) === "scoped" ? "Site" : "Global",
|
const scopeRaw = String(f.scope || f.site_scope || f.type || "").toLowerCase();
|
||||||
site: f.site || f.site_name || "",
|
const scoped = scopeRaw === "scoped" || scopeRaw === "site";
|
||||||
raw: f,
|
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]);
|
}, [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 (
|
{params.value || ""}
|
||||||
<Box component="span" sx={{ color: isFilter ? "rgba(226,232,240,0.7)" : "#e2e8f0" }}>
|
</Box>
|
||||||
{params.value || ""}
|
),
|
||||||
</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
|
||||||
|
? selectedFilterRows
|
||||||
|
: null) ||
|
||||||
|
(() => {
|
||||||
|
if (api && typeof api.getSelectedRows === "function") {
|
||||||
|
const rows = api.getSelectedRows();
|
||||||
|
if (Array.isArray(rows) && rows.length) return rows;
|
||||||
|
}
|
||||||
|
if (gridNodes.length) {
|
||||||
|
const rows = gridNodes.map((n) => n?.data).filter(Boolean);
|
||||||
|
if (rows.length) return rows;
|
||||||
|
}
|
||||||
|
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({
|
||||||
kind: "filter",
|
kind: "filter",
|
||||||
filter_id: f.id ?? f.filter_id,
|
filter_id: parsedId,
|
||||||
name: f.name,
|
name: row?.name || `Filter #${parsedId}`,
|
||||||
site_scope: f.scope || f.site_scope || f.type,
|
site_scope: row?.scopeKey || row?.scope || "global",
|
||||||
site: f.site,
|
site: null,
|
||||||
deviceCount: f.deviceCount ?? f.devices_targeted ?? f.matching_device_count,
|
deviceCount: row?.deviceCount,
|
||||||
}));
|
});
|
||||||
// Fallback to visible picker rows if catalog didn't return anything yet
|
});
|
||||||
if (!additions.length && selectedIds.size) {
|
|
||||||
filterPickerRows.forEach((row) => {
|
if (!additions.length && fromStateIds.size) {
|
||||||
if (!selectedIds.has(String(row.id))) return;
|
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({
|
additions.push({
|
||||||
kind: "filter",
|
kind: "filter",
|
||||||
filter_id: Number(row.id),
|
filter_id: parsedId,
|
||||||
name: row.name,
|
name: source?.name || `Filter #${parsedId}`,
|
||||||
site_scope: row.scope,
|
site_scope: source?.site_scope || source?.scope || source?.type || "global",
|
||||||
site: row.site,
|
site: null,
|
||||||
deviceCount: row.deviceCount,
|
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])
|
||||||
|
|||||||
Reference in New Issue
Block a user