diff --git a/package.json b/package.json
index 292491020d6d..5c3967584cb6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "10.7.0",
+ "version": "10.7.1",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
diff --git a/public/version.json b/public/version.json
index a0adb1556b16..ac3d65fd9d1c 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "10.7.0"
+ "version": "10.7.1"
}
diff --git a/src/components/CippComponents/CippPolicyCompareDialog.jsx b/src/components/CippComponents/CippPolicyCompareDialog.jsx
new file mode 100644
index 000000000000..123c9477fee2
--- /dev/null
+++ b/src/components/CippComponents/CippPolicyCompareDialog.jsx
@@ -0,0 +1,174 @@
+import { useEffect, useMemo } from 'react'
+import {
+ Alert,
+ AlertTitle,
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Skeleton,
+ Stack,
+ Typography,
+} from '@mui/material'
+import {
+ CheckCircle as CheckCircleIcon,
+ CompareArrows as CompareArrowsIcon,
+ Error as ErrorIcon,
+ Info as InfoIcon,
+ PolicyOutlined as PolicyOffIcon,
+} from '@mui/icons-material'
+import { ApiPostCall } from '../../api/ApiCall'
+import CippJsonView from '../CippFormPages/CippJSONView'
+import { CippPolicyDiffTable } from './CippPolicyDiffTable'
+
+/**
+ * Live "compare to baseline" for a standard backed by an Intune template.
+ *
+ * The drift and standards pages know which template a standard points at but not which policy it
+ * landed on in the tenant, so the tenant side is resolved server-side by the template's display
+ * name - the same lookup the IntuneTemplate standard itself performs. The baseline side is sent
+ * with the tenant so it goes through the standard's reusable-settings sync and text replacement,
+ * which is what makes the differences shown here match what drift reports.
+ */
+export const CippPolicyCompareDialog = ({
+ open,
+ onClose,
+ tenantFilter,
+ templateGuid,
+ templateName,
+ standardsTemplateId,
+}) => {
+ const compareApi = ApiPostCall({ relatedQueryKeys: [] })
+
+ useEffect(() => {
+ if (!open || !tenantFilter || !templateGuid) return
+ compareApi.mutate({
+ url: '/api/ExecCompareIntunePolicy',
+ data: {
+ sourceA: { type: 'template', templateGuid, tenantFilter },
+ // standardsTemplateId lets the lookup honour the standard's fuzzy match setting, so the
+ // comparison targets the policy remediation would actually overwrite.
+ sourceB: { type: 'tenantPolicyByTemplate', templateGuid, tenantFilter, standardsTemplateId },
+ },
+ })
+ // Refire on every open so the comparison is always against the tenant's current state.
+ }, [open, tenantFilter, templateGuid, standardsTemplateId])
+
+ const results = useMemo(() => {
+ if (!compareApi.isSuccess) return null
+ return compareApi.data?.data || compareApi.data
+ }, [compareApi.isSuccess, compareApi.data])
+
+ const errorMessage = useMemo(() => {
+ if (!compareApi.isError) return null
+ const errData = compareApi.error?.response?.data
+ return errData?.Results || compareApi.error?.message || 'An error occurred'
+ }, [compareApi.isError, compareApi.error])
+
+ const comparisonRows = useMemo(() => {
+ const raw = results?.Results
+ const rows = Array.isArray(raw) ? raw : raw ? [raw] : []
+ return rows.filter((row) => row && typeof row === 'object')
+ }, [results?.Results])
+
+ const handleClose = () => {
+ compareApi.reset()
+ onClose()
+ }
+
+ return (
+
+ )
+}
+
+export default CippPolicyCompareDialog
diff --git a/src/components/CippComponents/CippPolicyDiffTable.jsx b/src/components/CippComponents/CippPolicyDiffTable.jsx
new file mode 100644
index 000000000000..8accfbaf24da
--- /dev/null
+++ b/src/components/CippComponents/CippPolicyDiffTable.jsx
@@ -0,0 +1,112 @@
+import {
+ Box,
+ Chip,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography,
+} from '@mui/material'
+
+const hasValue = (val) => val !== null && val !== undefined && val !== ''
+
+// A row carries the two sides in ExpectedValue/ReceivedValue. Which of them are populated is what
+// distinguishes a genuine difference from a property that only one side has at all.
+export const getDiffStatus = (row) => {
+ const a = hasValue(row.ExpectedValue)
+ const b = hasValue(row.ReceivedValue)
+ if (a && b) return 'different'
+ if (a) return 'onlyA'
+ if (b) return 'onlyB'
+ return 'equal'
+}
+
+const diffStatusColors = {
+ different: 'error',
+ onlyA: 'warning',
+ onlyB: 'info',
+ equal: 'success',
+}
+
+const diffRowColors = {
+ different: { dark: 'rgba(244, 67, 54, 0.08)', light: 'rgba(244, 67, 54, 0.04)' },
+ onlyA: { dark: 'rgba(255, 152, 0, 0.08)', light: 'rgba(255, 152, 0, 0.04)' },
+ onlyB: { dark: 'rgba(33, 150, 243, 0.08)', light: 'rgba(33, 150, 243, 0.04)' },
+ equal: { dark: 'transparent', light: 'transparent' },
+}
+
+const getRowColor = (row, theme) => {
+ const colors = diffRowColors[getDiffStatus(row)]
+ return theme.palette.mode === 'dark' ? colors.dark : colors.light
+}
+
+export const formatDiffValue = (val) => {
+ if (val === null || val === undefined) return N/A
+ if (typeof val === 'object') {
+ return (
+
+ {JSON.stringify(val, null, 2)}
+
+ )
+ }
+ return String(val)
+}
+
+/**
+ * Renders the per-property differences returned by /api/ExecCompareIntunePolicy.
+ *
+ * labelA/labelB name the two sides in both the column headers and the status chips, so the same
+ * table reads correctly whether it is comparing two arbitrary sources or a tenant against its
+ * baseline template.
+ */
+export const CippPolicyDiffTable = ({ rows = [], labelA = 'Source A', labelB = 'Source B' }) => {
+ const diffChipLabels = {
+ different: 'Different',
+ onlyA: `Only in ${labelA}`,
+ onlyB: `Only in ${labelB}`,
+ equal: 'Equal',
+ }
+
+ return (
+
+
+
+
+ Property
+ {labelA}
+ {labelB}
+ Status
+
+
+
+ {rows.map((row, index) => {
+ const status = getDiffStatus(row)
+ return (
+ ({ backgroundColor: getRowColor(row, theme) })}>
+ {row.Property}
+ {formatDiffValue(row.ExpectedValue)}
+ {formatDiffValue(row.ReceivedValue)}
+
+
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+export default CippPolicyDiffTable
diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx
index 2cca5a975d05..b49e85be99f4 100644
--- a/src/components/CippComponents/CippTenantSelector.jsx
+++ b/src/components/CippComponents/CippTenantSelector.jsx
@@ -85,7 +85,12 @@ export const CippTenantSelector = React.forwardRef((props, ref) => {
{
key: "SharePoint_Admin",
label: "SharePoint Portal",
- link: `/api/ListSharePointAdminUrl?tenantFilter=${currentTenant?.value}`,
+ // The only portal whose host cannot be derived from the tenant - it has to be resolved
+ // through Graph. Use the URL the backend already resolved when it has one; otherwise fall
+ // back to the endpoint that resolves it and redirects.
+ link:
+ currentTenant?.addedFields?.sharepointAdminUrl ||
+ `/api/ListSharePointAdminUrl?tenantFilter=${currentTenant?.value}`,
icon: "Share",
external: true,
},
@@ -227,6 +232,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => {
displayName: matchingTenant.displayName,
customerId: matchingTenant.customerId,
initialDomainName: matchingTenant.initialDomainName,
+ sharepointAdminUrl: matchingTenant.SharepointAdminUrl,
},
});
}
@@ -287,6 +293,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => {
displayName: matchingTenant.displayName,
customerId: matchingTenant.customerId,
initialDomainName: matchingTenant.initialDomainName,
+ sharepointAdminUrl: matchingTenant.SharepointAdminUrl,
},
}
: {
@@ -351,16 +358,25 @@ export const CippTenantSelector = React.forwardRef((props, ref) => {
onChange={(nv) => setSelectedTenant(nv)}
options={
tenantList.isSuccess && tenantList.data && tenantList.data.length > 0
- ? tenantList.data.map(({ customerId, displayName, defaultDomainName, initialDomainName }) => ({
- value: defaultDomainName,
- label: `${displayName} (${defaultDomainName})`,
- addedFields: {
- defaultDomainName: defaultDomainName,
- displayName: displayName,
- customerId: customerId,
- initialDomainName: initialDomainName,
- },
- }))
+ ? tenantList.data.map(
+ ({
+ customerId,
+ displayName,
+ defaultDomainName,
+ initialDomainName,
+ SharepointAdminUrl,
+ }) => ({
+ value: defaultDomainName,
+ label: `${displayName} (${defaultDomainName})`,
+ addedFields: {
+ defaultDomainName: defaultDomainName,
+ displayName: displayName,
+ customerId: customerId,
+ initialDomainName: initialDomainName,
+ sharepointAdminUrl: SharepointAdminUrl,
+ },
+ })
+ )
: []
}
getOptionLabel={(option) => option?.label || ""}
diff --git a/src/components/CippSettings/CippGDAP/CippGDAPTrace.jsx b/src/components/CippSettings/CippGDAP/CippGDAPTrace.jsx
index 4cea7e5c94b4..7e24a05be48a 100644
--- a/src/components/CippSettings/CippGDAP/CippGDAPTrace.jsx
+++ b/src/components/CippSettings/CippGDAP/CippGDAPTrace.jsx
@@ -45,7 +45,9 @@ export function useCippGDAPTrace() {
icon: ,
noConfirm: true,
customFunction: (row) => ref.current?.open(row),
- condition: (row) => row.displayName !== "*Partner Tenant",
+ // Direct tenants have no GDAP relationship to trace.
+ condition: (row) =>
+ row.displayName !== "*Partner Tenant" && row.delegatedPrivilegeStatus !== "directTenant",
}),
[]
);
diff --git a/src/components/CippSettings/CippPermissionReport.jsx b/src/components/CippSettings/CippPermissionReport.jsx
index 8a56b995650b..7a7e72e74402 100644
--- a/src/components/CippSettings/CippPermissionReport.jsx
+++ b/src/components/CippSettings/CippPermissionReport.jsx
@@ -68,15 +68,17 @@ export const CippPermissionReport = (props) => {
"DisplayName",
"DefaultDomainName",
"UserPrincipalName",
+ "ServiceAccount",
"IPAddress",
- "GDAPRoles",
+ "AssignedRoles",
+ "GDAPRoles", // reports exported before AssignedRoles was renamed
];
if (formData.redactCustomerData) {
report.Tenants.Results = report?.Tenants?.Results?.map((tenant) => {
customerProps.forEach((prop) => {
if (tenant?.[prop]) {
- if (prop === "GDAPRoles") {
+ if (prop === "AssignedRoles" || prop === "GDAPRoles") {
tenant[prop] = tenant[prop].map((role) => {
if (Array.isArray(role?.Group)) {
role.Group = role.Group.map((group) => group?.split("@")[0]);
diff --git a/src/components/CippSettings/CippTenantResults.jsx b/src/components/CippSettings/CippTenantResults.jsx
index dc79285ccb4e..8185f564397b 100644
--- a/src/components/CippSettings/CippTenantResults.jsx
+++ b/src/components/CippSettings/CippTenantResults.jsx
@@ -14,17 +14,21 @@ export const CippTenantResults = (props) => {
actions={[]}
simpleColumns={[
"TenantName",
+ "TenantType",
"LastRun",
"GraphStatus",
"ExchangeStatus",
"MissingRoles",
- "GDAPRoles",
+ "AssignedRoles",
]}
offCanvas={{
extendedInfoFields: [
"TenantName",
"TenantId",
+ "TenantType",
"DefaultDomainName",
+ "ServiceAccount",
+ "ServiceAccountLastAuth",
"LastRun",
"GraphTest",
"ExchangeTest",
@@ -69,17 +73,22 @@ export const CippTenantResults = (props) => {
]}
simpleColumns={[
"TenantName",
+ "TenantType",
+ "ServiceAccount",
"LastRun",
"GraphStatus",
"ExchangeStatus",
"MissingRoles",
- "GDAPRoles",
+ "AssignedRoles",
]}
offCanvas={{
extendedInfoFields: [
"TenantName",
"TenantId",
+ "TenantType",
"DefaultDomainName",
+ "ServiceAccount",
+ "ServiceAccountLastAuth",
"LastRun",
"GraphTest",
"ExchangeTest",
diff --git a/src/components/CippWizard/CippWizardVacationActions.jsx b/src/components/CippWizard/CippWizardVacationActions.jsx
index 3f23bcc484a6..7a8db5b558c7 100644
--- a/src/components/CippWizard/CippWizardVacationActions.jsx
+++ b/src/components/CippWizard/CippWizardVacationActions.jsx
@@ -147,7 +147,10 @@ export const CippWizardVacationActions = (props) => {
Vacation mode uses group-based exclusions for reliability. The exclusion group
- follows the format: 'Vacation Exclusion - $Policy.displayName'
+ follows the format: 'Vacation Exclusion - $Policy.displayName'. For
+ longer policy names the name is shortened and suffixed with the start of the
+ policy ID, e.g. 'Vacation Exclusion - CA005-RegisterSecurityInfo: Require...
+ [a1b2c3d4]'
diff --git a/src/components/CippWizard/OnboardingWizardPage.jsx b/src/components/CippWizard/OnboardingWizardPage.jsx
index 6b0876ad9a58..3ae07c4e84da 100644
--- a/src/components/CippWizard/OnboardingWizardPage.jsx
+++ b/src/components/CippWizard/OnboardingWizardPage.jsx
@@ -22,6 +22,9 @@ const OnboardingWizardPage = () => {
? selectedOptionQuery[0]
: selectedOptionQuery
+ const tenantTypeQuery = router.query?.tenantType
+ const deepLinkedTenantType = Array.isArray(tenantTypeQuery) ? tenantTypeQuery[0] : tenantTypeQuery
+
const setupOptions = [
{
description:
@@ -62,6 +65,13 @@ const OnboardingWizardPage = () => {
typeof deepLinkedOption === 'string' &&
setupOptions.some((option) => option.value === deepLinkedOption)
+ // A deep link that already names the tenant type skips the type selection and lands the user
+ // straight on that type's step, e.g. re-authenticating a direct tenant from the tenants list.
+ const hasDeepLinkedTenantType =
+ hasDeepLinkedOption &&
+ deepLinkedOption === 'AddTenant' &&
+ ['GDAP', 'Direct', 'IndirectReseller'].includes(deepLinkedTenantType)
+
const steps = [
{
description: 'Onboarding',
@@ -90,7 +100,7 @@ const OnboardingWizardPage = () => {
{
description: 'Tenant Type',
component: CippAddTenantTypeSelection,
- showStepWhen: (values) => values?.selectedOption === 'AddTenant',
+ showStepWhen: (values) => values?.selectedOption === 'AddTenant' && !hasDeepLinkedTenantType,
},
{
description: 'Direct Tenant',
@@ -155,7 +165,14 @@ const OnboardingWizardPage = () => {
steps={steps}
wizardTitle="Setup Wizard"
postUrl="/api/ExecCombinedSetup"
- initialState={hasDeepLinkedOption ? { selectedOption: deepLinkedOption } : undefined}
+ initialState={
+ hasDeepLinkedOption
+ ? {
+ selectedOption: deepLinkedOption,
+ ...(hasDeepLinkedTenantType && { tenantType: deepLinkedTenantType }),
+ }
+ : undefined
+ }
/>
)
}
diff --git a/src/data/portals.json b/src/data/portals.json
index 30709d4b30d7..57fc5853ee2f 100644
--- a/src/data/portals.json
+++ b/src/data/portals.json
@@ -58,6 +58,7 @@
"name": "SharePoint_Admin",
"url": "/api/ListSharePointAdminUrl?tenantFilter=defaultDomainName",
"variable": "defaultDomainName",
+ "field": "SharepointAdminUrl",
"target": "_blank",
"external": true,
"icon": "Share"
diff --git a/src/pages/cipp/logs/logentry.js b/src/pages/cipp/logs/logentry.js
index 521026693fd8..a03397b3b85f 100644
--- a/src/pages/cipp/logs/logentry.js
+++ b/src/pages/cipp/logs/logentry.js
@@ -1,14 +1,46 @@
import { useRouter } from "next/router";
import { Layout as DashboardLayout } from "../../../layouts/index.js";
import { ApiGetCall } from "../../../api/ApiCall";
-import { Button, SvgIcon, Box, Container, Chip } from "@mui/material";
+import { Box, Container, Chip, TextField, Card, CardHeader, CardContent } from "@mui/material";
import { Stack } from "@mui/system";
-import ArrowLeftIcon from "@mui/icons-material/ArrowLeft";
import { CippPropertyListCard } from "../../../components/CippCards/CippPropertyListCard";
import { CippInfoBar } from "../../../components/CippCards/CippInfoBar";
import CippFormSkeleton from "../../../components/CippFormPages/CippFormSkeleton";
import { getCippTranslation } from "../../../utils/get-cipp-translation";
+const formatRawError = (rawError) => {
+ if (rawError == null || rawError === "") return "";
+ try {
+ if (typeof rawError === "object") {
+ return JSON.stringify(rawError, null, 2) || "";
+ }
+ const asString = String(rawError).trim();
+ if (!asString || asString === "null" || asString === "undefined" || asString === "Not available") {
+ return "";
+ }
+ try {
+ return JSON.stringify(JSON.parse(asString), null, 2) || asString;
+ } catch {
+ return asString;
+ }
+ } catch {
+ return "";
+ }
+};
+
+const formatLogDataValue = (value) => {
+ if (value == null || value === "") return "N/A";
+ try {
+ if (typeof value === "object") {
+ return JSON.stringify(value, null, 2) || "N/A";
+ }
+ const asString = String(value);
+ return asString || "N/A";
+ } catch {
+ return "Unable to display value";
+ }
+};
+
const Page = () => {
const router = useRouter();
const { logentry, dateFilter } = router.query;
@@ -23,24 +55,23 @@ const Page = () => {
waiting: !!logentry,
});
- const handleBackClick = () => {
- router.push("/cipp/logs");
- };
-
// Get the log data from array
const logData = logRequest.data?.[0];
// Top info bar data like dashboard
const logInfo = logData
? [
- { name: "Log ID", data: logData.RowKey },
- { name: "Date & Time", data: new Date(logData.DateTime).toLocaleString() },
- { name: "API", data: logData.API },
+ { name: "Log ID", data: logData.RowKey ?? "N/A" },
+ {
+ name: "Date & Time",
+ data: logData.DateTime ? new Date(logData.DateTime).toLocaleString() : "N/A",
+ },
+ { name: "API", data: logData.API ?? "N/A" },
{
name: "Severity",
data: (
{
// Main log properties
const propertyItems = logData
? [
- { label: "Tenant", value: logData.Tenant },
- { label: "User", value: logData.User },
- { label: "Message", value: logData.Message },
- { label: "Tenant ID", value: logData.TenantID },
+ { label: "Tenant", value: logData.Tenant ?? "N/A" },
+ { label: "User", value: logData.User ?? "N/A" },
+ { label: "Message", value: logData.Message ?? "N/A" },
+ { label: "Tenant ID", value: logData.TenantID ?? "N/A" },
{ label: "App ID", value: logData.AppId || "None" },
{ label: "IP Address", value: logData.IP || "None" },
]
: [];
- // LogData properties
+ const formattedRawError = formatRawError(logData?.LogData?.RawError);
+
+ // LogData properties (RawError is shown separately)
const logDataItems =
- logData?.LogData && typeof logData.LogData === "object"
- ? Object.entries(logData.LogData).map(([key, value]) => ({
- label: key,
- value: typeof value === "object" ? JSON.stringify(value, null, 2) : String(value),
+ logData?.LogData && typeof logData.LogData === "object" && !Array.isArray(logData.LogData)
+ ? Object.entries(logData.LogData)
+ .filter(([key, value]) => key !== "RawError" && value != null && value !== "")
+ .map(([key, value]) => ({
+ label: key,
+ value: formatLogDataValue(value),
+ }))
+ : [];
+
+ const standardItems =
+ logData?.Standard && typeof logData.Standard === "object" && !Array.isArray(logData.Standard)
+ ? Object.entries(logData.Standard).map(([key, value]) => ({
+ label: getCippTranslation(key),
+ value: value ?? "N/A",
}))
: [];
@@ -114,13 +157,33 @@ const Page = () => {
showDivider={false}
/>
)}
- {logData?.Standard?.Standard && (
+ {formattedRawError ? (
+
+
+
+
+
+
+ ) : null}
+ {standardItems.length > 0 && (
({
- label: getCippTranslation(key),
- value: value ?? "N/A",
- }))}
+ propertyItems={standardItems}
isFetching={logRequest.isLoading}
layout="multiple"
showDivider={false}
diff --git a/src/pages/cipp/settings/permissions.js b/src/pages/cipp/settings/permissions.js
index 29f7ceb452e1..4393a3e602e7 100644
--- a/src/pages/cipp/settings/permissions.js
+++ b/src/pages/cipp/settings/permissions.js
@@ -1,4 +1,4 @@
-import { Container } from "@mui/material";
+import { Alert, Container } from "@mui/material";
import { Grid } from "@mui/system";
import { TabbedLayout } from "../../../layouts/TabbedLayout";
import { Layout as DashboardLayout } from "../../../layouts/index.js";
@@ -6,10 +6,30 @@ import tabOptions from "./tabOptions";
import CippPermissionCheck from "../../../components/CippSettings/CippPermissionCheck";
import { CippPermissionReport } from "../../../components/CippSettings/CippPermissionReport";
import { useState } from "react";
+import { ApiGetCall } from "../../../api/ApiCall";
const Page = () => {
const [importReport, setImportReport] = useState(false);
+ // Same signal the Add Tenant wizard uses to decide whether partner-only flows apply.
+ // No tenantFilter means the backend defaults to the CIPP host tenant.
+ const organization = ApiGetCall({
+ url: "/api/ListGraphRequest",
+ queryKey: "ListGraphRequest-organization-partnerTenantType",
+ data: {
+ Endpoint: "organization",
+ $select: "partnerTenantType,displayName",
+ },
+ });
+
+ const partnerTenantType = organization.data?.Results?.[0]?.partnerTenantType;
+ const partnerCheckComplete = organization.isSuccess || organization.isError;
+ const isPartner = organization.isSuccess && Boolean(partnerTenantType);
+
+ // Keep the GDAP check visible until we know it does not apply, and always show it when an
+ // imported report contains GDAP data.
+ const showGdapCheck = !partnerCheckComplete || isPartner || Boolean(importReport?.GDAP);
+
return (
@@ -20,7 +40,15 @@ const Page = () => {
-
+ {showGdapCheck ? (
+
+ ) : (
+
+ GDAP checks do not apply to this environment. Your tenants are added directly rather
+ than through Microsoft Partner Center relationships, so access is verified per tenant
+ in the Tenants check below.
+
+ )}
diff --git a/src/pages/cipp/settings/tenants.js b/src/pages/cipp/settings/tenants.js
index d680c482227d..77491217563d 100644
--- a/src/pages/cipp/settings/tenants.js
+++ b/src/pages/cipp/settings/tenants.js
@@ -14,6 +14,7 @@ import {
Delete,
Add,
Refresh,
+ VpnKey,
} from "@mui/icons-material";
import cacheTypes from "../../../data/CIPPDBCacheTypes.json";
@@ -51,6 +52,14 @@ const Page = () => {
data: { tenantFilter: "customerId" },
confirmText: "Are you sure you want to refresh the CPV permissions for [displayName]?",
multiPost: false,
+ condition: (row) =>
+ row.displayName !== "*Partner Tenant" && row.delegatedPrivilegeStatus !== "directTenant",
+ },
+ {
+ label: "Re-authenticate Connection",
+ link: "/onboardingv2?selectedOption=AddTenant&tenantType=Direct",
+ icon: ,
+ condition: (row) => row.delegatedPrivilegeStatus === "directTenant",
},
{
label: "Reset CPV Permissions",
@@ -118,6 +127,8 @@ const Page = () => {
"displayName",
"defaultDomainName",
"delegatedPrivilegeStatus",
+ "directTenantUserPrincipalName",
+ "directTenantAuthDate",
"Excluded",
"ExcludeDate",
"ExcludeUser",
@@ -145,6 +156,16 @@ const Page = () => {
value: [{ id: "Excluded", value: "Yes" }],
type: "column",
},
+ {
+ filterName: "Direct tenants",
+ value: [{ id: "delegatedPrivilegeStatus", value: "Direct Tenant" }],
+ type: "column",
+ },
+ {
+ filterName: "GDAP tenants",
+ value: [{ id: "delegatedPrivilegeStatus", value: "GDAP Tenant" }],
+ type: "column",
+ },
];
return (
diff --git a/src/pages/dashboardv1.js b/src/pages/dashboardv1.js
index 8e45642518cd..268c9f123799 100644
--- a/src/pages/dashboardv1.js
+++ b/src/pages/dashboardv1.js
@@ -258,7 +258,12 @@ const Page = () => {
const menuItems = filteredPortals.map((portal) => ({
label: portal.label,
target: "_blank",
- link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]),
+ // 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);
diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js
index b2934a05964f..eb03b7618038 100644
--- a/src/pages/dashboardv2/index.js
+++ b/src/pages/dashboardv2/index.js
@@ -169,7 +169,12 @@ const Page = () => {
const menuItems = filteredPortals.map((portal) => ({
label: portal.label,
target: '_blank',
- link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]),
+ // 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)
diff --git a/src/pages/endpoint/MEM/compare-policies/index.js b/src/pages/endpoint/MEM/compare-policies/index.js
index 809b294c8c77..64cd745bd4b0 100644
--- a/src/pages/endpoint/MEM/compare-policies/index.js
+++ b/src/pages/endpoint/MEM/compare-policies/index.js
@@ -11,19 +11,11 @@ import {
Card,
CardContent,
CardHeader,
- Typography,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
Alert,
Stack,
- Chip,
Skeleton,
} from "@mui/material";
+import { CippPolicyDiffTable } from "../../../../components/CippComponents/CippPolicyDiffTable";
import {
CompareArrows as CompareArrowsIcon,
CheckCircle as CheckCircleIcon,
@@ -226,56 +218,6 @@ const SourceSelector = ({ prefix, formControl, label }) => {
);
};
-const hasValue = (val) => val !== null && val !== undefined && val !== "";
-
-const getDiffStatus = (row) => {
- const a = hasValue(row.ExpectedValue);
- const b = hasValue(row.ReceivedValue);
- if (a && b) return "different";
- if (a) return "onlyA";
- if (b) return "onlyB";
- return "equal";
-};
-
-const diffChipProps = {
- different: { label: "Different", color: "error" },
- onlyA: { label: "Only in A", color: "warning" },
- onlyB: { label: "Only in B", color: "info" },
- equal: { label: "Equal", color: "success" },
-};
-
-const DiffStatusChip = ({ row }) => {
- const props = diffChipProps[getDiffStatus(row)];
- return ;
-};
-
-const diffRowColors = {
- different: { dark: "rgba(244, 67, 54, 0.08)", light: "rgba(244, 67, 54, 0.04)" },
- onlyA: { dark: "rgba(255, 152, 0, 0.08)", light: "rgba(255, 152, 0, 0.04)" },
- onlyB: { dark: "rgba(33, 150, 243, 0.08)", light: "rgba(33, 150, 243, 0.04)" },
- equal: { dark: "transparent", light: "transparent" },
-};
-
-const getRowColor = (row, theme) => {
- const colors = diffRowColors[getDiffStatus(row)];
- return theme.palette.mode === "dark" ? colors.dark : colors.light;
-};
-
-const formatValue = (val) => {
- if (val === null || val === undefined) return N/A;
- if (typeof val === "object") {
- return (
-
- {JSON.stringify(val, null, 2)}
-
- );
- }
- return String(val);
-};
-
const Page = () => {
const formControl = useForm({
mode: "onChange",
@@ -419,35 +361,7 @@ const Page = () => {
{!results.identical && comparisonRows.length > 0 && (
-
-
-
-
- Property
- Source A
- Source B
- Status
-
-
-
- {comparisonRows.map((row, index) => (
- ({
- backgroundColor: getRowColor(row, theme),
- })}
- >
- {row.Property}
- {formatValue(row.ExpectedValue)}
- {formatValue(row.ReceivedValue)}
-
-
-
-
- ))}
-
-
-
+
)}
{
creatable: false,
api: {
url: "/api/ListGraphRequest",
+ queryKey: "TeamsVoiceAssignableUsers",
dataKey: "Results",
data: {
Endpoint: "users",
@@ -80,7 +81,16 @@ const Page = () => {
label: "Emergency Location",
api: {
url: "/api/ListTeamsLisLocation",
- labelField: "Description",
+ queryKey: "TeamsLisLocations",
+ // Description is optional on a location, so fall back to the place name and
+ // then the street address rather than rendering "No label found".
+ labelField: (location) =>
+ location.Description ||
+ location.Location ||
+ [location.HouseNumber, location.StreetName, location.City]
+ .filter(Boolean)
+ .join(" ") ||
+ location.LocationId,
valueField: "LocationId",
},
},
@@ -95,6 +105,7 @@ const Page = () => {
"AcquiredCapabilities",
"AssignmentStatus",
"AssignedTo",
+ "EmergencyLocation",
],
actions: actions,
};
@@ -109,10 +120,11 @@ const Page = () => {
offCanvas={offCanvas}
simpleColumns={[
...reportDB.cacheColumns,
- "AssignedTo",
+ "AssignedTo.userPrincipalName",
"TelephoneNumber",
"AssignmentStatus",
"NumberType",
+ "EmergencyLocation",
"AcquiredCapabilities",
"IsoCountryCode",
"PlaceName",
diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js
index 3a04c540961a..35988e99e142 100644
--- a/src/pages/tenant/manage/applied-standards.js
+++ b/src/pages/tenant/manage/applied-standards.js
@@ -37,6 +37,7 @@ import {
Schedule,
Check,
Warning,
+ CompareArrows,
} from '@mui/icons-material'
import { getStandards } from '../../../utils/standards-data'
import { CippApiDialog } from '../../../components/CippComponents/CippApiDialog'
@@ -54,6 +55,14 @@ import tabOptions from './tabOptions.json'
import { createDriftManagementActions } from './driftManagementActions'
import { CippApiLogsDrawer } from '../../../components/CippComponents/CippApiLogsDrawer'
import { CippHead } from '../../../components/CippComponents/CippHead'
+import { CippPolicyCompareDialog } from '../../../components/CippComponents/CippPolicyCompareDialog'
+
+// Only Intune template standards can be compared live against their baseline. The standard records
+// compliance as a boolean and discards the diff, so it has to be recomputed on demand.
+const getCompareTemplateGuid = (standardId) =>
+ standardId?.startsWith('standards.IntuneTemplate.')
+ ? standardId.substring('standards.IntuneTemplate.'.length)
+ : null
const Page = () => {
const router = useRouter()
@@ -72,6 +81,7 @@ const Page = () => {
const [filter, setFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const [filterMenuAnchor, setFilterMenuAnchor] = useState(null)
+ const [compareTarget, setCompareTarget] = useState(null)
const templateDetails = ApiGetCall({
url: `/api/listStandardTemplates`,
@@ -2113,6 +2123,22 @@ const Page = () => {
+ {getCompareTemplateGuid(standard.standardId) && (
+ }
+ sx={{ flexShrink: 0, ml: 2 }}
+ onClick={() =>
+ setCompareTarget({
+ templateGuid: getCompareTemplateGuid(standard.standardId),
+ templateName: standard.standardName,
+ })
+ }
+ >
+ Compare
+
+ )}
@@ -3223,6 +3249,15 @@ const Page = () => {
}}
relatedQueryKeys={['ListStandardsCompare']}
/>
+
+ setCompareTarget(null)}
+ tenantFilter={currentTenant}
+ templateGuid={compareTarget?.templateGuid}
+ templateName={compareTarget?.templateName}
+ standardsTemplateId={templateId}
+ />
)
diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js
index bf83a90f3516..f63a754bf0df 100644
--- a/src/pages/tenant/manage/drift.js
+++ b/src/pages/tenant/manage/drift.js
@@ -14,6 +14,7 @@ import {
FactCheck,
Search,
Edit,
+ CompareArrows,
} from '@mui/icons-material'
import {
Box,
@@ -44,6 +45,7 @@ import { createDriftManagementActions } from './driftManagementActions'
import { ExecutiveReportButton } from '../../../components/ExecutiveReportButton'
import { CippAutoComplete } from '../../../components/CippComponents/CippAutocomplete'
import CippFormComponent from '../../../components/CippComponents/CippFormComponent'
+import { CippPolicyCompareDialog } from '../../../components/CippComponents/CippPolicyCompareDialog'
const ManageDriftPage = () => {
const router = useRouter()
@@ -60,6 +62,7 @@ const ManageDriftPage = () => {
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState('name')
const [selectedItems, setSelectedItems] = useState([])
+ const [compareTarget, setCompareTarget] = useState(null)
const filterForm = useForm({
defaultValues: {
@@ -1466,101 +1469,80 @@ const ManageDriftPage = () => {
setSelectedItems([])
}, [tenantFilter])
- // Add action buttons to each deviation item
- const deviationItemsWithActions = actualDeviationItems.map((item) => {
- return {
- ...item,
- cardLabelBoxActions: (
+ // Only Intune template standards can be compared live against their baseline. The standard
+ // records compliance as a boolean and discards the diff, so it has to be recomputed on demand.
+ // Note the singular prefix: "IntuneTemplates." is a tenant-only policy, which has no
+ // baseline in the template and so nothing to compare against.
+ const getCompareTemplateGuid = (item) => {
+ const name = item?.standardName
+ if (!name) return null
+ const withoutPrefix = name.startsWith('standards.') ? name.substring('standards.'.length) : name
+ return withoutPrefix.startsWith('IntuneTemplate.')
+ ? withoutPrefix.substring('IntuneTemplate.'.length)
+ : null
+ }
+
+ const buildCardActions = (item, menuKey) => {
+ const templateGuid = getCompareTemplateGuid(item)
+ return (
+
+ {templateGuid && (
+ }
+ onClick={(e) => {
+ e.stopPropagation()
+ setCompareTarget({ templateGuid, templateName: item.text })
+ }}
+ size="small"
+ >
+ Compare
+
+ )}
}
onClick={(e) => {
e.stopPropagation()
- handleMenuClick(e, item.id)
+ handleMenuClick(e, menuKey)
}}
size="small"
>
Actions
- ),
- }
- })
+
+ )
+ }
+
+ // Add action buttons to each deviation item
+ const deviationItemsWithActions = actualDeviationItems.map((item) => ({
+ ...item,
+ cardLabelBoxActions: buildCardActions(item, item.id),
+ }))
// Add action buttons to accepted deviation items
- const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => {
- return {
- ...item,
- cardLabelBoxActions: (
- }
- onClick={(e) => {
- e.stopPropagation()
- handleMenuClick(e, `accepted-${item.id}`)
- }}
- size="small"
- >
- Actions
-
- ),
- }
- })
+ const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => ({
+ ...item,
+ cardLabelBoxActions: buildCardActions(item, `accepted-${item.id}`),
+ }))
// Add action buttons to customer specific deviation items
- const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => {
- return {
- ...item,
- cardLabelBoxActions: (
- }
- onClick={(e) => {
- e.stopPropagation()
- handleMenuClick(e, `customer-${item.id}`)
- }}
- size="small"
- >
- Actions
-
- ),
- }
- })
+ const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => ({
+ ...item,
+ cardLabelBoxActions: buildCardActions(item, `customer-${item.id}`),
+ }))
// Add action buttons to denied deviation items
const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({
...item,
- cardLabelBoxActions: (
- }
- onClick={(e) => {
- e.stopPropagation()
- handleMenuClick(e, `denied-${item.id}`)
- }}
- size="small"
- >
- Actions
-
- ),
+ cardLabelBoxActions: buildCardActions(item, `denied-${item.id}`),
}))
// Add action buttons to compliant/aligned items so previously denied and now compliant entries
// can be denied again or denied with remediation persistence.
const alignedItemsWithActions = allAlignedItems.map((item) => ({
...item,
- cardLabelBoxActions: (
- }
- onClick={(e) => {
- e.stopPropagation()
- handleMenuClick(e, `aligned-${item.id}`)
- }}
- size="small"
- >
- Actions
-
- ),
+ cardLabelBoxActions: buildCardActions(item, `aligned-${item.id}`),
}))
// Combined list used to resolve selected item IDs back to their deviation data
@@ -2165,6 +2147,15 @@ const ManageDriftPage = () => {
/>
)}
+ setCompareTarget(null)}
+ tenantFilter={tenantFilter}
+ templateGuid={compareTarget?.templateGuid}
+ templateName={compareTarget?.templateName}
+ standardsTemplateId={templateId}
+ />
+
{/* Render all Menu components outside of card structure */}
{deviationItemsWithActions.map((item) => {
return (
diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js
index c31b0eefc8ff..6bb56ee61e57 100644
--- a/src/utils/get-cipp-formatting.js
+++ b/src/utils/get-cipp-formatting.js
@@ -287,6 +287,8 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
'requestDate', // App Consent Requests
'reviewedDate', // App Consent Requests
'GeneratedAt', // Report Builder
+ 'directTenantAuthDate', // Direct tenant service account
+ 'ServiceAccountLastAuth', // Direct tenant service account
]
const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration|[Tt]imestamp|[sS]tart[Dd]ate)/