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" /> - - - - - - , - ]; - return ( <> - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result.Metadata.QueueId); - } - }, - }} + cardButton={reportDB.controls} /> + {reportDB.syncDialog} ); }; Page.getLayout = (page) => {page}; -export default Page; \ No newline at end of file +export default Page; diff --git a/src/pages/tenant/reports/application-consent/index.js b/src/pages/tenant/reports/application-consent/index.js index 743b4226b763..08ac8b35d47c 100644 --- a/src/pages/tenant/reports/application-consent/index.js +++ b/src/pages/tenant/reports/application-consent/index.js @@ -1,107 +1,39 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { Sync, CloudDone, Bolt } from "@mui/icons-material"; -import { Button, 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 { useState, useEffect } from "react"; -import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; const pageTitle = "Consented Applications"; const Page = () => { - 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 simpleColumns = isAllTenants - ? ["Tenant", "Name", "ApplicationID", "ObjectID", "Scope", "StartTime", "CacheTimestamp"] - : ["Name", "ApplicationID", "ObjectID", "Scope", "StartTime", "CacheTimestamp"]; + const reportDB = useCippReportDB({ + apiUrl: "/api/ListOAuthApps", + queryKey: "ListOAuthApps", + cacheName: "OAuth2PermissionGrants", + syncTitle: "Sync Consented Applications", + allowToggle: true, + defaultCached: true, + }); + + const simpleColumns = [ + ...reportDB.cacheColumns.filter((c) => c === "Tenant"), + "Name", + "ApplicationID", + "ObjectID", + "Scope", + "StartTime", + ...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" - /> - - - - } - /> - { - if (response?.Metadata?.QueueId) { - setSyncQueueId(response.Metadata.QueueId); - } - }, - }} + cardButton={reportDB.controls} /> + {reportDB.syncDialog} ); }; From a37e5d30f9eafc2784922f5e3013aef8cc8ba85b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 27 Apr 2026 14:40:33 -0400 Subject: [PATCH 02/13] fix: update role exclusions in cipp-roles.json Co-authored-by: Copilot --- src/data/cipp-roles.json | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/data/cipp-roles.json b/src/data/cipp-roles.json index f95e32fa18c6..ac3c389f65a2 100644 --- a/src/data/cipp-roles.json +++ b/src/data/cipp-roles.json @@ -1,10 +1,19 @@ { "readonly": { - "include": ["*.Read"], - "exclude": ["CIPP.SuperAdmin.*"] + "include": [ + "*.Read" + ], + "exclude": [ + "CIPP.SuperAdmin.*", + "CIPP.Admin.*", + "CIPP.AppSettings.*" + ] }, "editor": { - "include": ["*.Read", "*.ReadWrite"], + "include": [ + "*.Read", + "*.ReadWrite" + ], "exclude": [ "CIPP.SuperAdmin.*", "CIPP.Admin.*", @@ -13,11 +22,17 @@ ] }, "admin": { - "include": ["*"], - "exclude": ["CIPP.SuperAdmin.*"] + "include": [ + "*" + ], + "exclude": [ + "CIPP.SuperAdmin.*" + ] }, "superadmin": { - "include": ["*"], + "include": [ + "*" + ], "exclude": [] } -} +} \ No newline at end of file From 7d8d7cb843dafcae2cb46cc7cee8033fa3bbc872 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:57:04 +0800 Subject: [PATCH 03/13] Add SharePoint and Exchange standards Add three new standards: - standards.SPDisableCustomScripts: Prevents custom scripts on SharePoint/OneDrive (high impact). - standards.SPDisableStoreAccess: Disables SharePoint Store access for end users (low impact). - standards.DisableEWS: Disables Exchange Web Services org-wide to reduce legacy API attack surface (high impact). --- src/data/standards.json | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 07bff3d77f06..44a0c52d15e8 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6304,5 +6304,73 @@ "EXCHANGE_S_ENTERPRISE_GOV", "EXCHANGE_LITE" ] + }, + { + "name": "standards.SPDisableCustomScripts", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Prevents users from running custom scripts on SharePoint and OneDrive sites. Custom scripts can modify site behaviors and bypass governance controls.", + "docsDescription": "Disables the ability to add and run custom scripts on SharePoint and OneDrive sites at the tenant level. When custom scripts are allowed, governance cannot be enforced, and the capabilities of inserted code cannot be scoped or blocked. Microsoft recommends using the SharePoint Framework instead of custom scripts.", + "executiveText": "Blocks custom scripts from being added to SharePoint and OneDrive sites, enforcing governance controls and preventing unscoped code execution. This aligns with Microsoft's Baseline Security Mode recommendation to permanently remove the ability to add new custom scripts, directing organizations to use the SharePoint Framework instead.", + "addedComponent": [], + "label": "Disable custom scripts on SharePoint sites", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-04-28", + "powershellEquivalent": "Set-SPOTenant -CustomScriptsRestrictMode $true", + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] + }, + { + "name": "standards.SPDisableStoreAccess", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Disables end users from installing applications from the Microsoft Store into SharePoint sites.", + "docsDescription": "Removes the ability for end users to install applications directly from the Microsoft Store into SharePoint. This prevents uncontrolled app installations that can increase governance costs and go against organizational policies.", + "executiveText": "Prevents end users from installing applications from the Microsoft Store into SharePoint sites, ensuring that only approved applications are available. This reduces governance overhead and aligns with Microsoft's Baseline Security Mode recommendations.", + "addedComponent": [], + "label": "Disable SharePoint Store access", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-04-28", + "powershellEquivalent": "Set-SPOTenant -DisableSharePointStoreAccess $true", + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] + }, + { + "name": "standards.DisableEWS", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Disables Exchange Web Services (EWS) organization-wide. This reduces the attack surface by blocking legacy API access to mailbox data. Warning: This may break Office web add-ins on builds older than 16.0.19127.", + "docsDescription": "Disables Exchange Web Services (EWS) at the organization level to reduce attack surface. EWS provides cross-platform API access to sensitive Exchange Online data such as emails, meetings, and contacts. If compromised, attackers can access confidential data, send phishing emails, or spoof identities. Disabling EWS also reduces legacy app usage and minimizes exploitable endpoints. Note that this may break first-party features including web add-ins for Word, Excel, PowerPoint, and Outlook on builds older than 16.0.19127.", + "executiveText": "Disables Exchange Web Services (EWS) across the organization to reduce attack surface and prevent legacy API access to sensitive mailbox data. This aligns with Microsoft's Baseline Security Mode recommendation to minimize exploitable endpoints while requiring updates to applications that depend on EWS.", + "addedComponent": [], + "label": "Disable Exchange Web Services", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-04-28", + "powershellEquivalent": "Set-OrganizationConfig -EwsEnabled $false", + "recommendedBy": ["CIPP"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] } ] From 88105826f7d179cb66ee7eef6b6b47ce3e242a79 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:00:22 +0200 Subject: [PATCH 04/13] remove logbook tenantinTitle --- src/pages/cipp/logs/index.js | 192 +++++++++++++++++------------------ 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/src/pages/cipp/logs/index.js b/src/pages/cipp/logs/index.js index 1cbd2fa8e02d..1896cf3e2349 100644 --- a/src/pages/cipp/logs/index.js +++ b/src/pages/cipp/logs/index.js @@ -1,6 +1,6 @@ -import { useState } from "react"; -import { Layout as DashboardLayout } from "../../../layouts/index.js"; -import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; +import { useState } from 'react' +import { Layout as DashboardLayout } from '../../../layouts/index.js' +import { CippTablePage } from '../../../components/CippComponents/CippTablePage.jsx' import { Button, Accordion, @@ -11,73 +11,73 @@ import { Stack, Alert, Box, -} from "@mui/material"; -import { Grid } from "@mui/system"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { useForm } from "react-hook-form"; -import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; -import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { EyeIcon } from "@heroicons/react/24/outline"; -import { useSettings } from "../../../hooks/use-settings.js"; +} from '@mui/material' +import { Grid } from '@mui/system' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { useForm } from 'react-hook-form' +import CippFormComponent from '../../../components/CippComponents/CippFormComponent' +import { FunnelIcon, XMarkIcon } from '@heroicons/react/24/outline' +import { EyeIcon } from '@heroicons/react/24/outline' +import { useSettings } from '../../../hooks/use-settings.js' const simpleColumns = [ - "DateTime", - "Tenant", - "User", - "Message", - "API", - "Severity", - "AppId", - "IP", - "LogData", -]; + 'DateTime', + 'Tenant', + 'User', + 'Message', + 'API', + 'Severity', + 'AppId', + 'IP', + 'LogData', +] const offcanvas = { - extendedInfoFields: ["DateTime", "API", "Severity", "Message", "User", "AppId", "IP", "LogData"], -}; + extendedInfoFields: ['DateTime', 'API', 'Severity', 'Message', 'User', 'AppId', 'IP', 'LogData'], +} -const apiUrl = "/api/Listlogs"; -const pageTitle = "Logbook Results"; +const apiUrl = '/api/Listlogs' +const pageTitle = 'Logbook Results' const actions = [ { - label: "View Log Entry", - link: "/cipp/logs/logentry?logentry=[RowKey]&dateFilter=[DateFilter]", + label: 'View Log Entry', + link: '/cipp/logs/logentry?logentry=[RowKey]&dateFilter=[DateFilter]', icon: , - color: "primary", + color: 'primary', }, -]; +] const Page = () => { const formControl = useForm({ defaultValues: { startDate: null, endDate: null, - username: "", + username: '', severity: [], }, - }); + }) - const [expanded, setExpanded] = useState(false); // State for Accordion - const [filterEnabled, setFilterEnabled] = useState(false); // State for filter toggle - const [startDate, setStartDate] = useState(null); // State for start date filter - const [endDate, setEndDate] = useState(null); // State for end date filter - const [username, setUsername] = useState(null); // State for username filter - const [severity, setSeverity] = useState(null); // State for severity filter - const settings = useSettings(); // Hook to access settings - const currentTenant = settings?.currentTenant; + const [expanded, setExpanded] = useState(false) // State for Accordion + const [filterEnabled, setFilterEnabled] = useState(false) // State for filter toggle + const [startDate, setStartDate] = useState(null) // State for start date filter + const [endDate, setEndDate] = useState(null) // State for end date filter + const [username, setUsername] = useState(null) // State for username filter + const [severity, setSeverity] = useState(null) // State for severity filter + const settings = useSettings() // Hook to access settings + const currentTenant = settings?.currentTenant // Watch date fields to show warning for large date ranges - const watchStartDate = formControl.watch("startDate"); - const watchEndDate = formControl.watch("endDate"); + const watchStartDate = formControl.watch('startDate') + const watchEndDate = formControl.watch('endDate') // Component to display warning for large date ranges const DateRangeWarning = () => { - if (!watchStartDate || !watchEndDate) return null; + if (!watchStartDate || !watchEndDate) return null - const startDateMs = new Date(watchStartDate * 1000); - const endDateMs = new Date(watchEndDate * 1000); - const daysDifference = (endDateMs - startDateMs) / (1000 * 60 * 60 * 24); + const startDateMs = new Date(watchStartDate * 1000) + const endDateMs = new Date(watchEndDate * 1000) + const daysDifference = (endDateMs - startDateMs) / (1000 * 60 * 60 * 24) if (daysDifference > 10) { return ( @@ -88,11 +88,11 @@ const Page = () => { narrowing your date range if you encounter issues. - ); + ) } - return null; - }; + return null + } const onSubmit = (data) => { // Check if any filter is applied @@ -100,51 +100,51 @@ const Page = () => { data.startDate !== null || data.endDate !== null || data.username !== null || - data.severity?.length > 0; - setFilterEnabled(hasFilter); + data.severity?.length > 0 + setFilterEnabled(hasFilter) // Format start date if available setStartDate( data.startDate - ? new Date(data.startDate * 1000).toISOString().split("T")[0].replace(/-/g, "") - : null, - ); + ? new Date(data.startDate * 1000).toISOString().split('T')[0].replace(/-/g, '') + : null + ) // Format end date if available setEndDate( data.endDate - ? new Date(data.endDate * 1000).toISOString().split("T")[0].replace(/-/g, "") - : null, - ); + ? new Date(data.endDate * 1000).toISOString().split('T')[0].replace(/-/g, '') + : null + ) // Set username filter if available - setUsername(data.username || null); + setUsername(data.username || null) // Set severity filter if available (convert array to comma-separated string) setSeverity( data.severity && data.severity.length > 0 - ? data.severity.map((item) => item.value).join(",") - : null, - ); + ? data.severity.map((item) => item.value).join(',') + : null + ) // Close the accordion after applying filters - setExpanded(false); - }; + setExpanded(false) + } const clearFilters = () => { formControl.reset({ startDate: null, endDate: null, - username: "", + username: '', severity: [], - }); - setFilterEnabled(false); - setStartDate(null); - setEndDate(null); - setUsername(null); - setSeverity(null); - setExpanded(false); // Close the accordion when clearing filters - }; + }) + setFilterEnabled(false) + setStartDate(null) + setEndDate(null) + setUsername(null) + setSeverity(null) + setExpanded(false) // Close the accordion when clearing filters + } return ( { Logbook Filters {filterEnabled ? ( - + ( {startDate || endDate ? ( <> {startDate ? new Date( - startDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00", + startDate.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3') + 'T00:00:00' ).toLocaleDateString() : new Date().toLocaleDateString()} - {startDate && endDate ? " - " : ""} + {startDate && endDate ? ' - ' : ''} {endDate ? new Date( - endDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00", + endDate.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3') + 'T00:00:00' ).toLocaleDateString() - : ""} + : ''} ) : null} - {username && (startDate || endDate) && " | "} + {username && (startDate || endDate) && ' | '} {username && <>User: {username}} - {severity && (username || startDate || endDate) && " | "} - {severity && <>Severity: {severity.replace(/,/g, ", ")}}) + {severity && (username || startDate || endDate) && ' | '} + {severity && <>Severity: {severity.replace(/,/g, ', ')}}) ) : ( - + (Today: {new Date().toLocaleDateString()}) )} @@ -196,7 +196,7 @@ const Page = () => { Use the filters below to narrow down your logbook results. You can filter by date range, username, and severity levels. By default, the logbook shows the - current day based on UTC time. Your local time is{" "} + current day based on UTC time. Your local time is{' '} {new Date().getTimezoneOffset() / -60} hours offset from UTC. @@ -220,18 +220,18 @@ const Page = () => { formControl={formControl} validators={{ validate: (value) => { - const startDate = formControl.getValues("startDate"); + const startDate = formControl.getValues('startDate') if (value && !startDate) { - return "Start date must be set when using an end date"; + return 'Start date must be set when using an end date' } if ( startDate && value && new Date(value * 1000) < new Date(startDate * 1000) ) { - return "End date must be after start date"; + return 'End date must be after start date' } - return true; + return true }, }} /> @@ -263,12 +263,12 @@ const Page = () => { formControl={formControl} multiple={true} options={[ - { value: "Info", label: "Info" }, - { value: "Warn", label: "Warning" }, - { value: "Error", label: "Error" }, - { value: "Critical", label: "Critical" }, - { value: "Alert", label: "Alert" }, - { value: "Debug", label: "Debug" }, + { value: 'Info', label: 'Info' }, + { value: 'Warn', label: 'Warning' }, + { value: 'Error', label: 'Error' }, + { value: 'Critical', label: 'Critical' }, + { value: 'Alert', label: 'Alert' }, + { value: 'Debug', label: 'Debug' }, ]} placeholder="Select severity levels" /> @@ -312,7 +312,7 @@ const Page = () => { apiUrl={apiUrl} simpleColumns={simpleColumns} queryKey={`Listlogs-${startDate}-${endDate}-${username}-${severity}-${filterEnabled}-${currentTenant}`} - tenantInTitle={true} + tenantInTitle={false} apiData={{ StartDate: startDate, // Pass start date filter from state EndDate: endDate, // Pass end date filter from state @@ -324,9 +324,9 @@ const Page = () => { actions={actions} offCanvas={offcanvas} /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page} -export default Page; +export default Page From e0bc9303c78c9303afb3897330496f30ab2dd48b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:01:17 +0800 Subject: [PATCH 05/13] Update add.jsx --- src/pages/tools/custom-tests/add.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/tools/custom-tests/add.jsx b/src/pages/tools/custom-tests/add.jsx index 6951fe57c507..df7fe5f1cd50 100644 --- a/src/pages/tools/custom-tests/add.jsx +++ b/src/pages/tools/custom-tests/add.jsx @@ -300,6 +300,7 @@ const Page = () => { label: 'Category', type: 'autoComplete', required: true, + multiple: false, placeholder: 'Select or enter a category', options: categoryOptions, creatable: true, From 21057020d6ae50519d8c002cb7f40512e2d3aed4 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:23:46 +0800 Subject: [PATCH 06/13] Fix password settings to bool values --- .../cipp/settings/password-config/index.js | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/pages/cipp/settings/password-config/index.js b/src/pages/cipp/settings/password-config/index.js index 540dd2853b2f..06d2c452a60a 100644 --- a/src/pages/cipp/settings/password-config/index.js +++ b/src/pages/cipp/settings/password-config/index.js @@ -41,32 +41,32 @@ function normalizeConfigForBackend(config) { return { passwordType: String(config.passwordType || PASSWORD_TYPES.CLASSIC), charCount: String(parseInt(config.charCount, 10) || DEFAULT_VALUES.CHAR_COUNT), - includeUppercase: String(Boolean(config.includeUppercase)), - includeLowercase: String(Boolean(config.includeLowercase)), - includeDigits: String(Boolean(config.includeDigits)), - includeSpecialChars: String(Boolean(config.includeSpecialChars)), + includeUppercase: Boolean(config.includeUppercase), + includeLowercase: Boolean(config.includeLowercase), + includeDigits: Boolean(config.includeDigits), + includeSpecialChars: Boolean(config.includeSpecialChars), specialCharSet: String(config.specialCharSet || DEFAULT_VALUES.SPECIAL_CHAR_SET), wordCount: String(parseInt(config.wordCount, 10) || DEFAULT_VALUES.WORD_COUNT), separator: config.separator !== undefined && config.separator !== null ? String(config.separator) : DEFAULT_VALUES.SEPARATOR, - capitalizeWords: String(Boolean(config.capitalizeWords)), - appendNumber: String(Boolean(config.appendNumber)), - appendSpecialChar: String(Boolean(config.appendSpecialChar)), + capitalizeWords: Boolean(config.capitalizeWords), + appendNumber: Boolean(config.appendNumber), + appendSpecialChar: Boolean(config.appendSpecialChar), }; } const DEFAULT_CONFIG = { - passwordType: PASSWORD_TYPES.CLASSIC, + passwordType: PASSWORD_TYPES.CLASSIC, charCount: String(DEFAULT_VALUES.CHAR_COUNT), - includeUppercase: true, - includeLowercase: true, + includeUppercase: true, + includeLowercase: true, includeDigits: true, - includeSpecialChars: true, + includeSpecialChars: true, specialCharSet: DEFAULT_VALUES.SPECIAL_CHAR_SET, - wordCount: String(DEFAULT_VALUES.WORD_COUNT), + wordCount: String(DEFAULT_VALUES.WORD_COUNT), separator: DEFAULT_VALUES.SEPARATOR, - capitalizeWords: false, - appendNumber: false, + capitalizeWords: false, + appendNumber: false, appendSpecialChar: false, }; @@ -89,7 +89,7 @@ const Page = () => { if (typeof v === 'number') return v === 1; return def; }; - + setConfig({ passwordType: r.passwordType || DEFAULT_CONFIG.passwordType, charCount: String(parseInt(r.charCount, 10) || DEFAULT_CONFIG.charCount), @@ -115,11 +115,11 @@ const Page = () => { const handleSave = () => { const normalizedConfig = normalizeConfigForBackend(config); - + passwordSave.mutate( - { - url: "/api/ExecPasswordConfig", - data: normalizedConfig, + { + url: "/api/ExecPasswordConfig", + data: normalizedConfig, queryKey: "PasswordSettingsPost", } ); From 24304c6db3dcfe8b0302b3eb527d7e75757e2fa6 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:50:12 +0800 Subject: [PATCH 07/13] Fix for tenant group being set as an object rather than an array --- .../administration/alert-configuration/alert.jsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index dff059067894..d9af06f2b549 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -166,12 +166,14 @@ const AlertWizard = () => { } else if (alert.RawAlert.TenantGroup) { try { const tenantGroupObject = JSON.parse(alert.RawAlert.TenantGroup) - tenantFilterForForm = { - value: tenantGroupObject.value, - label: tenantGroupObject.label, - type: 'Group', - addedFields: tenantGroupObject, - } + tenantFilterForForm = [ + { + value: tenantGroupObject.value, + label: tenantGroupObject.label, + type: 'Group', + addedFields: tenantGroupObject, + }, + ] } catch (error) { console.error('Error parsing tenant group:', error) tenantFilterForForm = [ From 4a30f432757a4b007887f226d51f562276dac017 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 28 Apr 2026 12:03:08 -0400 Subject: [PATCH 08/13] fix: GDAP overview page --- src/pages/tenant/gdap-management/index.js | 134 +++++++++++----------- 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/src/pages/tenant/gdap-management/index.js b/src/pages/tenant/gdap-management/index.js index 25e1a1308489..f1bde321dd71 100644 --- a/src/pages/tenant/gdap-management/index.js +++ b/src/pages/tenant/gdap-management/index.js @@ -1,99 +1,103 @@ -import { TabbedLayout } from "../../../layouts/TabbedLayout"; -import { Layout as DashboardLayout } from "../../../layouts/index.js"; -import tabOptions from "./tabOptions"; -import { Container } from "@mui/system"; -import { Grid } from "@mui/system"; -import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; -import { ApiPostCall, ApiGetCallWithPagination } from "../../../api/ApiCall"; +import { TabbedLayout } from '../../../layouts/TabbedLayout' +import { Layout as DashboardLayout } from '../../../layouts/index.js' +import tabOptions from './tabOptions' +import { Container } from '@mui/system' +import { Grid } from '@mui/system' +import { CippInfoBar } from '../../../components/CippCards/CippInfoBar' +import { ApiPostCall, ApiGetCallWithPagination } from '../../../api/ApiCall' import { Add, AdminPanelSettings, HourglassBottom, Layers, SupervisorAccount, -} from "@mui/icons-material"; -import CippPermissionCheck from "../../../components/CippSettings/CippPermissionCheck"; -import { Button } from "@mui/material"; -import { useEffect, useState } from "react"; -import CippButtonCard from "../../../components/CippCards/CippButtonCard"; -import { WizardSteps } from "../../../components/CippWizard/wizard-steps"; -import Link from "next/link"; -import { CippHead } from "../../../components/CippComponents/CippHead"; -import { usePermissions } from "../../../hooks/use-permissions"; +} from '@mui/icons-material' +import CippPermissionCheck from '../../../components/CippSettings/CippPermissionCheck' +import { Button } from '@mui/material' +import { useEffect, useState } from 'react' +import CippButtonCard from '../../../components/CippCards/CippButtonCard' +import { WizardSteps } from '../../../components/CippWizard/wizard-steps' +import Link from 'next/link' +import { CippHead } from '../../../components/CippComponents/CippHead' +import { usePermissions } from '../../../hooks/use-permissions' const Page = () => { - const [createDefaults, setCreateDefaults] = useState(false); - const [activeStep, setActiveStep] = useState(0); - const { checkRoles } = usePermissions(); - const canViewGdapChecks = checkRoles(["CIPP.AppSettings.Read"]); + const [createDefaults, setCreateDefaults] = useState(false) + const [activeStep, setActiveStep] = useState(0) + const { checkPermissions } = usePermissions() + const canViewGdapChecks = checkPermissions(['CIPP.AppSettings.Read']) const relationships = ApiGetCallWithPagination({ - url: "/api/ListGDAPRelationships", - queryKey: "ListGDAPRelationships", - }); + url: '/api/ListGDAPRelationships', + queryKey: 'ListGDAPRelationships', + waiting: true, + }) const mappedRoles = ApiGetCallWithPagination({ - url: "/api/ListGDAPRoles", - queryKey: "ListGDAPRoles", - }); + url: '/api/ListGDAPRoles', + queryKey: 'ListGDAPRoles', + waiting: true, + }) const roleTemplates = ApiGetCallWithPagination({ - url: "/api/ExecGDAPRoleTemplate", - queryKey: "ListGDAPRoleTemplates", - }); + url: '/api/ExecGDAPRoleTemplate', + queryKey: 'ListGDAPRoleTemplates', + waiting: true, + }) const pendingInvites = ApiGetCallWithPagination({ - url: "/api/ListGDAPInvite", - queryKey: "ListGDAPInvite", - }); + url: '/api/ListGDAPInvite', + queryKey: 'ListGDAPInvite', + waiting: true, + }) const createCippDefaults = ApiPostCall({ urlFromData: true, - relatedQueryKeys: ["ListGDAPRoleTemplates", "ListGDAPRoles"], - }); + relatedQueryKeys: ['ListGDAPRoleTemplates', 'ListGDAPRoles'], + }) useEffect(() => { if (roleTemplates.isSuccess) { - var promptCreateDefaults = true; + var promptCreateDefaults = true // check templates for CIPP Defaults - const firstPageResults = roleTemplates?.data?.pages?.[0]?.Results; + const firstPageResults = roleTemplates?.data?.pages?.[0]?.Results if ( firstPageResults && Array.isArray(firstPageResults) && firstPageResults.length > 0 && - firstPageResults.find((t) => t?.TemplateId === "CIPP Defaults") + firstPageResults.find((t) => t?.TemplateId === 'CIPP Defaults') ) { - promptCreateDefaults = false; + promptCreateDefaults = false } - setCreateDefaults(promptCreateDefaults); + setCreateDefaults(promptCreateDefaults) } - }, [roleTemplates]); + }, [roleTemplates]) useEffect(() => { if (mappedRoles.isSuccess && roleTemplates.isSuccess && pendingInvites.isSuccess) { - const mappedRolesFirstPage = mappedRoles?.data?.pages?.[0]; + const mappedRolesFirstPage = mappedRoles?.data?.pages?.[0] if ( mappedRolesFirstPage && Array.isArray(mappedRolesFirstPage) && mappedRolesFirstPage.length > 0 ) { - setActiveStep(1); + setActiveStep(1) - const roleTemplatesFirstPage = roleTemplates?.data?.pages?.[0]?.Results; + const roleTemplatesFirstPage = roleTemplates?.data?.pages?.[0]?.Results if ( roleTemplatesFirstPage && Array.isArray(roleTemplatesFirstPage) && roleTemplatesFirstPage.length > 0 ) { - setActiveStep(2); + setActiveStep(2) - const pendingInvitesFirstPage = pendingInvites?.data?.pages?.[0]; + const pendingInvitesFirstPage = pendingInvites?.data?.pages?.[0] if ( pendingInvitesFirstPage && Array.isArray(pendingInvitesFirstPage) && pendingInvitesFirstPage.length > 0 ) { - setActiveStep(4); + setActiveStep(4) } } } @@ -104,7 +108,7 @@ const Page = () => { roleTemplates.isSuccess, roleTemplates.isFetching, pendingInvites.isSuccess, - ]); + ]) return ( { relationships.data?.pages ?.map((page) => page?.Results?.length || 0) .reduce((a, b) => (a || 0) + (b || 0), 0) ?? 0, - name: "GDAP Relationships", - color: "secondary", + name: 'GDAP Relationships', + color: 'secondary', }, { icon: , @@ -140,8 +144,8 @@ const Page = () => { mappedRoles.data?.pages ?.map((page) => page?.length || 0) .reduce((a, b) => (a || 0) + (b || 0), 0) ?? 0, - name: "Mapped Admin Roles", - color: "green", + name: 'Mapped Admin Roles', + color: 'green', }, { icon: , @@ -149,7 +153,7 @@ const Page = () => { roleTemplates.data?.pages ?.map((page) => page?.Results?.length || 0) .reduce((a, b) => (a || 0) + (b || 0), 0) ?? 0, - name: "Role Templates", + name: 'Role Templates', }, { icon: , @@ -157,7 +161,7 @@ const Page = () => { pendingInvites.data?.pages ?.map((page) => page?.length || 0) .reduce((a, b) => (a || 0) + (b || 0), 0) ?? 0, - name: "Pending Invites", + name: 'Pending Invites', }, ]} /> @@ -177,27 +181,27 @@ const Page = () => { { )} - ); -}; + ) +} Page.getLayout = (page) => ( {page} -); +) -export default Page; +export default Page From 9346fe7f48ec34666b3a1b2adb87d4318a37a1de Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 28 Apr 2026 16:21:50 -0400 Subject: [PATCH 09/13] fix: add caching to onedrive/sharepoint site lists --- .../CippComponents/CippReportDBControls.jsx | 100 ++++--- src/data/CIPPDBCacheTypes.json | 15 + src/pages/teams-share/onedrive/index.js | 121 ++++---- src/pages/teams-share/sharepoint/index.js | 269 ++++++++++-------- 4 files changed, 280 insertions(+), 225 deletions(-) diff --git a/src/components/CippComponents/CippReportDBControls.jsx b/src/components/CippComponents/CippReportDBControls.jsx index 063fd386bb5b..84e7955830db 100644 --- a/src/components/CippComponents/CippReportDBControls.jsx +++ b/src/components/CippComponents/CippReportDBControls.jsx @@ -1,11 +1,11 @@ -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"; +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. @@ -44,80 +44,79 @@ export function useCippReportDB(config) { syncData, allowToggle = true, defaultCached = true, - cacheColumns = ["CacheTimestamp"], - tenantColumn = "Tenant", + cacheColumns = ['CacheTimestamp'], + tenantColumn = 'Tenant', apiData: extraApiData, - } = config; + } = config - const currentTenant = useSettings().currentTenant; - const isAllTenants = currentTenant === "AllTenants"; - const dialog = useDialog(); - const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(defaultCached); + 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); + setUseReportDB(true) } else { - setUseReportDB(defaultCached); + setUseReportDB(defaultCached) } - }, [currentTenant, isAllTenants, defaultCached]); + }, [currentTenant, isAllTenants, defaultCached]) // Whether the toggle is actually clickable - const canToggle = allowToggle && !isAllTenants; + 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]); + 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 + // Keep mode flag in the URL only; CippTablePage merges apiData into query params. const resolvedApiData = useMemo(() => { - if (!useReportDB && !extraApiData) return undefined; + if (!extraApiData) return undefined return { - ...(extraApiData || {}), - ...(useReportDB ? { UseReportDB: true } : {}), - }; - }, [useReportDB, extraApiData]); + ...extraApiData, + } + }, [extraApiData]) // Query key that includes tenant + mode for proper cache separation const resolvedQueryKey = useMemo(() => { - return `${queryKey}-${currentTenant}-${useReportDB}`; - }, [queryKey, currentTenant, useReportDB]); + return `${queryKey}-${currentTenant}-${useReportDB}` + }, [queryKey, currentTenant, useReportDB]) // Extra columns to show when in cached mode const extraColumns = useMemo(() => { - const cols = []; + const cols = [] if (useReportDB && isAllTenants) { - cols.push(tenantColumn); + cols.push(tenantColumn) } if (useReportDB) { - cols.push(...cacheColumns); + cols.push(...cacheColumns) } - return cols; - }, [useReportDB, isAllTenants, tenantColumn, cacheColumns]); + return cols + }, [useReportDB, isAllTenants, tenantColumn, cacheColumns]) const handleSyncSuccess = useCallback((result) => { if (result?.Metadata?.QueueId) { - setSyncQueueId(result.Metadata.QueueId); + setSyncQueueId(result.Metadata.QueueId) } - }, []); + }, []) // Tooltip text const tooltipText = !allowToggle - ? "This page always uses cached data from the CIPP reporting database." + ? 'This page always uses cached data from the CIPP reporting database.' : isAllTenants - ? "AllTenants always uses cached data" + ? 'AllTenants always uses cached data' : useReportDB - ? "Showing cached data — click to switch to live" - : "Showing live data — click to switch to cache"; + ? '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.`; + `Run ${cacheName} cache sync for ${currentTenant}? This will update data immediately.` // The controls JSX const controls = ( @@ -147,7 +146,7 @@ export function useCippReportDB(config) { : } - label={useReportDB ? "Cached" : "Live"} + label={useReportDB ? 'Cached' : 'Live'} color="primary" size="small" onClick={canToggle ? () => setUseReportDB((prev) => !prev) : undefined} @@ -158,7 +157,7 @@ export function useCippReportDB(config) { - ); + ) // The sync dialog JSX — render alongside the table page const syncDialogElement = ( @@ -167,19 +166,18 @@ export function useCippReportDB(config) { title={syncTitle} fields={[]} api={{ - type: "GET", - url: "/api/ExecCIPPDBCache", + type: 'GET', + url: '/api/ExecCIPPDBCache', confirmText, relatedQueryKeys: [`${queryKey}-${currentTenant}-true`], data: { Name: cacheName, - Types: "None", ...(syncData || {}), }, onSuccess: handleSyncSuccess, }} /> - ); + ) return { useReportDB, @@ -191,5 +189,5 @@ export function useCippReportDB(config) { cacheColumns: extraColumns, controls, syncDialog: syncDialogElement, - }; + } } diff --git a/src/data/CIPPDBCacheTypes.json b/src/data/CIPPDBCacheTypes.json index 8ff123ff4aff..8742001441cd 100644 --- a/src/data/CIPPDBCacheTypes.json +++ b/src/data/CIPPDBCacheTypes.json @@ -254,11 +254,26 @@ "friendlyName": "Mailbox Usage", "description": "Exchange Online mailbox usage statistics" }, + { + "type": "OneDriveSiteListing", + "friendlyName": "OneDrive Site Listing", + "description": "OneDrive personal site listing details used for usage reporting" + }, { "type": "OneDriveUsage", "friendlyName": "OneDrive Usage", "description": "OneDrive usage statistics" }, + { + "type": "SharePointSiteListing", + "friendlyName": "SharePoint Site Listing", + "description": "SharePoint site listing details used for usage reporting" + }, + { + "type": "SharePointSiteUsage", + "friendlyName": "SharePoint Site Usage", + "description": "SharePoint site usage statistics" + }, { "type": "ConditionalAccessPolicies", "friendlyName": "Conditional Access Policies", diff --git a/src/pages/teams-share/onedrive/index.js b/src/pages/teams-share/onedrive/index.js index 8d279cffaf73..b5ef1f7a2792 100644 --- a/src/pages/teams-share/onedrive/index.js +++ b/src/pages/teams-share/onedrive/index.js @@ -1,43 +1,52 @@ -import { Layout as DashboardLayout } from "../../../layouts/index.js"; -import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; -import { PersonAdd, PersonRemove } from "@mui/icons-material"; +import { Layout as DashboardLayout } from '../../../layouts/index.js' +import { CippTablePage } from '../../../components/CippComponents/CippTablePage.jsx' +import { PersonAdd, PersonRemove } from '@mui/icons-material' +import { useCippReportDB } from '../../../components/CippComponents/CippReportDBControls' const Page = () => { - const pageTitle = "OneDrive"; + const pageTitle = 'OneDrive' + const reportDB = useCippReportDB({ + apiUrl: '/api/ListSites?type=OneDriveUsageAccount', + queryKey: 'ListSites-OneDriveUsageAccount', + cacheName: 'OneDriveUsage', + syncTitle: 'Sync OneDrive Usage', + allowToggle: true, + defaultCached: false, + }) const actions = [ { - label: "Add permissions to OneDrive", + label: 'Add permissions to OneDrive', icon: , - type: "POST", - url: "/api/ExecSharePointPerms", + type: 'POST', + url: '/api/ExecSharePointPerms', data: { - UPN: "ownerPrincipalName", - URL: "webUrl", + UPN: 'ownerPrincipalName', + URL: 'webUrl', RemovePermission: false, }, confirmText: "Select the User to add to this user's OneDrive permissions", fields: [ { - type: "autoComplete", - name: "onedriveAccessUser", - label: "Select User", + type: 'autoComplete', + name: 'onedriveAccessUser', + label: 'Select User', multiple: false, creatable: false, api: { - url: "/api/ListGraphRequest", + url: '/api/ListGraphRequest', data: { - Endpoint: "users", - $select: "id,displayName,userPrincipalName", + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName', $top: 999, $count: true, }, - queryKey: "ListUsersAutoComplete", - dataKey: "Results", + queryKey: 'ListUsersAutoComplete', + dataKey: 'Results', labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, - valueField: "userPrincipalName", + valueField: 'userPrincipalName', addedField: { - id: "id", + id: 'id', }, showRefresh: true, }, @@ -45,57 +54,67 @@ const Page = () => { ], }, { - label: "Remove permissions from OneDrive", + label: 'Remove permissions from OneDrive', icon: , - type: "POST", - url: "/api/ExecSharePointPerms", + type: 'POST', + url: '/api/ExecSharePointPerms', data: { - UPN: "ownerPrincipalName", - URL: "webUrl", + UPN: 'ownerPrincipalName', + URL: 'webUrl', RemovePermission: true, }, confirmText: "Select the User to remove from this user's OneDrive permissions", fields: [ { - type: "autoComplete", - name: "onedriveAccessUser", - label: "Select User", + type: 'autoComplete', + name: 'onedriveAccessUser', + label: 'Select User', multiple: false, creatable: false, api: { - url: "/api/listUsers", + url: '/api/listUsers', labelField: (onedriveAccessUser) => `${onedriveAccessUser.displayName} (${onedriveAccessUser.userPrincipalName})`, - valueField: "userPrincipalName", + valueField: 'userPrincipalName', addedField: { - displayName: "displayName", + displayName: 'displayName', }, }, }, ], }, - ]; + ] + + const simpleColumns = [ + ...reportDB.cacheColumns.filter((c) => c === 'Tenant'), + 'displayName', + 'createdDateTime', + 'ownerPrincipalName', + 'lastActivityDate', + 'fileCount', + 'storageUsedInGigabytes', + 'storageAllocatedInGigabytes', + 'reportRefreshDate', + 'webUrl', + ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'), + ] return ( - - ); -}; + <> + + {reportDB.syncDialog} + + ) +} -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page} -export default Page; +export default Page diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index 21cefc406ca4..ac08cec280cd 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -1,6 +1,6 @@ -import { Layout as DashboardLayout } from "../../../layouts/index.js"; -import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; -import { Button } from "@mui/material"; +import { Layout as DashboardLayout } from '../../../layouts/index.js' +import { CippTablePage } from '../../../components/CippComponents/CippTablePage.jsx' +import { Button } from '@mui/material' import { Add, AddToPhotos, @@ -9,49 +9,59 @@ import { AdminPanelSettings, NoAccounts, Delete, -} from "@mui/icons-material"; -import Link from "next/link"; -import { CippDataTable } from "../../../components/CippTable/CippDataTable"; -import { useSettings } from "../../../hooks/use-settings"; +} from '@mui/icons-material' +import Link from 'next/link' +import { Stack } from '@mui/system' +import { CippDataTable } from '../../../components/CippTable/CippDataTable' +import { useSettings } from '../../../hooks/use-settings' +import { useCippReportDB } from '../../../components/CippComponents/CippReportDBControls' const Page = () => { - const pageTitle = "SharePoint Sites"; - const tenantFilter = useSettings().currentTenant; + const pageTitle = 'SharePoint Sites' + const tenantFilter = useSettings().currentTenant + const reportDB = useCippReportDB({ + apiUrl: '/api/ListSites?type=SharePointSiteUsage', + queryKey: 'ListSites-SharePointSiteUsage', + cacheName: 'SharePointSiteUsage', + syncTitle: 'Sync SharePoint Site Usage', + allowToggle: true, + defaultCached: false, + }) const actions = [ { - label: "Add Member", - type: "POST", + label: 'Add Member', + type: 'POST', icon: , - url: "/api/ExecSetSharePointMember", + url: '/api/ExecSetSharePointMember', data: { - groupId: "ownerPrincipalName", + groupId: 'ownerPrincipalName', add: true, - URL: "webUrl", - SharePointType: "rootWebTemplate", + URL: 'webUrl', + SharePointType: 'rootWebTemplate', }, - confirmText: "Select the User to add as a member.", + confirmText: 'Select the User to add as a member.', fields: [ { - type: "autoComplete", - name: "user", - label: "Select User", + type: 'autoComplete', + name: 'user', + label: 'Select User', multiple: false, creatable: false, api: { - url: "/api/ListGraphRequest", + url: '/api/ListGraphRequest', data: { - Endpoint: "users", - $select: "id,displayName,userPrincipalName", + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName', $top: 999, $count: true, }, - queryKey: "ListUsersAutoComplete", - dataKey: "Results", + queryKey: 'ListUsersAutoComplete', + dataKey: 'Results', labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, - valueField: "userPrincipalName", + valueField: 'userPrincipalName', addedField: { - id: "id", + id: 'id', }, showRefresh: true, }, @@ -60,38 +70,38 @@ const Page = () => { multiPost: false, }, { - label: "Remove Member", - type: "POST", + label: 'Remove Member', + type: 'POST', icon: , - url: "/api/ExecSetSharePointMember", + url: '/api/ExecSetSharePointMember', data: { - groupId: "ownerPrincipalName", + groupId: 'ownerPrincipalName', add: false, - URL: "URL", - SharePointType: "rootWebTemplate", + URL: 'URL', + SharePointType: 'rootWebTemplate', }, - confirmText: "Select the User to remove as a member.", + confirmText: 'Select the User to remove as a member.', fields: [ { - type: "autoComplete", - name: "user", - label: "Select User", + type: 'autoComplete', + name: 'user', + label: 'Select User', multiple: false, creatable: false, api: { - url: "/api/ListGraphRequest", + url: '/api/ListGraphRequest', data: { - Endpoint: "users", - $select: "id,displayName,userPrincipalName", + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName', $top: 999, $count: true, }, - queryKey: "ListUsersAutoComplete", - dataKey: "Results", + queryKey: 'ListUsersAutoComplete', + dataKey: 'Results', labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, - valueField: "userPrincipalName", + valueField: 'userPrincipalName', addedField: { - id: "id", + id: 'id', }, showRefresh: true, }, @@ -100,37 +110,37 @@ const Page = () => { multiPost: false, }, { - label: "Add Site Admin", - type: "POST", + label: 'Add Site Admin', + type: 'POST', icon: , - url: "/api/ExecSharePointPerms", + url: '/api/ExecSharePointPerms', data: { - UPN: "ownerPrincipalName", + UPN: 'ownerPrincipalName', RemovePermission: false, - URL: "webUrl", + URL: 'webUrl', }, - confirmText: "Select the User to add to the Site Admins permissions", + confirmText: 'Select the User to add to the Site Admins permissions', fields: [ { - type: "autoComplete", - name: "user", - label: "Select User", + type: 'autoComplete', + name: 'user', + label: 'Select User', multiple: false, creatable: false, api: { - url: "/api/ListGraphRequest", + url: '/api/ListGraphRequest', data: { - Endpoint: "users", - $select: "id,displayName,userPrincipalName", + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName', $top: 999, $count: true, }, - queryKey: "ListUsersAutoComplete", - dataKey: "Results", + queryKey: 'ListUsersAutoComplete', + dataKey: 'Results', labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, - valueField: "userPrincipalName", + valueField: 'userPrincipalName', addedField: { - id: "id", + id: 'id', }, showRefresh: true, }, @@ -139,37 +149,37 @@ const Page = () => { multiPost: false, }, { - label: "Remove Site Admin", - type: "POST", + label: 'Remove Site Admin', + type: 'POST', icon: , - url: "/api/ExecSharePointPerms", + url: '/api/ExecSharePointPerms', data: { - UPN: "ownerPrincipalName", + UPN: 'ownerPrincipalName', RemovePermission: true, - URL: "webUrl", + URL: 'webUrl', }, - confirmText: "Select the User to remove from the Site Admins permissions", + confirmText: 'Select the User to remove from the Site Admins permissions', fields: [ { - type: "autoComplete", - name: "user", - label: "Select User", + type: 'autoComplete', + name: 'user', + label: 'Select User', multiple: false, creatable: false, api: { - url: "/api/ListGraphRequest", + url: '/api/ListGraphRequest', data: { - Endpoint: "users", - $select: "id,displayName,userPrincipalName", + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName', $top: 999, $count: true, }, - queryKey: "ListUsersAutoComplete", - dataKey: "Results", + queryKey: 'ListUsersAutoComplete', + dataKey: 'Results', labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, - valueField: "userPrincipalName", + valueField: 'userPrincipalName', addedField: { - id: "id", + id: 'id', }, showRefresh: true, }, @@ -178,75 +188,88 @@ const Page = () => { multiPost: false, }, { - label: "Delete Site", - type: "POST", + label: 'Delete Site', + type: 'POST', icon: , - url: "/api/DeleteSharepointSite", + url: '/api/DeleteSharepointSite', data: { - SiteId: "siteId", + SiteId: 'siteId', }, - confirmText: "Are you sure you want to delete this SharePoint site? This action cannot be undone.", - color: "error", + confirmText: + 'Are you sure you want to delete this SharePoint site? This action cannot be undone.', + color: 'error', multiPost: false, }, - ]; + ] const offCanvas = { - extendedInfoFields: ["displayName", "description", "webUrl"], + extendedInfoFields: ['displayName', 'description', 'webUrl'], actions: actions, children: (row) => ( ), - size: "lg", // Make the offcanvas extra large - }; + size: 'lg', // Make the offcanvas extra large + } + + const simpleColumns = [ + ...reportDB.cacheColumns.filter((c) => c === 'Tenant'), + 'displayName', + 'createdDateTime', + 'ownerPrincipalName', + 'lastActivityDate', + 'fileCount', + 'storageUsedInGigabytes', + 'storageAllocatedInGigabytes', + 'reportRefreshDate', + 'webUrl', + ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'), + ] + + const pageActions = ( + + + + {reportDB.controls} + + ) return ( - - - - - } - /> - ); -}; + <> + + {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) { <>