Fixed OS Overwrites in Agent Hearbeats (Legacy Inventory Code removed as well)

This commit is contained in:
2025-11-30 20:28:38 -07:00
parent 1fa84b0e31
commit d746912a96
3 changed files with 456 additions and 105 deletions

View File

@@ -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)

View File

@@ -1634,8 +1634,8 @@ export default function DeviceList({
</Box>
<Box sx={{ flex: "1 1 320px", minWidth: 0, position: "relative", zIndex: 1 }}>
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 1.2 }}>
{statTiles.map((tile) => (
<StatTile key={tile.key} {...tile} />
{statTiles.map(({ key, ...tileProps }) => (
<StatTile key={key} {...tileProps} />
))}
</Box>
</Box>

View File

@@ -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" && (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.25 }}>
<Stack direction="row" alignItems="center" spacing={1}>
<Switch
checked={applyToAllSites}
onChange={(e) => setApplyToAllSites(e.target.checked)}
color="info"
<Typography sx={{ fontWeight: 600 }}>Select sites to target</Typography>
<Typography sx={{ color: AURORA_SHELL.subtext, fontSize: "0.95rem" }}>
Use the grid to pick one or more sites. Filters scoped to sites will only apply to the selected rows.
</Typography>
<Box
className={gridTheme.themeName}
sx={{
...GRID_STYLE_BASE,
flex: 1,
minHeight: 0,
height: { xs: 320, md: 360 },
maxWidth: 760,
}}
style={{
"--ag-icon-font-family": iconFontFamily,
"--ag-background-color": "#070b1a",
"--ag-foreground-color": "#f4f7ff",
"--ag-header-background-color": "#0f172a",
"--ag-header-foreground-color": "#cfe0ff",
"--ag-odd-row-background-color": "rgba(255,255,255,0.02)",
"--ag-row-hover-color": "rgba(125,183,255,0.08)",
"--ag-selected-row-background-color": "rgba(64,164,255,0.18)",
"--ag-border-color": "rgba(125,183,255,0.18)",
"--ag-row-border-color": "rgba(125,183,255,0.14)",
"--ag-border-radius": "8px",
"--ag-checkbox-border-radius": "3px",
"--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",
}}
>
<AgGridReact
rowData={siteRows}
columnDefs={siteColumnDefs}
defaultColDef={siteDefaultColDef}
rowSelection="multiple"
rowMultiSelectWithClick
suppressCellFocus
animateRows
pagination
paginationPageSize={20}
paginationPageSizeSelector={[20, 50, 100]}
rowHeight={46}
headerHeight={44}
getRowId={(params) => params?.data?.id}
onGridReady={handleSiteGridReady}
onSelectionChanged={handleSiteSelectionChanged}
overlayNoRowsTemplate="<span class='ag-overlay-no-rows-center'>No sites available.</span>"
theme={gridTheme}
style={{ width: "100%", height: "100%", minHeight: "100%", fontFamily: gridFontFamily }}
/>
<Box>
<Typography sx={{ fontWeight: 600 }}>Add filter to all Sites</Typography>
<Typography sx={{ color: AURORA_SHELL.subtext, fontSize: "0.9rem" }}>
Future sites will also inherit this filter when enabled.
</Typography>
</Box>
</Stack>
{!applyToAllSites && (
<Autocomplete
disablePortal
loading={loadingSites}
options={sites}
value={sites.find((s) => s.value === targetSite) || null}
getOptionLabel={(option) => option?.label || ""}
isOptionEqualToValue={(option, value) => option?.value === value?.value}
onChange={(_, val) => setTargetSite(val?.value || "")}
renderInput={(params) => (
<TextField
{...params}
label="Target Site"
size="small"
placeholder="Search sites"
sx={{
width: { xs: "100%", md: "50%" },
maxWidth: 420,
"& .MuiInputBase-root": { backgroundColor: "rgba(4,7,17,0.65)" },
"& .MuiOutlinedInput-notchedOutline": { borderColor: AURORA_SHELL.border },
}}
/>
)}
/>
)}
</Box>
</Box>
)}
</Box>
@@ -1204,19 +1470,18 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
}}
>
<Box
className={gridTheme.themeName}
sx={{
flex: 1,
minHeight: 0,
height: "100%",
overflow: "hidden",
"& .ag-root-wrapper": { borderRadius: 1.5 },
"& .ag-cell.auto-col-tight": { paddingLeft: 2, paddingRight: 2 },
}}
style={{
"--ag-icon-font-family": iconFontFamily,
"--ag-background-color": "#070b1a",
"--ag-foreground-color": "#f4f7ff",
className={gridTheme.themeName}
sx={{
...GRID_STYLE_BASE,
flex: 1,
minHeight: 0,
height: "100%",
overflow: "hidden",
}}
style={{
"--ag-icon-font-family": iconFontFamily,
"--ag-background-color": "#070b1a",
"--ag-foreground-color": "#f4f7ff",
"--ag-header-background-color": "#0f172a",
"--ag-header-foreground-color": "#cfe0ff",
"--ag-odd-row-background-color": "rgba(255,255,255,0.02)",
@@ -1226,11 +1491,12 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
"--ag-row-border-color": "rgba(125,183,255,0.14)",
"--ag-border-radius": "8px",
"--ag-checkbox-border-radius": "3px",
"--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-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",
}}
>
<AgGridReact
rowData={previewRows}
columnDefs={previewColumns}
@@ -1238,12 +1504,13 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, o
animateRows
rowHeight={46}
headerHeight={44}
suppressCellFocus
suppressCellFocus
overlayNoRowsTemplate="<span class='ag-overlay-no-rows-center'>Apply criteria to preview devices.</span>"
onGridReady={handleGridReady}
onGridReady={handlePreviewGridReady}
theme={gridTheme}
pagination
paginationPageSize={20}
paginationPageSizeSelector={[20, 50, 100]}
style={{ width: "100%", height: "100%", minHeight: "100%", fontFamily: gridFontFamily }}
/>
</Box>