From dadec81d4cb5801863be582e517bf6de52eb669c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 18 Dec 2025 22:00:15 -0500 Subject: [PATCH 01/21] Update _app.js --- src/pages/_app.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pages/_app.js b/src/pages/_app.js index a26f46d7f701..22753fa0beed 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -366,15 +366,13 @@ const App = (props) => { {(settings) => { - if (!settings.isInitialized) { - return null; // Don't render until settings are loaded - } + // Create theme even while initializing to avoid blank screen const theme = createTheme({ colorPreset: "orange", - direction: settings.direction, + direction: settings.direction || "ltr", paletteMode: settings.currentTheme?.value !== "browser" - ? settings.currentTheme?.value + ? settings.currentTheme?.value || "light" : preferredTheme, contrast: "high", }); @@ -384,13 +382,15 @@ const App = (props) => { - - - - {getLayout()} - - - + {settings.isInitialized ? ( + + + + {getLayout()} + + + + ) : null} Date: Thu, 18 Dec 2025 22:14:21 -0500 Subject: [PATCH 02/21] Remove isInitialized check from app rendering The conditional rendering based on settings.isInitialized was removed from _app.js, allowing the main app components to render regardless of initialization state. Also, minor formatting changes were made to the CippTenantSelector effect dependencies for readability. --- .../CippComponents/CippTenantSelector.jsx | 8 +++++++- src/pages/_app.js | 16 +++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index 735ecfe82d40..de58af87c29a 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -230,7 +230,13 @@ export const CippTenantSelector = (props) => { }); } } - }, [router.isReady, router.query.tenantFilter, tenantList.isSuccess, settings.currentTenant, settings.isInitialized]); + }, [ + router.isReady, + router.query.tenantFilter, + tenantList.isSuccess, + settings.currentTenant, + settings.isInitialized, + ]); // This effect ensures the tenant filter parameter is included in the URL when missing useEffect(() => { diff --git a/src/pages/_app.js b/src/pages/_app.js index 22753fa0beed..b9b16e9d3ecd 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -382,15 +382,13 @@ const App = (props) => { - {settings.isInitialized ? ( - - - - {getLayout()} - - - - ) : null} + + + + {getLayout()} + + + Date: Fri, 19 Dec 2025 09:17:19 -0500 Subject: [PATCH 03/21] Refactor tenant selection and URL sync logic Simplifies tenant selection by making the URL parameter the single source of truth for tenant changes. Updates settings only when the URL changes, and removes redundant settings updates from the selection handler. Also streamlines effect dependencies and improves comments for clarity. --- .../CippComponents/CippTenantSelector.jsx | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index de58af87c29a..688afab83bb8 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -184,7 +184,7 @@ export const CippTenantSelector = (props) => { // Cancel all in-flight queries before changing tenant queryClient.cancelQueries(); - // Update router and settings + // Update router only - let the URL watcher handle settings query.tenantFilter = currentTenant.value; router.replace( { @@ -194,53 +194,52 @@ export const CippTenantSelector = (props) => { undefined, { shallow: true } ); - - settings.handleUpdate({ - currentTenant: currentTenant.value, - }); } - //if we have a tenantfilter, we add the tenantfilter to the title of the tab/page so its "Tenant - original title". } }, [currentTenant?.value]); - // This effect handles when the URL parameter changes externally + // This effect handles when the URL parameter changes (from deep link or user selection) + // This is the single source of truth for tenant changes useEffect(() => { - if (!router.isReady || !tenantList.isSuccess || !settings.isInitialized) return; + if (!router.isReady || !tenantList.isSuccess) return; - // Get the current tenant from URL or settings - const urlTenant = router.query.tenantFilter || settings.currentTenant; + const urlTenant = router.query.tenantFilter; - // Only update if there's a URL tenant and it's different from our current state - if (urlTenant && (!currentTenant || urlTenant !== currentTenant.value)) { + // Only process if we have a URL tenant + if (urlTenant) { // Find the tenant in our list const matchingTenant = tenantList.data.find( ({ defaultDomainName }) => defaultDomainName === urlTenant ); if (matchingTenant) { - setSelectedTenant({ - value: urlTenant, - label: `${matchingTenant.displayName} (${urlTenant})`, - addedFields: { - defaultDomainName: matchingTenant.defaultDomainName, - displayName: matchingTenant.displayName, - customerId: matchingTenant.customerId, - initialDomainName: matchingTenant.initialDomainName, - }, - }); + // Update local state if different + if (!currentTenant || urlTenant !== currentTenant.value) { + setSelectedTenant({ + value: urlTenant, + label: `${matchingTenant.displayName} (${urlTenant})`, + addedFields: { + defaultDomainName: matchingTenant.defaultDomainName, + displayName: matchingTenant.displayName, + customerId: matchingTenant.customerId, + initialDomainName: matchingTenant.initialDomainName, + }, + }); + } + + // Update settings if different (null filter in settings-context prevents saving null) + if (settings.currentTenant !== urlTenant) { + settings.handleUpdate({ + currentTenant: urlTenant, + }); + } } } - }, [ - router.isReady, - router.query.tenantFilter, - tenantList.isSuccess, - settings.currentTenant, - settings.isInitialized, - ]); + }, [router.isReady, router.query.tenantFilter, tenantList.isSuccess]); // This effect ensures the tenant filter parameter is included in the URL when missing useEffect(() => { - if (!router.isReady || !settings.isInitialized || !settings.currentTenant) return; + if (!router.isReady || !settings.currentTenant) return; // If the tenant parameter is missing from the URL but we have it in settings if (!router.query.tenantFilter && settings.currentTenant) { @@ -254,7 +253,7 @@ export const CippTenantSelector = (props) => { { shallow: true } ); } - }, [router.isReady, router.query, settings.currentTenant, settings.isInitialized]); + }, [router.isReady, router.query.tenantFilter, settings.currentTenant]); useEffect(() => { if (tenant && currentTenant?.value && currentTenant?.value !== "AllTenants") { From 19f1347d388a2b1845638d409876ae0aad76e3de Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 12:00:42 -0500 Subject: [PATCH 04/21] update text --- src/components/CippStandards/CippStandardsSideBar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 170c1b73e28f..53a6d571b8a8 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -556,7 +556,7 @@ const CippStandardsSideBar = ({ title="Add Standard" api={{ confirmText: isDriftMode - ? "This template will automatically every hour to detect drift. Are you sure you want to apply this Drift Template?" + ? "This template will automatically every 12 hours to detect drift. Are you sure you want to apply this Drift Template?" : watchForm.runManually ? "Are you sure you want to apply this standard? This template has been set to never run on a schedule. After saving the template you will have to run it manually." : "Are you sure you want to apply this standard? This will apply the template and run every 3 hours.", From 450e397b9914512d8e531cabcd7cb780c510c1a0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 12:00:55 -0500 Subject: [PATCH 05/21] fix conditions --- src/pages/endpoint/MEM/devices/index.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 49a658899ea9..e2d5c52ef4ef 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -178,10 +178,7 @@ const Page = () => { GUID: "id", Action: "resetPasscode", }, - condition: (row) => - row.operatingSystem === "iOS" || - row.operatingSystem === "macOS" || - row.operatingSystem === "Android", + condition: (row) => row.operatingSystem === "Android", confirmText: "Are you sure you want to reset the passcode for [deviceName]? A new passcode will be generated and displayed.", }, @@ -194,10 +191,7 @@ const Page = () => { GUID: "id", Action: "removeDevicePasscode", }, - condition: (row) => - row.operatingSystem === "iOS" || - row.operatingSystem === "macOS" || - row.operatingSystem === "Android", + condition: (row) => row.operatingSystem === "iOS", confirmText: "Are you sure you want to remove the passcode from [deviceName]? This will remove the device passcode requirement.", }, From dc8be5879ddd28594d5da131a34c6e987f77cde6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 12:39:21 -0500 Subject: [PATCH 06/21] Clean up breadcrumb query params and titles Introduced helper functions to remove unnecessary 'tenantFilter' query parameters and to clean 'AllTenants' from page titles in breadcrumbs. Updated all relevant breadcrumb and navigation logic to use these helpers, ensuring cleaner URLs and more accurate breadcrumb titles throughout the component. Added debug logging for easier troubleshooting. --- .../CippComponents/CippBreadcrumbNav.jsx | 114 +++++++++++++++--- src/utils/cippVersion.js | 1 - 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index c0db7786d253..62c33337f63d 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -62,6 +62,27 @@ export const CippBreadcrumbNav = () => { const titleCheckCountRef = useRef(0); const titleCheckIntervalRef = useRef(null); + // Helper function to filter out unnecessary query parameters + const getCleanQueryParams = (query) => { + const cleaned = { ...query }; + // Remove tenantFilter if it's "AllTenants" or not explicitly needed + if (cleaned.tenantFilter === "AllTenants" || cleaned.tenantFilter === undefined) { + delete cleaned.tenantFilter; + } + return cleaned; + }; + + // Helper function to clean page titles + const cleanPageTitle = (title) => { + if (!title) return title; + // Remove AllTenants and any surrounding separators + return title + .replace(/\s*-\s*AllTenants\s*/, "") + .replace(/AllTenants\s*-\s*/, "") + .replace(/AllTenants/, "") + .trim(); + }; + // Load tab options on mount useEffect(() => { loadTabOptions().then(setTabOptions); @@ -109,6 +130,9 @@ export const CippBreadcrumbNav = () => { pageTitle = parts.slice(0, -1).join(" - ").trim(); } + // Clean AllTenants from title + pageTitle = cleanPageTitle(pageTitle); + // Skip if title is empty, generic, or error page if ( !pageTitle || @@ -155,7 +179,10 @@ export const CippBreadcrumbNav = () => { if (samePath && !sameTitle) { // Same URL but title changed - update the entry const updated = [...prevHistory]; - updated[prevHistory.length - 1] = currentPage; + updated[prevHistory.length - 1] = { + ...currentPage, + query: getCleanQueryParams(currentPage.query), + }; if (titleCheckIntervalRef.current) { clearInterval(titleCheckIntervalRef.current); titleCheckIntervalRef.current = null; @@ -173,7 +200,11 @@ export const CippBreadcrumbNav = () => { // URL not in history (except possibly as last entry which we handled) - add as new entry if (existingIndex === -1) { - const newHistory = [...prevHistory, currentPage]; + const cleanedCurrentPage = { + ...currentPage, + query: getCleanQueryParams(currentPage.query), + }; + const newHistory = [...prevHistory, cleanedCurrentPage]; // Keep only the last MAX_HISTORY_STORAGE pages const trimmedHistory = @@ -192,7 +223,10 @@ export const CippBreadcrumbNav = () => { titleCheckIntervalRef.current = null; } const updated = prevHistory.slice(0, existingIndex + 1); - updated[existingIndex] = currentPage; + updated[existingIndex] = { + ...currentPage, + query: getCleanQueryParams(currentPage.query), + }; return updated; }); }; @@ -211,9 +245,10 @@ export const CippBreadcrumbNav = () => { const handleBreadcrumbClick = (index) => { const page = history[index]; if (page) { + const cleanedQuery = getCleanQueryParams(page.query); router.push({ pathname: page.path, - query: page.query, + query: cleanedQuery, }); } }; @@ -247,15 +282,18 @@ export const CippBreadcrumbNav = () => { return; } - const pageTitle = document.title.replace(" - CIPP", "").trim(); + let pageTitle = document.title.replace(" - CIPP", "").trim(); const parts = pageTitle.split(" - "); const cleanTitle = parts.length > 1 && parts[parts.length - 1].includes(".") ? parts.slice(0, -1).join(" - ").trim() : pageTitle; - if (cleanTitle && cleanTitle !== "CIPP" && !cleanTitle.toLowerCase().includes("loading")) { - setCurrentPageTitle(cleanTitle); + // Clean AllTenants from title + const finalTitle = cleanPageTitle(cleanTitle); + + if (finalTitle && finalTitle !== "CIPP" && !finalTitle.toLowerCase().includes("loading")) { + setCurrentPageTitle(finalTitle); // Stop checking once we have a valid title if (hierarchicalTitleCheckRef.current) { clearInterval(hierarchicalTitleCheckRef.current); @@ -316,11 +354,11 @@ export const CippBreadcrumbNav = () => { // Check if this item matches the current path if (item.path && pathsMatch(item.path, currentPath)) { - // If this is the current page, include current query params + // If this is the current page, include current query params (cleaned) if (item.path === currentPath) { const lastItem = currentBreadcrumb[currentBreadcrumb.length - 1]; if (lastItem) { - lastItem.query = { ...router.query }; + lastItem.query = getCleanQueryParams(router.query); } } return currentBreadcrumb; @@ -339,6 +377,42 @@ export const CippBreadcrumbNav = () => { let result = findPathInMenu(nativeMenuItems); + console.log("🍞 Breadcrumb Debug - currentPath:", currentPath); + console.log("🍞 Breadcrumb Debug - result from menu:", result); + console.log("🍞 Breadcrumb Debug - tabOptions available:", tabOptions.length); + + // If we found a menu item, check if the current path matches any tab + // If so, tabOptions wins and we use its label + if (result.length > 0 && tabOptions.length > 0) { + const normalizedCurrentPath = currentPath.replace(/\/$/, ""); + + // Check if current path matches any tab (exact match) + const matchingTab = tabOptions.find((tab) => { + const normalizedTabPath = tab.path.replace(/\/$/, ""); + console.log("🍞 Comparing tab path:", normalizedTabPath, "with currentPath:", normalizedCurrentPath, "match:", normalizedTabPath === normalizedCurrentPath); + return normalizedTabPath === normalizedCurrentPath; + }); + + console.log("🍞 Matching tab found:", matchingTab); + + if (matchingTab) { + // Tab matches the current path - use tab's label instead of config's + result = result.map((item, idx) => { + if (idx === result.length - 1) { + console.log("🍞 Updating last breadcrumb from:", item.title, "to:", matchingTab.title); + return { + ...item, + title: matchingTab.title, + type: "tab", + }; + } + return item; + }); + } + } + + console.log("🍞 Final result after tab matching:", result); + // If not found in main menu, check if it's a tab page if (result.length === 0 && tabOptions.length > 0) { const normalizedCurrentPath = currentPath.replace(/\/$/, ""); @@ -395,12 +469,12 @@ export const CippBreadcrumbNav = () => { if (basePagePath.length > 0) { result = basePagePath; - // Add the tab as the final breadcrumb with current query params + // Add the tab as the final breadcrumb with current query params (cleaned) result.push({ title: matchingTab.title, path: matchingTab.path, type: "tab", - query: { ...router.query }, // Include current query params for tab page + query: getCleanQueryParams(router.query), // Include current query params for tab page }); } } @@ -411,7 +485,10 @@ export const CippBreadcrumbNav = () => { const lastItem = result[result.length - 1]; if (lastItem.path && lastItem.path !== currentPath && currentPath.startsWith(lastItem.path)) { // Use the tracked page title if available, otherwise fall back to document.title - const tabTitle = currentPageTitle || document.title.replace(" - CIPP", "").trim(); + let tabTitle = currentPageTitle || document.title.replace(" - CIPP", "").trim(); + + // Clean AllTenants from title + tabTitle = cleanPageTitle(tabTitle); // Add tab as an additional breadcrumb item if ( @@ -423,7 +500,7 @@ export const CippBreadcrumbNav = () => { title: tabTitle, path: currentPath, type: "tab", - query: { ...router.query }, // Include current query params + query: getCleanQueryParams(router.query), // Include current query params (cleaned) }); } } @@ -435,10 +512,11 @@ export const CippBreadcrumbNav = () => { // Handle click for hierarchical breadcrumbs const handleHierarchicalClick = (path, query) => { if (path) { - if (query && Object.keys(query).length > 0) { + const cleanedQuery = getCleanQueryParams(query); + if (cleanedQuery && Object.keys(cleanedQuery).length > 0) { router.push({ pathname: path, - query: query, + query: cleanedQuery, }); } else { router.push(path); @@ -459,6 +537,8 @@ export const CippBreadcrumbNav = () => { if (mode === "hierarchical") { let breadcrumbs = buildHierarchicalBreadcrumbs(); + console.log("🍞 Hierarchical breadcrumbs:", breadcrumbs); + // Fallback: If no breadcrumbs found in navigation config, generate from URL path if (breadcrumbs.length === 0) { const pathSegments = router.pathname.split("/").filter((segment) => segment); @@ -478,7 +558,7 @@ export const CippBreadcrumbNav = () => { title, path, type: "fallback", - query: index === pathSegments.length - 1 ? { ...router.query } : {}, + query: index === pathSegments.length - 1 ? getCleanQueryParams(router.query) : {}, }; }); @@ -488,7 +568,7 @@ export const CippBreadcrumbNav = () => { currentPageTitle !== "CIPP" && !currentPageTitle.toLowerCase().includes("loading") ) { - breadcrumbs[breadcrumbs.length - 1].title = currentPageTitle; + breadcrumbs[breadcrumbs.length - 1].title = cleanPageTitle(currentPageTitle); } } } diff --git a/src/utils/cippVersion.js b/src/utils/cippVersion.js index 3de297a7c070..ed64050c0759 100644 --- a/src/utils/cippVersion.js +++ b/src/utils/cippVersion.js @@ -29,7 +29,6 @@ export async function getCippVersion() { // Build headers including X-CIPP-Version. Accept extra headers to merge. export async function buildVersionedHeaders(extra = {}) { const version = await getCippVersion(); - console.log("CIPP Version:", version); return { "Content-Type": "application/json", "X-CIPP-Version": version, From 034a98e0fc704fb7569ff8a22c4830e36e8d87c9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 12:41:13 -0500 Subject: [PATCH 07/21] fix useEffect --- src/pages/tenant/administration/tenants/groups/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/administration/tenants/groups/edit.js b/src/pages/tenant/administration/tenants/groups/edit.js index dd41f5f382ed..33edecd5e1c6 100644 --- a/src/pages/tenant/administration/tenants/groups/edit.js +++ b/src/pages/tenant/administration/tenants/groups/edit.js @@ -148,7 +148,7 @@ const Page = () => { dynamicRules: formattedDynamicRules, }); } - }, [groupDetails.isSuccess, groupDetails.data]); + }, [groupDetails.isSuccess, groupDetails.data, id]); const customDataFormatter = (values) => { const formattedData = { From 58b930de442d5c86a42853749d4fc8edbf6efec7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 12:42:39 -0500 Subject: [PATCH 08/21] Remove debug console.log statements from breadcrumb nav Eliminated multiple console.log statements used for debugging in CippBreadcrumbNav.jsx to clean up the code and reduce console noise. --- src/components/CippComponents/CippBreadcrumbNav.jsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index 62c33337f63d..e69377b4e070 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -377,10 +377,6 @@ export const CippBreadcrumbNav = () => { let result = findPathInMenu(nativeMenuItems); - console.log("🍞 Breadcrumb Debug - currentPath:", currentPath); - console.log("🍞 Breadcrumb Debug - result from menu:", result); - console.log("🍞 Breadcrumb Debug - tabOptions available:", tabOptions.length); - // If we found a menu item, check if the current path matches any tab // If so, tabOptions wins and we use its label if (result.length > 0 && tabOptions.length > 0) { @@ -389,17 +385,13 @@ export const CippBreadcrumbNav = () => { // Check if current path matches any tab (exact match) const matchingTab = tabOptions.find((tab) => { const normalizedTabPath = tab.path.replace(/\/$/, ""); - console.log("🍞 Comparing tab path:", normalizedTabPath, "with currentPath:", normalizedCurrentPath, "match:", normalizedTabPath === normalizedCurrentPath); return normalizedTabPath === normalizedCurrentPath; }); - console.log("🍞 Matching tab found:", matchingTab); - if (matchingTab) { // Tab matches the current path - use tab's label instead of config's result = result.map((item, idx) => { if (idx === result.length - 1) { - console.log("🍞 Updating last breadcrumb from:", item.title, "to:", matchingTab.title); return { ...item, title: matchingTab.title, @@ -411,8 +403,6 @@ export const CippBreadcrumbNav = () => { } } - console.log("🍞 Final result after tab matching:", result); - // If not found in main menu, check if it's a tab page if (result.length === 0 && tabOptions.length > 0) { const normalizedCurrentPath = currentPath.replace(/\/$/, ""); @@ -537,8 +527,6 @@ export const CippBreadcrumbNav = () => { if (mode === "hierarchical") { let breadcrumbs = buildHierarchicalBreadcrumbs(); - console.log("🍞 Hierarchical breadcrumbs:", breadcrumbs); - // Fallback: If no breadcrumbs found in navigation config, generate from URL path if (breadcrumbs.length === 0) { const pathSegments = router.pathname.split("/").filter((segment) => segment); From 547400605b6cfb253d9591ba9b02971ab673ee34 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 12:47:38 -0500 Subject: [PATCH 09/21] Update CippBreadcrumbNav.jsx --- src/components/CippComponents/CippBreadcrumbNav.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index e69377b4e070..ed7e4be84c1b 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -476,7 +476,7 @@ export const CippBreadcrumbNav = () => { if (lastItem.path && lastItem.path !== currentPath && currentPath.startsWith(lastItem.path)) { // Use the tracked page title if available, otherwise fall back to document.title let tabTitle = currentPageTitle || document.title.replace(" - CIPP", "").trim(); - + // Clean AllTenants from title tabTitle = cleanPageTitle(tabTitle); From 9d4d6430df9ecd90b4017a3f4dffaaf05b71095d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 13:14:21 -0500 Subject: [PATCH 10/21] Refactor standards pages and update alignment routes Moved and renamed standards-related pages for better organization, including aligning routes under /tenant/standards/alignment and /tenant/standards/templates. Updated navigation paths and tab options accordingly. Enhanced the alignment page with new drift management actions and removed obsolete files. --- src/layouts/config.js | 2 +- .../tenant/manage/driftManagementActions.js | 6 +- .../drift-alignment => alignment}/index.js | 29 ++- .../standards/list-standards/tabOptions.json | 10 - src/pages/tenant/standards/tabOptions.json | 10 + .../classic-standards => templates}/index.js | 19 +- .../standards/{ => templates}/template.jsx | 229 +++++++++--------- .../standards/tenant-alignment/index.js | 38 --- 8 files changed, 167 insertions(+), 176 deletions(-) rename src/pages/tenant/standards/{list-standards/drift-alignment => alignment}/index.js (53%) delete mode 100644 src/pages/tenant/standards/list-standards/tabOptions.json create mode 100644 src/pages/tenant/standards/tabOptions.json rename src/pages/tenant/standards/{list-standards/classic-standards => templates}/index.js (91%) rename src/pages/tenant/standards/{ => templates}/template.jsx (68%) delete mode 100644 src/pages/tenant/standards/tenant-alignment/index.js diff --git a/src/layouts/config.js b/src/layouts/config.js index cb3833d87085..8923b99832d1 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -195,7 +195,7 @@ export const nativeMenuItems = [ items: [ { title: "Standards Management", - path: "/tenant/standards/list-standards", + path: "/tenant/standards/alignment", permissions: ["Tenant.Standards.*"], }, { diff --git a/src/pages/tenant/manage/driftManagementActions.js b/src/pages/tenant/manage/driftManagementActions.js index 6141a292e5aa..5d7cd8e80488 100644 --- a/src/pages/tenant/manage/driftManagementActions.js +++ b/src/pages/tenant/manage/driftManagementActions.js @@ -49,11 +49,13 @@ export const createDriftManagementActions = ({ // Use Next.js router for internal navigation import("next/router") .then(({ default: router }) => { - router.push(`/tenant/standards/template?id=${templateId}&type=${templateType}`); + router.push( + `/tenant/standards/templates/template?id=${templateId}&type=${templateType}` + ); }) .catch(() => { // Fallback to window.location if router is not available - window.location.href = `/tenant/standards/template?id=${templateId}&type=${templateType}`; + window.location.href = `/tenant/standards/templates/template?id=${templateId}&type=${templateType}`; }); }, }); diff --git a/src/pages/tenant/standards/list-standards/drift-alignment/index.js b/src/pages/tenant/standards/alignment/index.js similarity index 53% rename from src/pages/tenant/standards/list-standards/drift-alignment/index.js rename to src/pages/tenant/standards/alignment/index.js index ef0aa4f974e3..6ddd7a186223 100644 --- a/src/pages/tenant/standards/list-standards/drift-alignment/index.js +++ b/src/pages/tenant/standards/alignment/index.js @@ -1,11 +1,12 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { Delete, Add } from "@mui/icons-material"; import { EyeIcon } from "@heroicons/react/24/outline"; import tabOptions from "../tabOptions.json"; const Page = () => { - const pageTitle = "Drift Alignment"; + const pageTitle = "Standard & Drift Alignment"; const actions = [ { @@ -15,6 +16,28 @@ const Page = () => { color: "info", target: "_self", }, + { + label: "Manage Drift", + link: "/tenant/manage/drift?templateId=[standardId]&tenantFilter=[tenantFilter]", + icon: , + color: "info", + target: "_self", + condition: (row) => row.standardType === "drift", + }, + { + label: "Remove Drift Customization", + type: "POST", + url: "/api/ExecUpdateDriftDeviation", + icon: , + data: { + RemoveDriftCustomization: "true", + tenantFilter: "tenantFilter", + }, + confirmText: + "Are you sure you want to remove all drift customizations? This resets the Drift Standard to the default template, and will generate alerts for the drifted items.", + multiPost: false, + condition: (row) => row.standardType === "drift", + }, ]; return ( @@ -23,15 +46,15 @@ const Page = () => { apiUrl="/api/ListTenantAlignment" tenantInTitle={false} actions={actions} - tableFilter={
} simpleColumns={[ "tenantFilter", "standardName", + "standardType", "alignmentScore", "LicenseMissingPercentage", "combinedAlignmentScore", ]} - queryKey="listDriftAlignment" + queryKey="listTenantAlignment" /> ); }; diff --git a/src/pages/tenant/standards/list-standards/tabOptions.json b/src/pages/tenant/standards/list-standards/tabOptions.json deleted file mode 100644 index 1c522e6ca8ca..000000000000 --- a/src/pages/tenant/standards/list-standards/tabOptions.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "label": "Standard & Drift Alignment", - "path": "/tenant/standards/list-standards" - }, - { - "label": "Templates", - "path": "/tenant/standards/list-standards/classic-standards" - } -] diff --git a/src/pages/tenant/standards/tabOptions.json b/src/pages/tenant/standards/tabOptions.json new file mode 100644 index 000000000000..26c6c751dc1a --- /dev/null +++ b/src/pages/tenant/standards/tabOptions.json @@ -0,0 +1,10 @@ +[ + { + "label": "Standard & Drift Alignment", + "path": "/tenant/standards/alignment" + }, + { + "label": "Templates", + "path": "/tenant/standards/templates" + } +] \ No newline at end of file diff --git a/src/pages/tenant/standards/list-standards/classic-standards/index.js b/src/pages/tenant/standards/templates/index.js similarity index 91% rename from src/pages/tenant/standards/list-standards/classic-standards/index.js rename to src/pages/tenant/standards/templates/index.js index a204fddb81b9..6b3c7c483f87 100644 --- a/src/pages/tenant/standards/list-standards/classic-standards/index.js +++ b/src/pages/tenant/standards/templates/index.js @@ -4,13 +4,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add import { TabbedLayout } from "/src/layouts/TabbedLayout"; import Link from "next/link"; import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub, ContentCopy } from "@mui/icons-material"; -import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; import { Grid } from "@mui/system"; -import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; +import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; import { EyeIcon } from "@heroicons/react/24/outline"; import tabOptions from "../tabOptions.json"; import { useSettings } from "/src/hooks/use-settings.js"; -import { CippPolicyImportDrawer } from "../../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; +import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; import { PermissionButton } from "/src/utils/permissions.js"; const Page = () => { @@ -36,14 +36,14 @@ const Page = () => { { label: "Edit Template", //when using a link it must always be the full path /identity/administration/users/[id] for example. - link: "/tenant/standards/template?id=[GUID]&type=[type]", + link: "/tenant/standards/templates/template?id=[GUID]&type=[type]", icon: , color: "success", target: "_self", }, { label: "Clone & Edit Template", - link: "/tenant/standards/template?id=[GUID]&clone=true&type=[type]", + link: "/tenant/standards/templates/template?id=[GUID]&clone=true&type=[type]", icon: , color: "success", target: "_self", @@ -183,12 +183,17 @@ const Page = () => { tenantInTitle={false} cardButton={ <> - - - {/* Drift management actions */} - {driftActions.length > 0 && ( - - )} - - - - - {/* Left Column for Accordions */} - - { - // Reset unsaved changes flag - setHasUnsavedChanges(false); - // Update reference for future change detection - initialStandardsRef.current = { ...selectedStandards }; - }} - /> - - - - {/* Show accordions based on selectedStandards (which is populated by API when editing) */} - {existingTemplate.isLoading ? ( - - ) : ( - - )} - - - - + + + + {editMode + ? isDriftMode + ? "Edit Drift Template" + : "Edit Standards Template" + : isDriftMode + ? "Add Drift Template" + : "Add Standards Template"} + + + + + {/* Drift management actions */} + {driftActions.length > 0 && ( + + )} + - {/* Only render the dialog when it's needed */} - {dialogOpen && ( - }> - - - )} - + + + {/* Left Column for Accordions */} + + { + // Reset unsaved changes flag + setHasUnsavedChanges(false); + // Update reference for future change detection + initialStandardsRef.current = { ...selectedStandards }; + }} + /> + + + + {/* Show accordions based on selectedStandards (which is populated by API when editing) */} + {existingTemplate.isLoading ? ( + + ) : ( + + )} + + + + + + + {/* Only render the dialog when it's needed */} + {dialogOpen && ( + }> + + + )} ); }; diff --git a/src/pages/tenant/standards/tenant-alignment/index.js b/src/pages/tenant/standards/tenant-alignment/index.js deleted file mode 100644 index e891f2a0576d..000000000000 --- a/src/pages/tenant/standards/tenant-alignment/index.js +++ /dev/null @@ -1,38 +0,0 @@ -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { EyeIcon } from "@heroicons/react/24/outline"; - -const Page = () => { - const pageTitle = "Tenant Alignment"; - - const actions = [ - { - label: "View Tenant Report", - link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]", - icon: , - color: "info", - target: "_self", - }, - ]; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; From 401c4cf0f3e3223b70ec9c675c556c0a2518edb2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 13:35:30 -0500 Subject: [PATCH 11/21] cleanup console logs --- .../CippStandards/CippStandardsSideBar.jsx | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 53a6d571b8a8..7e2ccdcb6647 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -144,34 +144,12 @@ const CippStandardsSideBar = ({ } // Filter out current template if editing - console.log("Duplicate detection debug:", { - edit, - currentGUID: watchForm.GUID, - allTemplates: driftValidationApi.data?.map((t) => ({ - GUID: t.GUID, - standardId: t.standardId, - standardName: t.standardName, - })), - }); - const existingTemplates = driftValidationApi.data.filter((template) => { const shouldInclude = edit && watchForm.GUID ? template.standardId !== watchForm.GUID : true; - console.log( - `Template ${template.standardId} (${template.standardName}): shouldInclude=${shouldInclude}, currentGUID=${watchForm.GUID}` - ); return shouldInclude; }); - console.log( - "Filtered templates:", - existingTemplates?.map((t) => ({ - GUID: t.GUID, - standardId: t.standardId, - standardName: t.standardName, - })) - ); - // Get tenant groups data const groups = tenantGroupsApi.data?.Results || []; @@ -198,45 +176,27 @@ const CippStandardsSideBar = ({ }); // Check for conflicts with unique templates - console.log("Checking conflicts with unique templates:", uniqueTemplates); - console.log("Selected tenant list:", selectedTenantList); - for (const templateId in uniqueTemplates) { const template = uniqueTemplates[templateId]; const templateTenants = template.tenants; - console.log( - `Checking template ${templateId} (${template.standardName}) with tenants:`, - templateTenants - ); - const hasConflict = selectedTenantList.some((selectedTenant) => { // Check if any template tenant matches the selected tenant const conflict = templateTenants.some((templateTenant) => { if (selectedTenant === "AllTenants" || templateTenant === "AllTenants") { - console.log( - `Conflict found: ${selectedTenant} vs ${templateTenant} (AllTenants match)` - ); return true; } const match = selectedTenant === templateTenant; - if (match) { - console.log(`Conflict found: ${selectedTenant} vs ${templateTenant} (exact match)`); - } return match; }); return conflict; }); - console.log(`Template ${templateId} has conflict: ${hasConflict}`); - if (hasConflict) { conflicts.push(template.standardName || "Unknown Template"); } } - console.log("Final conflicts:", conflicts); - if (conflicts.length > 0) { setDriftError( `This template has tenants that are assigned to another Drift Template. You can only assign one Drift Template to each tenant. Please check the ${conflicts.join( From d40f2f5968190b037eb0d5c3bcbfef2188a7b795 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 14:01:53 -0500 Subject: [PATCH 12/21] Improve 'hasValue' condition logic in CippFormCondition Refines the 'hasValue' comparison to better handle arrays and falsy values, ensuring more accurate condition evaluation. Also updates the prop validation to allow undefined compareValue only for 'hasValue' type. --- .../CippComponents/CippFormCondition.jsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx index 9ec49a57ef5c..dd9a48cbf95d 100644 --- a/src/components/CippComponents/CippFormCondition.jsx +++ b/src/components/CippComponents/CippFormCondition.jsx @@ -18,7 +18,7 @@ export const CippFormCondition = (props) => { if ( field === undefined || - compareValue === undefined || + (compareValue === undefined && compareType !== "hasValue") || children === undefined || formControl === undefined ) { @@ -148,10 +148,18 @@ export const CippFormCondition = (props) => { watcher.length >= compareValue ); case "hasValue": - return ( - (watcher !== undefined && watcher !== null && watcher !== "") || - (watcher?.value !== undefined && watcher?.value !== null && watcher?.value !== "") - ); + // Check watchedValue (the extracted value based on propertyName) + // For simple values (strings, numbers) + if (watchedValue === undefined || watchedValue === null || watchedValue === "") { + return false; + } + // If it's an array, check if it has elements + if (Array.isArray(watchedValue)) { + return watchedValue.length > 0; + } + console.log("watched value:", watchedValue); + // For any other truthy value (objects, numbers, strings), consider it as having a value + return true; case "labelEq": return Array.isArray(watcher) && watcher.some((item) => item?.label === compareValue); case "labelContains": From e8f8df6dd58bca1ee9a6d85a76e0ce23f268c22a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 14:02:27 -0500 Subject: [PATCH 13/21] Remove unused addedField from Intune templates config Deleted the 'addedField' property from the ListIntuneTemplates-tag-autcomplete API config in standards.json to clean up unused configuration. --- src/data/standards.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 2d6ffb3f010a..92d605cf2036 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -5056,10 +5056,7 @@ "queryKey": "ListIntuneTemplates-tag-autcomplete", "url": "/api/ListIntuneTemplates?mode=Tag", "labelField": "label", - "valueField": "value", - "addedField": { - "templates": "templates" - } + "valueField": "value" } }, { From 35f2eed30aa9d3162c32f3cd8e3cfd1a90885cb9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 14:11:46 -0500 Subject: [PATCH 14/21] Update standards route and remove list-standards page Changed all references from '/tenant/standards/list-standards' to '/tenant/standards' across components, hooks, and pages for consistency. Deleted the now-obsolete list-standards page implementation. --- generate-placeholders.js | 2 +- .../CippComponents/CippBreadcrumbNav.jsx | 2 +- .../CippComponents/CippCentralSearch.jsx | 2 +- src/hooks/use-securescore.js | 2 +- src/pages/tenant/manage/applied-standards.js | 2 +- src/pages/tenant/manage/policies-deployed.js | 2 +- .../tenant/standards/list-standards/index.js | 68 ------------------- 7 files changed, 6 insertions(+), 74 deletions(-) delete mode 100644 src/pages/tenant/standards/list-standards/index.js diff --git a/generate-placeholders.js b/generate-placeholders.js index 2b34888fca7a..304e6402e4ed 100644 --- a/generate-placeholders.js +++ b/generate-placeholders.js @@ -43,7 +43,7 @@ const pages = [ { title: "BPA Report Builder", path: "/tenant/tools/bpa-report-builder" }, { title: "Standards", path: "/tenant/standards" }, { title: "Edit Standards", path: "/tenant/standards/list-applied-standards" }, - { title: "List Standards", path: "/tenant/standards/list-standards" }, + { title: "List Standards", path: "/tenant/standards" }, { title: "Best Practice Analyser", path: "/tenant/standards/bpa-report" }, { title: "Domains Analyser", path: "/tenant/standards/domains-analyser" }, { title: "Conditional Access", path: "/tenant/administration" }, diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index ed7e4be84c1b..bb9e86981c5f 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -16,7 +16,7 @@ async function loadTabOptions() { "/email/administration/exchange-retention", "/cipp/custom-data", "/cipp/super-admin", - "/tenant/standards/list-standards", + "/tenant/standards", "/tenant/manage", "/tenant/administration/applications", "/tenant/administration/tenants", diff --git a/src/components/CippComponents/CippCentralSearch.jsx b/src/components/CippComponents/CippCentralSearch.jsx index 523615e7c659..0c7bf858e88b 100644 --- a/src/components/CippComponents/CippCentralSearch.jsx +++ b/src/components/CippComponents/CippCentralSearch.jsx @@ -46,7 +46,7 @@ async function loadTabOptions() { "/email/administration/exchange-retention", "/cipp/custom-data", "/cipp/super-admin", - "/tenant/standards/list-standards", + "/tenant/standards", "/tenant/manage", "/tenant/administration/applications", "/tenant/administration/tenants", diff --git a/src/hooks/use-securescore.js b/src/hooks/use-securescore.js index f96c2bd232b7..a51dc18d9138 100644 --- a/src/hooks/use-securescore.js +++ b/src/hooks/use-securescore.js @@ -68,7 +68,7 @@ export function useSecureScore({ waiting = true } = {}) { complianceInformation: translation?.complianceInformation, actionUrl: remediation ? //this needs to be updated to be a direct url to apply this standard. - "/tenant/standards/list-standards" + "/tenant/standards" : translation?.actionUrl, remediation: remediation ? `1. Enable the CIPP Standard: ${remediation.label}` diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 2647ac4651f4..71e4865d8771 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -1027,7 +1027,7 @@ const Page = () => { tabOptions={tabOptions} title={title} subtitle={subtitle} - backUrl="/tenant/standards/list-standards" + backUrl="/tenant/standards" actions={actions} actionsData={{}} isFetching={comparisonApi.isFetching || templateDetails.isFetching} diff --git a/src/pages/tenant/manage/policies-deployed.js b/src/pages/tenant/manage/policies-deployed.js index 46835ae9df55..b6cca34dd67f 100644 --- a/src/pages/tenant/manage/policies-deployed.js +++ b/src/pages/tenant/manage/policies-deployed.js @@ -405,7 +405,7 @@ const PoliciesDeployedPage = () => { subtitle={subtitle} actions={actions} actionsData={{}} - backUrl="/tenant/standards/list-standards" + backUrl="/tenant/standards" > diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js deleted file mode 100644 index 5e25f0b96d76..000000000000 --- a/src/pages/tenant/standards/list-standards/index.js +++ /dev/null @@ -1,68 +0,0 @@ -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { TabbedLayout } from "/src/layouts/TabbedLayout"; -import { Delete, Add } from "@mui/icons-material"; -import { EyeIcon } from "@heroicons/react/24/outline"; -import tabOptions from "./tabOptions.json"; - -const Page = () => { - const pageTitle = "Standard & Drift Alignment"; - - const actions = [ - { - label: "View Tenant Report", - link: "/tenant/manage/applied-standards/?tenantFilter=[tenantFilter]&templateId=[standardId]", - icon: , - color: "info", - target: "_self", - }, - { - label: "Manage Drift", - link: "/tenant/manage/drift?templateId=[standardId]&tenantFilter=[tenantFilter]", - icon: , - color: "info", - target: "_self", - condition: (row) => row.standardType === "drift", - }, - { - label: "Remove Drift Customization", - type: "POST", - url: "/api/ExecUpdateDriftDeviation", - icon: , - data: { - RemoveDriftCustomization: "true", - tenantFilter: "tenantFilter", - }, - confirmText: - "Are you sure you want to remove all drift customizations? This resets the Drift Standard to the default template, and will generate alerts for the drifted items.", - multiPost: false, - condition: (row) => row.standardType === "drift", - }, - ]; - - return ( - - ); -}; - -Page.getLayout = (page) => ( - - {page} - -); - -export default Page; From 9f97de3320098525a572a027bd7b89feb9c4f781 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 14:31:19 -0500 Subject: [PATCH 15/21] Improve breadcrumb title resolution and path validation Added a getPathInfo helper to resolve breadcrumb titles from navigation or tab options and validate paths. Breadcrumbs now display titles from navigation/tabs when available and render invalid paths as plain text instead of clickable links. --- .../CippComponents/CippBreadcrumbNav.jsx | 100 ++++++++++++++---- 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index bb9e86981c5f..ff146073ae1c 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -499,6 +499,46 @@ export const CippBreadcrumbNav = () => { return result; }; + // Check if a path is valid and return its title from navigation or tabs + const getPathInfo = (path) => { + if (!path) return { isValid: false, title: null }; + + const normalizedPath = path.replace(/\/$/, ""); + + // Helper function to recursively search menu items + const findInMenu = (items) => { + for (const item of items) { + if (item.path) { + const normalizedItemPath = item.path.replace(/\/$/, ""); + if (normalizedItemPath === normalizedPath) { + return { isValid: true, title: item.title }; + } + } + if (item.items && item.items.length > 0) { + const found = findInMenu(item.items); + if (found.isValid) { + return found; + } + } + } + return { isValid: false, title: null }; + }; + + // Check if path exists in navigation + const menuResult = findInMenu(nativeMenuItems); + if (menuResult.isValid) { + return menuResult; + } + + // Check if path exists in tab options + const matchingTab = tabOptions.find((tab) => tab.path.replace(/\/$/, "") === normalizedPath); + if (matchingTab) { + return { isValid: true, title: matchingTab.title }; + } + + return { isValid: false, title: null }; + }; + // Handle click for hierarchical breadcrumbs const handleHierarchicalClick = (path, query) => { if (path) { @@ -580,6 +620,9 @@ export const CippBreadcrumbNav = () => { > {breadcrumbs.map((crumb, index) => { const isLast = index === breadcrumbs.length - 1; + const pathInfo = getPathInfo(crumb.path); + // Use title from nav/tabs if available, otherwise use the crumb's title + const displayTitle = pathInfo.title || crumb.title; // Items without paths (headers/groups) - show as text if (!crumb.path) { @@ -590,31 +633,46 @@ export const CippBreadcrumbNav = () => { variant="subtitle2" sx={{ fontWeight: isLast ? 500 : 400 }} > - {crumb.title} + {displayTitle} ); } - // All items with paths are clickable, including the last one - return ( - handleHierarchicalClick(crumb.path, crumb.query)} - sx={{ - textDecoration: "none", - color: isLast ? "text.primary" : "text.secondary", - fontWeight: isLast ? 500 : 400, - "&:hover": { - textDecoration: "underline", - color: "primary.main", - }, - }} - > - {crumb.title} - - ); + // Items with valid paths are clickable + // Items with invalid paths (fallback) are shown as plain text + if (pathInfo.isValid) { + return ( + handleHierarchicalClick(crumb.path, crumb.query)} + sx={{ + textDecoration: "none", + color: isLast ? "text.primary" : "text.secondary", + fontWeight: isLast ? 500 : 400, + "&:hover": { + textDecoration: "underline", + color: "primary.main", + }, + }} + > + {displayTitle} + + ); + } else { + // Invalid path - show as text only + return ( + + {displayTitle} + + ); + } })} From f3f63922476a3c5c4815d405881f1396d48064e3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 15:37:16 -0500 Subject: [PATCH 16/21] Add conditional logic to CreateGroups switch Wrapped the CreateGroups switch in a CippFormCondition component to disable it unless 'Replace by display name' is selected. Added dynamic helper text to clarify group creation behavior based on the selected replace mode. --- .../CippComponents/CippCADeployDrawer.jsx | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/components/CippComponents/CippCADeployDrawer.jsx b/src/components/CippComponents/CippCADeployDrawer.jsx index 508f86df92e7..6b2a4a633ff4 100644 --- a/src/components/CippComponents/CippCADeployDrawer.jsx +++ b/src/components/CippComponents/CippCADeployDrawer.jsx @@ -9,6 +9,7 @@ import CippJsonView from "../CippFormPages/CippJSONView"; import { CippApiResults } from "./CippApiResults"; import { useSettings } from "../../hooks/use-settings"; import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { CippFormCondition } from "./CippFormCondition"; export const CippCADeployDrawer = ({ buttonText = "Deploy CA Policy", @@ -24,6 +25,10 @@ export const CippCADeployDrawer = ({ const CATemplates = ApiGetCall({ url: "/api/ListCATemplates", queryKey: "CATemplates" }); const [JSONData, setJSONData] = useState(); const watcher = useWatch({ control: formControl.control, name: "TemplateList" }); + const selectedReplaceMode = useWatch({ + control: formControl.control, + name: "replacename", + }); // Use external open state if provided, otherwise use internal state const drawerVisible = open !== null ? open : internalDrawerVisible; @@ -199,13 +204,25 @@ export const CippCADeployDrawer = ({ label="Disable Security Defaults if enabled when creating policy" formControl={formControl} /> - - + field="replacename" + compareType="is" + compareValue="displayName" + action="disable" + > + + From ccaf6b1c6b46dfe9342df25964f04f772c66c461 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 17:41:38 -0500 Subject: [PATCH 17/21] Bump version to 8.8.2 Update package.json and public/version.json to reflect the new version 8.8.2. --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bfce16283445..6d01e9706b32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "8.8.1", + "version": "8.8.2", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index e38593790096..7a3adc4ddaba 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.8.1" + "version": "8.8.2" } \ No newline at end of file From 3ee55ddbf6286b51c27ae697a46fe6855a02e887 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 18:47:30 -0500 Subject: [PATCH 18/21] Add tenant filter to backup history for AllTenants Introduces a tenant selector to the backup history view when 'AllTenants' is selected, allowing filtering of backup records by tenant. Updates backup display logic and UI to show tenant information and improve usability for multi-tenant scenarios. --- .../CippRestoreBackupDrawer.jsx | 7 +- src/layouts/HeaderedTabbedLayout.jsx | 2 +- .../tenant/manage/configuration-backup.js | 70 +++++++++++++++---- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx index f011b499820f..f09be034d28d 100644 --- a/src/components/CippComponents/CippRestoreBackupDrawer.jsx +++ b/src/components/CippComponents/CippRestoreBackupDrawer.jsx @@ -202,7 +202,12 @@ export const CippRestoreBackupDrawer = ({ queryKey: `BackupList-${tenantFilter}-autocomplete`, labelField: (option) => { const match = option.BackupName.match(/.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/); - return match ? `${match[1]} @ ${match[2]}:${match[3]}` : option.BackupName; + const dateTime = match + ? `${match[1]} @ ${match[2]}:${match[3]}` + : option.BackupName; + const tenantDisplay = + option.TenantFilter === "AllTenants" ? ` (${option.TenantFilter})` : ""; + return `${dateTime}${tenantDisplay}`; }, valueField: "BackupName", data: { diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx index 04a756494d5c..ece1d0659924 100644 --- a/src/layouts/HeaderedTabbedLayout.jsx +++ b/src/layouts/HeaderedTabbedLayout.jsx @@ -115,7 +115,7 @@ export const HeaderedTabbedLayout = (props) => { !mdDown && { flexGrow: 1, overflow: "auto", - height: "calc(100vh - 30px)", + height: "calc(100vh - 350px)", } } > diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js index 5421a3314e79..53966520cd24 100644 --- a/src/pages/tenant/manage/configuration-backup.js +++ b/src/pages/tenant/manage/configuration-backup.js @@ -1,5 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; import { Button, Box, @@ -31,6 +33,7 @@ import { CippBackupScheduleDrawer } from "/src/components/CippComponents/CippBac import { CippRestoreBackupDrawer } from "/src/components/CippComponents/CippRestoreBackupDrawer"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; import { useDialog } from "/src/hooks/use-dialog"; import ReactTimeAgo from "react-time-ago"; import tabOptions from "./tabOptions.json"; @@ -42,6 +45,8 @@ const Page = () => { const { templateId } = router.query; const settings = useSettings(); const removeDialog = useDialog(); + const tenantFilterForm = useForm({ defaultValues: { tenantFilter: null } }); + const backupTenantFilter = useWatch({ control: tenantFilterForm.control, name: "tenantFilter" }); // Prioritize URL query parameter, then fall back to settings const currentTenant = router.query.tenantFilter || settings.currentTenant; @@ -79,19 +84,28 @@ const Page = () => { return ["Configuration"]; }; - const backupDisplayItems = filteredBackupData.map((backup, index) => ({ + // Filter backup data by selected tenant if in AllTenants view + const tenantFilteredBackupData = + settings.currentTenant === "AllTenants" && + backupTenantFilter && + backupTenantFilter !== "AllTenants" + ? filteredBackupData.filter((backup) => backup.TenantFilter === backupTenantFilter) + : filteredBackupData; + + const backupDisplayItems = tenantFilteredBackupData.map((backup, index) => ({ id: backup.RowKey || index, name: backup.BackupName || "Unnamed Backup", timestamp: backup.Timestamp, - tenantSource: backup.BackupName?.includes("AllTenants") - ? "All Tenants" - : backup.BackupName?.replace("CIPP Backup - ", "") || settings.currentTenant, + tenantSource: backup.TenantFilter || settings.currentTenant, tags: generateBackupTags(backup), })); // Process existing backup configuration, find tenantFilter. by comparing settings.currentTenant with Tenant.value const currentConfig = Array.isArray(existingBackupConfig.data) - ? existingBackupConfig.data.find((tenant) => tenant.Tenant.value === settings.currentTenant) + ? existingBackupConfig.data.find( + (tenant) => + tenant.Tenant.value === settings.currentTenant || tenant.Tenant.value === "AllTenants" + ) : null; const hasExistingConfig = currentConfig && currentConfig.Parameters?.ScheduledBackupValues; @@ -189,7 +203,7 @@ const Page = () => { isFetching={backupList.isFetching || existingBackupConfig.isFetching} > - + {/* Two Side-by-Side Displays */} @@ -281,13 +295,33 @@ const Page = () => { {/* Backup History */} - - - - - - Backup History - + + + + + + + Backup History + + {settings.currentTenant === "AllTenants" && ( + + + + )} + {settings.currentTenant === "AllTenants" @@ -307,7 +341,7 @@ const Page = () => { ) : ( - + {backupDisplayItems.map((backup) => ( @@ -334,6 +368,14 @@ const Page = () => { + {settings.currentTenant === "AllTenants" && ( + + )} Date: Fri, 19 Dec 2025 18:51:07 -0500 Subject: [PATCH 19/21] Update CippRestoreBackupDrawer.jsx --- src/components/CippComponents/CippRestoreBackupDrawer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx index f09be034d28d..986558fbd335 100644 --- a/src/components/CippComponents/CippRestoreBackupDrawer.jsx +++ b/src/components/CippComponents/CippRestoreBackupDrawer.jsx @@ -206,7 +206,7 @@ export const CippRestoreBackupDrawer = ({ ? `${match[1]} @ ${match[2]}:${match[3]}` : option.BackupName; const tenantDisplay = - option.TenantFilter === "AllTenants" ? ` (${option.TenantFilter})` : ""; + tenantFilter === "AllTenants" ? ` (${option.TenantFilter})` : ""; return `${dateTime}${tenantDisplay}`; }, valueField: "BackupName", From 0bbc79ccf991fcc872919b859d5211bb2adaf22a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 19:02:02 -0500 Subject: [PATCH 20/21] Use backup tenant in AllTenants restore context When restoring a backup in the AllTenants context, the tenant filter now defaults to the tenant specified in the backup data if available. This ensures the correct tenant is targeted during the restore operation. --- src/components/CippComponents/CippRestoreBackupDrawer.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx index 986558fbd335..b8782aa6d401 100644 --- a/src/components/CippComponents/CippRestoreBackupDrawer.jsx +++ b/src/components/CippComponents/CippRestoreBackupDrawer.jsx @@ -13,6 +13,7 @@ import { ApiPostCall } from "../../api/ApiCall"; export const CippRestoreBackupDrawer = ({ buttonText = "Restore Backup", backupName = null, + backupData = null, requiredPermissions = [], PermissionButton = Button, ...props @@ -85,7 +86,12 @@ export const CippRestoreBackupDrawer = ({ const values = formControl.getValues(); const startDate = new Date(); const unixTime = Math.floor(startDate.getTime() / 1000) - 45; - const tenantFilterValue = tenantFilter; + + // If in AllTenants context, use the tenant from the backup data + let tenantFilterValue = tenantFilter; + if (tenantFilter === "AllTenants" && backupData?.tenantSource) { + tenantFilterValue = backupData.tenantSource; + } const shippedValues = { TenantFilter: tenantFilterValue, From bac437a5499101164b3b408ecb132fda38a866bb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 19 Dec 2025 19:12:53 -0500 Subject: [PATCH 21/21] Update configuration-backup.js --- src/pages/tenant/manage/configuration-backup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js index 53966520cd24..d2f8e5e19ac4 100644 --- a/src/pages/tenant/manage/configuration-backup.js +++ b/src/pages/tenant/manage/configuration-backup.js @@ -203,7 +203,7 @@ const Page = () => { isFetching={backupList.isFetching || existingBackupConfig.isFetching} > - + {/* Two Side-by-Side Displays */} @@ -341,7 +341,7 @@ const Page = () => { ) : ( - + {backupDisplayItems.map((backup) => (