diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py
index c684c711..5c463e4e 100644
--- a/Data/Agent/agent.py
+++ b/Data/Agent/agent.py
@@ -1977,9 +1977,6 @@ def detect_agent_os():
plat = platform.system().lower()
if plat.startswith('win'):
- # Aim for: "Microsoft Windows 11 Pro 24H2 Build 26100.5074"
- # Pull details from the registry when available and correct
- # historical quirks like CurrentVersion reporting 6.3.
try:
import winreg # Only available on Windows
@@ -2002,6 +1999,7 @@ def detect_agent_os():
return default
product_name = _get("ProductName", "") # e.g., "Windows 11 Pro"
+ installation_type = _get("InstallationType", "") # e.g., "Server"
edition_id = _get("EditionID", "") # e.g., "Professional"
display_version = _get("DisplayVersion", "") # e.g., "24H2" / "22H2"
release_id = _get("ReleaseId", "") # e.g., "2004" on older Windows 10
@@ -2014,19 +2012,113 @@ def detect_agent_os():
build_int = int(str(build_number).split(".")[0]) if build_number else 0
except Exception:
build_int = 0
- if build_int >= 22000:
- major_label = "11"
- elif build_int >= 10240:
- major_label = "10"
+
+ wmi_info = {}
+ try:
+ cmd = "Get-CimInstance Win32_OperatingSystem | Select-Object Caption,ProductType,BuildNumber | ConvertTo-Json -Compress"
+ out = subprocess.run(
+ ["powershell", "-NoProfile", "-Command", cmd],
+ capture_output=True,
+ text=True,
+ timeout=5,
+ )
+ raw = (out.stdout or "").strip()
+ if raw:
+ data = json.loads(raw)
+ if isinstance(data, list):
+ data = data[0] if data else {}
+ if isinstance(data, dict):
+ wmi_info = data
+ except Exception:
+ wmi_info = {}
+
+ wmi_caption = ""
+ caption_val = wmi_info.get("Caption")
+ if isinstance(caption_val, str):
+ wmi_caption = caption_val.strip()
+ if wmi_caption.lower().startswith("microsoft "):
+ wmi_caption = wmi_caption[10:].strip()
+
+ def _parse_int(value) -> int:
+ try:
+ return int(str(value).split(".")[0])
+ except Exception:
+ return 0
+
+ if not build_int:
+ for candidate in (wmi_info.get("BuildNumber"), None):
+ if candidate:
+ parsed = _parse_int(candidate)
+ if parsed:
+ build_int = parsed
+ break
+ if not build_int:
+ try:
+ build_int = _parse_int(sys.getwindowsversion().build) # type: ignore[attr-defined]
+ except Exception:
+ build_int = 0
+
+ product_type_val = wmi_info.get("ProductType")
+ if isinstance(product_type_val, str):
+ try:
+ product_type_val = int(product_type_val.strip())
+ except Exception:
+ product_type_val = None
+ if not isinstance(product_type_val, int):
+ try:
+ product_type_val = getattr(sys.getwindowsversion(), 'product_type', None) # type: ignore[attr-defined]
+ except Exception:
+ product_type_val = None
+ if not isinstance(product_type_val, int):
+ product_type_val = 0
+
+ def _contains_server(text) -> bool:
+ try:
+ return isinstance(text, str) and 'server' in text.lower()
+ except Exception:
+ return False
+
+ server_hints = []
+ if isinstance(product_type_val, int) and product_type_val not in (0, 1):
+ server_hints.append("product_type")
+ if isinstance(product_type_val, int) and product_type_val == 1 and _contains_server(product_name):
+ server_hints.append("product_type_mismatch")
+ for hint in (product_name, wmi_caption, edition_id, installation_type):
+ if _contains_server(hint):
+ server_hints.append("string_hint")
+ break
+ if installation_type and str(installation_type).strip().lower() == 'server':
+ server_hints.append("installation_type")
+ if isinstance(edition_id, str) and edition_id.lower().startswith('server'):
+ server_hints.append("edition_id")
+ if build_int in (20348, 26100, 17763) and _contains_server(product_name or wmi_caption or edition_id or installation_type):
+ server_hints.append("build_hint")
+
+ is_server = bool(server_hints)
+
+ if is_server:
+ if build_int >= 26100:
+ family = "Windows Server 2025"
+ elif build_int >= 20348:
+ family = "Windows Server 2022"
+ elif build_int >= 17763:
+ family = "Windows Server 2019"
+ else:
+ family = "Windows Server"
else:
- major_label = platform.release()
+ if build_int >= 22000:
+ family = "Windows 11"
+ elif build_int >= 10240:
+ family = "Windows 10"
+ else:
+ family = platform.release() or "Windows"
# Derive friendly edition name, prefer parsing from ProductName
edition = ""
pn = product_name or ""
if pn.lower().startswith("windows "):
tokens = pn.split()
- # tokens like ["Windows", "11", "Pro", ...]
+ # tokens like ["Windows", "Server", "2022", "Standard", ...] or ["Windows", "11", "Pro", ...]
if len(tokens) >= 3:
edition = " ".join(tokens[2:])
if not edition and edition_id:
@@ -2047,12 +2139,8 @@ def detect_agent_os():
}
edition = eid_map.get(edition_id, edition_id)
- os_name = f"Windows {major_label}"
-
- # Choose version label: DisplayVersion (preferred) then ReleaseId
version_label = display_version or release_id or ""
- # Build string with UBR if present
if isinstance(ubr, int):
build_str = f"{build_number}.{ubr}" if build_number else str(ubr)
else:
@@ -2061,7 +2149,7 @@ def detect_agent_os():
except Exception:
build_str = build_number
- parts = ["Microsoft", os_name]
+ parts = ["Microsoft", family]
if edition:
parts.append(edition)
if version_label:
@@ -2069,8 +2157,6 @@ def detect_agent_os():
if build_str:
parts.append(f"Build {build_str}")
- # Correct possible mislabeling in ProductName (e.g., says Windows 10 on Win 11)
- # by trusting build-based major_label.
return " ".join(p for p in parts if p).strip()
except Exception:
@@ -2123,7 +2209,6 @@ def _system_uptime_seconds() -> Optional[int]:
def _collect_heartbeat_metrics() -> Dict[str, Any]:
metrics: Dict[str, Any] = {
- "operating_system": detect_agent_os(),
"service_mode": SERVICE_MODE,
}
uptime = _system_uptime_seconds()
@@ -2450,7 +2535,6 @@ async def send_heartbeat():
payload = {
"guid": client.guid or _read_agent_guid_from_disk(),
"hostname": socket.gethostname(),
- "inventory": {},
"metrics": _collect_heartbeat_metrics(),
}
await client.async_post_json("/api/agent/heartbeat", payload, require_auth=True)
diff --git a/Data/Engine/web-interface/src/Devices/Device_List.jsx b/Data/Engine/web-interface/src/Devices/Device_List.jsx
index 44178c90..bf37f27c 100644
--- a/Data/Engine/web-interface/src/Devices/Device_List.jsx
+++ b/Data/Engine/web-interface/src/Devices/Device_List.jsx
@@ -1634,8 +1634,8 @@ export default function DeviceList({
- {statTiles.map((tile) => (
-
+ {statTiles.map(({ key, ...tileProps }) => (
+
))}
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 256f4aac..a8619c0d 100644
--- a/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx
+++ b/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx
@@ -9,7 +9,6 @@ import {
TextField,
ToggleButton,
ToggleButtonGroup,
- Switch,
Chip,
Tooltip,
Autocomplete,
@@ -150,9 +149,37 @@ const TabPanel = ({ value, active, children }) => {
);
};
-const AUTO_SIZE_COLUMNS = ["status", "site", "hostname", "description", "type"];
+const GRID_STYLE_BASE = {
+ "& .ag-root-wrapper": { borderRadius: 1.5 },
+ "& .ag-center-cols-container .ag-cell, & .ag-pinned-left-cols-container .ag-cell, & .ag-pinned-right-cols-container .ag-cell": {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "flex-start",
+ textAlign: "left",
+ padding: "8px 12px 8px 18px",
+ },
+ "& .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",
+ },
+};
-const resolveApplyAll = (filter) => Boolean(filter?.applyToAllSites ?? filter?.apply_to_all_sites);
+const PREVIEW_AUTO_SIZE_COLUMNS = ["status", "site", "hostname", "description", "type"];
+const SITE_AUTO_SIZE_COLUMNS = ["site"];
const resolveLastEdited = (filter) =>
filter?.lastEdited || filter?.last_edited || filter?.updated_at || filter?.updated || null;
@@ -196,10 +223,55 @@ const formatLastEditedLabel = (ts, user) => {
return `Last edited by ${editor} @ ${datePart} @ ${timePart}`;
};
+const normalizeSiteValue = (site) => {
+ if (site == null) return "";
+ if (typeof site === "object") {
+ const candidate =
+ site.id ??
+ site.site_id ??
+ site.siteId ??
+ site.value ??
+ site.site_value ??
+ site.name ??
+ site.site_name;
+ if (candidate != null && candidate !== "") return String(candidate);
+ }
+ if (typeof site === "string" || typeof site === "number") {
+ const value = String(site).trim();
+ return value;
+ }
+ return "";
+};
+
+const resolveSiteSelection = (filter) => {
+ if (!filter) return [];
+ const candidates =
+ filter?.sites || filter?.site_list || filter?.site_scope_values || filter?.site_scope_value || filter?.siteScopeValues;
+ if (Array.isArray(candidates)) {
+ return candidates
+ .map((c) => normalizeSiteValue(c))
+ .filter((v, idx, arr) => v && arr.indexOf(v) === idx);
+ }
+ const single =
+ filter?.site ||
+ filter?.site_id ||
+ filter?.siteId ||
+ filter?.site_scope_value ||
+ filter?.siteScopeValue ||
+ filter?.siteName ||
+ filter?.site_name ||
+ filter?.target_site ||
+ (typeof candidates === "string" || typeof candidates === "number" ? candidates : null);
+ const normalized = normalizeSiteValue(single);
+ return normalized ? [normalized] : [];
+};
+
const resolveSiteScope = (filter) => {
const raw = filter?.site_scope || filter?.siteScope || filter?.scope || filter?.type;
const normalized = String(raw || "").toLowerCase();
- return normalized === "scoped" ? "scoped" : "global";
+ if (normalized === "scoped" || normalized === "site") return "scoped";
+ const sites = resolveSiteSelection(filter);
+ return sites.length ? "scoped" : "global";
};
const normalizeGroupsForUI = (rawGroups) => {
@@ -229,9 +301,9 @@ const normalizeGroupsForUI = (rawGroups) => {
export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, onPageMetaChange }) {
const [name, setName] = useState(initialFilter?.name || "");
const initialScope = resolveSiteScope(initialFilter);
+ const initialSelectedSites = resolveSiteSelection(initialFilter);
const [scope, setScope] = useState(initialScope === "scoped" ? "site" : "global");
- const [applyToAllSites, setApplyToAllSites] = useState(initialScope !== "scoped");
- const [targetSite, setTargetSite] = useState(initialFilter?.site || initialFilter?.siteName || "");
+ const [selectedSites, setSelectedSites] = useState(initialSelectedSites);
const [groups, setGroups] = useState(normalizeGroupsForUI(initialFilter?.groups || initialFilter?.raw?.groups));
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
@@ -247,7 +319,8 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
const [previewAppliedAt, setPreviewAppliedAt] = useState(null);
const [tab, setTab] = useState(TABS[0].value);
const isEditing = Boolean(initialFilter);
- const gridRef = useRef(null);
+ const previewGridRef = useRef(null);
+ const siteGridRef = useRef(null);
const sendNotification = useCallback(async (message) => {
if (!message) return;
try {
@@ -288,9 +361,9 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
if (!filter) return;
setName(filter?.name || "");
const resolvedScope = resolveSiteScope(filter);
+ const resolvedSites = resolveSiteSelection(filter);
setScope(resolvedScope === "scoped" ? "site" : "global");
- setApplyToAllSites(resolvedScope !== "scoped");
- setTargetSite(filter?.site || filter?.site_scope || filter?.siteName || filter?.site_name || "");
+ setSelectedSites(resolvedSites);
setGroups(normalizeGroupsForUI(filter?.groups || filter?.raw?.groups));
setLastEditedTs(resolveLastEdited(filter));
setLastEditedBy(resolveLastEditedBy(filter));
@@ -309,27 +382,27 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
return () => onPageMetaChange?.(null);
}, [onPageMetaChange, pageSubtitle, pageTitle]);
- const handleGridReady = useCallback((params) => {
- gridRef.current = params.api;
+ const handlePreviewGridReady = useCallback((params) => {
+ previewGridRef.current = params.api;
requestAnimationFrame(() => {
try {
- params.api.autoSizeColumns(AUTO_SIZE_COLUMNS, true);
+ params.api.autoSizeColumns(PREVIEW_AUTO_SIZE_COLUMNS, true);
} catch {}
});
}, []);
- const autoSizeGrid = useCallback(() => {
- if (!gridRef.current || !previewRows.length) return;
+ const autoSizePreviewGrid = useCallback(() => {
+ if (!previewGridRef.current || !previewRows.length) return;
requestAnimationFrame(() => {
try {
- gridRef.current.autoSizeColumns(AUTO_SIZE_COLUMNS, true);
+ previewGridRef.current.autoSizeColumns(PREVIEW_AUTO_SIZE_COLUMNS, true);
} catch {}
});
}, [previewRows.length]);
useEffect(() => {
- autoSizeGrid();
- }, [previewRows, autoSizeGrid]);
+ autoSizePreviewGrid();
+ }, [previewRows, autoSizePreviewGrid]);
const getDeviceField = (device, field) => {
const summary = device && typeof device.summary === "object" ? device.summary : {};
@@ -413,9 +486,118 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
[groups]
);
+ const siteRows = useMemo(
+ () =>
+ sites.map((s, idx) => {
+ const value = String(s.value ?? s.id ?? idx);
+ const label = s.label || `Site ${idx + 1}`;
+ return {
+ id: s.id ? String(s.id) : value,
+ value,
+ label,
+ labelLower: String(label).toLowerCase(),
+ valueLower: value.toLowerCase(),
+ };
+ }),
+ [sites]
+ );
+
+ const selectedSiteMatchers = useMemo(() => {
+ const tokens = [];
+ selectedSites.forEach((id) => {
+ const value = String(id || "").trim();
+ if (value) tokens.push(value.toLowerCase());
+ const match = sites.find((s) => String(s.value) === String(id) || String(s.id) === String(id));
+ if (match?.label) tokens.push(String(match.label).toLowerCase());
+ if (match?.value) tokens.push(String(match.value).toLowerCase());
+ });
+ return Array.from(new Set(tokens.filter(Boolean)));
+ }, [selectedSites, sites]);
+
+ const selectedSiteLabels = useMemo(
+ () =>
+ selectedSites
+ .map((id) => {
+ const match = sites.find((s) => String(s.value) === String(id) || String(s.id) === String(id));
+ const label = match?.label || match?.name;
+ const fallback = String(id || "").trim();
+ return label || fallback;
+ })
+ .filter((v, idx, arr) => v && arr.indexOf(v) === idx),
+ [selectedSites, sites]
+ );
+
+ const syncSiteGridSelection = useCallback(
+ (apiOverride = null) => {
+ const api = apiOverride || siteGridRef.current;
+ if (!api) return;
+ const selectedSet = new Set(selectedSites.map((s) => String(s)));
+ api.forEachNode((node) => {
+ const nodeId = String(node?.data?.id ?? node?.data?.value ?? "");
+ const shouldSelect = selectedSet.has(nodeId);
+ if (node.isSelected() !== shouldSelect) {
+ node.setSelected(shouldSelect);
+ }
+ });
+ },
+ [selectedSites]
+ );
+
+ const autoSizeSiteGrid = useCallback(() => {
+ if (!siteGridRef.current || !siteRows.length) return;
+ requestAnimationFrame(() => {
+ try {
+ siteGridRef.current.autoSizeColumns(SITE_AUTO_SIZE_COLUMNS, true);
+ } catch {}
+ });
+ }, [siteRows.length]);
+
+ const handleSiteGridReady = useCallback(
+ (params) => {
+ siteGridRef.current = params.api;
+ autoSizeSiteGrid();
+ syncSiteGridSelection(params.api);
+ },
+ [autoSizeSiteGrid, syncSiteGridSelection]
+ );
+
+ useEffect(() => {
+ autoSizeSiteGrid();
+ syncSiteGridSelection();
+ }, [autoSizeSiteGrid, siteRows, syncSiteGridSelection]);
+
+ useEffect(() => {
+ const api = siteGridRef.current;
+ if (!api) return;
+ if (loadingSites) {
+ api.showLoadingOverlay();
+ } else if (!siteRows.length) {
+ api.showNoRowsOverlay();
+ } else {
+ api.hideOverlay();
+ }
+ }, [loadingSites, siteRows]);
+
+ const handleSiteSelectionChanged = useCallback(() => {
+ const api = siteGridRef.current;
+ if (!api) return;
+ const selected = api
+ .getSelectedNodes()
+ .map((node) => String(node?.data?.id ?? node?.data?.value ?? ""))
+ .filter(Boolean);
+ setSelectedSites(selected);
+ }, []);
+
const applyCriteria = useCallback(async () => {
setPreviewLoading(true);
setPreviewError(null);
+ if (scope === "site" && !selectedSiteMatchers.length) {
+ setPreviewRows([]);
+ setPreviewAppliedAt(null);
+ setPreviewLoading(false);
+ setPreviewError("Select at least one site to preview scoped results.");
+ return;
+ }
try {
const resp = await fetch("/api/devices");
if (!resp.ok) {
@@ -423,7 +605,16 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
}
const payload = await resp.json();
const list = Array.isArray(payload?.devices) ? payload.devices : [];
- const filtered = list.filter((d) => evaluateCriteria(d));
+ const siteMatchSet = new Set(selectedSiteMatchers);
+ const scopedMode = scope === "site";
+ const filtered = list.filter((d) => {
+ if (!evaluateCriteria(d)) return false;
+ if (!scopedMode) return true;
+ if (!siteMatchSet.size) return false;
+ const deviceSite = getDeviceField(d, "site");
+ const normalizedSite = String(deviceSite || "").toLowerCase();
+ return siteMatchSet.has(normalizedSite);
+ });
const rows = filtered.map((d, idx) => ({
id: d.agent_guid || d.agent_id || d.hostname || `device-${idx}`,
status: getDeviceField(d, "status"),
@@ -441,9 +632,9 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
setPreviewRows([]);
} finally {
setPreviewLoading(false);
- autoSizeGrid();
+ autoSizePreviewGrid();
}
- }, [autoSizeGrid, evaluateCriteria]);
+ }, [autoSizePreviewGrid, evaluateCriteria, scope, selectedSiteMatchers]);
const applyCriteriaRef = useRef(applyCriteria);
useEffect(() => {
@@ -556,6 +747,45 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
[]
);
+ const siteColumnDefs = useMemo(
+ () => [
+ {
+ headerName: "",
+ field: "select",
+ width: 52,
+ maxWidth: 52,
+ minWidth: 52,
+ checkboxSelection: true,
+ headerCheckboxSelection: true,
+ pinned: "left",
+ suppressMenu: true,
+ sortable: false,
+ resizable: false,
+ lockPosition: true,
+ cellClass: "auto-col-tight",
+ },
+ {
+ headerName: "Site",
+ field: "label",
+ flex: 1,
+ minWidth: 220,
+ cellClass: "auto-col-tight",
+ },
+ ],
+ []
+ );
+
+ const siteDefaultColDef = useMemo(
+ () => ({
+ sortable: true,
+ filter: "agTextColumnFilter",
+ resizable: true,
+ cellClass: "auto-col-tight",
+ suppressMenu: true,
+ }),
+ []
+ );
+
useEffect(() => {
if (!initialFilter?.id) return;
const missingGroups = !initialFilter.groups || initialFilter.groups.length === 0;
@@ -591,10 +821,18 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
const resp = await fetch("/api/sites");
const json = await resp.json().catch(() => []);
const siteList = Array.isArray(json?.sites) ? json.sites : Array.isArray(json) ? json : [];
- const normalized = siteList.map((s, idx) => ({
- label: s.name || s.site_name || `Site ${idx + 1}`,
- value: s.id || s.site_id || s.name || s.site_name || idx,
- }));
+ const normalized = siteList.map((s, idx) => {
+ const normalizedValue = normalizeSiteValue(s);
+ const value = normalizedValue || String(idx);
+ const label = s.name || s.site_name || s.label || `Site ${idx + 1}`;
+ return {
+ label,
+ value,
+ id: value,
+ labelLower: String(label).toLowerCase(),
+ valueLower: String(value).toLowerCase(),
+ };
+ });
setSites(normalized);
} catch {
setSites([]);
@@ -661,12 +899,26 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
const handleSave = useCallback(async () => {
setSaving(true);
setSaveError(null);
- const siteScope = scope === "site" && !applyToAllSites ? "scoped" : "global";
+ const siteScope = scope === "site" ? "scoped" : "global";
+ const scopedSites =
+ siteScope === "scoped"
+ ? Array.from(new Set(selectedSites.map((s) => String(s || "").trim()).filter(Boolean)))
+ : [];
+ const primarySite = siteScope === "scoped" ? scopedSites[0] || null : null;
+ if (siteScope === "scoped" && !scopedSites.length) {
+ setSaveError("Select at least one site when scoping a filter to sites.");
+ setSaving(false);
+ return;
+ }
const payload = {
id: initialFilter?.id || initialFilter?.filter_id,
name: name.trim() || "Unnamed Filter",
site_scope: siteScope,
- site: siteScope === "scoped" ? targetSite : null,
+ site_scope_value: primarySite,
+ site_scope_values: scopedSites,
+ sites: scopedSites,
+ site_names: siteScope === "scoped" ? selectedSiteLabels : [],
+ site: siteScope === "scoped" ? primarySite : null,
groups: groups.map((g, gIdx) => ({
join_with: gIdx === 0 ? null : g.joinWith || "OR",
conditions: (g.conditions || []).map((c, cIdx) => ({
@@ -701,7 +953,7 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
} finally {
setSaving(false);
}
- }, [applyToAllSites, groups, initialFilter, name, onSaved, scope, sendNotification, targetSite]);
+ }, [groups, initialFilter, name, onSaved, scope, selectedSiteLabels, selectedSites, sendNotification]);
const renderConditionRow = (groupId, condition, isFirst) => {
const label = DEVICE_FIELDS.find((f) => f.value === condition.field)?.label || condition.field;
@@ -1016,45 +1268,59 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
{scope === "site" && (
-
- setApplyToAllSites(e.target.checked)}
- color="info"
+ Select sites to target
+
+ Use the grid to pick one or more sites. Filters scoped to sites will only apply to the selected rows.
+
+
+ params?.data?.id}
+ onGridReady={handleSiteGridReady}
+ onSelectionChanged={handleSiteSelectionChanged}
+ overlayNoRowsTemplate="No sites available."
+ theme={gridTheme}
+ style={{ width: "100%", height: "100%", minHeight: "100%", fontFamily: gridFontFamily }}
/>
-
- Add filter to all Sites
-
- Future sites will also inherit this filter when enabled.
-
-
-
-
- {!applyToAllSites && (
- s.value === targetSite) || null}
- getOptionLabel={(option) => option?.label || ""}
- isOptionEqualToValue={(option, value) => option?.value === value?.value}
- onChange={(_, val) => setTargetSite(val?.value || "")}
- renderInput={(params) => (
-
- )}
- />
- )}
+
)}
@@ -1204,19 +1470,18 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
}}
>
+ "--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",
+ "--ag-cell-horizontal-padding": "18px",
+ }}
+ >