diff --git a/.github/workflows/azure-static-web-apps-red-stone-0d8c5ed03.yml b/.github/workflows/azure-static-web-apps-red-stone-0d8c5ed03.yml index bb56188dbcb5..1598fc12cde1 100644 --- a/.github/workflows/azure-static-web-apps-red-stone-0d8c5ed03.yml +++ b/.github/workflows/azure-static-web-apps-red-stone-0d8c5ed03.yml @@ -4,6 +4,7 @@ on: push: branches: - dev + - main jobs: build_and_deploy_job: @@ -29,14 +30,3 @@ jobs: output_location: "/out" # Built app content directory - optional ###### End of Repository/Build Configurations ###### - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_RED_STONE_0D8C5ED03 }} - action: "close" diff --git a/package.json b/package.json index aedbd44fc940..a1b73c8fff04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "11.0.0", + "version": "11.0.1", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 0d0e3ba17009..eb7e33bf33be 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "11.0.0" + "version": "11.0.1" } diff --git a/src/components/CippCards/CippUniversalSearch.jsx b/src/components/CippCards/CippUniversalSearch.jsx deleted file mode 100644 index e5d446349e18..000000000000 --- a/src/components/CippCards/CippUniversalSearch.jsx +++ /dev/null @@ -1,206 +0,0 @@ -import React, { useState } from "react"; -import { - TextField, - Box, - Typography, - Card, - CardContent, - CardHeader, - Skeleton, - Button, - Link, -} from "@mui/material"; -import { Grid } from "@mui/system"; -import { ApiGetCall } from "../../api/ApiCall"; - -export const CippUniversalSearch = React.forwardRef( - ({ onConfirm = () => {}, onChange = () => {}, maxResults = 7, value = "" }, ref) => { - const [searchValue, setSearchValue] = useState(value); - const handleChange = (event) => { - const newValue = event.target.value; - setSearchValue(newValue); - onChange(newValue); - }; - - const search = ApiGetCall({ - url: `/api/ExecUniversalSearch?name=${searchValue}`, - queryKey: `search-${searchValue}`, - waiting: false, - }); - const handleKeyDown = async (event) => { - if (event.key === "Enter") { - search.refetch(); - } - }; - - return ( - - - - {search.isFetching && ( - - - - )} - {search.isSuccess && search?.data?.length > 0 ? ( - - ) : ( - search.isSuccess && "No results found." - )} - - ); - } -); - -CippUniversalSearch.displayName = "CippUniversalSearch"; - -const Results = ({ items = [], searchValue }) => { - const [currentPage, setCurrentPage] = useState(1); - const resultsPerPage = 9; // Number of results per page - const totalResults = items.length; // Total number of results - const totalPages = Math.ceil(totalResults / resultsPerPage); // Total pages - - // Calculate the results to display for the current page - const startIndex = (currentPage - 1) * resultsPerPage; - const endIndex = startIndex + resultsPerPage; - const displayedResults = items.slice(startIndex, endIndex); - - const handleNextPage = () => { - if (currentPage < totalPages) { - setCurrentPage(currentPage + 1); - } - }; - - const handlePreviousPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - - return ( - <> - - {totalResults} results (Page {currentPage} of {totalPages}) - - - {displayedResults.map((item, key) => ( - - - - ))} - - - - - - - ); -}; - -const ResultsRow = ({ match, searchValue }) => { - const highlightMatch = (text) => { - const parts = text?.split(new RegExp(`(${searchValue})`, "gi")); - return parts?.map((part, index) => - part.toLowerCase() === searchValue.toLowerCase() ? ( - - {part} - - ) : ( - part - ) - ); - }; - const currentTenantInfo = ApiGetCall({ - url: "/api/ListTenants", - queryKey: `ListTenants`, - }); - return ( - - - {highlightMatch(match.displayName)} - - {highlightMatch(match.userPrincipalName)} - - - Found in tenant{" "} - { - //translate match._tenantId to tenant name by finding it in currentTenantInfo, if its not there, show the ID. the prop we are matching on is customerId, the return is displayName (defaultDomainName) - currentTenantInfo.data?.find((tenant) => tenant.customerId === match._tenantId) - ?.defaultDomainName || match._tenantId - } - - - - - - - - ); -}; diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index f98600b7df38..a25419d6604e 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -28,6 +28,7 @@ import React from "react"; import { Stack } from "@mui/system"; import countryList from "../../data/countryList"; import languageList from "../../data/languageList"; +import GDAPRoles from "../../data/GDAPRoles"; // ISO 3166-1 alpha-2 country/region codes (uppercase), used by the CountryCodeMultiSelect type. const countryCodeOptions = countryList @@ -46,6 +47,13 @@ const languageCodeOptions = Object.values( }, {}), ).sort((a, b) => a.label.localeCompare(b.label)); +// Built-in Entra admin roles keyed by role template id, used by the AdminRolesMultiSelect type. +// Static so the picker works without a tenant (template/baseline editors) and never hits Graph. +const adminRoleOptions = GDAPRoles.map((role) => ({ + label: role.Name, + value: role.ObjectId, +})); + // The tiptap / prosemirror / mui-tiptap editor tree is large and only used by `richText` fields. // Load it on demand via next/dynamic so it is code-split into an async chunk instead of being // pulled into the shared bundle that every page using CippFormComponent loads. See CippRichTextField.jsx. @@ -660,6 +668,15 @@ export const CippFormComponent = (props) => { creatable: false, }); + // Built-in Entra admin roles (e.g. EnableAppConsentRequests reviewer roles). + case "AdminRolesMultiSelect": + return renderAutoCompleteField({ + ...other, + options: adminRoleOptions, + multiple: true, + creatable: false, + }); + // ISO 639-1 alpha-2 language code multiselect (e.g. Spam Filter LanguageBlockList). case "LanguageCodeMultiSelect": return renderAutoCompleteField({ diff --git a/src/components/CippSettings/CippLogRetentionSettings.jsx b/src/components/CippSettings/CippLogRetentionSettings.jsx index a585560ed7ee..c4fb81a4882c 100644 --- a/src/components/CippSettings/CippLogRetentionSettings.jsx +++ b/src/components/CippSettings/CippLogRetentionSettings.jsx @@ -4,18 +4,23 @@ import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import { CippApiResults } from "../CippComponents/CippApiResults"; import { useState, useEffect } from "react"; -const CippLogRetentionSettings = () => { +const CippLogRetentionSettings = ({ + title = "Log Retention", + endpoint = "ExecLogRetentionConfig", + defaultDays = 90, + description = "Configure how long to keep CIPP log entries. Logs will be automatically deleted after this period.", +}) => { const retentionSetting = ApiGetCall({ - url: "/api/ExecLogRetentionConfig?List=true", - queryKey: "LogRetentionSettings", + url: `/api/${endpoint}?List=true`, + queryKey: `${endpoint}Settings`, }); const retentionChange = ApiPostCall({ datafromUrl: true, - relatedQueryKeys: "LogRetentionSettings", + relatedQueryKeys: `${endpoint}Settings`, }); - const [retentionDays, setRetentionDays] = useState(90); + const [retentionDays, setRetentionDays] = useState(defaultDays); const [error, setError] = useState(""); useEffect(() => { @@ -39,9 +44,9 @@ const CippLogRetentionSettings = () => { setError(""); retentionChange.mutate({ - url: "/api/ExecLogRetentionConfig", + url: `/api/${endpoint}`, data: { RetentionDays: days }, - queryKey: "LogRetentionPost", + queryKey: `${endpoint}Post`, }); }; @@ -63,7 +68,7 @@ const CippLogRetentionSettings = () => { return ( @@ -95,8 +100,7 @@ const CippLogRetentionSettings = () => { } > - Configure how long to keep CIPP log entries. Logs will be automatically deleted after this - period. Minimum retention is 7 days, maximum is 365 days, default is 90 days. + {description} Minimum retention is 7 days, maximum is 365 days, default is {defaultDays} days. diff --git a/src/data/sponsors.json b/src/data/sponsors.json index 40c306f3697a..1a2cfd6f0cf0 100644 --- a/src/data/sponsors.json +++ b/src/data/sponsors.json @@ -24,7 +24,7 @@ "priority": 1 }, { - "link": "https://augmentt.com", + "link": "https://www.augmentt.com/?utm_source=cipp&utm_medium=referral&utm_campaign=cipp-sponsorship&utm_content=sidebar-logo", "imagesrcLight": "/sponsors/augmentt-light.png", "imagesrcDark": "/sponsors/augmentt-dark.png", "altText": "Augmentt", diff --git a/src/data/standards.json b/src/data/standards.json index 196396f5fa63..bd2ab73da703 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -8179,7 +8179,7 @@ "name": "standards.DisableEWS", "cat": "Exchange Standards", "tag": [], - "helpText": "Disables Exchange Web Services (EWS) organization-wide. This reduces the attack surface by blocking legacy API access to mailbox data. Warning: This may break Office web add-ins on builds older than 16.0.19127.", + "helpText": "Disables Exchange Web Services (EWS) organization-wide. This reduces the attack surface by blocking legacy API access to mailbox data. Warning: This may break Office web add-ins on builds older than 16.0.19127. Conflicts with the \"Configure EWS allowed applications\" standard, which sets EwsEnabled to true: do not apply both to a tenant.", "docsDescription": "Disables Exchange Web Services (EWS) at the organization level to reduce attack surface. EWS provides cross-platform API access to sensitive Exchange Online data such as emails, meetings, and contacts. If compromised, attackers can access confidential data, send phishing emails, or spoof identities. Disabling EWS also reduces legacy app usage and minimizes exploitable endpoints. Note that this may break first-party features including web add-ins for Word, Excel, PowerPoint, and Outlook on builds older than 16.0.19127.", "executiveText": "Disables Exchange Web Services (EWS) across the organization to reduce attack surface and prevent legacy API access to sensitive mailbox data. This aligns with Microsoft's Baseline Security Mode recommendation to minimize exploitable endpoints while requiring updates to applications that depend on EWS.", "addedComponent": [], @@ -8197,6 +8197,72 @@ "EXCHANGE_LITE" ] }, + { + "name": "standards.EWSAllowedAppIds", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Adds the selected applications to the Exchange Online EWS app allow list (EwsAllowedAppIDs) and sets EwsEnabled to true. Apps already on the list are kept, and known-malicious apps are never added. Do not use together with the \"Disable Exchange Web Services\" standard on the same tenant: they set EwsEnabled to opposite values. Once CIPP writes the list, Microsoft stops auto-populating it for that tenant, so include every app the tenant needs (check the EWS usage report in the Microsoft 365 admin center). Changes can take up to 24 hours to apply. Keep \"Include the dedicated Exchange hybrid app\" on for hybrid organisations so Free/Busy and MailTips keep working.", + "docsDescription": "When EWS is enabled, Exchange Online only allows EWS access from the application IDs on the organization's EwsAllowedAppIDs list, and an empty list blocks all EWS. Microsoft populates that list from recent usage only while the admin has never set it, so the list CIPP writes must be complete. This standard reads the current list and adds the selected presets, the custom application IDs (tenant variables are supported, invalid IDs are skipped with a warning) and, optionally, every app that holds the Exchange Online full_access_as_app application permission or a delegated EWS permission, plus the dedicated Exchange hybrid app (ExchangeServerApp-*) that hybrid Free/Busy, MailTips, profile photos and archive moves depend on. IDs already on the list are never removed; the only exception is known-malicious apps (CIPP's curated list), which are removed only when that option is enabled and are reported as drift otherwise. Known-malicious apps are never added. This conflicts with the \"Disable Exchange Web Services\" standard. Changes can take up to 24 hours to take effect. See Microsoft's guidance on the deprecation of EWS in Exchange Online: https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/deprecation-of-ews-exchange-online", + "executiveText": "Keeps the business applications that still rely on Exchange Web Services working as Microsoft retires unrestricted EWS access, by maintaining the approved-application list for each tenant. Existing approvals are preserved and applications known to be used in attacks are never approved.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "required": false, + "name": "standards.EWSAllowedAppIds.presets", + "label": "Known applications to allow (empty = Microsoft Office, Power Query for Excel, Power BI Data Refresh, Apple Mail/Calendar)", + "options": [ + { "label": "Microsoft Office", "value": "MicrosoftOffice" }, + { "label": "Microsoft Power Query for Excel", "value": "PowerQuery" }, + { "label": "Power BI Data Refresh", "value": "PowerBIDataRefresh" }, + { "label": "Apple Mail/Calendar (macOS)", "value": "AppleMail" }, + { + "label": "AvePoint Cloud Backup / Fly / Cloud Governance (hosted)", + "value": "AvePointCloud" + }, + { "label": "AvePoint Fly Server", "value": "AvePointFlyServer" } + ] + }, + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": false, + "name": "standards.EWSAllowedAppIds.customAppIds", + "label": "Additional application (client) IDs, tenant variables such as %veeam_ews_appid% are supported" + }, + { + "type": "switch", + "name": "standards.EWSAllowedAppIds.includeEwsPermissionApps", + "label": "Include apps holding EWS permissions (full_access_as_app, EWS.AccessAsUser.All, full_access_as_user)" + }, + { + "type": "switch", + "name": "standards.EWSAllowedAppIds.includeHybridApp", + "label": "Include the dedicated Exchange hybrid app (ExchangeServerApp-*)", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.EWSAllowedAppIds.removeMaliciousApps", + "label": "Remove known-malicious apps from the list" + } + ], + "label": "Configure EWS allowed applications", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-09-28", + "powershellEquivalent": "Set-OrganizationConfig -EwsEnabled $true -EwsAllowedAppIDs", + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, { "name": "standards.MessageEncryption", "cat": "Exchange Standards", diff --git a/src/pages/cipp/settings/index.jsx b/src/pages/cipp/settings/index.jsx index bc513c298231..f086b3a2665a 100644 --- a/src/pages/cipp/settings/index.jsx +++ b/src/pages/cipp/settings/index.jsx @@ -38,6 +38,14 @@ const Page = () => { + + + diff --git a/src/pages/dashboardv1.jsx b/src/pages/dashboardv1.jsx deleted file mode 100644 index ca4ce30603ed..000000000000 --- a/src/pages/dashboardv1.jsx +++ /dev/null @@ -1,464 +0,0 @@ -import Head from "next/head"; -import { useEffect, useState } from "react"; -import { Box, Container, Button, Card, CardContent } from "@mui/material"; -import { Grid } from "@mui/system"; -import { CippInfoBar } from "../components/CippCards/CippInfoBar"; -import { CippChartCard } from "../components/CippCards/CippChartCard"; -import { CippPropertyListCard } from "../components/CippCards/CippPropertyListCard"; -import { Layout as DashboardLayout } from "../layouts/index"; -import { useSettings } from "../hooks/use-settings"; -import { getCippFormatting } from "../utils/get-cipp-formatting"; -import Portals from "../data/portals"; -import { BulkActionsMenu } from "../components/bulk-actions-menu"; -import { CippUniversalSearch } from "../components/CippCards/CippUniversalSearch.jsx"; -import { ApiGetCall } from "../api/ApiCall.jsx"; -import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClipboard.jsx"; -import { ExecutiveReportButton } from "../components/ExecutiveReportButton"; - -const Page = () => { - const settings = useSettings(); - const { currentTenant } = settings; - const [domainVisible, setDomainVisible] = useState(false); - - const organization = ApiGetCall({ - url: "/api/ListGraphRequest", - queryKey: `${currentTenant}-ListGraphRequest-organization`, - data: { tenantFilter: currentTenant, Endpoint: "organization" }, - }); - - const organizationRecord = organization.data?.Results?.[0]; - - const dashboard = ApiGetCall({ - url: "/api/ListuserCounts", - data: { tenantFilter: currentTenant }, - queryKey: `${currentTenant}-ListuserCounts`, - }); - - const sharepoint = ApiGetCall({ - url: "/api/ListSharepointQuota", - queryKey: `${currentTenant}-ListSharepointQuota`, - data: { tenantFilter: currentTenant }, - }); - - const standards = ApiGetCall({ - url: "/api/ListStandardTemplates", - queryKey: `${currentTenant}-ListStandardTemplates`, - }); - - const driftApi = ApiGetCall({ - url: "/api/listTenantDrift", - data: { - TenantFilter: currentTenant, - }, - queryKey: `TenantDrift-${currentTenant}`, - }); - - const partners = ApiGetCall({ - url: "/api/ListGraphRequest", - queryKey: `${currentTenant}-ListPartners`, - data: { - Endpoint: "policies/crossTenantAccessPolicy/partners", - tenantFilter: currentTenant, - ReverseTenantLookup: true, - }, - }); - - const currentTenantInfo = ApiGetCall({ - url: "/api/ListTenants", - queryKey: `ListTenants`, - }); - - // Top bar data - const tenantInfo = [ - { name: "Tenant Name", data: organizationRecord?.displayName }, - { - name: "Tenant ID", - data: ( - <> - - - ), - }, - { - name: "Default Domain", - data: ( - <> - domain.isDefault === true)?.name - } - type="chip" - /> - - ), - }, - { - name: "AD Sync Enabled", - data: getCippFormatting(organizationRecord?.onPremisesSyncEnabled, "dirsync"), - }, - ]; - - // Process drift data for chart - filter by current tenant and aggregate - const processDriftDataForTenant = (driftData, currentTenant) => { - if (!driftData) { - return { - alignedCount: 0, - acceptedDeviationsCount: 0, - currentDeviationsCount: 0, - customerSpecificDeviations: 0, - hasData: false, - }; - } - - const rawDriftData = driftData || []; - const tenantDriftData = Array.isArray(rawDriftData) - ? rawDriftData.filter((item) => item.tenantFilter === currentTenant) - : []; - - const hasData = tenantDriftData.length > 0; - - // Aggregate data across all standards for this tenant - const aggregatedData = tenantDriftData.reduce( - (acc, item) => { - acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0; - acc.currentDeviationsCount += item.currentDeviationsCount || 0; - acc.alignedCount += item.alignedCount || 0; - acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; - return acc; - }, - { - acceptedDeviationsCount: 0, - currentDeviationsCount: 0, - alignedCount: 0, - customerSpecificDeviations: 0, - } - ); - - return { ...aggregatedData, hasData }; - }; - - function getActionCountsForTenant(standardsData, currentTenant) { - if (!standardsData) { - return { - remediateCount: 0, - alertCount: 0, - reportCount: 0, - total: 0, - }; - } - - const applicableTemplates = standardsData.filter((template) => { - const tenantFilterArr = Array.isArray(template?.tenantFilter) ? template.tenantFilter : []; - const excludedTenantsArr = Array.isArray(template?.excludedTenants) - ? template.excludedTenants - : []; - - const tenantInFilter = - tenantFilterArr.length > 0 && tenantFilterArr.some((tf) => tf.value === currentTenant); - - const allTenantsTemplate = - tenantFilterArr.some((tf) => tf.value === "AllTenants") && - (excludedTenantsArr.length === 0 || - !excludedTenantsArr.some((et) => et.value === currentTenant)); - - return tenantInFilter || allTenantsTemplate; - }); - - // Combine standards from all applicable templates: - let combinedStandards = {}; - for (const template of applicableTemplates) { - for (const [standardKey, standardValue] of Object.entries(template.standards)) { - combinedStandards[standardKey] = standardValue; - } - } - - // Count each action type: - let remediateCount = 0; - let alertCount = 0; - let reportCount = 0; - - for (const [, standard] of Object.entries(combinedStandards)) { - let actions = standard.action || []; - if (!Array.isArray(actions)) { - actions = [actions]; - } - actions.forEach((actionObj) => { - if (actionObj?.value === "Remediate") { - remediateCount++; - } else if (actionObj?.value === "Alert") { - alertCount++; - } else if (actionObj?.value === "Report") { - reportCount++; - } - }); - } - - const total = Object.keys(combinedStandards).length; - - return { remediateCount, alertCount, reportCount, total }; - } - - const driftData = processDriftDataForTenant(driftApi.data, currentTenant); - const { remediateCount, alertCount, reportCount, total } = getActionCountsForTenant( - standards.data, - currentTenant - ); - - const [PortalMenuItems, setPortalMenuItems] = useState([]); - const [partnersVisible, setPartnersVisible] = useState(false); - - const formatStorageSize = (sizeInMB) => { - if (sizeInMB >= 1024) { - return `${(sizeInMB / 1024).toFixed(2)}GB`; - } - return `${sizeInMB}MB`; - }; - - // Function to filter portals based on user preferences - const getFilteredPortals = () => { - const defaultLinks = { - M365_Portal: true, - Exchange_Portal: true, - Entra_Portal: true, - Teams_Portal: true, - Azure_Portal: true, - Intune_Portal: true, - SharePoint_Admin: true, - Security_Portal: true, - Compliance_Portal: true, - Power_Platform_Portal: true, - Power_BI_Portal: true, - }; - - let portalLinks; - if (settings.UserSpecificSettings?.portalLinks) { - portalLinks = { ...defaultLinks, ...settings.UserSpecificSettings.portalLinks }; - } else if (settings.portalLinks) { - portalLinks = { ...defaultLinks, ...settings.portalLinks }; - } else { - portalLinks = defaultLinks; - } - - // Filter the portals based on user settings - return Portals.filter((portal) => { - const settingKey = portal.name; - return settingKey ? portalLinks[settingKey] === true : true; - }); - }; - - useEffect(() => { - if (currentTenantInfo.isSuccess) { - const tenantLookup = currentTenantInfo.data?.find( - (tenant) => tenant.defaultDomainName === currentTenant - ); - - // Get filtered portals based on user preferences - const filteredPortals = getFilteredPortals(); - - const menuItems = filteredPortals.map((portal) => ({ - label: portal.label, - target: "_blank", - // A portal with a `field` has a URL the backend resolved for us (SharePoint's host cannot be - // derived from the tenant). Use it when it's there, otherwise fall back to the templated URL. - link: - portal.field && tenantLookup?.[portal.field] - ? tenantLookup[portal.field] - : portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), - icon: portal.icon, - })); - setPortalMenuItems(menuItems); - } - }, [ - currentTenantInfo.isSuccess, - currentTenant, - settings.portalLinks, - settings.UserSpecificSettings, - ]); - - return ( - <> - - Dashboard - - - - - - - - - - - {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} - - - - - - - - - - - - - - - - - - - - - {/* Converted Domain Names to Property List */} - - ({ - label: "", - value: domain.name, - }))} - actionButton={ - organizationRecord?.verifiedDomains?.length > 3 && ( - - ) - } - /> - - - - ({ - label: partner.TenantInfo?.displayName, - value: partner.TenantInfo?.defaultDomainName, - }))} - actionButton={ - partners.data?.Results?.length > 3 && ( - - ) - } - /> - - - - - plan.capabilityStatus === "Enabled" && - ["exchange", "AADPremiumService", "WindowsDefenderATP"].includes( - plan.service - ) - ) - .reduce((uniqueServices, curr) => { - const serviceLabel = - curr.service === "exchange" - ? "Exchange" - : curr.service === "AADPremiumService" - ? "AAD Premium" - : curr.service === "Windows Defender" - ? "Windows Defender" - : curr.service; - - if (!uniqueServices.includes(serviceLabel)) { - uniqueServices.push(serviceLabel); - } - return uniqueServices; - }, []) - .join(", "), - }, - ]} - /> - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/dashboardv2/tabOptions.json b/src/pages/dashboardv2/tabOptions.json index 6014478ba77b..f1545ba140eb 100644 --- a/src/pages/dashboardv2/tabOptions.json +++ b/src/pages/dashboardv2/tabOptions.json @@ -18,10 +18,5 @@ "label": "Custom", "path": "/dashboardv2/custom", "icon": "Apps" - }, - { - "label": "Previous Dashboard Experience", - "path": "/dashboardv1", - "icon": "History" } ] diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index a18a01ffb91c..3ee60f1f70e8 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -22,7 +22,7 @@ import { CippFormTenantSelector } from '../../../../components/CippComponents/Ci import CippButtonCard from '../../../../components/CippCards/CippButtonCard' import alertList from '../../../../data/alerts.json' import auditLogTemplates from '../../../../data/AuditLogTemplates' -import alertActions from '../../../../data/AuditLogTemplates' +import alertActions from '../../../../data/alertActions.json' import auditLogSchema from '../../../../data/AuditLogSchema.json' import { Layout as DashboardLayout } from '../../../../layouts/index' // Dashboard layout import { CippApiResults } from '../../../../components/CippComponents/CippApiResults' diff --git a/src/pages/tenant/baselines/template.jsx b/src/pages/tenant/baselines/template.jsx index b2e4ec8231e9..013eadb9cc46 100644 --- a/src/pages/tenant/baselines/template.jsx +++ b/src/pages/tenant/baselines/template.jsx @@ -23,6 +23,7 @@ import { Grid } from '@mui/system' import { useCallback, useEffect, useRef, useState } from 'react' import { useForm, useWatch } from 'react-hook-form' import { useRouter } from 'next/router' +import Link from 'next/link' import { get } from 'lodash' import { Layout as DashboardLayout } from '../../../layouts/index' import { CippHead } from '../../../components/CippComponents/CippHead' @@ -60,6 +61,8 @@ const unitOptions = [ { label: 'Weeks', value: 'weeks' }, ] +const MAX_STAGES = 20 + const logicOptions = [ { label: 'All conditions must match (AND)', value: 'and' }, { label: 'Any condition may match (OR)', value: 'or' }, @@ -766,6 +769,7 @@ const Page = () => { ) const handleAddStage = () => { + if (stages.length >= MAX_STAGES) return mutateStages((prev) => [ ...prev, { @@ -789,6 +793,7 @@ const Page = () => { // Duplicate a stage (standards + graduation condition structure) as a new stage at the end. const handleCopyStage = (stageIndex) => { + if (stages.length >= MAX_STAGES) return mutateStages((prev) => { const source = prev[stageIndex] return [ @@ -948,7 +953,20 @@ const Page = () => { alignItems: { xs: 'stretch', sm: 'center' }, mb: 1 }}> + + {/* A fixed link, not router.back(): the editor is often opened from Alignment, + and "back" should land on the Baselines list either way. */} + {pageTitle} + { color="primary" startIcon={} endIcon={} + disabled={stages.length >= MAX_STAGES} onClick={(event) => setAddStageAnchor(event.currentTarget)} > - Add Stage + {stages.length >= MAX_STAGES + ? `Maximum of ${MAX_STAGES} stages reached` + : 'Add Stage'} { 'updatedBy', ]} queryKey="ListBaselines-table" + defaultSorting={[{ id: 'baselineName', desc: false }]} filters={filterList} /> ); diff --git a/tests/utils/get-filtered-portals.test.js b/tests/utils/get-filtered-portals.test.js index bbd1fbc0ad17..0eab592f661c 100644 --- a/tests/utils/get-filtered-portals.test.js +++ b/tests/utils/get-filtered-portals.test.js @@ -5,8 +5,8 @@ import Portals from "../../src/data/portals"; const names = (portals) => portals.map((p) => p.name); // Pre-existing mismatch, documented rather than fixed here: portals.json splits Power -// Platform into _Admin/_Maker entries, while the defaults map (and the preferences toggle, -// and dashboardv1) still key on the un-suffixed Power_Platform_Portal — so those two are +// Platform into _Admin/_Maker entries, while the defaults map (and the preferences toggle) +// still key on the un-suffixed Power_Platform_Portal — so those two are // filtered out for everyone and their preference toggle controls nothing. const UNREACHABLE_BY_DEFAULT = ["Power_Platform_Portal_Admin", "Power_Platform_Portal_Maker"]; const defaultVisible = names(Portals).filter((n) => !UNREACHABLE_BY_DEFAULT.includes(n));