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 e2ae8852..00be6781 100644
--- a/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx
+++ b/Data/Engine/web-interface/src/Devices/Filters/Filter_Editor.jsx
@@ -13,6 +13,8 @@ import {
Chip,
Tooltip,
Autocomplete,
+ Tabs,
+ Tab,
} from "@mui/material";
import {
FilterAlt as HeaderIcon,
@@ -123,6 +125,22 @@ const OS_ICON_MAP = {
const TAB_HOVER_GRADIENT = "linear-gradient(120deg, rgba(125,211,252,0.18), rgba(192,132,252,0.22))";
+const TABS = [
+ { value: "name", label: "Name" },
+ { value: "scope", label: "Scope" },
+ { value: "criteria", label: "Criteria" },
+ { value: "results", label: "Results" },
+];
+
+const TabPanel = ({ value, active, children }) => {
+ if (value !== active) return null;
+ return (
+
+ {children}
+
+ );
+};
+
const AUTO_SIZE_COLUMNS = ["status", "site", "hostname", "description", "type", "os"];
const resolveApplyAll = (filter) => Boolean(filter?.applyToAllSites ?? filter?.apply_to_all_sites);
@@ -199,7 +217,7 @@ const normalizeGroupsForUI = (rawGroups) => {
});
};
-export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved }) {
+export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved, onPageMetaChange }) {
const [name, setName] = useState(initialFilter?.name || "");
const initialScope = resolveSiteScope(initialFilter);
const [scope, setScope] = useState(initialScope === "scoped" ? "site" : "global");
@@ -218,6 +236,8 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved })
const [previewLoading, setPreviewLoading] = useState(false);
const [previewError, setPreviewError] = useState(null);
const [previewAppliedAt, setPreviewAppliedAt] = useState(null);
+ const [tab, setTab] = useState(TABS[0].value);
+ const isEditing = Boolean(initialFilter);
const gridRef = useRef(null);
const sendNotification = useCallback(async (message) => {
if (!message) return;
@@ -251,6 +271,9 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved })
);
const gridFontFamily = "'IBM Plex Sans','Helvetica Neue',Arial,sans-serif";
const iconFontFamily = "'Quartz Regular'";
+ const pageTitle = isEditing ? "Edit Device Filter" : "Create Device Filter";
+ const pageSubtitle =
+ "Combine grouped criteria with AND/OR logic to build reusable device scopes for automation and reporting.";
const applyFilterData = useCallback((filter) => {
if (!filter) return;
@@ -268,6 +291,15 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved })
applyFilterData(initialFilter);
}, [applyFilterData, initialFilter]);
+ useEffect(() => {
+ onPageMetaChange?.({
+ page_title: pageTitle,
+ page_subtitle: pageSubtitle,
+ page_icon: HeaderIcon,
+ });
+ return () => onPageMetaChange?.(null);
+ }, [onPageMetaChange, pageSubtitle, pageTitle]);
+
const handleGridReady = useCallback((params) => {
gridRef.current = params.api;
requestAnimationFrame(() => {
@@ -659,7 +691,7 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved })
key={condition.id}
sx={{
display: "grid",
- gridTemplateColumns: "110px 220px 220px 1fr auto",
+ gridTemplateColumns: "94px 220px 220px 1fr auto",
gap: 0.5,
alignItems: "center",
background: "rgba(12,18,35,0.7)",
@@ -777,6 +809,9 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved })
elevation={0}
sx={{
minHeight: "100vh",
+ height: "100vh",
+ flex: 1,
+ position: "relative",
backgroundColor: "transparent",
color: AURORA_SHELL.text,
p: 3,
@@ -784,41 +819,39 @@ export default function DeviceFilterEditor({ initialFilter, onCancel, onSaved })
display: "flex",
flexDirection: "column",
gap: 3,
- pb: 6,
+ pb: 3,
+ overflow: "hidden",
}}
>
-
-
-
-
-
-
-
- {initialFilter ? "Edit Device Filter" : "Create Device Filter"}
-
-
- Combine grouped criteria with AND/OR logic to build reusable device scopes for automation and reporting.
-
- {lastEditedTs && (
-
- {formatLastEditedLabel(lastEditedTs, lastEditedBy)}
-
- )}
-
+ {loadingFilter ? (
+ Loading filter...
+ ) : null}
+ {loadError ? (
+
+ {loadError}
+ ) : null}
-
+
+
- {loadingFilter ? (
- Loading filter...
- ) : null}
- {loadError ? (
-
+ setTab(val)}
+ variant="scrollable"
+ scrollButtons="auto"
+ TabIndicatorProps={{
+ style: {
+ height: 3,
+ borderRadius: 3,
+ background: "linear-gradient(90deg,#7dd3fc,#c084fc)",
+ },
}}
- >
- {loadError}
-
- ) : null}
-
-
-
- Name
- setName(e.target.value)}
- placeholder="Filter name or convention (e.g., RMM targeting)"
- sx={{
- width: { xs: "100%", md: "50%" },
- maxWidth: 420,
- "& .MuiInputBase-root": { backgroundColor: "rgba(4,7,17,0.65)" },
- "& .MuiOutlinedInput-notchedOutline": { borderColor: AURORA_SHELL.border },
- }}
- />
-
-
-
- Scope
-
- Choose whether this filter is global or pinned to a specific site.
-
- {
- if (!val) return;
- setScope(val);
- }}
- color="info"
- sx={{
- alignSelf: "flex-start",
- background: "rgba(7,12,26,0.7)",
- borderRadius: 2,
- "& .MuiToggleButton-root": {
- textTransform: "none",
- color: AURORA_SHELL.text,
- borderColor: "rgba(148,163,184,0.35)",
- minHeight: 32,
- paddingTop: 0.25,
- paddingBottom: 0.25,
- paddingLeft: 1.6,
- paddingRight: 1.6,
- fontWeight: 700,
- },
- "& .Mui-selected": {
- background: TAB_HOVER_GRADIENT,
- color: "#0b1220",
- boxShadow: "0 0 0 1px rgba(148,163,184,0.35) inset",
- },
- }}
- >
- Global
- Site
-
-
- {scope === "site" && (
-
-
- setApplyToAllSites(e.target.checked)}
- color="info"
- />
-
- 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) => (
-
- )}
- />
- )}
-
- )}
-
-
-
-
- Criteria
-
-
-
- Add conditions inside each group, mixing AND/OR as needed. Groups themselves can be chained with AND or OR to
- mirror complex targeting logic (e.g., (A AND B) OR (C AND D)).
-
-
- {groups.map((group, idx) => (
-
- {idx > 0 && (
- {
- if (!val) return;
- updateGroup(group.id, { ...group, joinWith: val });
- }}
- color="info"
- sx={{
- alignSelf: "center",
- "& .MuiToggleButton-root": { px: 2, textTransform: "uppercase", fontSize: "0.8rem" },
- }}
- >
- AND
- OR
-
- )}
-
-
-
- Criteria Group {idx + 1}
-
- }
- onClick={() => addCondition(group.id)}
- sx={{
- textTransform: "none",
- color: "#7dd3fc",
- borderColor: "rgba(125,211,252,0.5)",
- borderRadius: 1.5,
- }}
- >
- Add Condition
-
- }
- disabled={groups.length === 1}
- onClick={() => removeGroup(group.id)}
- sx={{
- textTransform: "none",
- color: "#ffb4b4",
- borderColor: "rgba(255,180,180,0.5)",
- borderRadius: 1.5,
- }}
- >
- Remove Group
-
-
-
-
-
- {group.conditions.map((condition, cIdx) =>
- renderConditionRow(group.id, condition, cIdx === 0)
- )}
-
-
-
- ))}
-
- }
- variant="outlined"
- onClick={() => addGroup("OR")}
- sx={{
+ mt: 0,
+ borderBottom: `1px solid ${AURORA_SHELL.border}`,
+ "& .MuiTab-root": {
+ color: AURORA_SHELL.subtext,
+ fontFamily: gridFontFamily,
+ fontSize: 15,
textTransform: "none",
- alignSelf: "flex-start",
- color: "#a5e0ff",
- borderColor: "rgba(125,183,255,0.5)",
- borderRadius: 1.5,
- }}
- >
- Add Group
-
-
-
-
-
-
- Results
-
- Apply criteria to preview matching devices.
-
- {previewAppliedAt && (
-
- Last applied: {previewAppliedAt.toLocaleString()}
-
- )}
- {previewError ? (
- {previewError}
- ) : null}
-
- : }
- onClick={applyCriteria}
- disabled={previewLoading}
- sx={gradientButtonSx}
- >
- {previewLoading ? "Applying..." : "Apply Criteria"}
-
-
-
-
-
+ {TABS.map((tabDef) => (
+
+ ))}
+
+
+
+
+ Name
+ setName(e.target.value)}
+ placeholder="Filter name or convention (e.g., RMM targeting)"
+ sx={{
+ width: { xs: "100%", md: "65%" },
+ maxWidth: 546,
+ "& .MuiInputBase-root": { backgroundColor: "rgba(4,7,17,0.65)" },
+ "& .MuiOutlinedInput-notchedOutline": { borderColor: AURORA_SHELL.border },
+ }}
/>
-
+
+
+
+
+ Scope
+
+ Choose whether this filter is global or pinned to a specific site.
+
+ {
+ if (!val) return;
+ setScope(val);
+ }}
+ color="info"
+ sx={{
+ alignSelf: "flex-start",
+ background: "rgba(7,12,26,0.7)",
+ borderRadius: 2,
+ "& .MuiToggleButton-root": {
+ textTransform: "none",
+ color: AURORA_SHELL.text,
+ borderColor: "rgba(148,163,184,0.35)",
+ minHeight: 32,
+ paddingTop: 0.25,
+ paddingBottom: 0.25,
+ paddingLeft: 1.6,
+ paddingRight: 1.6,
+ fontWeight: 700,
+ },
+ "& .Mui-selected": {
+ background: TAB_HOVER_GRADIENT,
+ color: "#0b1220",
+ boxShadow: "0 0 0 1px rgba(148,163,184,0.35) inset",
+ },
+ }}
+ >
+ Global
+ Site
+
+
+ {scope === "site" && (
+
+
+ setApplyToAllSites(e.target.checked)}
+ color="info"
+ />
+
+ 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) => (
+
+ )}
+ />
+ )}
+
+ )}
+
+
+
+
+
+
+ Criteria
+
+
+
+ Add conditions inside each group, mixing AND/OR as needed. Groups themselves can be chained with AND or OR to
+ mirror complex targeting logic (e.g., (A AND B) OR (C AND D)).
+
+
+ {groups.map((group, idx) => (
+
+ {idx > 0 && (
+ {
+ if (!val) return;
+ updateGroup(group.id, { ...group, joinWith: val });
+ }}
+ color="info"
+ sx={{
+ alignSelf: "flex-start",
+ "& .MuiToggleButton-root": { px: 2, textTransform: "uppercase", fontSize: "0.8rem" },
+ }}
+ >
+ AND
+ OR
+
+ )}
+
+
+
+ Criteria Group {idx + 1}
+
+ }
+ onClick={() => addCondition(group.id)}
+ sx={{
+ textTransform: "none",
+ color: "#7dd3fc",
+ borderColor: "rgba(125,211,252,0.5)",
+ borderRadius: 1.5,
+ }}
+ >
+ Add Condition
+
+ }
+ disabled={groups.length === 1}
+ onClick={() => removeGroup(group.id)}
+ sx={{
+ textTransform: "none",
+ color: "#ffb4b4",
+ borderColor: "rgba(255,180,180,0.5)",
+ borderRadius: 1.5,
+ }}
+ >
+ Remove Group
+
+
+
+
+
+ {group.conditions.map((condition, cIdx) =>
+ renderConditionRow(group.id, condition, cIdx === 0)
+ )}
+
+
+
+ ))}
+
+ }
+ variant="outlined"
+ onClick={() => addGroup("OR")}
+ sx={{
+ textTransform: "none",
+ alignSelf: "flex-start",
+ color: "#a5e0ff",
+ borderColor: "rgba(125,183,255,0.5)",
+ borderRadius: 1.5,
+ }}
+ >
+ Add Group
+
+
+
+
+
+
+
+
+ Results
+
+ Apply criteria to preview matching devices.
+
+ {previewAppliedAt && (
+
+ Last applied: {previewAppliedAt.toLocaleString()}
+
+ )}
+ {previewError ? (
+ {previewError}
+ ) : null}
+
+ : }
+ onClick={applyCriteria}
+ disabled={previewLoading}
+ sx={gradientButtonSx}
+ >
+ {previewLoading ? "Applying..." : "Apply Criteria"}
+
+
+
+
+
+
+
+
+
+
{saveError ? (