diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index c0db7786d253..ed7e4be84c1b 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,32 @@ export const CippBreadcrumbNav = () => { let result = findPathInMenu(nativeMenuItems); + // 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(/\/$/, ""); + return normalizedTabPath === normalizedCurrentPath; + }); + + 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) { + return { + ...item, + title: matchingTab.title, + type: "tab", + }; + } + return item; + }); + } + } + // 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 +459,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 +475,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 +490,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 +502,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); @@ -478,7 +546,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 +556,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/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") { 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.", 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/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.", }, 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 = { 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; 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,