From eb1b096f42539fe6a15b6e97c51edf01b8856a8a Mon Sep 17 00:00:00 2001
From: Zacgoose <107489668+Zacgoose@users.noreply.github.com>
Date: Sun, 26 Apr 2026 02:50:58 +0800
Subject: [PATCH 01/13] Feat: Central cached page component
---
.../CippComponents/CippReportDBControls.jsx | 195 ++++++++++++++++++
.../administration/mailbox-rules/index.js | 94 +++------
.../email/administration/mailboxes/index.js | 121 ++---------
.../reports/calendar-permissions/index.js | 120 +++--------
.../email/reports/mailbox-forwarding/index.js | 100 ++-------
.../reports/mailbox-permissions/index.js | 120 +++--------
src/pages/endpoint/MEM/list-policies/index.js | 98 ++-------
.../reports/inactive-users-report/index.js | 87 ++------
.../identity/reports/mfa-report/index.js | 135 +++---------
.../security/reports/mde-onboarding/index.js | 78 ++-----
.../reports/application-consent/index.js | 114 +++-------
11 files changed, 435 insertions(+), 827 deletions(-)
create mode 100644 src/components/CippComponents/CippReportDBControls.jsx
diff --git a/src/components/CippComponents/CippReportDBControls.jsx b/src/components/CippComponents/CippReportDBControls.jsx
new file mode 100644
index 000000000000..063fd386bb5b
--- /dev/null
+++ b/src/components/CippComponents/CippReportDBControls.jsx
@@ -0,0 +1,195 @@
+import { useState, useEffect, useMemo, useCallback } from "react";
+import { Button, Chip, SvgIcon, Tooltip } from "@mui/material";
+import { Stack } from "@mui/system";
+import { Sync, CloudDone, Bolt } from "@mui/icons-material";
+import { useSettings } from "../../hooks/use-settings";
+import { useDialog } from "../../hooks/use-dialog";
+import { CippApiDialog } from "./CippApiDialog";
+import { CippQueueTracker } from "../CippTable/CippQueueTracker";
+
+/**
+ * Hook + UI component that encapsulates all CIPP Reporting DB cache/live mode logic.
+ *
+ * @param {Object} config
+ * @param {string} config.apiUrl - Base API URL without query params (e.g. "/api/ListMailboxes")
+ * @param {string} config.queryKey - Base query key (e.g. "ListMailboxes")
+ * @param {string} config.cacheName - Cache type name for sync (e.g. "Mailboxes", "IntunePolicies")
+ * @param {string} config.syncTitle - Title for the sync dialog (e.g. "Sync Mailboxes")
+ * @param {string} [config.syncConfirmText] - Custom confirm text. Default auto-generated from cacheName + tenant.
+ * @param {Object} [config.syncData] - Extra data to pass to ExecCIPPDBCache. Merged with { Name: cacheName }.
+ * @param {boolean} [config.allowToggle=true] - Whether the user can toggle between cached and live. False = always cached.
+ * @param {boolean} [config.defaultCached=true] - Initial cached state (when toggle is allowed).
+ * @param {string[]} [config.cacheColumns=["CacheTimestamp"]] - Extra columns to show when in cached mode.
+ * @param {string} [config.tenantColumn="Tenant"] - Column name for tenant (shown in AllTenants mode).
+ * @param {Object} [config.apiData] - Additional static API data to merge (e.g. extra params).
+ *
+ * @returns {Object}
+ * - useReportDB {boolean} - Current cache mode
+ * - setUseReportDB {Function} - Manual override (rarely needed)
+ * - isAllTenants {boolean} - Whether AllTenants is selected
+ * - resolvedApiUrl {string} - API URL with ?UseReportDB=true appended when needed
+ * - resolvedApiData {Object|undefined} - Merged apiData (for pages that use apiData instead of URL params)
+ * - resolvedQueryKey {string} - Query key including tenant and cache mode
+ * - cacheColumns {string[]} - Columns to prepend/append when cached (includes Tenant for AllTenants)
+ * - controls {JSX.Element} - Ready-to-render JSX for the cache toggle, sync button, and queue tracker
+ * - syncDialog {JSX.Element} - The CippApiDialog element to render alongside CippTablePage
+ */
+export function useCippReportDB(config) {
+ const {
+ apiUrl,
+ queryKey,
+ cacheName,
+ syncTitle,
+ syncConfirmText,
+ syncData,
+ allowToggle = true,
+ defaultCached = true,
+ cacheColumns = ["CacheTimestamp"],
+ tenantColumn = "Tenant",
+ apiData: extraApiData,
+ } = config;
+
+ const currentTenant = useSettings().currentTenant;
+ const isAllTenants = currentTenant === "AllTenants";
+ const dialog = useDialog();
+ const [syncQueueId, setSyncQueueId] = useState(null);
+ const [useReportDB, setUseReportDB] = useState(defaultCached);
+
+ // Reset to default whenever tenant changes; AllTenants always forces cached
+ useEffect(() => {
+ if (isAllTenants) {
+ setUseReportDB(true);
+ } else {
+ setUseReportDB(defaultCached);
+ }
+ }, [currentTenant, isAllTenants, defaultCached]);
+
+ // Whether the toggle is actually clickable
+ const canToggle = allowToggle && !isAllTenants;
+
+ // Resolved API URL — append UseReportDB param when cached
+ const resolvedApiUrl = useMemo(() => {
+ if (!useReportDB) return apiUrl;
+ const sep = apiUrl.includes("?") ? "&" : "?";
+ return `${apiUrl}${sep}UseReportDB=true`;
+ }, [apiUrl, useReportDB]);
+
+ // Alternative: for pages that pass apiData prop instead of URL params
+ const resolvedApiData = useMemo(() => {
+ if (!useReportDB && !extraApiData) return undefined;
+ return {
+ ...(extraApiData || {}),
+ ...(useReportDB ? { UseReportDB: true } : {}),
+ };
+ }, [useReportDB, extraApiData]);
+
+ // Query key that includes tenant + mode for proper cache separation
+ const resolvedQueryKey = useMemo(() => {
+ return `${queryKey}-${currentTenant}-${useReportDB}`;
+ }, [queryKey, currentTenant, useReportDB]);
+
+ // Extra columns to show when in cached mode
+ const extraColumns = useMemo(() => {
+ const cols = [];
+ if (useReportDB && isAllTenants) {
+ cols.push(tenantColumn);
+ }
+ if (useReportDB) {
+ cols.push(...cacheColumns);
+ }
+ return cols;
+ }, [useReportDB, isAllTenants, tenantColumn, cacheColumns]);
+
+ const handleSyncSuccess = useCallback((result) => {
+ if (result?.Metadata?.QueueId) {
+ setSyncQueueId(result.Metadata.QueueId);
+ }
+ }, []);
+
+ // Tooltip text
+ const tooltipText = !allowToggle
+ ? "This page always uses cached data from the CIPP reporting database."
+ : isAllTenants
+ ? "AllTenants always uses cached data"
+ : useReportDB
+ ? "Showing cached data — click to switch to live"
+ : "Showing live data — click to switch to cache";
+
+ const confirmText =
+ syncConfirmText ||
+ `Run ${cacheName} cache sync for ${currentTenant}? This will update data immediately.`;
+
+ // The controls JSX
+ const controls = (
+
+ {useReportDB && (
+ <>
+
+
+ >
+ )}
+
+
+ : }
+ label={useReportDB ? "Cached" : "Live"}
+ color="primary"
+ size="small"
+ onClick={canToggle ? () => setUseReportDB((prev) => !prev) : undefined}
+ clickable={canToggle}
+ disabled={!canToggle}
+ variant="outlined"
+ />
+
+
+
+ );
+
+ // The sync dialog JSX — render alongside the table page
+ const syncDialogElement = (
+
+ );
+
+ return {
+ useReportDB,
+ setUseReportDB,
+ isAllTenants,
+ resolvedApiUrl,
+ resolvedApiData,
+ resolvedQueryKey,
+ cacheColumns: extraColumns,
+ controls,
+ syncDialog: syncDialogElement,
+ };
+}
diff --git a/src/pages/email/administration/mailbox-rules/index.js b/src/pages/email/administration/mailbox-rules/index.js
index eb414aae760e..4b9a9ece88cb 100644
--- a/src/pages/email/administration/mailbox-rules/index.js
+++ b/src/pages/email/administration/mailbox-rules/index.js
@@ -3,30 +3,31 @@ import { CippTablePage } from "../../../../components/CippComponents/CippTablePa
import { getCippTranslation } from "../../../../utils/get-cipp-translation";
import { getCippFormatting } from "../../../../utils/get-cipp-formatting";
import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard";
-import { Block, PlayArrow, DeleteForever, Sync, Info } from "@mui/icons-material";
-import { Button, SvgIcon, IconButton, Tooltip } from "@mui/material";
-import { Stack } from "@mui/system";
-import { useDialog } from "../../../../hooks/use-dialog";
-import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog";
-import { useSettings } from "../../../../hooks/use-settings";
-import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker";
-import { useState } from "react";
+import { Block, PlayArrow, DeleteForever } from "@mui/icons-material";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const Page = () => {
const pageTitle = "Mailbox Rules";
- const currentTenant = useSettings().currentTenant;
- const syncDialog = useDialog();
- const [syncQueueId, setSyncQueueId] = useState(null);
- const isAllTenants = currentTenant === "AllTenants";
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListMailboxRules",
+ queryKey: "ListMailboxRules",
+ cacheName: "Mailboxes",
+ syncTitle: "Sync Mailbox Rules",
+ syncData: { Types: "Rules" },
+ allowToggle: false,
+ defaultCached: true,
+ });
- const apiData = {
- UseReportDB: true,
- };
-
- const simpleColumns = isAllTenants
- ? ["Tenant", "UserPrincipalName", "Name", "Priority", "Enabled", "From", "CacheTimestamp"]
- : ["UserPrincipalName", "Name", "Priority", "Enabled", "From", "CacheTimestamp"];
+ const simpleColumns = [
+ ...reportDB.cacheColumns.filter((c) => c === "Tenant"),
+ "UserPrincipalName",
+ "Name",
+ "Priority",
+ "Enabled",
+ "From",
+ ...reportDB.cacheColumns.filter((c) => c !== "Tenant"),
+ ];
const actions = [
{
@@ -96,64 +97,19 @@ const Page = () => {
},
};
- const pageActions = [
-
-
-
-
-
-
-
-
- ,
- ];
-
return (
<>
- {
- if (result?.Metadata?.QueueId) {
- setSyncQueueId(result.Metadata.QueueId);
- }
- },
- url: "/api/ExecCIPPDBCache",
- confirmText: `Run mailbox rules cache sync for ${currentTenant}? This will update mailbox rules data immediately.`,
- relatedQueryKeys: [`ListMailboxRules-${currentTenant}`],
- data: {
- Name: "Mailboxes",
- Types: "Rules",
- },
- }}
+ cardButton={reportDB.controls}
/>
+ {reportDB.syncDialog}
>
);
};
diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js
index bb3d72bb40fe..f0187d414983 100644
--- a/src/pages/email/administration/mailboxes/index.js
+++ b/src/pages/email/administration/mailboxes/index.js
@@ -3,27 +3,20 @@ import { CippTablePage } from '../../../../components/CippComponents/CippTablePa
import CippExchangeActions from '../../../../components/CippComponents/CippExchangeActions'
import { CippHVEUserDrawer } from '../../../../components/CippComponents/CippHVEUserDrawer.jsx'
import { CippSharedMailboxDrawer } from '../../../../components/CippComponents/CippSharedMailboxDrawer.jsx'
-import { Sync, CloudDone, Bolt } from '@mui/icons-material'
-import { Button, SvgIcon, Tooltip, Chip } from '@mui/material'
-import { useSettings } from '../../../../hooks/use-settings'
+import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
import { Stack } from '@mui/system'
-import { useDialog } from '../../../../hooks/use-dialog'
-import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog'
-import { useState, useEffect } from 'react'
-import { CippQueueTracker } from '../../../../components/CippTable/CippQueueTracker'
const Page = () => {
const pageTitle = 'Mailboxes'
- const currentTenant = useSettings().currentTenant
- const syncDialog = useDialog()
- const [syncQueueId, setSyncQueueId] = useState(null)
- const isAllTenants = currentTenant === 'AllTenants'
- const [useReportDB, setUseReportDB] = useState(true)
-
- useEffect(() => {
- setUseReportDB(true)
- }, [currentTenant])
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListMailboxes',
+ queryKey: 'ListMailboxes',
+ cacheName: 'Mailboxes',
+ syncTitle: 'Sync Mailboxes',
+ allowToggle: true,
+ defaultCached: true,
+ })
// Define off-canvas details
const offCanvas = {
@@ -55,31 +48,22 @@ const Page = () => {
]
// Simplified columns for the table
- const simpleColumns = isAllTenants
- ? [
- 'Tenant', // Tenant
- 'displayName', // Display Name
- 'recipientTypeDetails', // Recipient Type Details
- 'UPN', // User Principal Name
- 'primarySmtpAddress', // Primary Email Address
- 'AdditionalEmailAddresses', // Additional Email Addresses
- 'CacheTimestamp', // Cache Timestamp
- ]
- : [
- 'displayName', // Display Name
- 'recipientTypeDetails', // Recipient Type Details
- 'UPN', // User Principal Name
- 'primarySmtpAddress', // Primary Email Address
- 'AdditionalEmailAddresses', // Additional Email Addresses
- 'CacheTimestamp', // Cache Timestamp
- ]
+ const simpleColumns = [
+ ...reportDB.cacheColumns.filter((c) => c === 'Tenant'),
+ 'displayName',
+ 'recipientTypeDetails',
+ 'UPN',
+ 'primarySmtpAddress',
+ 'AdditionalEmailAddresses',
+ ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'),
+ ]
return (
<>
{
- {useReportDB && (
- <>
-
-
- >
- )}
-
-
- : }
- label={useReportDB ? 'Cached' : 'Live'}
- color="primary"
- size="small"
- onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)}
- clickable={!isAllTenants}
- disabled={isAllTenants}
- variant="outlined"
- />
-
-
+ {reportDB.controls}
}
/>
- {
- if (response?.Metadata?.QueueId) {
- setSyncQueueId(response.Metadata.QueueId)
- }
- },
- }}
- />
+ {reportDB.syncDialog}
>
)
}
diff --git a/src/pages/email/reports/calendar-permissions/index.js b/src/pages/email/reports/calendar-permissions/index.js
index 6ca2faac6d0d..eb4620f1cf4f 100644
--- a/src/pages/email/reports/calendar-permissions/index.js
+++ b/src/pages/email/reports/calendar-permissions/index.js
@@ -1,54 +1,44 @@
import { Layout as DashboardLayout } from '../../../../layouts/index.js'
import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
import { useState } from 'react'
-import { Button, Alert, SvgIcon, Tooltip, Chip } from '@mui/material'
-import { useSettings } from '../../../../hooks/use-settings'
+import { Tooltip, Chip } from '@mui/material'
import { Stack } from '@mui/system'
-import { Sync, CloudDone, Person, CalendarMonth } from '@mui/icons-material'
-import { useDialog } from '../../../../hooks/use-dialog'
-import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog'
-import { CippQueueTracker } from '../../../../components/CippTable/CippQueueTracker'
+import { Person, CalendarMonth } from '@mui/icons-material'
+import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
const Page = () => {
const [byUser, setByUser] = useState(true)
- const currentTenant = useSettings().currentTenant
- const syncDialog = useDialog()
- const [syncQueueId, setSyncQueueId] = useState(null)
- const isAllTenants = currentTenant === 'AllTenants'
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListCalendarPermissions',
+ queryKey: 'calendar-permissions',
+ cacheName: 'Mailboxes',
+ syncTitle: 'Sync Calendar Permissions Cache',
+ syncData: { Types: 'CalendarPermissions' },
+ allowToggle: false,
+ defaultCached: true,
+ cacheColumns: ['MailboxCacheTimestamp', 'PermissionCacheTimestamp'],
+ })
const columns = byUser
? [
- ...(isAllTenants ? ['Tenant'] : []),
+ ...reportDB.cacheColumns.filter((c) => c === 'Tenant'),
'User',
'UserMailboxType',
'Permissions',
- 'MailboxCacheTimestamp',
- 'PermissionCacheTimestamp',
+ ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'),
]
: [
- ...(isAllTenants ? ['Tenant'] : []),
+ ...reportDB.cacheColumns.filter((c) => c === 'Tenant'),
'CalendarUPN',
'CalendarDisplayName',
'CalendarType',
'Permissions',
- 'MailboxCacheTimestamp',
- 'PermissionCacheTimestamp',
+ ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'),
]
- // Compute apiData based on byUser directly (no useState needed)
- const apiData = {
- UseReportDB: true,
- ByUser: byUser,
- }
-
- const pageActions = [
-
-
+ const pageActions = (
+
{
variant="outlined"
/>
-
-
-
- }
- label="Cached"
- color="primary"
- size="small"
- disabled
- variant="outlined"
- />
-
-
- ,
- ]
+ {reportDB.controls}
+
+ )
return (
<>
- {currentTenant && currentTenant !== '' ? (
-
- ) : (
- Please select a tenant to view calendar permissions.
- )}
- {
- if (result?.Metadata?.QueueId) {
- setSyncQueueId(result.Metadata.QueueId)
- }
- },
- }}
+
+ {reportDB.syncDialog}
>
)
}
diff --git a/src/pages/email/reports/mailbox-forwarding/index.js b/src/pages/email/reports/mailbox-forwarding/index.js
index 008637df1a97..a24c324f983f 100644
--- a/src/pages/email/reports/mailbox-forwarding/index.js
+++ b/src/pages/email/reports/mailbox-forwarding/index.js
@@ -1,36 +1,28 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { useState } from "react";
-import { Button, Alert, SvgIcon, IconButton, Tooltip } from "@mui/material";
-import { useSettings } from "../../../../hooks/use-settings";
-import { Stack } from "@mui/system";
-import { Sync, Info } from "@mui/icons-material";
-import { useDialog } from "../../../../hooks/use-dialog";
-import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog";
-import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const Page = () => {
- const currentTenant = useSettings().currentTenant;
- const syncDialog = useDialog();
- const [syncQueueId, setSyncQueueId] = useState(null);
-
- const isAllTenants = currentTenant === "AllTenants";
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListMailboxForwarding",
+ queryKey: "mailbox-forwarding",
+ cacheName: "Mailboxes",
+ syncTitle: "Sync Mailbox Cache",
+ allowToggle: false,
+ defaultCached: true,
+ });
const columns = [
- ...(isAllTenants ? ["Tenant"] : []),
+ ...reportDB.cacheColumns.filter((c) => c === "Tenant"),
"UPN",
"DisplayName",
"RecipientTypeDetails",
"ForwardingType",
"ForwardTo",
"DeliverToMailboxAndForward",
- "CacheTimestamp",
+ ...reportDB.cacheColumns.filter((c) => c !== "Tenant"),
];
- const apiData = {
- UseReportDB: true,
- };
-
const filters = [
{
filterName: "External Forwarding",
@@ -44,69 +36,19 @@ const Page = () => {
},
];
- const pageActions = [
-
-
-
-
-
-
-
-
- ,
- ];
-
return (
<>
- {currentTenant && currentTenant !== "" ? (
-
- ) : (
- Please select a tenant to view mailbox forwarding settings.
- )}
- {
- if (result?.Metadata?.QueueId) {
- setSyncQueueId(result.Metadata.QueueId);
- }
- },
- }}
+
+ {reportDB.syncDialog}
>
);
};
diff --git a/src/pages/email/reports/mailbox-permissions/index.js b/src/pages/email/reports/mailbox-permissions/index.js
index cdeff1997a46..da771b6e90f0 100644
--- a/src/pages/email/reports/mailbox-permissions/index.js
+++ b/src/pages/email/reports/mailbox-permissions/index.js
@@ -1,54 +1,44 @@
import { Layout as DashboardLayout } from '../../../../layouts/index.js'
import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
import { useState } from 'react'
-import { Button, Alert, SvgIcon, Tooltip, Chip } from '@mui/material'
-import { useSettings } from '../../../../hooks/use-settings'
+import { Tooltip, Chip } from '@mui/material'
import { Stack } from '@mui/system'
-import { Sync, CloudDone, Person, Inbox } from '@mui/icons-material'
-import { useDialog } from '../../../../hooks/use-dialog'
-import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog'
-import { CippQueueTracker } from '../../../../components/CippTable/CippQueueTracker'
+import { Person, Inbox } from '@mui/icons-material'
+import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
const Page = () => {
const [byUser, setByUser] = useState(true)
- const currentTenant = useSettings().currentTenant
- const syncDialog = useDialog()
- const [syncQueueId, setSyncQueueId] = useState(null)
- const isAllTenants = currentTenant === 'AllTenants'
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListMailboxPermissions',
+ queryKey: 'mailbox-permissions',
+ cacheName: 'Mailboxes',
+ syncTitle: 'Sync Mailbox Permissions Cache',
+ syncData: { Types: 'Permissions' },
+ allowToggle: false,
+ defaultCached: true,
+ cacheColumns: ['MailboxCacheTimestamp', 'PermissionCacheTimestamp'],
+ })
const columns = byUser
? [
- ...(isAllTenants ? ['Tenant'] : []),
+ ...reportDB.cacheColumns.filter((c) => c === 'Tenant'),
'User',
'UserMailboxType',
'Permissions',
- 'MailboxCacheTimestamp',
- 'PermissionCacheTimestamp',
+ ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'),
]
: [
- ...(isAllTenants ? ['Tenant'] : []),
+ ...reportDB.cacheColumns.filter((c) => c === 'Tenant'),
'MailboxUPN',
'MailboxDisplayName',
'MailboxType',
'Permissions',
- 'MailboxCacheTimestamp',
- 'PermissionCacheTimestamp',
+ ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'),
]
- // Compute apiData based on byUser directly (no useState needed)
- const apiData = {
- UseReportDB: true,
- ByUser: byUser,
- }
-
- const pageActions = [
-
-
+ const pageActions = (
+
{
variant="outlined"
/>
-
-
-
- }
- label="Cached"
- color="primary"
- size="small"
- disabled
- variant="outlined"
- />
-
-
- ,
- ]
+ {reportDB.controls}
+
+ )
return (
<>
- {currentTenant && currentTenant !== '' ? (
-
- ) : (
- Please select a tenant to view mailbox permissions.
- )}
- {
- if (result?.Metadata?.QueueId) {
- setSyncQueueId(result.Metadata.QueueId)
- }
- },
- }}
+
+ {reportDB.syncDialog}
>
)
}
diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js
index 1a78f45906cb..8cc3cd2cb653 100644
--- a/src/pages/endpoint/MEM/list-policies/index.js
+++ b/src/pages/endpoint/MEM/list-policies/index.js
@@ -4,27 +4,22 @@ import { PermissionButton } from '../../../../utils/permissions.js'
import { CippPolicyDeployDrawer } from '../../../../components/CippComponents/CippPolicyDeployDrawer.jsx'
import { useSettings } from '../../../../hooks/use-settings.js'
import { useCippIntunePolicyActions } from '../../../../components/CippComponents/CippIntunePolicyActions.jsx'
-import { Sync, Info, CloudDone, Bolt } from '@mui/icons-material'
-import { Button, SvgIcon, IconButton, Tooltip, Chip } from '@mui/material'
+import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
import { Stack } from '@mui/system'
-import { useDialog } from '../../../../hooks/use-dialog'
-import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog'
-import { CippQueueTracker } from '../../../../components/CippTable/CippQueueTracker'
-import { useState, useEffect } from 'react'
const Page = () => {
const pageTitle = 'Configuration Policies'
const cardButtonPermissions = ['Endpoint.MEM.ReadWrite']
const tenant = useSettings().currentTenant
- const isAllTenants = tenant === 'AllTenants'
- const syncDialog = useDialog()
- const [syncQueueId, setSyncQueueId] = useState(null)
- const [useReportDB, setUseReportDB] = useState(isAllTenants)
- // Reset toggle whenever the tenant changes
- useEffect(() => {
- setUseReportDB(tenant === 'AllTenants')
- }, [tenant])
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListIntunePolicy',
+ queryKey: 'ListIntunePolicy',
+ cacheName: 'IntunePolicies',
+ syncTitle: 'Sync Intune Policy Report',
+ allowToggle: true,
+ defaultCached: false,
+ })
const actions = useCippIntunePolicyActions(tenant, 'URLName', {
templateData: {
@@ -45,7 +40,7 @@ const Page = () => {
}
const simpleColumns = [
- ...(useReportDB ? ['Tenant', 'CacheTimestamp'] : []),
+ ...reportDB.cacheColumns,
'displayName',
'PolicyTypeName',
'PolicyAssignment',
@@ -54,62 +49,15 @@ const Page = () => {
'lastModifiedDateTime',
]
- const pageActions = [
-
- {useReportDB && (
- <>
-
-
- >
- )}
-
-
- : }
- label={useReportDB ? 'Cached' : 'Live'}
- color="primary"
- size="small"
- onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)}
- clickable={!isAllTenants}
- disabled={isAllTenants}
- variant="outlined"
- />
-
-
- ,
- ]
-
return (
<>
{
requiredPermissions={cardButtonPermissions}
PermissionButton={PermissionButton}
/>
- {pageActions}
+ {reportDB.controls}
}
/>
- {
- if (result?.Metadata?.QueueId) {
- setSyncQueueId(result?.Metadata?.QueueId)
- }
- },
- }}
- />
+ {reportDB.syncDialog}
>
)
}
diff --git a/src/pages/identity/reports/inactive-users-report/index.js b/src/pages/identity/reports/inactive-users-report/index.js
index 168231bf6c26..8e8e7a0edc50 100644
--- a/src/pages/identity/reports/inactive-users-report/index.js
+++ b/src/pages/identity/reports/inactive-users-report/index.js
@@ -1,26 +1,21 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline";
-import { Edit, Block, Sync, Info } from "@mui/icons-material";
-import {
- Button,
- SvgIcon,
- IconButton,
- Tooltip,
- Alert,
-} from "@mui/material";
-import { Stack } from "@mui/system";
-import { useSettings } from "../../../../hooks/use-settings";
-import { useDialog } from "../../../../hooks/use-dialog";
-import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog";
+import { Edit, Block } from "@mui/icons-material";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const Page = () => {
const pageTitle = "Inactive users (6 months)";
- const apiUrl = "/api/ListInactiveAccounts";
- const currentTenant = useSettings().currentTenant;
- const syncDialog = useDialog();
- const isAllTenants = currentTenant === "AllTenants";
+ const reportDB = useCippReportDB({
+ apiUrl: "/api/ListInactiveAccounts",
+ queryKey: "inactive-users",
+ cacheName: "Users",
+ syncTitle: "Sync User Cache",
+ allowToggle: false,
+ defaultCached: true,
+ cacheColumns: ["lastRefreshedDateTime"],
+ });
const actions = [
{
@@ -74,6 +69,7 @@ const Page = () => {
};
const simpleColumns = [
+ ...reportDB.cacheColumns.filter((c) => c === "Tenant"),
"tenantDisplayName",
"userPrincipalName",
"displayName",
@@ -81,60 +77,21 @@ const Page = () => {
"lastNonInteractiveSignInDateTime",
"numberOfAssignedLicenses",
"daysSinceLastSignIn",
- "lastRefreshedDateTime",
- ];
-
- const pageActions = [
-
-
-
-
-
-
-
- ,
+ ...reportDB.cacheColumns.filter((c) => c !== "Tenant"),
];
return (
<>
- {currentTenant && currentTenant !== "" ? (
-
- ) : (
- Please select a tenant to view inactive users.
- )}
-
+ {reportDB.syncDialog}
>
);
};
diff --git a/src/pages/identity/reports/mfa-report/index.js b/src/pages/identity/reports/mfa-report/index.js
index d6ee22e99081..668030f9f923 100644
--- a/src/pages/identity/reports/mfa-report/index.js
+++ b/src/pages/identity/reports/mfa-report/index.js
@@ -1,58 +1,39 @@
import { Layout as DashboardLayout } from '../../../../layouts/index.js'
import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx'
-import { LockPerson, Sync, CloudDone } from '@mui/icons-material'
-import { Button, Alert, SvgIcon, Tooltip, Chip } from '@mui/material'
-import { useSettings } from '../../../../hooks/use-settings'
-import { Stack } from '@mui/system'
-import { useDialog } from '../../../../hooks/use-dialog'
-import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog'
+import { LockPerson } from '@mui/icons-material'
+import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls'
import { useRouter } from 'next/router'
-import { useMemo, useState } from 'react'
-import { CippQueueTracker } from '../../../../components/CippTable/CippQueueTracker'
+import { useMemo } from 'react'
const Page = () => {
const pageTitle = 'MFA Report'
- const apiUrl = '/api/ListMFAUsers'
- const currentTenant = useSettings().currentTenant
- const syncDialog = useDialog()
const router = useRouter()
- const [syncQueueId, setSyncQueueId] = useState(null)
- const isAllTenants = currentTenant === 'AllTenants'
+ const reportDB = useCippReportDB({
+ apiUrl: '/api/ListMFAUsers',
+ queryKey: 'ListMFAUsers',
+ cacheName: 'MFAState',
+ syncTitle: 'Sync MFA Report',
+ allowToggle: false,
+ defaultCached: true,
+ })
+
+ const simpleColumns = [
+ ...reportDB.cacheColumns.filter((c) => c === 'Tenant'),
+ 'UPN',
+ 'AccountEnabled',
+ 'isLicensed',
+ 'MFARegistration',
+ 'PerUser',
+ 'CoveredBySD',
+ 'CoveredByCA',
+ 'MFAMethods',
+ 'CAPolicies',
+ 'IsAdmin',
+ 'UserType',
+ ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'),
+ ]
- const apiData = {
- UseReportDB: true,
- }
- const simpleColumns = isAllTenants
- ? [
- 'Tenant',
- 'UPN',
- 'AccountEnabled',
- 'isLicensed',
- 'MFARegistration',
- 'PerUser',
- 'CoveredBySD',
- 'CoveredByCA',
- 'MFAMethods',
- 'CAPolicies',
- 'IsAdmin',
- 'UserType',
- 'CacheTimestamp',
- ]
- : [
- 'UPN',
- 'AccountEnabled',
- 'isLicensed',
- 'MFARegistration',
- 'PerUser',
- 'CoveredBySD',
- 'CoveredByCA',
- 'MFAMethods',
- 'CAPolicies',
- 'IsAdmin',
- 'UserType',
- 'CacheTimestamp',
- ]
const filters = [
{
filterName: 'Enabled, licensed users',
@@ -88,7 +69,6 @@ const Page = () => {
},
]
- // Parse filters from URL query parameters
const urlFilters = useMemo(() => {
if (router.query.filters) {
try {
@@ -127,71 +107,20 @@ const Page = () => {
},
]
- const pageActions = [
-
-
-
-
-
- }
- label="Cached"
- color="primary"
- size="small"
- disabled
- variant="outlined"
- />
-
-
- ,
- ]
-
return (
<>
- {
- if (result?.Metadata?.QueueId) {
- setSyncQueueId(result?.Metadata?.QueueId)
- }
- },
- }}
- />
+ {reportDB.syncDialog}
>
)
}
diff --git a/src/pages/security/reports/mde-onboarding/index.js b/src/pages/security/reports/mde-onboarding/index.js
index 015653b411c1..839d85f6906a 100644
--- a/src/pages/security/reports/mde-onboarding/index.js
+++ b/src/pages/security/reports/mde-onboarding/index.js
@@ -12,16 +12,15 @@ import {
CircularProgress,
Button,
SvgIcon,
- IconButton,
- Tooltip,
} from "@mui/material";
-import { Sync, Info, OpenInNew } from "@mui/icons-material";
+import { Sync, OpenInNew } from "@mui/icons-material";
import { ApiGetCall } from "../../../../api/ApiCall";
import { CippHead } from "../../../../components/CippComponents/CippHead";
import { useDialog } from "../../../../hooks/use-dialog";
import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog";
import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker";
import { useState } from "react";
+import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls";
const statusColors = {
enabled: "success",
@@ -78,11 +77,6 @@ const SingleTenantView = ({ tenant }) => {
queryKey={`MDEOnboarding-${tenant}`}
title="MDE Onboarding Sync"
/>
-
-
-
-
-
+ }
+ >
+ Bulk Add Sites
+
+ {reportDB.controls}
+
+ )
return (
-
- }>
- Add Site
-
- }
- >
- Bulk Add Sites
-
- >
- }
- />
- );
-};
+ <>
+
+ {reportDB.syncDialog}
+ >
+ )
+}
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page}
-export default Page;
+export default Page
From 1eddb9f141ae4931883ab533e56c60f3169ddfdd Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 28 Apr 2026 17:01:31 -0400
Subject: [PATCH 10/13] fix: add corrupt bookmark sanitization
---
src/hooks/use-user-bookmarks.js | 40 +++++++++++++++++++++++++--------
1 file changed, 31 insertions(+), 9 deletions(-)
diff --git a/src/hooks/use-user-bookmarks.js b/src/hooks/use-user-bookmarks.js
index 0e6b1512bb35..7427ea5c06f0 100644
--- a/src/hooks/use-user-bookmarks.js
+++ b/src/hooks/use-user-bookmarks.js
@@ -4,18 +4,40 @@ import { ApiGetCall, ApiPostCall } from "../api/ApiCall";
const SETTINGS_STORAGE_KEY = "app.settings";
+const sanitizeBookmark = (bookmark) => {
+ if (!bookmark || typeof bookmark !== "object") {
+ return null;
+ }
+
+ if (typeof bookmark.path !== "string") {
+ return null;
+ }
+
+ const path = bookmark.path.trim();
+ if (!path) {
+ return null;
+ }
+
+ const label =
+ typeof bookmark.label === "string" && bookmark.label.trim()
+ ? bookmark.label.trim()
+ : path;
+
+ return {
+ ...bookmark,
+ path,
+ label,
+ };
+};
+
const normalizeBookmarks = (value) => {
if (Array.isArray(value)) {
- return value;
+ return value.map(sanitizeBookmark).filter(Boolean);
}
- if (
- value &&
- typeof value === "object" &&
- typeof value.path === "string" &&
- typeof value.label === "string"
- ) {
- return [value];
+ const singleBookmark = sanitizeBookmark(value);
+ if (singleBookmark) {
+ return [singleBookmark];
}
return [];
@@ -103,7 +125,7 @@ export const useUserBookmarks = () => {
const persistBookmarks = useCallback(
(nextBookmarks, callbacks = {}) => {
- const safeBookmarks = Array.isArray(nextBookmarks) ? nextBookmarks : [];
+ const safeBookmarks = normalizeBookmarks(nextBookmarks);
queryClient.setQueryData(["userSettings"], (previous) => ({
...(previous || {}),
From a442361473e13802c700b3a8ece4a0aaa02e3065 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 28 Apr 2026 17:30:08 -0400
Subject: [PATCH 11/13] fix: add sync button to remove groups
---
src/components/CippFormPages/CippAddEditUser.jsx | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx
index ac2b9a0262c8..d33a9705dfdb 100644
--- a/src/components/CippFormPages/CippAddEditUser.jsx
+++ b/src/components/CippFormPages/CippAddEditUser.jsx
@@ -851,6 +851,17 @@ const CippAddEditUser = (props) => {
}))}
creatable={false}
formControl={formControl}
+ customAction={{
+ icon: ,
+ tooltip: 'Refresh groups',
+ onClick: () => {
+ tenantGroups.refetch()
+ if (formType === 'edit') {
+ userGroups.refetch()
+ }
+ },
+ position: 'outside',
+ }}
/>
)}
From 3a709a13999faec107e333b0cd143f110fcac0eb Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 28 Apr 2026 17:31:26 -0400
Subject: [PATCH 12/13] chore: bump version to 10.4.2
---
package.json | 2 +-
public/version.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 0560615cafee..b6cd7f8010a3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "10.4.1",
+ "version": "10.4.2",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
diff --git a/public/version.json b/public/version.json
index 4d2e87f1cdee..fdddd5f6239a 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "10.4.1"
+ "version": "10.4.2"
}
From fbe9bc25519e474c01485c971a9990c3562e3519 Mon Sep 17 00:00:00 2001
From: John Duprey
Date: Tue, 28 Apr 2026 17:39:17 -0400
Subject: [PATCH 13/13] fix: dbcache resolved query key
---
src/components/CippComponents/CippReportDBControls.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/CippComponents/CippReportDBControls.jsx b/src/components/CippComponents/CippReportDBControls.jsx
index 84e7955830db..75d9c70e32c8 100644
--- a/src/components/CippComponents/CippReportDBControls.jsx
+++ b/src/components/CippComponents/CippReportDBControls.jsx
@@ -125,7 +125,7 @@ export function useCippReportDB(config) {
<>