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/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 diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index ed7e4be84c1b..ff146073ae1c 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", @@ -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} + + ); + } })} 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" + > + + 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/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": diff --git a/src/components/CippComponents/CippRestoreBackupDrawer.jsx b/src/components/CippComponents/CippRestoreBackupDrawer.jsx index f011b499820f..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, @@ -202,7 +208,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 = + tenantFilter === "AllTenants" ? ` (${option.TenantFilter})` : ""; + return `${dateTime}${tenantDisplay}`; }, valueField: "BackupName", data: { 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( 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" } }, { 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/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/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/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js index 5421a3314e79..d2f8e5e19ac4 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; @@ -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" && ( + + )} { 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;