From 83f7b0ef52513848244d39c6c4c202096afdf66d Mon Sep 17 00:00:00 2001 From: Steven van Beek Date: Tue, 10 Feb 2026 11:40:24 +0100 Subject: [PATCH 1/7] Added new alerts --- src/data/alerts.json | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index 40cb347adeb3..7b07645c5408 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -47,6 +47,44 @@ "inputName": "ExcludeDisabled" } ] + }, + { + "name": "InactiveGuestUsers", + "label": "Alert on guest users that have not logged in for X days", + "recommendedRunInterval": "1d", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "number", + "inputLabel": "Days since last login (default: 90)", + "inputName": "DaysSinceLastLogin" + }, + { + "inputType": "switch", + "inputLabel": "Exclude disabled guest users?", + "inputName": "ExcludeDisabled" + } + ] + }, + { + "name": "InactiveUsers", + "label": "Alert on users that have not logged in for X days", + "recommendedRunInterval": "1d", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "number", + "inputLabel": "Days since last login (default: 90)", + "inputName": "DaysSinceLastLogin" + }, + { + "inputType": "switch", + "inputLabel": "Exclude disabled users?", + "inputName": "ExcludeDisabled" + } + ] }, { "name": "EntraConnectSyncStatus", @@ -341,6 +379,25 @@ "label": "Alert on quarantine release requests", "recommendedRunInterval": "30m", "description": "Monitors for user requests to release quarantined messages and provides a CIPP-native alternative to the external email forwarding method. This helps MSPs maintain secure configurations while getting timely notifications about quarantine activity. Links to the tenant's quarantine page are provided in alerts." + }, + { + "name": "AlertStaleEntraDevices", + "label": "Alert on Stale Entra devices that have not been active for X days", + "recommendedRunInterval": "1d", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "number", + "inputLabel": "Days since last activity (default: 90)", + "inputName": "DaysSinceLastActivity" + }, + { + "inputType": "switch", + "inputLabel": "Exclude disabled devices?", + "inputName": "ExcludeDisabled" + } + ] }, { "name": "SecureScore", From e475cd1c6399d3cffd4b66943e01f9b387ad008d Mon Sep 17 00:00:00 2001 From: Steven van Beek Date: Tue, 10 Feb 2026 11:47:05 +0100 Subject: [PATCH 2/7] Added new alert --- src/data/alerts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index 7b07645c5408..775fc54c4e7c 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -381,7 +381,7 @@ "description": "Monitors for user requests to release quarantined messages and provides a CIPP-native alternative to the external email forwarding method. This helps MSPs maintain secure configurations while getting timely notifications about quarantine activity. Links to the tenant's quarantine page are provided in alerts." }, { - "name": "AlertStaleEntraDevices", + "name": "StaleEntraDevices", "label": "Alert on Stale Entra devices that have not been active for X days", "recommendedRunInterval": "1d", "requiresInput": true, From 3ad4ec663a7226088ae82708aee157d0516656e8 Mon Sep 17 00:00:00 2001 From: Steven van Beek Date: Tue, 10 Feb 2026 13:35:22 +0100 Subject: [PATCH 3/7] added new alerts --- src/data/alerts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index 775fc54c4e7c..1e7f3cb74d01 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -382,7 +382,7 @@ }, { "name": "StaleEntraDevices", - "label": "Alert on Stale Entra devices that have not been active for X days", + "label": "Alert on stale Entra devices that have not been active for X days", "recommendedRunInterval": "1d", "requiresInput": true, "multipleInput": true, From 2b570c8526146fca3639d90ef8f6117b4d67cffc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 12 Feb 2026 00:34:43 -0500 Subject: [PATCH 4/7] Add simple Graph Explorer filter & UI tweaks Introduce a simplified Graph Explorer filter and several UI/behavior improvements. Key changes: - Add CippGraphExplorerSimpleFilter component to provide a compact preset selector, Run/Edit Filters toggle, and view-mode switch (table/json). - Update graph-explorer page to use the simple filter, add viewMode state, fetch JSON results for a new JSON editor view, and show loading overlay when fetching. - Enhance CippGraphExplorerFilter: support parent-driven selectedPreset, onPresetSelect callback, hideButtons flag, sync preset selection with parent, refactor footer actions, and minor layout/spacing fixes. - Modify CippOffCanvas: add contentPadding and keepMounted props, apply padding to content container and allow controlling ModalProps.keepMounted. - Update CippButtonCard to hide header/divider when no title is provided. - Update CippCodeBlock to pass code via value and set the editor to readOnly. - Wire CIPPTableToptoolbar to pass selectedPreset/onPresetSelect and off-canvas options (contentPadding, keepMounted). These changes enable a lightweight preset-driven workflow, a JSON view for debugging, better off-canvas behavior, and improved preset synchronization between components. --- src/components/CippCards/CippButtonCard.jsx | 8 +- .../CippComponents/CippCodeBlock.jsx | 3 +- .../CippComponents/CippOffCanvas.jsx | 12 +- .../CippTable/CIPPTableToptoolbar.js | 12 + .../CippTable/CippGraphExplorerFilter.js | 256 ++++++++++++------ .../CippGraphExplorerSimpleFilter.js | 197 ++++++++++++++ .../tenant/tools/graph-explorer/index.js | 84 +++++- 7 files changed, 480 insertions(+), 92 deletions(-) create mode 100644 src/components/CippTable/CippGraphExplorerSimpleFilter.js diff --git a/src/components/CippCards/CippButtonCard.jsx b/src/components/CippCards/CippButtonCard.jsx index 73f1009e1ec7..ca9429af940b 100644 --- a/src/components/CippCards/CippButtonCard.jsx +++ b/src/components/CippCards/CippButtonCard.jsx @@ -42,8 +42,12 @@ export default function CippButtonCard({ {component === "card" && ( <> - - + {title && ( + <> + + + + )} {isFetching ? : children} diff --git a/src/components/CippComponents/CippCodeBlock.jsx b/src/components/CippComponents/CippCodeBlock.jsx index 507a26667bbd..bce1d9e7d65d 100644 --- a/src/components/CippComponents/CippCodeBlock.jsx +++ b/src/components/CippComponents/CippCodeBlock.jsx @@ -48,13 +48,14 @@ export const CippCodeBlock = (props) => { {type === "editor" && ( diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index b8e5b548e94d..abbe5aa682a4 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -23,6 +23,8 @@ export const CippOffCanvas = (props) => { onNavigateDown, canNavigateUp = false, canNavigateDown = false, + contentPadding = 2, + keepMounted = false, } = props; const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); @@ -80,7 +82,7 @@ export const CippOffCanvas = (props) => { sx: { width: drawerWidth }, }} ModalProps={{ - keepMounted: false, + keepMounted: keepMounted, }} anchor={"right"} open={visible} @@ -152,7 +154,13 @@ export const CippOffCanvas = (props) => { sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }} > {/* Render children if provided, otherwise render default content */} {typeof children === "function" ? children(extendedData) : children} diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 32c28c31e84a..a1a14f3ecc40 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -1322,10 +1322,22 @@ export const CIPPTableToptoolbar = ({ title="Edit Filters" visible={filterCanvasVisible} onClose={() => setFilterCanvasVisible(!filterCanvasVisible)} + contentPadding={1} + keepMounted={true} > f.filterName === activeFilterName) + : null + } + onPresetSelect={(preset) => { + if (preset?.value && preset?.type === "graph") { + setTableFilter(preset.value, preset.type, preset.filterName); + } + }} onSubmitFilter={(filter) => { setTableFilter(filter, "graph", "Custom Filter"); if (filter?.$select) { diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index 9d296e50ef2f..d95654de1594 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import { Button, Link, Typography } from "@mui/material"; +import { Box, Button, Link, Typography } from "@mui/material"; import { Save as SaveIcon, Delete, @@ -29,6 +29,9 @@ const CippGraphExplorerFilter = ({ onPresetChange, component = "accordion", relatedQueryKeys = [], + selectedPreset = null, + onPresetSelect, + hideButtons = false, }) => { const [offCanvasOpen, setOffCanvasOpen] = useState(false); const [cardExpanded, setCardExpanded] = useState(true); @@ -123,7 +126,7 @@ const CippGraphExplorerFilter = ({ .filter( (item) => !endpointFilter || - normalizeEndpoint(item.params.endpoint) === normalizeEndpoint(endpointFilter) + normalizeEndpoint(item.params.endpoint) === normalizeEndpoint(endpointFilter), ) .forEach((item) => { presetOptionList.push({ @@ -153,7 +156,7 @@ const CippGraphExplorerFilter = ({ propertyList.refetch(); } }, 1000), - [currentEndpoint] // Dependencies that the debounce function depends on + [currentEndpoint], // Dependencies that the debounce function depends on ); useEffect(() => { @@ -180,14 +183,29 @@ const CippGraphExplorerFilter = ({ }); }; + const deletePreset = (id) => { + savePresetApi.mutate({ + url: "/api/ExecGraphExplorerPreset", + data: { action: "Delete", preset: { id: selectedPresetState } }, + }); + }; + const selectedPresets = useWatch({ control: presetControl.control, name: "reportTemplate" }); + + // Sync with parent component's selected preset + useEffect(() => { + if (selectedPreset && selectedPreset.value !== selectedPresets?.value) { + presetControl.setValue("reportTemplate", selectedPreset); + } + }, [selectedPreset?.value]); + useEffect(() => { if (selectedPresets?.addedFields?.params) { setPresetOwner(selectedPresets?.addedFields?.IsMyPreset ?? false); Object.keys(selectedPresets.addedFields.params).forEach( (key) => selectedPresets.addedFields.params[key] == null && - delete selectedPresets.addedFields.params[key] + delete selectedPresets.addedFields.params[key], ); //if $select is a blank array, set it to a string. if ( @@ -233,6 +251,11 @@ const CippGraphExplorerFilter = ({ // save last preset title setLastPresetTitle(selectedPresets.label); formControl.reset(selectedPresets?.addedFields?.params, { keepDefaultValues: true }); + + // Notify parent when preset changes in this component + if (onPresetSelect) { + onPresetSelect(selectedPresets); + } } }, [selectedPresets]); @@ -375,7 +398,7 @@ const CippGraphExplorerFilter = ({ Schedule Graph Explorer Report - + , ); setOffCanvasOpen(true); }; @@ -488,17 +511,10 @@ const CippGraphExplorerFilter = ({ }; //console.log(cardExpanded); - const deletePreset = (id) => { - savePresetApi.mutate({ - url: "/api/ExecGraphExplorerPreset", - data: { action: "Delete", preset: { id: selectedPresetState } }, - }); - }; - return (
setCardExpanded(expanded)} @@ -507,76 +523,8 @@ const CippGraphExplorerFilter = ({ height: "100%", mb: 2, }} - CardButton={ - <> - - - - - - - - - {selectedPresetState && ( - - )} - - - - - - - } > - + - + {/* Reverse Tenant Lookup Switch */} + + {/* Footer-style action section */} + {!hideButtons && ( + + + + {component === "accordion" ? ( + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + + + + + + )} + + + )} - { + const [offCanvasVisible, setOffCanvasVisible] = useState(false); + const [presetOptions, setPresetOptions] = useState([]); + const [currentFilterValues, setCurrentFilterValues] = useState(null); + + const presetControl = useForm({ + mode: "onChange", + defaultValues: { + reportTemplate: null, + }, + }); + + const selectedPreset = useWatch({ control: presetControl.control, name: "reportTemplate" }); + + // API call for available presets + const presetList = ApiGetCall({ + url: "/api/ListGraphExplorerPresets", + queryKey: "ListGraphExplorerPresets", + }); + + useEffect(() => { + var presetOptionList = []; + defaultPresets.forEach((item) => { + presetOptionList.push({ + label: item.name, + value: item.id, + addedFields: item, + type: "Built-In", + }); + }); + if (presetList.isSuccess && presetList.data?.Results.length > 0) { + presetList.data.Results.forEach((item) => { + presetOptionList.push({ + label: item.name, + value: item.id, + addedFields: item, + type: "Custom", + }); + }); + } + setPresetOptions(presetOptionList); + }, [defaultPresets, presetList.isSuccess, presetList.data]); + + const handleRunPreset = () => { + if (selectedPreset?.addedFields?.params) { + const params = selectedPreset.addedFields.params; + const values = { ...params }; + + // Handle $select array/string conversion + if (values.$select && Array.isArray(values.$select) && values.$select.length > 0) { + values.$select = values.$select + .map((item) => (typeof item === "string" ? item : item.value)) + .join(","); + } else if (values.$select === "") { + delete values.$select; + } + + // Handle version conversion + if (values.version && values.version.value) { + values.version = values.version.value; + } else if (!values.version) { + values.version = "beta"; + } + + // Clean up false boolean values + if (values.ReverseTenantLookup === false) { + delete values.ReverseTenantLookup; + } + if (values.NoPagination === false) { + delete values.NoPagination; + } + if (values.$count === false) { + delete values.$count; + } + if (values.AsApp === false) { + delete values.AsApp; + } + + // Remove null/empty values + Object.keys(values).forEach((key) => { + if (values[key] === null || values[key] === "") { + delete values[key]; + } + }); + + // Update page title if callback provided + if (onPresetChange && selectedPreset.label) { + onPresetChange(`Graph Explorer - ${selectedPreset.label}`); + } + + setCurrentFilterValues(values); + onSubmitFilter(values); + } + }; + + const handleFilterSubmit = (values) => { + setCurrentFilterValues(values); + onSubmitFilter(values); + setOffCanvasVisible(false); + }; + + const handlePresetChange = (preset) => { + presetControl.setValue("reportTemplate", preset); + }; + + return ( + <> + + + option.type} + renderGroup={(params) => ( +
  • + {params.group} + {params.children} +
  • + )} + placeholder="Select a preset to run" + /> +
    + + + + {onViewModeChange && ( + + )} + +
    + + setOffCanvasVisible(false)} + contentPadding={1} + > + + + + ); +}; + +export default CippGraphExplorerSimpleFilter; diff --git a/src/pages/tenant/tools/graph-explorer/index.js b/src/pages/tenant/tools/graph-explorer/index.js index 2ba20baa9332..3efb93604479 100644 --- a/src/pages/tenant/tools/graph-explorer/index.js +++ b/src/pages/tenant/tools/graph-explorer/index.js @@ -1,22 +1,100 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import CippGraphExplorerFilter from "../../../../components/CippTable/CippGraphExplorerFilter"; +import CippGraphExplorerSimpleFilter from "../../../../components/CippTable/CippGraphExplorerSimpleFilter"; import { useState } from "react"; -import { Grid } from "@mui/system"; +import { Grid, Stack, Box, Container } from "@mui/system"; import { useSettings } from "../../../../hooks/use-settings"; +import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; +import { ApiGetCallWithPagination } from "../../../../api/ApiCall"; +import { CircularProgress, Typography, Card } from "@mui/material"; +import { CippHead } from "../../../../components/CippComponents/CippHead"; const Page = () => { const [apiFilter, setApiFilter] = useState([]); const [pageTitle, setPageTitle] = useState("Graph Explorer"); + const [viewMode, setViewMode] = useState("table"); const tenantFilter = useSettings().currentTenant; const queryKey = JSON.stringify({ apiFilter, tenantFilter }); + const apiData = ApiGetCallWithPagination({ + url: apiFilter.endpoint ? "/api/ListGraphRequest" : "/api/ListEmptyResults", + data: apiFilter, + queryKey: queryKey, + waiting: !!apiFilter.endpoint, + }); + + const jsonData = apiData?.data?.pages?.[0]?.Results || apiData?.data || {}; + + if (viewMode === "json") { + return ( + <> + + + + + + + + + + + {pageTitle} + + + {apiData.isLoading || apiData.isFetching ? ( + + + + Loading data... + + + ) : null} + + + + + + + ); + } + return ( - + } From dfc303b142331f107a3184ba515688fd2f942120 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 12 Feb 2026 00:55:26 -0500 Subject: [PATCH 5/7] Rename filter UI to query and update icon Replace filter-focused wording and icon with query-focused alternatives in CippGraphExplorerSimpleFilter. Swapped import of FilterList for ManageSearch, updated the button icon and label from "Edit Filters" to "Edit Query", and changed the off-canvas title from "Graph Explorer Filters" to "Graph Explorer Query" to reflect the new terminology. --- src/components/CippTable/CippGraphExplorerSimpleFilter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/CippTable/CippGraphExplorerSimpleFilter.js b/src/components/CippTable/CippGraphExplorerSimpleFilter.js index cac7a80868f6..c6d5c751ba06 100644 --- a/src/components/CippTable/CippGraphExplorerSimpleFilter.js +++ b/src/components/CippTable/CippGraphExplorerSimpleFilter.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Button, Stack, Box } from "@mui/material"; -import { PlayCircle, FilterList, TableChart, Code } from "@mui/icons-material"; +import { PlayCircle, ManageSearch, TableChart, Code } from "@mui/icons-material"; import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "../CippComponents/CippFormComponent"; import { ApiGetCall } from "../../api/ApiCall"; @@ -155,11 +155,11 @@ const CippGraphExplorerSimpleFilter = ({ {onViewModeChange && ( diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/index.js b/src/pages/endpoint/MEM/reusable-settings-templates/index.js index 230f214e3fca..fe6a5810e995 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/index.js +++ b/src/pages/endpoint/MEM/reusable-settings-templates/index.js @@ -1,10 +1,10 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; import { Button } from "@mui/material"; import Link from "next/link"; import { AddBox, GitHub, Delete, Edit } from "@mui/icons-material"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall } from "../../../../api/ApiCall"; const Page = () => { const pageTitle = "Reusable Settings Templates"; diff --git a/src/pages/endpoint/MEM/reusable-settings/edit.jsx b/src/pages/endpoint/MEM/reusable-settings/edit.jsx index b6dcd1825ffa..3fe089ac520d 100644 --- a/src/pages/endpoint/MEM/reusable-settings/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings/edit.jsx @@ -3,13 +3,13 @@ import { Alert, Box, Stack } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import { useRouter } from "next/router"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import CippJsonView from "/src/components/CippFormPages/CippJSONView"; -import { ApiGetCall } from "/src/api/ApiCall"; -import { useSettings } from "/src/hooks/use-settings"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "../../../../components/CippFormPages/CippFormSkeleton"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useSettings } from "../../../../hooks/use-settings"; const EditReusableSetting = () => { const router = useRouter(); @@ -72,7 +72,9 @@ const EditReusableSetting = () => { return ( { return ( } + cardButton={ + + } apiUrl="/api/ListIntuneReusableSettings" queryKey={`ListIntuneReusableSettings-${currentTenant}`} actions={actions}