From c958401045bf32a2326446cb24b4550e63b15f75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:43:59 +0000 Subject: [PATCH 01/61] chore(deps): bump dompurify from 3.4.2 to 3.4.3 Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.3. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.3) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..76e6c08f07d0 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "axios": "1.15.0", "date-fns": "4.1.0", "diff": "^8.0.3", - "dompurify": "^3.4.2", + "dompurify": "^3.4.3", "eml-parse-js": "^1.2.0-beta.0", "export-to-csv": "^1.3.0", "formik": "2.4.9", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..56f92541f6b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3717,17 +3717,10 @@ dompurify@3.2.7: optionalDependencies: "@types/trusted-types" "^2.0.7" -dompurify@^3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6" - integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA== - optionalDependencies: - "@types/trusted-types" "^2.0.7" - -dompurify@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.2.tgz#f0ff81be682c485505097ba8195a058d8f575218" - integrity sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA== +dompurify@^3.3.1, dompurify@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.3.tgz#3ef336e7a757c3bf1efbd3781afb149a3ae5cfa4" + integrity sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A== optionalDependencies: "@types/trusted-types" "^2.0.7" From a783d28ab623dc69767ffd758e5cd140f171f2d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:44:16 +0000 Subject: [PATCH 02/61] chore(deps): bump @tiptap/extension-table from 3.20.4 to 3.20.5 Bumps [@tiptap/extension-table](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-table) from 3.20.4 to 3.20.5. - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-table/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.20.5/packages/extension-table) --- updated-dependencies: - dependency-name: "@tiptap/extension-table" dependency-version: 3.20.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..ac434fa694a5 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^3.4.1", "@tiptap/extension-heading": "^3.4.1", - "@tiptap/extension-table": "^3.19.0", + "@tiptap/extension-table": "^3.20.5", "@tiptap/pm": "^3.22.3", "@tiptap/react": "^3.20.5", "@tiptap/starter-kit": "^3.20.5", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..fd4f55d4b40e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2259,10 +2259,10 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.20.5.tgz#a3689fc17ad89a23c88f11b27c7f53896caa54f3" integrity sha512-uwhvmfS4ciGYJRLUg0AHbWsprMCwyWVWd2RXOLRm0ZQeWkvzonPXZhJvzIhIgsFkPLj/dsN5t0+LdiK4UQMnyA== -"@tiptap/extension-table@^3.19.0": - version "3.20.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.20.4.tgz#b2067cf1609bb1c39b61e504dc4aa05cba13d9ca" - integrity sha512-vEHXRL9k9G02pp3P+DyUnN4YRaRAHGfTBC6gck0s9TpsCM9NIchL0qI1fb/u46Bu6UaoMMk58DGr7xaJ29g7KQ== +"@tiptap/extension-table@^3.20.5": + version "3.20.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.20.5.tgz#bac3d76e1c5fc8a4672f1495532a934651f50ce8" + integrity sha512-YvTB5OfGqjqHqutkSyywplouFvJwlsDTpZAjtAh5TzKfOan42aiVepmHVpteoQP6LH0mSjw69RndFMIYhIGmSQ== "@tiptap/extension-text@^3.20.5": version "3.20.5" From 2285d39cfdfdbbdffe377fbcc357200763f27e84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:44:34 +0000 Subject: [PATCH 03/61] chore(deps): bump @tiptap/core from 3.20.5 to 3.22.3 Bumps [@tiptap/core](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/core) from 3.20.5 to 3.22.3. - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/core/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.22.3/packages/core) --- updated-dependencies: - dependency-name: "@tiptap/core" dependency-version: 3.22.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..cfb4488fb562 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@tanstack/react-query-devtools": "^5.96.2", "@tanstack/react-query-persist-client": "^5.96.2", "@tanstack/react-table": "^8.19.2", - "@tiptap/core": "^3.4.1", + "@tiptap/core": "^3.22.3", "@tiptap/extension-heading": "^3.4.1", "@tiptap/extension-table": "^3.19.0", "@tiptap/pm": "^3.22.3", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..2a1e1d68ef0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2145,10 +2145,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== -"@tiptap/core@^3.20.5", "@tiptap/core@^3.4.1": - version "3.20.5" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.20.5.tgz#edf98b45f98463b12ed59357ea9b4bf155e3e194" - integrity sha512-Pkjd41UJ4F6Z8cPV+gEvqnt1VhY2g66xMjbpxREs0ECA5jRezCNKSZcc2pueQRTMtmn1SaSzGM9U/ifhVlVYOA== +"@tiptap/core@^3.20.5", "@tiptap/core@^3.22.3": + version "3.22.3" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.22.3.tgz#89cd6d3d374f5f757bcb5e18e70c346a9eb9b2cd" + integrity sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q== "@tiptap/extension-blockquote@^3.20.5": version "3.20.5" From d392d1cfebf3dd1fc82e50e0cc0fda449814d693 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:45:13 +0000 Subject: [PATCH 04/61] chore(deps): bump @tanstack/react-query from 5.96.2 to 5.100.10 Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.96.2 to 5.100.10. - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/HEAD/packages/react-query) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-version: 5.100.10 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..3b319eaa4bcc 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@react-pdf/renderer": "^4.3.2", "@reduxjs/toolkit": "^2.11.2", "@tanstack/query-sync-storage-persister": "^5.90.25", - "@tanstack/react-query": "^5.96.2", + "@tanstack/react-query": "^5.100.10", "@tanstack/react-query-devtools": "^5.96.2", "@tanstack/react-query-persist-client": "^5.96.2", "@tanstack/react-table": "^8.19.2", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..856baa2a6b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2051,6 +2051,11 @@ dependencies: remove-accents "0.5.0" +"@tanstack/query-core@5.100.10": + version "5.100.10" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.10.tgz#aeb34d301fd4ff9762e67dfa018adc33b7a18be4" + integrity sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w== + "@tanstack/query-core@5.91.2": version "5.91.2" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.91.2.tgz#d83825a928aa49ded38d3910f05284178cce89d3" @@ -2102,12 +2107,12 @@ dependencies: "@tanstack/query-persist-client-core" "5.96.2" -"@tanstack/react-query@^5.96.2": - version "5.96.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.96.2.tgz#a164abfb80eb5e7772bbcddfa7240f3fd8d0d7be" - integrity sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA== +"@tanstack/react-query@^5.100.10": + version "5.100.10" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.100.10.tgz#3bf1844efd76f5f68f9f39da2917fc4c6023e726" + integrity sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q== dependencies: - "@tanstack/query-core" "5.96.2" + "@tanstack/query-core" "5.100.10" "@tanstack/react-table@8.20.6": version "8.20.6" From c9bfe909582d9cc9d308903d7e74b44c6d926087 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 10:44:12 +0200 Subject: [PATCH 05/61] feat(endpoint): add MEM enrollment profiles page (Apple ADE, Android, Autopilot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new "Enrollment Profiles" page. Autopilot profiles page is kept, but could be removed in the future. Refs KelvinTegelaar/CIPP#5941 (related work — does not fully close). --- .../CippCards/CippUniversalSearchV2.jsx | 1 + .../CippComponents/CippBreadcrumbNav.jsx | 1 + src/layouts/config.js | 5 + .../EnrollmentProfileTabs.jsx | 466 ++++++++++++++++++ .../enrollment-profiles/android-enterprise.js | 14 + .../endpoint/MEM/enrollment-profiles/index.js | 14 + .../MEM/enrollment-profiles/tabOptions.json | 14 + .../enrollment-profiles/windows-autopilot.js | 14 + 8 files changed, 529 insertions(+) create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/index.js create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 070396f0a56e..7ffa2bfdb725 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -40,6 +40,7 @@ async function loadTabOptions() { "/email/administration/exchange-retention", "/cipp/custom-data", "/cipp/advanced/super-admin", + "/endpoint/MEM/enrollment-profiles", "/tenant/standards", "/tenant/manage", "/tenant/administration/applications", diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index 560f84efb631..d890e84a0554 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -16,6 +16,7 @@ async function loadTabOptions() { "/email/administration/exchange-retention", "/cipp/custom-data", "/cipp/advanced/super-admin", + "/endpoint/MEM/enrollment-profiles", "/tenant/standards", "/tenant/manage", "/tenant/administration/applications", diff --git a/src/layouts/config.js b/src/layouts/config.js index a9df0957241d..c34329156da5 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -564,6 +564,11 @@ export const nativeMenuItems = [ path: '/endpoint/MEM/list-scripts', permissions: ['Endpoint.MEM.*'], }, + { + title: 'Enrollment Profiles', + path: '/endpoint/MEM/enrollment-profiles', + permissions: ['Endpoint.MEM.*'], + }, ], }, { diff --git a/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx b/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx new file mode 100644 index 000000000000..73fd35519bd0 --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx @@ -0,0 +1,466 @@ +import { useMemo, useState } from 'react' +import { + Alert, + Button, + Card, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography, +} from '@mui/material' +import { Box, Container, Stack } from '@mui/system' +import { + AccountTree, + Apple, + ContentCopy, + Delete, + EventAvailable, + QrCode2, + Sync, +} from '@mui/icons-material' +import { CippHead } from '../../../../components/CippComponents/CippHead.jsx' +import { CippDataTable } from '../../../../components/CippTable/CippDataTable.js' +import { CippInfoBar } from '../../../../components/CippCards/CippInfoBar.jsx' +import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog.jsx' +import { CippAutopilotProfileDrawer } from '../../../../components/CippComponents/CippAutopilotProfileDrawer.jsx' +import CippJsonView from '../../../../components/CippFormPages/CippJSONView.jsx' +import { ApiGetCall } from '../../../../api/ApiCall.jsx' +import { useDialog } from '../../../../hooks/use-dialog.js' +import { useSettings } from '../../../../hooks/use-settings.js' + +const pageTitle = 'Enrollment Profiles' +const appleADEPageTitle = 'Apple Enrollment Profiles' +const androidEnterprisePageTitle = 'Android Enrollment Profiles' +const windowsAutopilotPageTitle = 'Windows Autopilot Profiles' + +const EnrollmentProfilesPage = ({ children, title = pageTitle }) => { + return ( + <> + + + + + {children} + + + + + ) +} + +const AndroidQrDialog = ({ row, drawerVisible, setDrawerVisible }) => { + const [copied, setCopied] = useState(false) + + const tokenValue = useMemo(() => { + if (row?.tokenValue) { + return row.tokenValue + } + + if (!row?.qrCodeContent) { + return '' + } + + try { + const parsed = JSON.parse(row.qrCodeContent) + const adminExtras = parsed?.['android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE'] + return adminExtras?.['com.google.android.apps.work.clouddpc.EXTRA_ENROLLMENT_TOKEN'] || '' + } catch { + return '' + } + }, [row?.qrCodeContent, row?.tokenValue]) + + const handleClose = () => { + setDrawerVisible(false) + } + + const handleCopy = async () => { + if (!tokenValue) { + return + } + + try { + await navigator.clipboard.writeText(tokenValue) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch { + setCopied(false) + } + } + + const qrCodeImageValue = row?.qrCodeImage?.value + const qrCodeImageType = row?.qrCodeImage?.type || 'image/png' + + return ( + + Enrollment QR - {row?.displayName} + + {qrCodeImageValue && ( + + Enrollment QR code + + )} + + Token value + + + + {tokenValue || 'No token value available.'} + + + + + + + ) +} + +export const AppleADEEnrollmentProfiles = () => { + const currentTenant = useSettings().currentTenant + const depSyncDialog = useDialog() + + const appleProfiles = ApiGetCall({ + url: '/api/ListAppleEnrollmentProfiles', + data: { tenantFilter: currentTenant }, + queryKey: `AppleEnrollmentProfiles-${currentTenant}`, + waiting: Boolean(currentTenant), + }) + + const appleData = appleProfiles.data?.Results || {} + const tokens = appleData.Tokens || [] + const profiles = appleData.Profiles || [] + const syncErrorCodes = { + 1: { + label: 'Expired', + message: 'The ADE token sync has expired.', + severity: 'error', + }, + 2: { + label: 'Unknown', + message: 'The ADE token sync state is unknown.', + severity: 'error', + }, + 3: { + label: 'Terms & Conditions', + message: 'New Apple Business Manager terms are ready to accept.', + severity: 'warning', + }, + 4: { + label: 'Warning', + message: 'The ADE token sync completed with a warning.', + severity: 'warning', + }, + } + const syncErrorTokens = tokens.filter( + (token) => token.lastSyncErrorCode != null && Number(token.lastSyncErrorCode) !== 0 + ) + const expiringTokens = tokens.filter( + (token) => token.daysUntilExpiration !== null && token.daysUntilExpiration <= 30 + ) + const totalSyncedDevices = tokens.reduce( + (sum, token) => sum + Number(token.syncedDeviceCount || 0), + 0 + ) + const lastSuccessfulSync = tokens + .map((token) => token.lastSuccessfulSyncDateTime) + .filter(Boolean) + .sort() + .pop() + + const infoBarData = [ + { + icon: , + name: 'ADE Tokens', + data: tokens.length, + offcanvas: { + title: 'Apple ADE Tokens', + propertyItems: tokens.flatMap((token) => [ + { + label: `${token.tokenName || token.id} - Apple ID`, + value: token.appleIdentifier || 'N/A', + }, + { + label: `${token.tokenName || token.id} - Expiration`, + value: token.tokenExpirationDateTime || 'N/A', + }, + { + label: `${token.tokenName || token.id} - Synced Devices`, + value: token.syncedDeviceCount ?? 'N/A', + }, + { + label: `${token.tokenName || token.id} - Last Sync`, + value: token.lastSuccessfulSyncDateTime || 'N/A', + }, + ]), + }, + }, + { + icon: , + name: 'Expiring Tokens', + data: expiringTokens.length, + color: expiringTokens.length ? 'error' : 'success', + toolTip: 'Tokens expiring within 30 days', + }, + { + icon: , + name: 'ADE Profiles', + data: profiles.length, + }, + { + icon: , + name: 'Last Successful Sync', + data: lastSuccessfulSync ? new Date(lastSuccessfulSync).toLocaleString() : 'N/A', + toolTip: `${totalSyncedDevices} synced devices across all tokens`, + }, + ] + + const appleActions = [ + { + label: 'Delete Profile', + type: 'POST', + icon: , + url: '/api/ExecRemoveEnrollmentProfile', + relatedQueryKeys: [`AppleEnrollmentProfiles*-${currentTenant}`], + data: { + profileId: 'id', + tokenId: 'tokenId', + profileType: 'profileType', + displayName: 'displayName', + }, + confirmText: 'Are you sure you want to delete enrollment profile [displayName]?', + color: 'danger', + }, + ] + + const appleFilters = useMemo( + () => [ + { filterName: 'All', value: [] }, + { filterName: 'macOS', value: [{ id: 'platform', value: 'macOS' }] }, + { + filterName: 'iOS/iPadOS', + value: [{ id: 'platform', value: 'iOS/iPadOS' }], + }, + ], + [] + ) + + return ( + <> + + {!appleProfiles.isFetching && + syncErrorTokens.map((token, index) => { + const errorCode = Number(token.lastSyncErrorCode) + const syncError = syncErrorCodes[errorCode] || { + label: 'Unknown Error', + message: 'The ADE token sync was not successful.', + severity: 'warning', + } + const tokenName = token.tokenName || token.id || 'Unknown token' + const appleIdentifier = token.appleIdentifier ? ` (${token.appleIdentifier})` : '' + const lastSuccessfulSyncText = token.lastSuccessfulSyncDateTime + ? ` Last successful sync: ${new Date( + token.lastSuccessfulSyncDateTime + ).toLocaleString()}.` + : '' + + return ( + + {`Token "${tokenName}"${appleIdentifier}: ${syncError.message} (Code ${errorCode} - ${syncError.label}).${lastSuccessfulSyncText}`} + + ) + })} + + + + + + + , + size: 'xl', + }} + cardButton={ + + + + } + /> + + + + + ) +} + +export const AndroidEnterpriseEnrollmentProfiles = () => { + const currentTenant = useSettings().currentTenant + const androidActions = [ + { + label: 'Show QR', + icon: , + hideBulk: true, + noConfirm: true, + condition: (row) => Boolean(row?.tokenValue || row?.qrCodeImage?.value || row?.qrCodeContent), + customComponent: (row, { drawerVisible, setDrawerVisible }) => ( + + ), + }, + { + label: 'Delete Profile', + type: 'POST', + icon: , + url: '/api/ExecRemoveEnrollmentProfile', + data: { + profileId: 'id', + profileType: '!android', + displayName: 'displayName', + }, + confirmText: 'Are you sure you want to delete Android enrollment profile [displayName]?', + color: 'danger', + }, + ] + + return ( + + + , + size: 'xl', + }} + /> + + + ) +} + +export const WindowsAutopilotEnrollmentProfiles = () => { + const currentTenant = useSettings().currentTenant + const autopilotActions = [ + { + label: 'Delete Profile', + icon: , + type: 'POST', + url: '/api/RemoveAutopilotConfig', + data: { ID: 'id', displayName: 'displayName', assignments: 'assignments' }, + confirmText: + 'Are you sure you want to delete this Autopilot profile? This action cannot be undone.', + color: 'danger', + }, + ] + + return ( + + + , + size: 'xl', + }} + cardButton={} + /> + + + ) +} diff --git a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js new file mode 100644 index 000000000000..826afd75f085 --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js @@ -0,0 +1,14 @@ +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { AndroidEnterpriseEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import tabOptions from './tabOptions.json' + +const Page = () => + +Page.getLayout = (page) => ( + + {page} + +) + +export default Page diff --git a/src/pages/endpoint/MEM/enrollment-profiles/index.js b/src/pages/endpoint/MEM/enrollment-profiles/index.js new file mode 100644 index 000000000000..e6fc7bfad50d --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/index.js @@ -0,0 +1,14 @@ +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { AppleADEEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import tabOptions from './tabOptions.json' + +const Page = () => + +Page.getLayout = (page) => ( + + {page} + +) + +export default Page diff --git a/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json new file mode 100644 index 000000000000..4bf6047e9d31 --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json @@ -0,0 +1,14 @@ +[ + { + "label": "Apple ADE", + "path": "/endpoint/MEM/enrollment-profiles" + }, + { + "label": "Android Enterprise", + "path": "/endpoint/MEM/enrollment-profiles/android-enterprise" + }, + { + "label": "Windows Autopilot", + "path": "/endpoint/MEM/enrollment-profiles/windows-autopilot" + } +] diff --git a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js new file mode 100644 index 000000000000..4c072ed7302d --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js @@ -0,0 +1,14 @@ +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { WindowsAutopilotEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import tabOptions from './tabOptions.json' + +const Page = () => + +Page.getLayout = (page) => ( + + {page} + +) + +export default Page From b19024214e96bd207640c5e63dcefb38d408e161 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 16:51:32 +0200 Subject: [PATCH 06/61] feat: Bit more margin to make tabbed layout of first item less cramped --- src/layouts/HeaderedTabbedLayout.jsx | 11 ++++++++++- src/layouts/TabbedLayout.jsx | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx index 1b5585a6812a..d36ca26f0497 100644 --- a/src/layouts/HeaderedTabbedLayout.jsx +++ b/src/layouts/HeaderedTabbedLayout.jsx @@ -102,7 +102,16 @@ export const HeaderedTabbedLayout = (props) => { )}
- + {tabOptions.map((option) => ( ))} diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index 031f363c4dac..c69157050da6 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -55,7 +55,7 @@ export const TabbedLayout = (props) => { variant="scrollable" sx={{ '& .MuiTab-root:first-of-type': { - ml: 1, + ml: 2, }, }} > From 6dab9339978f195dc8e6db35633071da8f16eb8a Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 17:50:36 +0200 Subject: [PATCH 07/61] feat(tabs): support icons in tabbed layouts --- src/layouts/HeaderedTabbedLayout.jsx | 19 +++- src/layouts/TabbedLayout.jsx | 17 +++- .../MEM/enrollment-profiles/tabOptions.json | 9 +- src/utils/icon-registry.js | 95 +++++++++++++++++++ 4 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/utils/icon-registry.js diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx index d36ca26f0497..c1abbe1d6e2b 100644 --- a/src/layouts/HeaderedTabbedLayout.jsx +++ b/src/layouts/HeaderedTabbedLayout.jsx @@ -17,6 +17,7 @@ import { } from "@mui/material"; import { ActionsMenu } from "../components/actions-menu"; import { useMediaQuery } from "@mui/material"; +import { getIconByName } from "../utils/icon-registry"; export const HeaderedTabbedLayout = (props) => { const { @@ -112,9 +113,19 @@ export const HeaderedTabbedLayout = (props) => { }, }} > - {tabOptions.map((option) => ( - - ))} + {tabOptions.map((option) => { + const icon = getIconByName(option.icon, { fontSize: "small" }); + + return ( + + ); + })}
@@ -142,6 +153,8 @@ HeaderedTabbedLayout.propTypes = { PropTypes.shape({ label: PropTypes.string.isRequired, path: PropTypes.string.isRequired, + icon: PropTypes.string, + iconPosition: PropTypes.oneOf(["bottom", "end", "start", "top"]), }) ).isRequired, title: PropTypes.string.isRequired, diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index c69157050da6..fc3f7e440773 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -3,6 +3,7 @@ import { usePathname, useRouter } from 'next/navigation' import { Box, Divider, Stack, Tab, Tabs } from '@mui/material' import { useSearchParams } from 'next/navigation' import { ApiGetCall } from '../api/ApiCall' +import { getIconByName } from '../utils/icon-registry' export const TabbedLayout = (props) => { const { tabOptions, children } = props @@ -59,9 +60,19 @@ export const TabbedLayout = (props) => { }, }} > - {visibleTabs.map((option) => ( - - ))} + {visibleTabs.map((option) => { + const icon = getIconByName(option.icon, { fontSize: 'small' }) + + return ( + + ) + })} diff --git a/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json index 4bf6047e9d31..9ed4566b1448 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json +++ b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json @@ -1,14 +1,17 @@ [ { "label": "Apple ADE", - "path": "/endpoint/MEM/enrollment-profiles" + "path": "/endpoint/MEM/enrollment-profiles", + "icon": "Apple" }, { "label": "Android Enterprise", - "path": "/endpoint/MEM/enrollment-profiles/android-enterprise" + "path": "/endpoint/MEM/enrollment-profiles/android-enterprise", + "icon": "Android" }, { "label": "Windows Autopilot", - "path": "/endpoint/MEM/enrollment-profiles/windows-autopilot" + "path": "/endpoint/MEM/enrollment-profiles/windows-autopilot", + "icon": "Window" } ] diff --git a/src/utils/icon-registry.js b/src/utils/icon-registry.js new file mode 100644 index 000000000000..a87db806b2f0 --- /dev/null +++ b/src/utils/icon-registry.js @@ -0,0 +1,95 @@ +import { + AdminPanelSettings, + Android, + Apple, + Apps, + Assignment, + BarChart, + Business, + CheckCircle, + Cloud, + Computer, + Dashboard, + Description, + Devices, + Dns, + Domain, + Email, + FactCheck, + FilePresent, + Group, + Groups, + Home, + Key, + Laptop, + List, + Lock, + Mail, + ManageAccounts, + Notifications, + Person, + Policy, + PrecisionManufacturing, + Security, + Settings, + Share, + Shield, + ShieldMoon, + Storage, + Sync, + Timeline, + Window, + Warning, +} from '@mui/icons-material' + +export const iconRegistry = { + AdminPanelSettings, + Android, + Apple, + Apps, + Assignment, + BarChart, + Business, + CheckCircle, + Cloud, + Computer, + Dashboard, + Description, + Devices, + Dns, + Domain, + Email, + FactCheck, + FilePresent, + Group, + Groups, + Home, + Key, + Laptop, + List, + Lock, + Mail, + ManageAccounts, + Notifications, + Person, + Policy, + PrecisionManufacturing, + Security, + Settings, + Share, + Shield, + ShieldMoon, + Storage, + Sync, + Timeline, + Window, + Warning, +} + +export const getIconComponentByName = (iconName) => iconRegistry[iconName] ?? null + +export const getIconByName = (iconName, props = {}) => { + const Icon = getIconComponentByName(iconName) + + return Icon ? : null +} From 9b6164999145da5dab8cb726cc2b4d6a25cba44b Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 18:46:23 +0200 Subject: [PATCH 08/61] feat: Migrate to use shared icon registry for string to icon conversion --- .../CippCards/CippPropertyListCard.jsx | 9 ++- .../CippCards/CippUniversalSearchV2.jsx | 4 +- .../CippComponents/CippTenantSelector.jsx | 53 ++++--------- src/components/bulk-actions-menu.js | 78 ++++++------------- src/data/portals.json | 6 +- src/layouts/HeaderedTabbedLayout.jsx | 5 +- src/layouts/TabbedLayout.jsx | 5 +- src/utils/icon-registry.js | 2 + 8 files changed, 63 insertions(+), 99 deletions(-) diff --git a/src/components/CippCards/CippPropertyListCard.jsx b/src/components/CippCards/CippPropertyListCard.jsx index 4e7bb2d81f0f..01eb598409da 100644 --- a/src/components/CippCards/CippPropertyListCard.jsx +++ b/src/components/CippCards/CippPropertyListCard.jsx @@ -15,6 +15,7 @@ import { PropertyListItem } from "../../components/property-list-item"; import { useDialog } from "../../hooks/use-dialog"; import { CippApiDialog } from "../CippComponents/CippApiDialog"; import { useState } from "react"; +import { getIconByName } from "../../utils/icon-registry"; export const CippPropertyListCard = (props) => { const { @@ -51,6 +52,12 @@ export const CippPropertyListCard = (props) => { return false; }; + const renderActionIcon = (icon) => { + if (!icon) return null; + if (typeof icon === "string") return getIconByName(icon, { fontSize: "small" }); + return {icon}; + }; + return ( <> @@ -160,7 +167,7 @@ export const CippPropertyListCard = (props) => { actionItems.map((item, index) => ( {item.icon}} + icon={renderActionIcon(item.icon)} label={item.label} onClick={() => { setActionData({ diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 7ffa2bfdb725..83e127352851 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -392,7 +392,7 @@ export const CippUniversalSearchV2 = React.forwardRef( const typeMenuActions = [ { label: "Users", - icon: "UsersIcon", + icon: "Groups", onClick: () => handleTypeChange("Users"), }, { @@ -412,7 +412,7 @@ export const CippUniversalSearchV2 = React.forwardRef( }, { label: "Pages", - icon: "GlobeAltIcon", + icon: "Public", onClick: () => handleTypeChange("Pages"), }, ]; diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index 74447184f4ba..0b2c3ce62508 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -1,30 +1,15 @@ import PropTypes from "prop-types"; import { CippAutoComplete } from "../CippComponents/CippAutocomplete"; import { ApiGetCall } from "../../api/ApiCall"; -import { IconButton, SvgIcon, Tooltip, Box } from "@mui/material"; -import { - FilePresent, - Laptop, - Mail, - Refresh, - Share, - Shield, - ShieldMoon, - PrecisionManufacturing, - BarChart, -} from "@mui/icons-material"; -import { - BuildingOfficeIcon, - GlobeAltIcon, - ServerIcon, - UsersIcon, -} from "@heroicons/react/24/outline"; +import { IconButton, Tooltip, Box } from "@mui/material"; +import { Refresh } from "@mui/icons-material"; import React, { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { useRouter } from "next/router"; import { CippOffCanvas } from "./CippOffCanvas"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; import { useQueryClient } from "@tanstack/react-query"; +import { getIconByName } from "../../utils/icon-registry"; export const CippTenantSelector = React.forwardRef((props, ref) => { const { width, allTenants = false, multiple = false, refreshButton, tenantButton } = props; @@ -65,68 +50,68 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { key: "M365_Portal", label: "M365 Admin Portal", link: `https://admin.cloud.microsoft/?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, - icon: , + icon: "Public", }, { key: "Exchange_Portal", label: "Exchange Portal", link: `https://admin.cloud.microsoft/exchange?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, - icon: , + icon: "Mail", }, { key: "Entra_Portal", label: "Entra Portal", link: `https://entra.microsoft.com/${currentTenant?.value}`, - icon: , + icon: "Groups", }, { key: "Teams_Portal", label: "Teams Portal", link: `https://admin.teams.microsoft.com/?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, - icon: , + icon: "FilePresent", }, { key: "Azure_Portal", label: "Azure Portal", link: `https://portal.azure.com/${currentTenant?.value}`, - icon: , + icon: "Dns", }, { key: "Intune_Portal", label: "Intune Portal", link: `https://intune.microsoft.com/${currentTenant?.value}`, - icon: , + icon: "Laptop", }, { key: "SharePoint_Admin", label: "SharePoint Portal", link: `/api/ListSharePointAdminUrl?tenantFilter=${currentTenant?.value}`, - icon: , + icon: "Share", external: true, }, { key: "Security_Portal", label: "Security Portal", link: `https://security.microsoft.com/?tid=${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "Shield", }, { key: "Compliance_Portal", label: "Compliance Portal", link: `https://purview.microsoft.com/?tid=${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "ShieldMoon", }, { key: "Power_Platform_Portal", label: "Power Platform Portal", link: `https://admin.powerplatform.microsoft.com/account/login/${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "PrecisionManufacturing", }, { key: "Power_BI_Portal", label: "Power BI Portal", link: `https://app.powerbi.com/admin-portal?ctid=${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "BarChart", }, ]; @@ -164,7 +149,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { key: "Manage_Tenant", label: "Manage Tenant", link: `/tenant/manage/edit?tenantFilter=${currentTenant?.value}`, - icon: , + icon: "Business", }); return filteredActions; @@ -343,9 +328,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { disabled={!currentTenant || currentTenant.value === "AllTenants"} > - - - + {getIconByName("Business")} )} @@ -396,9 +379,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { }} > - - - + )} diff --git a/src/components/bulk-actions-menu.js b/src/components/bulk-actions-menu.js index ff9e8e1f36dd..18f1ad5af1c0 100644 --- a/src/components/bulk-actions-menu.js +++ b/src/components/bulk-actions-menu.js @@ -1,46 +1,12 @@ -import PropTypes from "prop-types"; -import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon"; -import { Button, Link, ListItemText, Menu, MenuItem, SvgIcon } from "@mui/material"; -import { usePopover } from "../hooks/use-popover"; -import { FilePresent, Laptop, Mail, Share, Shield, ShieldMoon, PrecisionManufacturing, BarChart, Group, Apps } from "@mui/icons-material"; -import { GlobeAltIcon, UsersIcon, ServerIcon } from "@heroicons/react/24/outline"; - -function getIconByName(iconName) { - switch (iconName) { - case "GlobeAltIcon": - return ; - case "Mail": - return ; - case "UsersIcon": - return ; - case "FilePresent": - return ; - case "ServerIcon": - return ; - case "Laptop": - return ; - case "Share": - return ; - case "Shield": - return ; - case "ShieldMoon": - return ; - case "PrecisionManufacturing": - return ; - case "BarChart": - return ; - case "Group": - return ; - case "Apps": - return ; - default: - return null; - } -} +import PropTypes from 'prop-types' +import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon' +import { Button, Link, ListItemText, Menu, MenuItem, SvgIcon } from '@mui/material' +import { usePopover } from '../hooks/use-popover' +import { getIconByName } from '../utils/icon-registry' export const BulkActionsMenu = (props) => { - const { buttonName, sx, row, actions = [], ...other } = props; - const popover = usePopover(); + const { buttonName, sx, row, actions = [], ...other } = props + const popover = usePopover() return ( <> @@ -55,7 +21,7 @@ export const BulkActionsMenu = (props) => { variant="outlined" sx={{ flexShrink: 0, - whiteSpace: "nowrap", + whiteSpace: 'nowrap', ...sx, }} {...other} @@ -65,8 +31,8 @@ export const BulkActionsMenu = (props) => { { onClose={popover.handleClose} open={popover.open} transformOrigin={{ - horizontal: "right", - vertical: "top", + horizontal: 'right', + vertical: 'top', }} > {actions.map((action, index) => { + const icon = getIconByName(action.icon, { sx: { mr: 1 } }) + if (action.link) { return ( { target="_blank" rel="noreferrer" > - {getIconByName(action.icon)} + {icon} - ); + ) } else { return ( { if (action.onClick) { - action.onClick(); + action.onClick() } - popover.handleClose(); + popover.handleClose() }} > - {getIconByName(action.icon)} + {icon} - ); + ) } })} - ); -}; + ) +} BulkActionsMenu.propTypes = { onArchive: PropTypes.func, onDelete: PropTypes.func, selectedCount: PropTypes.number, -}; +} diff --git a/src/data/portals.json b/src/data/portals.json index a4402305faca..874810c6d976 100644 --- a/src/data/portals.json +++ b/src/data/portals.json @@ -6,7 +6,7 @@ "variable": "initialDomainName", "target": "_blank", "external": true, - "icon": "GlobeAltIcon" + "icon": "Public" }, { "label": "Exchange", @@ -24,7 +24,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "UsersIcon" + "icon": "Groups" }, { "label": "Teams", @@ -42,7 +42,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "ServerIcon" + "icon": "Dns" }, { "label": "Intune", diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx index c1abbe1d6e2b..d217c9c87d5e 100644 --- a/src/layouts/HeaderedTabbedLayout.jsx +++ b/src/layouts/HeaderedTabbedLayout.jsx @@ -115,6 +115,8 @@ export const HeaderedTabbedLayout = (props) => { > {tabOptions.map((option) => { const icon = getIconByName(option.icon, { fontSize: "small" }); + const iconPosition = option.iconPosition ?? "start"; + const compactIcon = icon && ["end", "start"].includes(iconPosition); return ( { label={option.label} value={option.path} icon={icon ?? undefined} - iconPosition={icon ? (option.iconPosition ?? "start") : undefined} + iconPosition={icon ? iconPosition : undefined} + sx={compactIcon ? { minHeight: 48, py: 1.5 } : undefined} /> ); })} diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index fc3f7e440773..33c861fb85dd 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -62,6 +62,8 @@ export const TabbedLayout = (props) => { > {visibleTabs.map((option) => { const icon = getIconByName(option.icon, { fontSize: 'small' }) + const iconPosition = option.iconPosition ?? 'start' + const compactIcon = icon && ['end', 'start'].includes(iconPosition) return ( { label={option.label} value={option.path} icon={icon ?? undefined} - iconPosition={icon ? (option.iconPosition ?? 'start') : undefined} + iconPosition={icon ? iconPosition : undefined} + sx={compactIcon ? { minHeight: 48, py: 1.5 } : undefined} /> ) })} diff --git a/src/utils/icon-registry.js b/src/utils/icon-registry.js index a87db806b2f0..2863958edbb4 100644 --- a/src/utils/icon-registry.js +++ b/src/utils/icon-registry.js @@ -30,6 +30,7 @@ import { Person, Policy, PrecisionManufacturing, + Public, Security, Settings, Share, @@ -74,6 +75,7 @@ export const iconRegistry = { Person, Policy, PrecisionManufacturing, + Public, Security, Settings, Share, From 983b48a1b1c35c329c9ad8405635c26e6859b0b9 Mon Sep 17 00:00:00 2001 From: Clint Thomon Date: Thu, 14 May 2026 13:59:43 -0500 Subject: [PATCH 09/61] fix: Remove accidentally committed .claude/worktrees directory The .claude/worktrees directory was accidentally committed, causing CI failures due to git treating it as a submodule without a URL in .gitmodules. - Remove .claude/worktrees from git tracking - Add .claude/ to .gitignore to prevent future accidents --- .claude/worktrees/blissful-golick-d405ab | 1 - .gitignore | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 160000 .claude/worktrees/blissful-golick-d405ab diff --git a/.claude/worktrees/blissful-golick-d405ab b/.claude/worktrees/blissful-golick-d405ab deleted file mode 160000 index 0710355e2ada..000000000000 --- a/.claude/worktrees/blissful-golick-d405ab +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0710355e2adac37fffe4c7eef48d6f2c3a04993d diff --git a/.gitignore b/.gitignore index 44dac6dd492c..2758639d59a4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ AGENTS.md # azurite __* AzuriteConfig +# Claude/Cursor worktrees and local AI tooling +.claude/ From 0c32a84eb21d3df3a719427b54d10821a94b40a4 Mon Sep 17 00:00:00 2001 From: "jwebster@protectedtrust.com" Date: Thu, 14 May 2026 16:26:44 -0400 Subject: [PATCH 10/61] Add additional portal links to Invoke-HuduExtensionSync Added links to Defender, Compliance (purview) and the partner center page that is managed by Microsoft. --- src/data/Extensions.json | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 66ff5b1a482d..7bb91b6165f9 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -614,6 +614,50 @@ "action": "disable" } }, + { + "type": "switch", + "name": "Hudu.HideEmptyRoles", + "label": "Hide Empty Roles in Magic Dash", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, + { + "type": "switch", + "name": "Hudu.IncludeParterCenterLink", + "label": "Include link to Partner Center Service management page (partner.microsoft.com)", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, + { + "type": "switch", + "name": "Hudu.IncludeDefenderLink", + "label": "Include link to Defender Portal (security.microsoft.com)", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, + { + "type": "switch", + "name": "Hudu.IncludeComplianceLink", + "label": "Include link to Compliance Portal (compliance.microsoft.com)", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, { "_comment": "I have added this switch as a logic check for the Hudu integration script to check against when CIPP first connects to the Hudu Instance via Connect-HuduAPI.ps1", "type": "switch", From 186a2c6ea22589ca28f655eba01526ceb74ec2d9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 15 May 2026 02:29:31 -0500 Subject: [PATCH 11/61] audit log template tweak --- src/data/AuditLogTemplates.json | 2 +- src/layouts/config.js | 7 + src/pages/cipp/advanced/worker-health.js | 442 +++++++++++++++++++++++ 3 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 src/pages/cipp/advanced/worker-health.js diff --git a/src/data/AuditLogTemplates.json b/src/data/AuditLogTemplates.json index 1762fb2eb7bb..63df852bd318 100644 --- a/src/data/AuditLogTemplates.json +++ b/src/data/AuditLogTemplates.json @@ -450,7 +450,7 @@ { "Property": { "value": "String", "label": "SecuredAccessPassData" }, "Operator": { "value": "like", "label": "Like" }, - "Input": { "value": "*" } + "Input": { "value": "[*]" } } ] } diff --git a/src/layouts/config.js b/src/layouts/config.js index a9df0957241d..987ab9d8f984 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -1116,6 +1116,13 @@ export const nativeMenuItems = [ permissions: ['CIPP.SuperAdmin.*'], scope: 'global', }, + { + title: 'Worker Health', + path: '/cipp/advanced/worker-health', + roles: ['superadmin'], + permissions: ['CIPP.SuperAdmin.*'], + scope: 'global', + }, ], }, ], diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js new file mode 100644 index 000000000000..fd23de33c031 --- /dev/null +++ b/src/pages/cipp/advanced/worker-health.js @@ -0,0 +1,442 @@ +import { useMemo } from "react"; +import Head from "next/head"; +import { + Box, + Button, + Card, + CardContent, + CardHeader, + Chip, + CircularProgress, + Container, + LinearProgress, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { + Memory, + Speed, + PlayArrow, + HourglassEmpty, + CheckCircle, + Warning, + Cancel, + Delete, + LowPriority, + DeleteSweep, +} from "@mui/icons-material"; +import { Grid } from "@mui/system"; +import { Layout as DashboardLayout } from "../../../layouts/index.js"; +import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; +import { CippPropertyListCard } from "../../../components/CippCards/CippPropertyListCard"; +import { CippDataTable } from "../../../components/CippTable/CippDataTable"; +import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; + +const formatDuration = (ms) => { + if (ms === 0 || ms == null) return "—"; + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${(ms / 60000).toFixed(1)}m`; +}; + +const formatUptime = (seconds) => { + if (!seconds) return "—"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +}; + +const WorkerStatusChip = ({ isBusy, currentFunction }) => { + if (isBusy) { + return ( + } + sx={{ maxWidth: 200 }} + /> + ); + } + return } />; +}; + +const UtilizationBar = ({ value }) => ( + + + 80 ? "error" : value > 50 ? "warning" : "primary"} + sx={{ height: 8, borderRadius: 4 }} + /> + + + {value}% + + +); + +const WorkerTable = ({ workers, title }) => { + if (!workers || workers.length === 0) return null; + + return ( + + + + + + + + Worker + Status + Invocations + Utilization + Avg + Min + Max + Last + Faults + + + + {workers.map((w) => ( + + + + W{w.WorkerId} + + + + + + {w.TotalInvocations?.toLocaleString() ?? 0} + + + + {formatDuration(w.AvgDurationMs)} + {formatDuration(w.MinDurationMs)} + {formatDuration(w.MaxDurationMs)} + {formatDuration(w.LastDurationMs)} + + {w.TotalFaults > 0 ? ( + + ) : ( + "0" + )} + + + ))} + +
+
+
+
+ ); +}; + +const Page = () => { + const healthQuery = ApiGetCall({ + url: "/api/ListWorkerHealth", + data: { Action: "Snapshot" }, + queryKey: "WorkerHealth", + refetchInterval: 5000, + }); + + const jobAction = ApiPostCall({ + relatedQueryKeys: ["WorkerHealthJobs", "WorkerHealth"], + }); + + const snapshot = healthQuery.data?.Results; + + const infoBarData = useMemo(() => { + if (!snapshot) return []; + const http = snapshot.HttpPool || {}; + const bg = snapshot.BgPool || {}; + const jobs = snapshot.Jobs || {}; + const limiter = snapshot.Limiter || {}; + + return [ + { + icon: , + name: "HTTP Workers", + data: `${http.BusyCount ?? 0} / ${http.PoolSize ?? 0} busy`, + color: http.BusyCount >= http.PoolSize ? "error" : "primary", + }, + { + icon: , + name: "BG Workers", + data: `${bg.BusyCount ?? 0} / ${bg.PoolSize ?? 0} busy`, + color: bg.BusyCount >= bg.PoolSize ? "error" : "primary", + }, + { + icon: jobs.Running > 0 ? : , + name: "Job Queue", + data: `${jobs.Running ?? 0} running, ${jobs.Queued ?? 0} queued`, + color: jobs.Queued > 10 ? "warning" : "primary", + }, + { + icon: limiter.IsHttpThrottled ? : , + name: "BG Limiter", + data: limiter.IsHttpThrottled + ? "HTTP Throttled" + : `${limiter.Active ?? 0} / ${limiter.CurrentMax ?? 0} active`, + color: limiter.IsHttpThrottled ? "error" : "primary", + }, + ]; + }, [snapshot]); + + const httpPoolItems = useMemo(() => { + if (!snapshot?.HttpPool) return []; + const p = snapshot.HttpPool; + return [ + { label: "Pool Size", value: p.PoolSize }, + { label: "Available", value: p.Available }, + { label: "Busy", value: p.BusyCount }, + { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, + { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, + { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, + { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, + { label: "Total Faults", value: p.TotalFaults ?? 0 }, + ]; + }, [snapshot]); + + const bgPoolItems = useMemo(() => { + if (!snapshot?.BgPool) return []; + const p = snapshot.BgPool; + return [ + { label: "Pool Size", value: p.PoolSize }, + { label: "Available", value: p.Available }, + { label: "Busy", value: p.BusyCount }, + { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, + { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, + { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, + { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, + { label: "Total Faults", value: p.TotalFaults ?? 0 }, + ]; + }, [snapshot]); + + const limiterItems = useMemo(() => { + if (!snapshot?.Limiter) return []; + const l = snapshot.Limiter; + return [ + { label: "Base Concurrency", value: l.BaseConcurrency }, + { label: "Ceiling Concurrency", value: l.CeilingConcurrency }, + { label: "Current Max", value: l.CurrentMax }, + { label: "Active", value: l.Active }, + { label: "Waiting", value: l.Waiting }, + { + label: "HTTP Throttled", + value: l.IsHttpThrottled ? "Yes" : "No", + }, + ]; + }, [snapshot]); + + const jobItems = useMemo(() => { + if (!snapshot?.Jobs) return []; + const j = snapshot.Jobs; + return [ + { label: "Running", value: j.Running }, + { label: "Queued", value: j.Queued }, + { label: "Completed", value: j.Completed?.toLocaleString() ?? 0 }, + { label: "Failed", value: j.Failed }, + { label: "Total Processed", value: j.TotalProcessed?.toLocaleString() ?? 0 }, + { label: "Max Concurrency", value: j.MaxConcurrency }, + { label: "Active Concurrency", value: j.ActiveConcurrency }, + ]; + }, [snapshot]); + + const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "WaitSeconds", "DurationSeconds"]; + + const jobActions = useMemo( + () => [ + { + label: "Cancel Job", + icon: , + color: "error.main", + noConfirm: true, + customFunction: (row) => { + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "CancelJob", JobId: row.Id }, + }); + }, + condition: (row) => row.Status === "Queued", + }, + { + label: "Change Priority", + icon: , + fields: [ + { + type: "textField", + name: "Priority", + label: "New Priority (0 = highest)", + }, + ], + url: "/api/ListWorkerHealth", + data: { Action: "ChangePriority" }, + dataFunction: (row, formData) => ({ + Action: "ChangePriority", + JobId: row.Id, + Priority: parseInt(formData.Priority, 10), + }), + confirmText: "Change", + condition: (row) => row.Status === "Queued", + relatedQueryKeys: ["WorkerHealthJobs", "WorkerHealth"], + }, + { + label: "Cancel Run", + icon: , + color: "error.main", + noConfirm: true, + customFunction: (row) => { + if (row.RunName) { + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "CancelRun", RunName: row.RunName }, + }); + } + }, + condition: (row) => row.Status === "Queued" && row.RunName, + }, + { + label: "Delete", + icon: , + noConfirm: true, + customFunction: (row) => { + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "DeleteJob", JobId: row.Id }, + }); + }, + condition: (row) => row.Status !== "Queued" && row.Status !== "Running", + }, + ], + [jobAction] + ); + + const jobFilters = useMemo( + () => [ + { + filterName: "Queued", + value: [{ id: "Status", value: "Queued" }], + }, + { + filterName: "Running", + value: [{ id: "Status", value: "Running" }], + }, + { + filterName: "Failed", + value: [{ id: "Status", value: "Failed" }], + }, + ], + [] + ); + + return ( + <> + + Worker Health | CIPP + + + + + + Worker Health + + {healthQuery.isFetching && } + {snapshot && ( + + Uptime: {formatUptime(snapshot.UptimeSeconds)} | Auto-refreshing every 5s + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + } + color="warning" + onClick={() => + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "PurgeCompleted" }, + }) + } + > + Purge Completed + + } + /> + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 7a85827ef1072955a48cb1d48c3ce2aafe3ab88d Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 16:34:51 +0200 Subject: [PATCH 12/61] feat(users): add bulk update contact and UPN fields Fixes #6015 Fixes #6013 --- .../administration/users/patch-wizard.jsx | 81 ++++++++++++++++++- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx index 1168f12f593e..67e1d08041a9 100644 --- a/src/pages/identity/administration/users/patch-wizard.jsx +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -21,11 +21,17 @@ import { CippWizardStepButtons } from '../../../../components/CippWizard/CippWiz import { ApiPostCall, ApiGetCall } from '../../../../api/ApiCall' import { CippApiResults } from '../../../../components/CippComponents/CippApiResults' import { CippDataTable } from '../../../../components/CippTable/CippDataTable' +import { CippFormDomainSelector } from '../../../../components/CippComponents/CippFormDomainSelector' import { CippFormUserSelector } from '../../../../components/CippComponents/CippFormUserSelector' import { Delete } from '@mui/icons-material' // User properties that can be patched const PATCHABLE_PROPERTIES = [ + { + property: 'businessPhones', + label: 'Business Phone', + type: 'string', + }, { property: 'city', label: 'City', @@ -51,6 +57,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Employee Type', type: 'string', }, + { + property: 'faxNumber', + label: 'Fax Number', + type: 'string', + }, { property: 'jobTitle', label: 'Job Title', @@ -61,6 +72,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Manager', type: 'userSelector', }, + { + property: 'mobilePhone', + label: 'Mobile Phone', + type: 'string', + }, { property: 'officeLocation', label: 'Office Location', @@ -106,6 +122,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Usage Location', type: 'string', }, + { + property: 'userPrincipalName', + label: 'UPN Domain Suffix', + type: 'domainPicker', + }, ] // Step 1: Display users to be updated @@ -195,12 +216,15 @@ const PropertySelectionStep = (props) => { const tenantDomains = [...new Set(users?.map((user) => user.Tenant || user.tenantFilter).filter(Boolean))] || [] const firstTenantDomain = tenantDomains[0] + const isSingleTenant = tenantDomains.length <= 1 const hasManagerSelected = selectedProperties.includes('manager') const hasSponsorSelected = selectedProperties.includes('sponsor') const hasRelationshipSelected = hasManagerSelected || hasSponsorSelected + const hasUPNSelected = selectedProperties.includes('userPrincipalName') + const hasTenantScopedSelectorSelected = hasRelationshipSelected || hasUPNSelected useEffect(() => { - if (!hasRelationshipSelected || !firstTenantDomain) { + if (!hasTenantScopedSelectorSelected || !firstTenantDomain) { return } @@ -208,7 +232,7 @@ const PropertySelectionStep = (props) => { if (currentTenantFilter?.value !== firstTenantDomain) { formControl.setValue('tenantFilter', { value: firstTenantDomain }) } - }, [firstTenantDomain, formControl, hasRelationshipSelected]) + }, [firstTenantDomain, formControl, hasTenantScopedSelectorSelected]) // Fetch custom data mappings for all tenants const customDataMappings = ApiGetCall({ @@ -242,8 +266,13 @@ const PropertySelectionStep = (props) => { // Combine standard properties with custom data properties const allProperties = useMemo(() => { - return [...PATCHABLE_PROPERTIES, ...customDataProperties] - }, [customDataProperties]) + return [ + ...PATCHABLE_PROPERTIES.filter( + (property) => isSingleTenant || property.property !== 'userPrincipalName' + ), + ...customDataProperties, + ] + }, [customDataProperties, isSingleTenant]) // Register form fields formControl.register('selectedProperties', { required: true }) @@ -290,6 +319,24 @@ const PropertySelectionStep = (props) => { ) } + if (property?.type === 'domainPicker') { + return ( + + + Changes the domain after @ only. Users will be logged out and must sign in with the new + UPN. + + + + ) + } + // Default to text input for string types with consistent styling return ( { Loading custom data mappings... )} + {!isSingleTenant && ( + + UPN domain suffix changes are only available when all selected users are from the same + tenant. + + )} { return } + if (propName === 'businessPhones') { + userData[propName] = [propertyValue] + return + } + + if (propName === 'userPrincipalName') { + const selectedDomain = propertyValue?.value || propertyValue?.label || propertyValue + const currentUPN = user.userPrincipalName || '' + const upnPrefix = currentUPN.includes('@') ? currentUPN.split('@')[0] : currentUPN + + if (selectedDomain && upnPrefix) { + userData[propName] = `${upnPrefix}@${selectedDomain}` + } + + return + } + // Handle dot notation properties (e.g., "extension_abc123.customField") if (propName.includes('.')) { const parts = propName.split('.') @@ -581,6 +651,9 @@ const ConfirmationStep = (props) => { if (propName === 'manager' || propName === 'sponsor') { displayValue = value?.label || value?.value || 'Not set' + } else if (propName === 'userPrincipalName') { + const selectedDomain = value?.label || value?.value || value + displayValue = selectedDomain ? `Change domain to: ${selectedDomain}` : 'Not set' } else if (property?.type === 'boolean') { displayValue = value ? 'Yes' : 'No' } From 8232e5c11b26c897151d6daaaaa74f8288f52110 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 20:30:01 +0200 Subject: [PATCH 13/61] feat(standards): add intuneRestrictUserDeviceJoin entry Frontend pair for the new backend standard covering azureADJoin. Co-Authored-By: Claude Opus 4.7 --- src/data/standards.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 4c61c04cf250..410739a2ebb5 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4455,6 +4455,29 @@ "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", "recommendedBy": [] }, + { + "name": "standards.intuneRestrictUserDeviceJoin", + "cat": "Entra (AAD) Standards", + "tag": [], + "appliesToTest": [], + "helpText": "Controls whether users can join devices to Entra.", + "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", + "executiveText": "Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.intuneRestrictUserDeviceJoin.disableUserDeviceJoin", + "label": "Disable users from joining devices", + "defaultValue": true + } + ], + "label": "Configure user restriction for Entra device join", + "impact": "High Impact", + "impactColour": "warning", + "addedDate": "2026-05-15", + "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", + "recommendedBy": [] + }, { "name": "standards.intuneRequireMFA", "cat": "Intune Standards", From f768330cd45a2b2e1f6a7cae857b7a5e2ec7d999 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 20:30:31 +0200 Subject: [PATCH 14/61] fix(standards): move CIS 5.1.4.1 and SMB1001 (2.8) tags to join standard Both controls describe restricting device join, not registration. Without the move, BPA would report them green whenever registration was disabled, even with join wide open. Co-Authored-By: Claude Opus 4.7 --- src/data/standards.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 410739a2ebb5..6db99775997c 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4432,11 +4432,8 @@ { "name": "standards.intuneRestrictUserDeviceRegistration", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], - "appliesToTest": [ - "CIS_5_1_4_1", - "SMB1001_2_8" - ], + "tag": [], + "appliesToTest": [], "helpText": "Controls whether users can register devices with Entra.", "docsDescription": "Configures whether users can register devices with Entra. When disabled, users are unable to register devices with Entra.", "executiveText": "Controls whether employees can register their devices for corporate access. Disabling user device registration prevents unauthorized or unmanaged devices from connecting to company resources, enhancing overall security posture.", @@ -4458,8 +4455,11 @@ { "name": "standards.intuneRestrictUserDeviceJoin", "cat": "Entra (AAD) Standards", - "tag": [], - "appliesToTest": [], + "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], + "appliesToTest": [ + "CIS_5_1_4_1", + "SMB1001_2_8" + ], "helpText": "Controls whether users can join devices to Entra.", "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", "executiveText": "Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture.", From 9d5ce40275098a4a442d247fe42541958d08ba88 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 07:27:51 -0400 Subject: [PATCH 15/61] Org auto expanding archive property usage --- src/components/CippCards/CippExchangeInfoCard.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index 6a00f53c0248..8bb2737f76c3 100644 --- a/src/components/CippCards/CippExchangeInfoCard.jsx +++ b/src/components/CippCards/CippExchangeInfoCard.jsx @@ -247,7 +247,9 @@ export const CippExchangeInfoCard = (props) => { <> - Auto Expanding Archive: + {exchangeData?.AutoExpandingArchiveScope === 'Organization' + ? 'Auto Expanding Archive: (org)' + : 'Auto Expanding Archive:'} {getCippFormatting( From 6db7e7760fc02807df809897fcda40b48a8b365b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 14:37:01 -0400 Subject: [PATCH 16/61] Delete .claude directory Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- .claude/worktrees/blissful-golick-d405ab | 1 - 1 file changed, 1 deletion(-) delete mode 160000 .claude/worktrees/blissful-golick-d405ab diff --git a/.claude/worktrees/blissful-golick-d405ab b/.claude/worktrees/blissful-golick-d405ab deleted file mode 160000 index 0710355e2ada..000000000000 --- a/.claude/worktrees/blissful-golick-d405ab +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0710355e2adac37fffe4c7eef48d6f2c3a04993d From 1e7aef11995feb42e9872ec4aefac39fc7ba67c5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 08:19:29 -0400 Subject: [PATCH 17/61] Update alerts.json --- src/data/alerts.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index 9bce965852ab..ac8a28fa6cc0 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -125,7 +125,7 @@ "inputType": "textField", "inputLabel": "Enter quota percentage", "inputName": "QuotaUsedQuota", - "recommendedRunInterval": "4h" + "recommendedRunInterval": "1d" }, { "name": "SharePointQuota", @@ -134,7 +134,7 @@ "inputType": "textField", "inputLabel": "Enter quota percentage", "inputName": "SharePointQuota", - "recommendedRunInterval": "4h" + "recommendedRunInterval": "1d" }, { "name": "OneDriveQuota", @@ -143,7 +143,7 @@ "inputType": "textField", "inputLabel": "Enter quota percentage (default: 90)", "inputName": "OneDriveQuota", - "recommendedRunInterval": "4h" + "recommendedRunInterval": "1d" }, { "name": "ExpiringLicenses", From fc246a54ee6c1720f9d442f4cf22568b53add85d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 10:02:02 -0400 Subject: [PATCH 18/61] update default value for standard --- src/data/standards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/standards.json b/src/data/standards.json index 4c61c04cf250..16a6406db1fa 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4100,7 +4100,7 @@ "type": "number", "name": "standards.IntuneComplianceSettings.deviceComplianceCheckinThresholdDays", "label": "Compliance status validity period (days)", - "defaultValue": 130, + "defaultValue": 120, "validators": { "min": { "value": 1, "message": "Minimum value is 1" }, "max": { "value": 120, "message": "Maximum value is 120" } From 766a3c52814dcc09d9170c780b4caa5e59d7a343 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 20 May 2026 22:10:11 -0400 Subject: [PATCH 19/61] feat: add in missing options for Windows Hello standard Fixes #6034 --- src/data/standards.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 16a6406db1fa..bc21aa38b6c3 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4358,6 +4358,28 @@ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.remotePassportEnabled", "label": "Allow phone sign-in", "default": true + }, + { + "type": "autoComplete", + "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.enhancedSignInSecurity", + "label": "Enable enhanced sign-in security", + "multiple": false, + "options": [ + { "label": "Not configured", "value": "0" }, + { "label": "Enabled on capable hardware", "value": "1" }, + { "label": "Disabled on all systems", "value": "2" } + ] + }, + { + "type": "autoComplete", + "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.securityKeyForSignIn", + "label": "Use security keys for sign-in", + "multiple": false, + "options": [ + { "label": "Not configured", "value": "notConfigured" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ] } ], "label": "Windows Hello for Business enrollment configuration", From 5b5302ca907b971e86c5ed5fd8c1f56491f796c8 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 20 May 2026 23:18:38 -0400 Subject: [PATCH 20/61] feat(standards): add DLP via DCS OWA standard --- src/data/standards.json | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 16a6406db1fa..eb49a0e5b91d 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2707,6 +2707,40 @@ "EXCHANGE_LITE" ] }, + { + "name": "standards.DlpViaDcsEnabled", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Sets whether Outlook on the web uses Data Classification Services for DLP evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users).", + "docsDescription": "Configures whether Outlook on the web uses Data Classification Services (DCS)-based Data Loss Prevention (DLP) policy evaluation instead of Exchange-based evaluation. Review DLP policies before enabling this setting, as some legacy Exchange-based predicates are not supported with DCS-based evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users).", + "executiveText": "Improves how Outlook on the web applies Data Loss Prevention policies, giving users clearer guidance when sensitive information may be shared and helping reduce accidental data exposure.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Select value", + "name": "standards.DlpViaDcsEnabled.state", + "options": [ + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } + ] + } + ], + "label": "Set OWA DLP evaluation via DCS", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-20", + "powershellEquivalent": "Set-OrganizationConfig -DlpViaDcsEnabled", + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, { "name": "standards.UserSubmissions", "cat": "Exchange Standards", From 131927b943aebebd6844ba986fbcf30fb21ac8bc Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 22 May 2026 12:51:00 -0400 Subject: [PATCH 21/61] Stats --- src/components/PrivateRoute.js | 33 +- src/layouts/index.js | 4 +- src/pages/cipp/advanced/worker-health.js | 785 +++++++++++++++++++---- 3 files changed, 674 insertions(+), 148 deletions(-) diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 5b067cf4e7c3..15b438b2c608 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -2,8 +2,11 @@ import { ApiGetCall } from "../api/ApiCall.jsx"; import UnauthenticatedPage from "../pages/unauthenticated.js"; import LoadingPage from "../pages/loading.js"; import ApiOfflinePage from "../pages/api-offline.js"; +import { useState, useEffect } from "react"; export const PrivateRoute = ({ children, routeType }) => { + const [unauthLatched, setUnauthLatched] = useState(false); + const session = ApiGetCall({ url: "/.auth/me", queryKey: "authmeswa", @@ -11,13 +14,34 @@ export const PrivateRoute = ({ children, routeType }) => { staleTime: 120000, // 2 minutes }); + // Latch the unauthenticated state so refetches from child components + // don't flip us back to loading. Clear the latch when session succeeds (after login). + useEffect(() => { + if ( + !session.isLoading && + !session.isFetching && + (session.isError || + null === session?.data?.clientPrincipal || + session?.data === undefined) + ) { + setUnauthLatched(true); + } else if (session.isSuccess && session.data?.clientPrincipal) { + setUnauthLatched(false); + } + }, [session.isLoading, session.isFetching, session.isError, session.isSuccess, session.data]); + const apiRoles = ApiGetCall({ url: "/api/me", queryKey: "authmecipp", - retry: 2, // Reduced retry count to show offline message sooner - waiting: !session.isSuccess || session.data?.clientPrincipal === null, + retry: 2, + waiting: session.isSuccess && session.data?.clientPrincipal !== null, }); + // If latched as unauthenticated, always show unauthenticated page + if (unauthLatched) { + return ; + } + // Check if the session is still loading before determining authentication status if ( session.isLoading || @@ -38,11 +62,6 @@ export const PrivateRoute = ({ children, routeType }) => { return ; } - // if not logged into swa - if (null === session?.data?.clientPrincipal || session?.data === undefined) { - return ; - } - let roles = null; if ( diff --git a/src/layouts/index.js b/src/layouts/index.js index d00dc338a82d..f69628a3c706 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -35,7 +35,7 @@ const OnboardingWizardPage = dynamic( { ssr: false } ) -const SIDE_NAV_WIDTH = 270 +const SIDE_NAV_WIDTH = 290 const SIDE_NAV_PINNED_WIDTH = 50 const TOP_NAV_HEIGHT = 50 @@ -111,7 +111,7 @@ export const Layout = (props) => { const currentRole = ApiGetCall({ url: '/api/me', queryKey: 'authmecipp', - waiting: !swaStatus.isSuccess || swaStatus.data?.clientPrincipal === null, + waiting: swaStatus.isSuccess && swaStatus.data?.clientPrincipal !== null, }) const featureFlags = ApiGetCall({ diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index fd23de33c031..ccc19a335570 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import Head from "next/head"; import { Box, @@ -9,6 +9,7 @@ import { Chip, CircularProgress, Container, + IconButton, LinearProgress, Stack, Table, @@ -17,6 +18,9 @@ import { TableContainer, TableHead, TableRow, + ToggleButton, + ToggleButtonGroup, + Tooltip, Typography, } from "@mui/material"; import { @@ -30,11 +34,33 @@ import { Delete, LowPriority, DeleteSweep, + Timeline, + RocketLaunch, + Pause, + FileDownload, + FileUpload, + Refresh, + Close, } from "@mui/icons-material"; import { Grid } from "@mui/system"; +import { useTheme } from "@mui/material/styles"; +import { useQueryClient } from "@tanstack/react-query"; +import { + AreaChart, + Area, + LineChart, + Line, + BarChart, + Bar, + CartesianGrid, + XAxis, + YAxis, + ResponsiveContainer, + Tooltip as RechartsTooltip, + Legend, +} from "recharts"; import { Layout as DashboardLayout } from "../../../layouts/index.js"; import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; -import { CippPropertyListCard } from "../../../components/CippCards/CippPropertyListCard"; import { CippDataTable } from "../../../components/CippTable/CippDataTable"; import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; @@ -142,19 +168,376 @@ const WorkerTable = ({ workers, title }) => { ); }; +const TIME_RANGES = [ + { label: "1h", minutes: 60 }, + { label: "6h", minutes: 360 }, + { label: "24h", minutes: 1440 }, + { label: "3d", minutes: 4320 }, + { label: "7d", minutes: 10080 }, +]; + +const formatChartTime = (timestamp, rangeMinutes) => { + const d = new Date(timestamp); + if (rangeMinutes <= 1440) { + return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); + } + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}; + +const STARTUP_PHASES = [ + { key: "BaseWorkerMs", label: "Base Worker", fkey: "BaseFunctionCount", color: "#7c4dff" }, + { key: "WarmupMs", label: "Warmup", fkey: null, color: "#ffc107" }, + { key: "HttpReadyMs", label: "HTTP Ready", fkey: "HttpFunctionCount", color: "#00c853" }, + { key: "HttpPoolFullMs", label: "HTTP Pool Full", fkey: null, color: "#69f0ae" }, + { key: "BgReadyMs", label: "BG Ready", fkey: "BgFunctionCount", color: "#29b6f6" }, + { key: "FullyReadyMs", label: "Fully Ready", fkey: null, color: "#66bb6a" }, +]; + +const StartupTimingBar = ({ startup }) => { + if (!startup) return null; + + // Build segments as incremental durations between phases + const phases = STARTUP_PHASES.filter((p) => startup[p.key] > 0); + const totalMs = startup.FullyReadyMs || Math.max(...phases.map((p) => startup[p.key]), 1); + + // Compute incremental segments (each phase = cumulative time to that point) + const segments = phases.map((phase, i) => { + const cumMs = startup[phase.key]; + const prevMs = i > 0 ? startup[phases[i - 1].key] : 0; + const deltaMs = Math.max(cumMs - prevMs, 0); + return { + ...phase, + cumMs, + deltaMs, + pct: totalMs > 0 ? (deltaMs / totalMs) * 100 : 0, + functions: phase.fkey ? startup[phase.fkey] : null, + }; + }); + + return ( + + } + subheader={`${startup.ReadinessMode} / ${startup.WarmupMode} — ${startup.CpuCount} CPUs, ${startup.HttpPoolSize}H + ${startup.BgPoolSize}BG — Total: ${formatDuration(totalMs)}`} + subheaderTypographyProps={{ variant: "caption" }} + sx={{ pb: 0 }} + /> + + {/* Single horizontal stacked bar */} + + {segments.map((seg) => ( + + + {seg.label} + + + {formatDuration(seg.deltaMs)} (cumulative: {formatDuration(seg.cumMs)}) + + {seg.functions != null && ( + + {seg.functions} functions loaded + + )} + + } + > + 8 ? 0 : 4, + cursor: "pointer", + transition: "filter 0.15s", + "&:hover": { filter: "brightness(1.2)" }, + }} + > + {seg.pct > 12 && ( + + {formatDuration(seg.deltaMs)} + + )} + + + ))} + + {/* Legend */} + + {segments.map((seg) => ( + + + + {seg.label} + {seg.functions != null && ` (${seg.functions})`} + + + ))} + + Modules: {startup.SharedModuleCount} shared + {startup.HttpOnlyModuleCount > 0 && `, ${startup.HttpOnlyModuleCount} HTTP`} + {startup.BgOnlyModuleCount > 0 && `, ${startup.BgOnlyModuleCount} BG`} + + + + + ); +}; + +const CompactStatsRow = ({ snapshot }) => { + if (!snapshot) return null; + + const http = snapshot.HttpPool || {}; + const bg = snapshot.BgPool || {}; + const jobs = snapshot.Jobs || {}; + const limiter = snapshot.Limiter || {}; + + const sections = [ + { + label: "HTTP Pool", + color: "primary", + stats: [ + { k: "Size", v: http.PoolSize ?? 0 }, + { k: "Busy", v: http.BusyCount ?? 0, w: http.BusyCount >= http.PoolSize }, + { k: "Invocations", v: http.TotalInvocations?.toLocaleString() ?? 0 }, + { k: "Util", v: `${http.AvgUtilizationPct ?? 0}%`, w: http.AvgUtilizationPct > 80 }, + { k: "Avg", v: formatDuration(http.AvgDurationMs) }, + { k: "Faults", v: http.TotalFaults ?? 0, w: http.TotalFaults > 0 }, + ], + }, + { + label: "BG Pool", + color: "warning", + stats: [ + { k: "Size", v: bg.PoolSize ?? 0 }, + { k: "Busy", v: bg.BusyCount ?? 0, w: bg.BusyCount >= bg.PoolSize }, + { k: "Invocations", v: bg.TotalInvocations?.toLocaleString() ?? 0 }, + { k: "Util", v: `${bg.AvgUtilizationPct ?? 0}%`, w: bg.AvgUtilizationPct > 80 }, + { k: "Avg", v: formatDuration(bg.AvgDurationMs) }, + { k: "Faults", v: bg.TotalFaults ?? 0, w: bg.TotalFaults > 0 }, + ], + }, + { + label: "Jobs", + color: "info", + stats: [ + { k: "Running", v: jobs.Running ?? 0 }, + { k: "Queued", v: jobs.Queued ?? 0, w: jobs.Queued > 10 }, + { k: "Done", v: jobs.Completed?.toLocaleString() ?? 0 }, + { k: "Failed", v: jobs.Failed ?? 0, w: jobs.Failed > 0 }, + ], + }, + { + label: "Limiter", + color: "default", + stats: [ + { k: "Active", v: `${limiter.Active ?? 0} / ${limiter.CurrentMax ?? 0}` }, + { k: "Waiting", v: limiter.Waiting ?? 0 }, + ...(limiter.IsHttpThrottled ? [{ k: "Status", v: "Throttled", w: true }] : []), + ], + }, + ]; + + return ( + + + + + + {sections.map((sec) => ( + + + + + {sec.stats.map((s) => ( + + + {s.k} + + + {s.v} + + + ))} + {/* Pad empty cells so columns stay aligned */} + {Array.from({ length: Math.max(0, 6 - sec.stats.length) }).map((_, i) => ( + + ))} + + ))} + +
+
+
+
+ ); +}; + +const HistoryChart = ({ data, rangeMinutes, title, icon, children }) => { + const theme = useTheme(); + + if (!data || data.length === 0) { + return ( + + + + + + No historical data available yet — data collection starts after 60 seconds + + + + + ); + } + + return ( + + + + + + {children(data, theme)} + + + + + ); +}; + const Page = () => { + const theme = useTheme(); + const queryClient = useQueryClient(); + const fileInputRef = useRef(null); + const [historyRange, setHistoryRange] = useState(60); + const [paused, setPaused] = useState(false); + const [importedData, setImportedData] = useState(null); + + const isImported = importedData !== null; + const effectivePaused = paused || isImported; + const healthQuery = ApiGetCall({ url: "/api/ListWorkerHealth", data: { Action: "Snapshot" }, queryKey: "WorkerHealth", - refetchInterval: 5000, + refetchInterval: effectivePaused ? false : 5000, + }); + + const startupQuery = ApiGetCall({ + url: "/api/ListWorkerHealth", + data: { Action: "Startup" }, + queryKey: "WorkerStartup", + }); + + const historyQuery = ApiGetCall({ + url: "/api/ListWorkerHealth", + data: { Action: "History", Minutes: String(historyRange), MaxPoints: "500" }, + queryKey: `WorkerHistory-${historyRange}`, + refetchInterval: effectivePaused ? false : 60000, }); const jobAction = ApiPostCall({ relatedQueryKeys: ["WorkerHealthJobs", "WorkerHealth"], }); - const snapshot = healthQuery.data?.Results; + // Resolve data: imported overrides live + const snapshot = isImported ? importedData.snapshot : healthQuery.data?.Results; + const startupInfo = isImported ? importedData.startup : startupQuery.data?.Results; + const importedJobs = useMemo(() => { + if (!isImported || !importedData.jobs) return null; + // Handle both array and { Results: [...] } shapes from query cache + if (Array.isArray(importedData.jobs)) return importedData.jobs; + if (Array.isArray(importedData.jobs?.Results)) return importedData.jobs.Results; + if (Array.isArray(importedData.jobs?.data?.Results)) return importedData.jobs.data.Results; + if (Array.isArray(importedData.jobs?.data)) return importedData.jobs.data; + return []; + }, [isImported, importedData]); + + const historyData = useMemo(() => { + const raw = isImported + ? importedData.history?.Data ?? importedData.history + : historyQuery.data?.Results?.Data; + if (!raw || !Array.isArray(raw)) return []; + return raw.map((p) => ({ + ...p, + time: formatChartTime(p.TimestampUtc, isImported ? importedData.historyRange ?? 60 : historyRange), + })); + }, [historyQuery.data, historyRange, importedData, isImported]); + + // ── Export ── + const handleExport = useCallback(() => { + const payload = { + exportedAt: new Date().toISOString(), + historyRange, + snapshot: healthQuery.data?.Results ?? null, + startup: startupQuery.data?.Results ?? null, + history: historyQuery.data?.Results ?? null, + jobs: null, + }; + // Try to grab current job data from query cache + // CippDataTable may store the key with extra params, so search by prefix + const allQueries = queryClient.getQueriesData({ queryKey: ["WorkerHealthJobs"] }); + for (const [, data] of allQueries) { + if (data) { + const rows = data?.Results ?? data?.data?.Results ?? data; + if (Array.isArray(rows)) { + payload.jobs = rows; + break; + } + } + } + + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `worker-health-${new Date().toISOString().slice(0, 16).replace(/:/g, "")}.json`; + a.click(); + URL.revokeObjectURL(url); + }, [healthQuery.data, startupQuery.data, historyQuery.data, historyRange, queryClient]); + + // ── Import ── + const handleImport = useCallback((event) => { + const file = event.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target.result); + setImportedData(data); + setPaused(true); + } catch { + // invalid JSON — ignore + } + }; + reader.readAsText(file); + // Reset input so same file can be re-imported + event.target.value = ""; + }, []); + + const handleClearImport = useCallback(() => { + setImportedData(null); + setPaused(false); + }, []); + + const handleRefreshHistory = useCallback(() => { + queryClient.invalidateQueries({ queryKey: [`WorkerHistory-${historyRange}`] }); + }, [queryClient, historyRange]); const infoBarData = useMemo(() => { if (!snapshot) return []; @@ -193,66 +576,6 @@ const Page = () => { ]; }, [snapshot]); - const httpPoolItems = useMemo(() => { - if (!snapshot?.HttpPool) return []; - const p = snapshot.HttpPool; - return [ - { label: "Pool Size", value: p.PoolSize }, - { label: "Available", value: p.Available }, - { label: "Busy", value: p.BusyCount }, - { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, - { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, - { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, - { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, - { label: "Total Faults", value: p.TotalFaults ?? 0 }, - ]; - }, [snapshot]); - - const bgPoolItems = useMemo(() => { - if (!snapshot?.BgPool) return []; - const p = snapshot.BgPool; - return [ - { label: "Pool Size", value: p.PoolSize }, - { label: "Available", value: p.Available }, - { label: "Busy", value: p.BusyCount }, - { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, - { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, - { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, - { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, - { label: "Total Faults", value: p.TotalFaults ?? 0 }, - ]; - }, [snapshot]); - - const limiterItems = useMemo(() => { - if (!snapshot?.Limiter) return []; - const l = snapshot.Limiter; - return [ - { label: "Base Concurrency", value: l.BaseConcurrency }, - { label: "Ceiling Concurrency", value: l.CeilingConcurrency }, - { label: "Current Max", value: l.CurrentMax }, - { label: "Active", value: l.Active }, - { label: "Waiting", value: l.Waiting }, - { - label: "HTTP Throttled", - value: l.IsHttpThrottled ? "Yes" : "No", - }, - ]; - }, [snapshot]); - - const jobItems = useMemo(() => { - if (!snapshot?.Jobs) return []; - const j = snapshot.Jobs; - return [ - { label: "Running", value: j.Running }, - { label: "Queued", value: j.Queued }, - { label: "Completed", value: j.Completed?.toLocaleString() ?? 0 }, - { label: "Failed", value: j.Failed }, - { label: "Total Processed", value: j.TotalProcessed?.toLocaleString() ?? 0 }, - { label: "Max Concurrency", value: j.MaxConcurrency }, - { label: "Active Concurrency", value: j.ActiveConcurrency }, - ]; - }, [snapshot]); - const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "WaitSeconds", "DurationSeconds"]; const jobActions = useMemo( @@ -324,18 +647,9 @@ const Page = () => { const jobFilters = useMemo( () => [ - { - filterName: "Queued", - value: [{ id: "Status", value: "Queued" }], - }, - { - filterName: "Running", - value: [{ id: "Status", value: "Running" }], - }, - { - filterName: "Failed", - value: [{ id: "Status", value: "Failed" }], - }, + { filterName: "Queued", value: [{ id: "Status", value: "Queued" }] }, + { filterName: "Running", value: [{ id: "Status", value: "Running" }] }, + { filterName: "Failed", value: [{ id: "Status", value: "Failed" }] }, ], [] ); @@ -347,89 +661,282 @@ const Page = () => { - + + {/* ── Header toolbar ── */} Worker Health - {healthQuery.isFetching && } - {snapshot && ( - - Uptime: {formatUptime(snapshot.UptimeSeconds)} | Auto-refreshing every 5s + {isImported && ( + } + /> + )} + {!isImported && healthQuery.isFetching && } + {!isImported && snapshot && ( + + Uptime: {formatUptime(snapshot.UptimeSeconds)} )} + + setPaused((p) => !p)} + color={effectivePaused ? "warning" : "default"} + disabled={isImported} + > + {effectivePaused ? : } + + + + + + + + + fileInputRef.current?.click()}> + + + + + {/* ── KPI bar ── */} + {/* ── Compact pool / jobs / limiter stats ── */} + + + {/* ── Worker tables ── */} + + + + {/* ── Job Queue ── */} + {isImported && importedJobs ? ( + + + + {importedJobs.length === 0 ? ( + + + No job data was captured in this export + + + ) : ( + + + + + {jobSimpleColumns.map((col) => ( + {col} + ))} + + + + {importedJobs.slice(0, 200).map((row, i) => ( + + {jobSimpleColumns.map((col) => ( + + {row[col] != null ? String(row[col]) : "—"} + + ))} + + ))} + +
+
+ )} +
+
+ ) : ( + } + color="warning" + onClick={() => + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "PurgeCompleted" }, + }) + } + > + Purge Completed + + } + /> + )} + + {/* ── Historical Trends header with controls ── */} + + } + action={ + + {!isImported && ( + + + + + + )} + val !== null && setHistoryRange(val)} + size="small" + disabled={isImported} + > + {TIME_RANGES.map((r) => ( + + {r.label} + + ))} + + + } + /> + + - - + + } + > + {(data, t) => ( + + + + + + + + + + )} + - - + + } + > + {(data, t) => ( + + + + + + + + + + )} + - - + + } + > + {(data, t) => ( + + + + + + + + + + )} + - - + + } + > + {(data, t) => ( + + + + + + + + + + + + + )} + - - - - } - color="warning" - onClick={() => - jobAction.mutate({ - url: "/api/ListWorkerHealth", - data: { Action: "PurgeCompleted" }, - }) - } - > - Purge Completed - - } - /> + {/* ── Startup Timing (bottom) ── */} +
From a32790443fc1297fab6900acc498df9cff107aa0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 24 May 2026 09:13:35 +1000 Subject: [PATCH 22/61] CIPP Hosted Notices --- .../CippComponents/FailedPaymentDialog.jsx | 49 +++++++++++++++++++ .../SubscriptionEndedDialog.jsx | 25 ++++++++++ src/layouts/index.js | 4 ++ 3 files changed, 78 insertions(+) create mode 100644 src/components/CippComponents/FailedPaymentDialog.jsx create mode 100644 src/components/CippComponents/SubscriptionEndedDialog.jsx diff --git a/src/components/CippComponents/FailedPaymentDialog.jsx b/src/components/CippComponents/FailedPaymentDialog.jsx new file mode 100644 index 000000000000..5668f619c443 --- /dev/null +++ b/src/components/CippComponents/FailedPaymentDialog.jsx @@ -0,0 +1,49 @@ +import { useEffect, useState, useCallback } from 'react' +import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' + +const DISMISS_KEY = 'cipp_hosted_payment_dismissed' +const DISMISS_DURATION_MS = 24 * 60 * 60 * 1000 // 1 day + +export const FailedPaymentDialog = ({ hostedFailedPayments }) => { + const [open, setOpen] = useState(false) + + useEffect(() => { + if (!hostedFailedPayments) return + + const dismissedAt = localStorage.getItem(DISMISS_KEY) + if (dismissedAt && Date.now() - Number(dismissedAt) < DISMISS_DURATION_MS) return + + setOpen(true) + }, [hostedFailedPayments]) + + const handleDismiss = useCallback(() => { + localStorage.setItem(DISMISS_KEY, String(Date.now())) + setOpen(false) + }, []) + + return ( + e.stopPropagation() } }} + > + Payment Issue + + + There is a payment issue with your CIPP subscription. + + + A recent payment has failed. Please contact your account holder to update payment + information and avoid service interruption. + + + + + + + ) +} diff --git a/src/components/CippComponents/SubscriptionEndedDialog.jsx b/src/components/CippComponents/SubscriptionEndedDialog.jsx new file mode 100644 index 000000000000..e715cce54d4e --- /dev/null +++ b/src/components/CippComponents/SubscriptionEndedDialog.jsx @@ -0,0 +1,25 @@ +import { Alert, Dialog, DialogContent, DialogTitle, Typography } from '@mui/material' + +export const SubscriptionEndedDialog = ({ hostedSubscriptionEnded }) => { + const open = !!hostedSubscriptionEnded + + return ( + e.stopPropagation() } }} + > + Subscription Ended + + + Your CIPP subscription has ended. Access to this instance is no longer available. + + + Please contact your account holder to renew the subscription and restore access. + + + + ) +} diff --git a/src/layouts/index.js b/src/layouts/index.js index f69628a3c706..07f2f152a562 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -29,6 +29,8 @@ import { nativeMenuItems } from './config' import { CippBreadcrumbNav } from '../components/CippComponents/CippBreadcrumbNav' import { SsoMigrationDialog } from '../components/CippComponents/SsoMigrationDialog' import { ForcedSsoMigrationDialog } from '../components/CippComponents/ForcedSsoMigrationDialog' +import { SubscriptionEndedDialog } from '../components/CippComponents/SubscriptionEndedDialog' +import { FailedPaymentDialog } from '../components/CippComponents/FailedPaymentDialog' const OnboardingWizardPage = dynamic( () => import('../components/CippWizard/OnboardingWizardPage.jsx'), @@ -337,6 +339,8 @@ export const Layout = (props) => { + + {!setupCompleted && ( From 04c63849f689bbd2686fb473cbaef1462a1d1b99 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 22:41:10 +0200 Subject: [PATCH 23/61] implement standards template deployment for intune apps --- src/data/standards.json | 361 ++++++++++------------------------------ 1 file changed, 84 insertions(+), 277 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 7748d7dee0c7..266bb8af19cd 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -126,16 +126,8 @@ { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": [ - "CIS M365 6.0.1 (3.1.1)", - "mip_search_auditlog", - "NIST CSF 2.0 (DE.CM-09)" - ], - "appliesToTest": [ - "CISAMSEXO171", - "CISAMSEXO173", - "CIS_3_1_1" - ], + "tag": ["CIS M365 6.0.1 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CISAMSEXO171", "CISAMSEXO173", "CIS_3_1_1"], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.", "addedComponent": [], @@ -368,11 +360,7 @@ "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], - "appliesToTest": [ - "CISAMSEXO51", - "CIS_6_5_4", - "ZTNA21799" - ], + "appliesToTest": ["CISAMSEXO51", "CIS_6_5_4", "ZTNA21799"], "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)", "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).", "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.", @@ -394,17 +382,8 @@ { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": [ - "CIS M365 6.0.1 (1.3.2)", - "spo_idle_session_timeout", - "NIST CSF 2.0 (PR.AA-03)" - ], - "appliesToTest": [ - "CIS_1_3_2", - "ZTNA21813", - "ZTNA21814", - "ZTNA21815" - ], + "tag": ["CIS M365 6.0.1 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], + "appliesToTest": ["CIS_1_3_2", "ZTNA21813", "ZTNA21814", "ZTNA21815"], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.", "addedComponent": [ @@ -490,10 +469,7 @@ "name": "standards.AdminSSPR", "cat": "Entra (AAD) Standards", "tag": ["EIDSCA.AP01"], - "appliesToTest": [ - "EIDSCAAP01", - "ZTNA21842" - ], + "appliesToTest": ["EIDSCAAP01", "ZTNA21842"], "helpText": "Controls whether administrators are allowed to use Self-Service Password Reset through the Microsoft Entra authorization policy.", "docsDescription": "Configures the allowedToUseSSPR property on the Microsoft Entra authorization policy. Microsoft documents this property as controlling whether administrators of the tenant can use Self-Service Password Reset. Use this standard to explicitly enable or disable administrator SSPR based on your security policy.", "executiveText": "Controls whether tenant administrators can reset their own passwords through Self-Service Password Reset. Disabling this capability forces privileged accounts through more controlled recovery processes and reduces the risk of self-service recovery being misused on administrative identities.", @@ -593,13 +569,7 @@ "name": "standards.laps", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.5)", "SMB1001 (2.2)"], - "appliesToTest": [ - "CIS_5_1_4_5", - "SMB1001_2_2", - "ZTNA21953", - "ZTNA21955", - "ZTNA24560" - ], + "appliesToTest": ["CIS_5_1_4_5", "SMB1001_2_2", "ZTNA21953", "ZTNA21955", "ZTNA24560"], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", "executiveText": "Enables Local Administrator Password Solution (LAPS) capability, which automatically manages and rotates local administrator passwords on company computers. This significantly improves security by preventing the use of shared or static administrator passwords that could be exploited by attackers.", @@ -733,11 +703,7 @@ "name": "standards.EnableHardwareOAuth", "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], - "appliesToTest": [ - "SMB1001_2_5", - "SMB1001_2_6", - "SMB1001_2_9" - ], + "appliesToTest": ["SMB1001_2_5", "SMB1001_2_6", "SMB1001_2_9"], "helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.", "docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.", "executiveText": "Enables physical hardware tokens that generate secure authentication codes, providing an alternative to smartphone-based authentication. This is particularly valuable for employees who cannot use mobile devices or require the highest security standards for accessing sensitive systems.", @@ -753,10 +719,7 @@ "name": "standards.allowOAuthTokens", "cat": "Entra (AAD) Standards", "tag": ["EIDSCA.AT01", "EIDSCA.AT02"], - "appliesToTest": [ - "EIDSCAAT01", - "EIDSCAAT02" - ], + "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02"], "helpText": "Allows you to use any software OAuth token generator", "docsDescription": "Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method.", "executiveText": "Allows employees to use third-party authentication apps and password managers to generate secure login codes, providing flexibility in authentication methods while maintaining security standards. This accommodates diverse user preferences and existing security tools.", @@ -788,12 +751,7 @@ "name": "standards.TAP", "cat": "Entra (AAD) Standards", "tag": [], - "appliesToTest": [ - "EIDSCAAT01", - "EIDSCAAT02", - "ZTNA21845", - "ZTNA21846" - ], + "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02", "ZTNA21845", "ZTNA21846"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", "docsDescription": "Enables Temporary Password generation for the tenant.", "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", @@ -836,10 +794,7 @@ { "name": "standards.CustomBannedPasswordList", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS M365 6.0.1 (5.2.3.2)", - "SMB1001 (2.1)" - ], + "tag": ["CIS M365 6.0.1 (5.2.3.2)", "SMB1001 (2.1)"], "appliesToTest": [ "CIS_5_2_3_2", "EIDSCAPR01", @@ -875,10 +830,7 @@ "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", "tag": [], - "appliesToTest": [ - "ZTNA21803", - "ZTNA21804" - ], + "appliesToTest": ["ZTNA21803", "ZTNA21804"], "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", "executiveText": "Allows external partners and vendors to use their own organization's multi-factor authentication when accessing company resources, streamlining collaboration while maintaining security standards. This reduces friction for external users while ensuring they still meet authentication requirements.", "addedComponent": [ @@ -904,17 +856,8 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS M365 6.0.1 (5.1.2.3)", - "CISA (MS.AAD.6.1v1)", - "SMB1001 (2.8)" - ], - "appliesToTest": [ - "CIS_5_1_2_3", - "SMB1001_2_8", - "ZTNA21772", - "ZTNA21787" - ], + "tag": ["CIS M365 6.0.1 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_5_1_2_3", "SMB1001_2_8", "ZTNA21772", "ZTNA21787"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.", @@ -971,10 +914,7 @@ "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.5)"], - "appliesToTest": [ - "SMB1001_2_5", - "ZTNA21889" - ], + "appliesToTest": ["SMB1001_2_5", "ZTNA21889"], "helpText": "Sets the state of the registration campaign for the tenant", "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.", @@ -1012,10 +952,7 @@ "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", "tag": ["CISA (MS.AAD.21.1v1)", "SMB1001 (2.8)"], - "appliesToTest": [ - "SMB1001_2_8", - "ZTNA21868" - ], + "appliesToTest": ["SMB1001_2_8", "ZTNA21868"], "helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "executiveText": "Restricts the creation of Microsoft 365 groups, Teams, and SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces. This ensures proper governance, naming conventions, and resource management while maintaining oversight of all collaborative environments.", @@ -1046,11 +983,7 @@ "NIST CSF 2.0 (PR.AA-05)", "SMB1001 (2.8)" ], - "appliesToTest": [ - "CIS_5_1_2_2", - "EIDSCAAP10", - "SMB1001_2_8" - ], + "appliesToTest": ["CIS_5_1_2_2", "EIDSCAAP10", "SMB1001_2_8"], "helpText": "Disables the ability for users to create App registrations in the tenant.", "docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.", "executiveText": "Prevents regular employees from creating application registrations that could be used to maintain unauthorized access to company systems. This security measure ensures that only authorized IT personnel can create applications, reducing the risk of persistent security breaches through malicious applications.", @@ -1066,10 +999,7 @@ "name": "standards.BitLockerKeysForOwnedDevice", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.6)"], - "appliesToTest": [ - "CIS_5_1_4_6", - "ZTNA21954" - ], + "appliesToTest": ["CIS_5_1_4_6", "ZTNA21954"], "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.", "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.", "executiveText": "Gives administrators centralized control over BitLocker recovery secrets—restrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.", @@ -1102,11 +1032,7 @@ "NIST CSF 2.0 (PR.AA-05)", "SMB1001 (2.8)" ], - "appliesToTest": [ - "CIS_5_1_3_2", - "SMB1001_2_8", - "ZTNA21868" - ], + "appliesToTest": ["CIS_5_1_3_2", "SMB1001_2_8", "ZTNA21868"], "helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams", "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.", "addedComponent": [], @@ -1135,11 +1061,7 @@ "name": "standards.DisableSelfServiceLicenses", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (1.3.4)", "SMB1001 (2.8)"], - "appliesToTest": [ - "CIS_1_3_4", - "EIDSCAAP05", - "SMB1001_2_8" - ], + "appliesToTest": ["CIS_1_3_4", "EIDSCAAP05", "SMB1001_2_8"], "helpText": "**Requires 'Billing Administrator' GDAP role.** This standard disables all self service licenses and enables all exclusions", "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.", "addedComponent": [ @@ -1166,10 +1088,7 @@ "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.8)"], - "appliesToTest": [ - "SMB1001_2_8", - "ZTNA21858" - ], + "appliesToTest": ["SMB1001_2_8", "ZTNA21858"], "helpText": "Blocks login for guest users that have not logged in for a number of days", "executiveText": "Automatically disables external guest accounts that haven't been used for a number of days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", "addedComponent": [ @@ -1247,19 +1166,8 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": [ - "CISA (MS.AAD.18.1v1)", - "EIDSCA.AP04", - "EIDSCA.AP07", - "SMB1001 (2.8)" - ], - "appliesToTest": [ - "CIS_5_1_6_3", - "EIDSCAAP04", - "EIDSCAAP07", - "SMB1001_2_8", - "ZTNA21791" - ], + "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_5_1_6_3", "EIDSCAAP04", "EIDSCAAP07", "SMB1001_2_8", "ZTNA21791"], "helpText": "This setting controls who can invite guests to your directory to collaborate on resources secured by your company, such as SharePoint sites or Azure resources.", "executiveText": "Controls who within the organization can invite external partners and vendors to access company resources, ensuring proper oversight of external access while enabling necessary business collaboration. This helps maintain security while supporting partnership and vendor relationships.", "addedComponent": [ @@ -1332,12 +1240,7 @@ "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", "tag": ["CISA (MS.AAD.11.1v1)", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], - "appliesToTest": [ - "SMB1001_2_5", - "SMB1001_2_6", - "SMB1001_2_9", - "ZTNA21843" - ], + "appliesToTest": ["SMB1001_2_5", "SMB1001_2_6", "SMB1001_2_9", "ZTNA21843"], "helpText": "Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access.", "docsDescription": "Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft.", "executiveText": "Activates Microsoft's baseline security configuration that requires multi-factor authentication and blocks legacy authentication methods. This provides essential security protection for organizations without complex conditional access policies, significantly improving security posture with minimal configuration.", @@ -1419,13 +1322,7 @@ "SMB1001 (2.6)", "SMB1001 (2.9)" ], - "appliesToTest": [ - "CIS_5_2_3_7", - "SMB1001_2_5", - "SMB1001_2_5_L4", - "SMB1001_2_6", - "SMB1001_2_9" - ], + "appliesToTest": ["CIS_5_2_3_7", "SMB1001_2_5", "SMB1001_2_5_L4", "SMB1001_2_6", "SMB1001_2_9"], "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.", "addedComponent": [], @@ -1856,12 +1753,7 @@ "name": "standards.SpoofWarn", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (6.2.3)"], - "appliesToTest": [ - "CISAMSEXO71", - "CIS_6_2_3", - "ORCA111", - "ORCA240" - ], + "appliesToTest": ["CISAMSEXO71", "CIS_6_2_3", "ORCA111", "ORCA240"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", "executiveText": "Displays visual warnings in Outlook when emails come from external senders, helping employees identify potentially suspicious messages and reducing the risk of phishing attacks. This security feature makes it easier for staff to distinguish between internal and external communications.", @@ -1981,10 +1873,7 @@ "name": "standards.RotateDKIM", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], - "appliesToTest": [ - "CIS_2_1_9", - "SMB1001_2_12" - ], + "appliesToTest": ["CIS_2_1_9", "SMB1001_2_12"], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.", "addedComponent": [], @@ -2038,13 +1927,7 @@ "name": "standards.AddDKIM", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], - "appliesToTest": [ - "CISAMSEXO31", - "CIS_2_1_9", - "ORCA108", - "ORCA108_1", - "SMB1001_2_12" - ], + "appliesToTest": ["CISAMSEXO31", "CIS_2_1_9", "ORCA108", "ORCA108_1", "SMB1001_2_12"], "helpText": "Enables DKIM for all domains that currently support it", "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.", "addedComponent": [], @@ -2066,10 +1949,7 @@ "name": "standards.AddDMARCToMOERA", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (2.1.10)", "Security", "PhishingProtection", "SMB1001 (2.12)"], - "appliesToTest": [ - "CIS_2_1_10", - "SMB1001_2_12" - ], + "appliesToTest": ["CIS_2_1_10", "SMB1001_2_12"], "helpText": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "docsDescription": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "executiveText": "Implements advanced email security for Microsoft's default domain names (onmicrosoft.com) to prevent criminals from impersonating your organization. This blocks fraudulent emails that could damage your company's reputation and protects partners and customers from phishing attacks using your domain names.", @@ -2107,12 +1987,7 @@ "Essential 8 (1683)", "NIST CSF 2.0 (DE.CM-09)" ], - "appliesToTest": [ - "CISAMSEXO131", - "CIS_6_1_1", - "CIS_6_1_2", - "CIS_6_1_3" - ], + "appliesToTest": ["CISAMSEXO131", "CIS_6_1_1", "CIS_6_1_2", "CIS_6_1_3"], "helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "executiveText": "Enables comprehensive logging of all email access and modifications across all employee mailboxes, providing detailed audit trails for security investigations and compliance requirements. This helps detect unauthorized access, data breaches, and supports regulatory compliance efforts.", @@ -2383,11 +2258,7 @@ "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (1.3.3)", "exo_individualsharing"], - "appliesToTest": [ - "CISAMSEXO62", - "CIS_1_3_3", - "ZTNA21803" - ], + "appliesToTest": ["CISAMSEXO62", "CIS_1_3_3", "ZTNA21803"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.", @@ -2426,10 +2297,7 @@ "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (6.5.3)", "exo_storageproviderrestricted"], - "appliesToTest": [ - "CIS_6_5_3", - "ZTNA21817" - ], + "appliesToTest": ["CIS_6_5_3", "ZTNA21817"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.", @@ -2601,10 +2469,7 @@ "NIST CSF 2.0 (PR.AA-05)", "NIST CSF 2.0 (PR.PS-05)" ], - "appliesToTest": [ - "CIS_6_3_1", - "ZTNA21817" - ], + "appliesToTest": ["CIS_6_3_1", "ZTNA21817"], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", "docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.", "executiveText": "Prevents employees from installing third-party add-ins in Outlook without administrative approval, reducing security risks from potentially malicious extensions. This ensures only vetted and approved tools can access company email data while maintaining centralized control over email functionality.", @@ -2756,10 +2621,7 @@ "NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)" ], - "appliesToTest": [ - "CIS_1_2_2", - "SMB1001_2_3" - ], + "appliesToTest": ["CIS_1_2_2", "SMB1001_2_3"], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", @@ -3239,12 +3101,7 @@ "mdo_safeattachmentpolicy", "NIST CSF 2.0 (DE.CM-09)" ], - "appliesToTest": [ - "CIS_2_1_4", - "ORCA158", - "ORCA189", - "ORCA227" - ], + "appliesToTest": ["CIS_2_1_4", "ORCA158", "ORCA189", "ORCA227"], "helpText": "This creates a Safe Attachment policy", "addedComponent": [ { @@ -3341,10 +3198,7 @@ "name": "standards.PhishingSimulations", "cat": "Defender Standards", "tag": ["SMB1001 (1.11)", "SMB1001 (5.1)"], - "appliesToTest": [ - "SMB1001_1_11", - "SMB1001_5_1" - ], + "appliesToTest": ["SMB1001_1_11", "SMB1001_5_1"], "helpText": "This creates a phishing simulation policy that enables phishing simulations for the entire tenant.", "addedComponent": [ { @@ -4394,12 +4248,7 @@ "name": "standards.intuneDeviceReg", "cat": "Intune Standards", "tag": ["CIS M365 6.0.1 (5.1.4.2)", "CISA (MS.AAD.17.1v1)"], - "appliesToTest": [ - "CIS_5_1_4_2", - "ZTNA21801", - "ZTNA21802", - "ZTNA21837" - ], + "appliesToTest": ["CIS_5_1_4_2", "ZTNA21801", "ZTNA21802", "ZTNA21837"], "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users", "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.", "addedComponent": [ @@ -4422,11 +4271,7 @@ "name": "standards.intuneDeviceRegLocalAdmins", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.3)", "CIS M365 6.0.1 (5.1.4.4)", "SMB1001 (2.2)"], - "appliesToTest": [ - "CIS_5_1_4_3", - "CIS_5_1_4_4", - "SMB1001_2_2" - ], + "appliesToTest": ["CIS_5_1_4_3", "CIS_5_1_4_4", "SMB1001_2_2"], "helpText": "Controls whether users who register Microsoft Entra joined devices are granted local administrator rights on those devices and if Global Administrators are added as local admins.", "docsDescription": "Configures the Device Registration Policy local administrator behavior for registering users. When enabled, users who register devices are not granted local administrator rights, you can also configure if Global Administrators are added as local admins.", "executiveText": "Controls whether employees who enroll devices automatically receive local administrator access. Disabling registering-user admin rights follows least-privilege principles and reduces security risk from over-privileged endpoints.", @@ -4478,10 +4323,7 @@ "name": "standards.intuneRestrictUserDeviceJoin", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], - "appliesToTest": [ - "CIS_5_1_4_1", - "SMB1001_2_8" - ], + "appliesToTest": ["CIS_5_1_4_1", "SMB1001_2_8"], "helpText": "Controls whether users can join devices to Entra.", "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", "executiveText": "Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture.", @@ -4504,11 +4346,7 @@ "name": "standards.intuneRequireMFA", "cat": "Intune Standards", "tag": [], - "appliesToTest": [ - "ZTNA21782", - "ZTNA21796", - "ZTNA21872" - ], + "appliesToTest": ["ZTNA21782", "ZTNA21796", "ZTNA21872"], "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.", "executiveText": "Requires employees to use multi-factor authentication when registering devices for corporate access, adding an extra security layer to prevent unauthorized device enrollment. This helps ensure only legitimate users can connect their devices to company systems.", "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra", @@ -4656,15 +4494,8 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.3.1)", - "CISA (MS.SPO.3.1v1)", - "NIST CSF 2.0 (DE.CM-09)" - ], - "appliesToTest": [ - "CIS_7_3_1", - "ZTNA21817" - ], + "tag": ["CIS M365 6.0.1 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CIS_7_3_1", "ZTNA21817"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", "addedComponent": [], @@ -4731,12 +4562,7 @@ "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", "tag": ["CIS M365 6.0.1 (7.2.9)", "CISA (MS.SPO.1.5v1)"], - "appliesToTest": [ - "CIS_7_2_9", - "ZTNA21803", - "ZTNA21804", - "ZTNA21858" - ], + "appliesToTest": ["CIS_7_2_9", "ZTNA21803", "ZTNA21804", "ZTNA21858"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", "addedComponent": [ @@ -4770,11 +4596,7 @@ "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", "tag": ["CIS M365 6.0.1 (7.2.10)", "CISA (MS.SPO.1.6v1)"], - "appliesToTest": [ - "CIS_7_2_10", - "ZTNA21803", - "ZTNA21804" - ], + "appliesToTest": ["CIS_7_2_10", "ZTNA21803", "ZTNA21804"], "helpText": "Ensure re-authentication with verification code is restricted", "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", "addedComponent": [ @@ -4807,17 +4629,8 @@ { "name": "standards.DefaultSharingLink", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.2.7)", - "CIS M365 6.0.1 (7.2.11)", - "CISA (MS.SPO.1.4v1)" - ], - "appliesToTest": [ - "CIS_7_2_11", - "CIS_7_2_7", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 6.0.1 (7.2.7)", "CIS M365 6.0.1 (7.2.11)", "CISA (MS.SPO.1.4v1)"], + "appliesToTest": ["CIS_7_2_11", "CIS_7_2_7", "ZTNA21803", "ZTNA21804"], "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.", "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.", "executiveText": "Configures SharePoint default sharing links to implement the principle of least privilege for document sharing. This security measure reduces the risk of accidental data modification while maintaining collaboration functionality, requiring users to explicitly select Edit permissions when necessary. The sharing type setting controls whether links are restricted to specific recipients or available to the entire organization. This reduces the risk of accidental data exposure through link sharing.", @@ -4927,11 +4740,7 @@ "CISA (MS.AAD.3.1v1)", "NIST CSF 2.0 (PR.IR-01)" ], - "appliesToTest": [ - "CIS_7_2_1", - "ZTNA21776", - "ZTNA21797" - ], + "appliesToTest": ["CIS_7_2_1", "ZTNA21776", "ZTNA21797"], "helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.", "docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.", "executiveText": "Disables outdated authentication methods for SharePoint access, forcing applications and users to use modern, more secure authentication protocols. This significantly improves security by eliminating vulnerable authentication pathways while requiring updates to older applications.", @@ -4960,12 +4769,7 @@ "CISA (MS.AAD.14.1v1)", "CISA (MS.SPO.1.1v1)" ], - "appliesToTest": [ - "CIS_7_2_3", - "CIS_7_2_4", - "ZTNA21803", - "ZTNA21804" - ], + "appliesToTest": ["CIS_7_2_3", "CIS_7_2_4", "ZTNA21803", "ZTNA21804"], "helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Defines the organization's default policy for sharing files and folders in SharePoint and OneDrive, balancing collaboration needs with security requirements. This fundamental setting determines whether employees can share with external users, anonymous links, or only internal colleagues.", "addedComponent": [ @@ -5012,16 +4816,8 @@ { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.2.5)", - "CISA (MS.AAD.14.2v1)", - "CISA (MS.SPO.1.2v1)" - ], - "appliesToTest": [ - "CIS_7_2_5", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 6.0.1 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], + "appliesToTest": ["CIS_7_2_5", "ZTNA21803", "ZTNA21804"], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Prevents external users from sharing company documents with additional people, maintaining control over document distribution and preventing unauthorized access expansion. This security measure ensures that external sharing remains within intended boundaries set by internal employees.", @@ -5118,15 +4914,8 @@ { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.3.2)", - "CISA (MS.SPO.2.1v1)", - "NIST CSF 2.0 (PR.AA-05)" - ], - "appliesToTest": [ - "CIS_7_3_2", - "ZTNA24824" - ], + "tag": ["CIS M365 6.0.1 (7.3.2)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "appliesToTest": ["CIS_7_3_2", "ZTNA24824"], "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", @@ -5155,16 +4944,8 @@ { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.2.6)", - "CISA (MS.AAD.14.3v1)", - "CISA (MS.SPO.1.3v1)" - ], - "appliesToTest": [ - "CIS_7_2_6", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 6.0.1 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], + "appliesToTest": ["CIS_7_2_6", "ZTNA21803", "ZTNA21804"], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.", "addedComponent": [ @@ -5523,10 +5304,7 @@ "name": "standards.TeamsExternalAccessPolicy", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.2.1)", "CIS M365 6.0.1 (8.2.2)"], - "appliesToTest": [ - "CIS_8_2_1", - "CIS_8_2_2" - ], + "appliesToTest": ["CIS_8_2_1", "CIS_8_2_2"], "helpText": "Sets the properties of the Global external access policy.", "docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.", "executiveText": "Defines the organization's policy for communicating with external users through Teams, including other organizations, Skype users, and unmanaged accounts. This fundamental setting determines the scope of external collaboration while maintaining security boundaries for business communications.", @@ -7289,5 +7067,34 @@ "addedDate": "2026-05-06", "powershellEquivalent": "Graph API PATCH https://graph.microsoft.com/beta/policies/b2bManagementPolicies/default", "recommendedBy": ["CIS"] + }, + { + "name": "standards.IntuneAppTemplateDeploy", + "cat": "Intune Standards", + "tag": [], + "helpText": "Deploys selected Intune application templates to the tenant. Supports WinGet/Store apps, Office apps, Chocolatey apps, Win32 script apps, and MSP apps.", + "docsDescription": "Uses CIPP Intune Application Templates to deploy applications across tenants as a standard. Each template can contain multiple applications of different types which will be queued for deployment.", + "executiveText": "Automatically deploys approved Intune applications across all managed tenants, ensuring consistent software availability and reducing manual deployment overhead. Supports WinGet, Office, Chocolatey, Win32, and MSP application types.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "label": "Select Application Templates", + "name": "standards.IntuneAppTemplateDeploy.templateIds", + "api": { + "url": "/api/ListAppTemplates", + "labelField": "displayName", + "valueField": "GUID", + "queryKey": "StdIntuneAppTemplateList" + } + } + ], + "label": "Deploy Intune Application Template", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-23", + "powershellEquivalent": "Graph API - /deviceAppManagement/mobileApps", + "recommendedBy": [] } ] From 28cafc931d46c705b3481a1e5bd185b39ca8879d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 23:06:16 +0200 Subject: [PATCH 24/61] added third party notice --- src/pages/cipp/integrations/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index 6d3f24f86ee4..ceb37a1cd0cb 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -141,7 +141,7 @@ const Page = () => { height: 8, }} /> - Coming Soon + Coming Soon through third-Party ) : ( <> From 30455f273d7cfcba5add1012bc260bb789f7c405 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 23:06:35 +0200 Subject: [PATCH 25/61] third party --- src/pages/cipp/integrations/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index ceb37a1cd0cb..cc3067675bab 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -141,7 +141,7 @@ const Page = () => { height: 8, }} /> - Coming Soon through third-Party + Coming Soon through Third-Party ) : ( <> From d4f458a15b67d7c8cdd8b8095d3a638a040a9dd4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 23:07:33 +0200 Subject: [PATCH 26/61] Third party text --- src/pages/cipp/integrations/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index cc3067675bab..a60530323c46 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -141,7 +141,7 @@ const Page = () => { height: 8, }} /> - Coming Soon through Third-Party + Coming Soon Through Third-Party ) : ( <> From ee0ab2abe3341bd2f0270391f36e781f6f4ef8d2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 00:13:35 +0200 Subject: [PATCH 27/61] add extendedValues --- src/components/CippFormPages/CippAddEditUser.jsx | 9 +++++++++ src/pages/tenant/manage/user-defaults.js | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 1e4ac2353fd8..ad578556a517 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -351,6 +351,15 @@ const CippAddEditUser = (props) => { } } } + + // Populate custom user attributes from template + if (template.defaultAttributes) { + Object.entries(template.defaultAttributes).forEach(([key, attr]) => { + if (attr?.Value) { + setFieldIfEmpty(`defaultAttributes.${key}.Value`, attr.Value) + } + }) + } } }, [watchedFields.userTemplate, formType]) diff --git a/src/pages/tenant/manage/user-defaults.js b/src/pages/tenant/manage/user-defaults.js index 25fd4b63362d..7f512cae7763 100644 --- a/src/pages/tenant/manage/user-defaults.js +++ b/src/pages/tenant/manage/user-defaults.js @@ -194,6 +194,13 @@ const Page = () => { name: 'businessPhones[0]', type: 'textField', }, + ...(userSettings?.userAttributes + ?.filter((attribute) => attribute.value !== 'sponsor') + .map((attribute) => ({ + label: attribute.label, + name: `defaultAttributes.${attribute.label}.Value`, + type: 'textField', + })) || []), ] const actions = [ @@ -241,6 +248,9 @@ const Page = () => { 'department', 'mobilePhone', 'businessPhones', + ...(userSettings?.userAttributes + ?.filter((attribute) => attribute.value !== 'sponsor') + .map((attribute) => `defaultAttributes.${attribute.label}.Value`) || []), ], actions: actions, } From 17bf1f8dbbd6fe16a63fa268c60dfa8fe8a0c053 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 00:14:08 +0200 Subject: [PATCH 28/61] fixes #5995 --- src/pages/tenant/manage/user-defaults.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/manage/user-defaults.js b/src/pages/tenant/manage/user-defaults.js index 7f512cae7763..8eb4f2592f61 100644 --- a/src/pages/tenant/manage/user-defaults.js +++ b/src/pages/tenant/manage/user-defaults.js @@ -91,7 +91,8 @@ const Page = () => { labelField: 'id', valueField: 'id', queryKey: `ListGraphRequest-domains-${userSettings.currentTenant}`, - dataFilter: (options) => options.filter((option) => option?.addedFields?.isVerified === true), // Only include verified domains + dataFilter: (options) => + options.filter((option) => option?.addedFields?.isVerified === true), // Only include verified domains }, multiple: false, creatable: false, From 8097e6ede3991b61e65b03bd21496dc55c96bc33 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 01:34:03 +0200 Subject: [PATCH 29/61] FIDO2 profile standards --- src/data/standards.json | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 266bb8af19cd..0322daffd1a3 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7096,5 +7096,64 @@ "addedDate": "2026-05-23", "powershellEquivalent": "Graph API - /deviceAppManagement/mobileApps", "recommendedBy": [] + }, + { + "name": "standards.FIDO2PasskeyProfiles", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Configures FIDO2 passkey profiles including AAGUID allowlists, attestation enforcement, and passkey types for the tenant.", + "docsDescription": "Manages FIDO2 passkey profiles on the tenant authentication methods policy. Allows defining passkey profiles that control which authenticators (hardware keys, password managers, Microsoft Authenticator) are permitted via AAGUID allowlists, whether attestation is enforced, and which passkey types (device-bound, synced, or both) are allowed. This enables MSPs to centrally deploy phishing-resistant MFA configurations across tenants.", + "executiveText": "Configures passkey (FIDO2) profiles that control which authenticators users can register for phishing-resistant MFA. Supports allowlisting specific hardware keys (e.g., YubiKey models), password managers (e.g., 1Password), and Microsoft Authenticator by AAGUID, with control over attestation enforcement and passkey types.", + "addedComponent": [ + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.PasskeyTypes", + "label": "Allowed Passkey Types", + "options": [ + { "label": "Device-bound only", "value": "deviceBound" }, + { "label": "Synced only", "value": "synced" }, + { "label": "Both device-bound and synced", "value": "deviceBound,synced" } + ], + "required": true + }, + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.AttestationEnforcement", + "label": "Attestation Enforcement", + "options": [ + { "label": "Disabled (required for synced passkeys)", "value": "disabled" }, + { "label": "Registration only", "value": "registrationOnly" } + ], + "required": true + }, + { + "type": "switch", + "name": "standards.FIDO2PasskeyProfiles.EnforceKeyRestrictions", + "label": "Enforce AAGUID Key Restrictions" + }, + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.EnforcementType", + "label": "Key Restriction Type", + "options": [ + { "label": "Allow listed AAGUIDs only", "value": "allow" }, + { "label": "Block listed AAGUIDs", "value": "block" } + ] + }, + { + "type": "textField", + "name": "standards.FIDO2PasskeyProfiles.AAGUIDs", + "label": "AAGUIDs (comma-separated list of authenticator AAGUIDs)" + } + ], + "label": "Configure FIDO2 Passkey Profile", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-25", + "powershellEquivalent": "Graph API PATCH /policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2", + "recommendedBy": ["CIPP"] } ] From 389babe3e5641be1363afed31b78eddd1a0e3949 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 01:58:14 +0200 Subject: [PATCH 30/61] add global var showing --- .../CippComponents/CippCustomVariables.jsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/CippComponents/CippCustomVariables.jsx b/src/components/CippComponents/CippCustomVariables.jsx index 69b5975d1777..d53b789428ae 100644 --- a/src/components/CippComponents/CippCustomVariables.jsx +++ b/src/components/CippComponents/CippCustomVariables.jsx @@ -60,6 +60,7 @@ const CippCustomVariables = ({ id }) => { confirmText: "Update the custom variable '[RowKey]'?", hideBulk: true, setDefaultValues: true, + condition: (row) => row.Scope !== "Global" || id === "AllTenants", fields: [ { type: "textField", @@ -74,7 +75,6 @@ const CippCustomVariables = ({ id }) => { type: "textField", name: "Value", label: "Value", - disableVariables: true, placeholder: "Enter the value for the custom variable.", required: true, }, @@ -99,6 +99,7 @@ const CippCustomVariables = ({ id }) => { label: "Delete", icon: , confirmText: "Are you sure you want to delete [RowKey]?", + condition: (row) => row.Scope !== "Global" || id === "AllTenants", type: "POST", url: "/api/ExecCippReplacemap", data: { @@ -127,10 +128,17 @@ const CippCustomVariables = ({ id }) => { title={id === "AllTenants" ? "Global Variables" : "Custom Variables"} actions={actions} api={{ - url: `/api/ExecCippReplacemap?Action=List&tenantId=${id}`, + url: + id === "AllTenants" + ? `/api/ExecCippReplacemap?Action=List&tenantId=${id}` + : `/api/ExecCippReplacemap?Action=List&tenantId=${id}&includeGlobal=true`, dataKey: "Results", }} - simpleColumns={["RowKey", "Value", "Description"]} + simpleColumns={ + id === "AllTenants" + ? ["RowKey", "Value", "Description"] + : ["RowKey", "Value", "Description", "Scope"] + } cardButton={ - diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index 444ee7dcd4ac..dd45430feee4 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -65,7 +65,7 @@ export const AccountPopover = (props) => { // delete query cache and persisted data queryClient.clear(); - router.push("/.auth/logout?post_logout_redirect_uri=" + encodeURIComponent(paths.index)); + router.push("/.auth/logout?prompt=select_account&post_logout_redirect_uri=" + encodeURIComponent(paths.index)); } catch (err) { console.error(err); console.log(orgData); diff --git a/src/layouts/index.js b/src/layouts/index.js index 07f2f152a562..f3c178556ff3 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -341,7 +341,7 @@ export const Layout = (props) => { - + {!setupCompleted && ( From 16b4503f014b4d5b99ff0e49485dcc97544e3cee Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 25 May 2026 11:29:56 +0800 Subject: [PATCH 34/61] login/out testing --- src/layouts/account-popover.js | 2 +- staticwebapp.config.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index dd45430feee4..444ee7dcd4ac 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -65,7 +65,7 @@ export const AccountPopover = (props) => { // delete query cache and persisted data queryClient.clear(); - router.push("/.auth/logout?prompt=select_account&post_logout_redirect_uri=" + encodeURIComponent(paths.index)); + router.push("/.auth/logout?post_logout_redirect_uri=" + encodeURIComponent(paths.index)); } catch (err) { console.error(err); console.log(orgData); diff --git a/staticwebapp.config.json b/staticwebapp.config.json index 1f57342751ca..4c6b5d68b840 100644 --- a/staticwebapp.config.json +++ b/staticwebapp.config.json @@ -20,7 +20,7 @@ }, { "route": "/login", - "rewrite": "/.auth/login/aad" + "redirect": "/.auth/login/aad?prompt=select_account" }, { "route": "/.auth/login/twitter", @@ -70,7 +70,7 @@ }, "responseOverrides": { "401": { - "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer", + "redirect": "/.auth/login/aad?prompt=select_account&post_login_redirect_uri=.referrer", "statusCode": 302, "exclude": ["/assets/illustrations/*.{png,jpg,gif}", "/css/*"] }, From c1c5693c09ef99c196b80d17b4b8aeb61f1d9852 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Mon, 25 May 2026 11:49:27 +0200 Subject: [PATCH 35/61] feat: add admin role member removal functionality --- .../identity/administration/roles/index.js | 77 ++- .../administration/users/user/index.jsx | 588 ++++++++++-------- 2 files changed, 379 insertions(+), 286 deletions(-) diff --git a/src/pages/identity/administration/roles/index.js b/src/pages/identity/administration/roles/index.js index f09fb8a01388..27b95f7ed9c9 100644 --- a/src/pages/identity/administration/roles/index.js +++ b/src/pages/identity/administration/roles/index.js @@ -1,24 +1,71 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import CippFormComponent from '../../../../components/CippComponents/CippFormComponent' +import { usePermissions } from '../../../../hooks/use-permissions' +import { PersonRemove } from '@mui/icons-material' + +const RemoveRoleMembersForm = ({ formHook, row }) => { + const memberOptions = (row?.Members ?? []).map((member) => ({ + label: member.userPrincipalName + ? `${member.displayName} (${member.userPrincipalName})` + : member.displayName, + value: member.id, + addedFields: { + displayName: member.displayName, + userPrincipalName: member.userPrincipalName, + }, + })) + + return ( + + ) +} const Page = () => { - const pageTitle = "Roles"; + const pageTitle = 'Roles' + const { checkPermissions } = usePermissions() + const canWriteRole = checkPermissions(['Identity.Role.ReadWrite']) - const actions = []; + const actions = [ + { + label: 'Remove Members', + type: 'POST', + icon: , + url: '/api/ExecRemoveAdminRole', + children: ({ formHook, row }) => , + data: { + RoleId: 'Id', + RoleName: 'DisplayName', + }, + confirmText: 'Select the members to remove from [DisplayName].', + allowResubmit: true, + hideBulk: true, + condition: (row) => canWriteRole && (row?.Members ?? []).length > 0, + }, + ] const offCanvas = { extendedInfoFields: [ - "DisplayName", // Role Group Name - "Members", // Member Names + 'DisplayName', // Role Group Name + 'Members', // Member Names ], actions: actions, - }; + } const columns = [ - "DisplayName", // Role Name - "Description", // Description - "Members", // Members - ]; + 'DisplayName', // Role Name + 'Description', // Description + 'Members', // Members + ] return ( { offCanvas={offCanvas} simpleColumns={columns} /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page} -export default Page; +export default Page diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 95adbe3d4fed..cce0e8f472fd 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -1,33 +1,43 @@ -import { Layout as DashboardLayout } from "../../../../../layouts/index.js"; -import { useSettings } from "../../../../../hooks/use-settings"; -import { useRouter } from "next/router"; -import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; -import CippFormSkeleton from "../../../../../components/CippFormPages/CippFormSkeleton"; -import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { AdminPanelSettings, Check, Group, Mail, Fingerprint, Launch, Devices } from "@mui/icons-material"; -import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; -import tabOptions from "./tabOptions"; -import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; -import { Box, Stack } from "@mui/system"; -import { Grid } from "@mui/system"; -import { CippUserInfoCard } from "../../../../../components/CippCards/CippUserInfoCard"; -import { SvgIcon, Typography } from "@mui/material"; -import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; -import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; -import { useEffect, useState } from "react"; -import { useCippUserActions } from "../../../../../components/CippComponents/CippUserActions"; -import { EyeIcon, PencilIcon } from "@heroicons/react/24/outline"; -import { CippDataTable } from "../../../../../components/CippTable/CippDataTable"; -import dynamic from "next/dynamic"; -const CippMap = dynamic(() => import("../../../../../components/CippComponents/CippMap"), { +import { Layout as DashboardLayout } from '../../../../../layouts/index.js' +import { useSettings } from '../../../../../hooks/use-settings' +import { useRouter } from 'next/router' +import { ApiGetCall, ApiPostCall } from '../../../../../api/ApiCall' +import CippFormSkeleton from '../../../../../components/CippFormPages/CippFormSkeleton' +import CalendarIcon from '@heroicons/react/24/outline/CalendarIcon' +import { + AdminPanelSettings, + Check, + Group, + Mail, + Fingerprint, + Launch, + Devices, + PersonRemove, +} from '@mui/icons-material' +import { HeaderedTabbedLayout } from '../../../../../layouts/HeaderedTabbedLayout' +import tabOptions from './tabOptions' +import { CippCopyToClipBoard } from '../../../../../components/CippComponents/CippCopyToClipboard' +import { Box, Stack } from '@mui/system' +import { Grid } from '@mui/system' +import { CippUserInfoCard } from '../../../../../components/CippCards/CippUserInfoCard' +import { SvgIcon, Typography } from '@mui/material' +import { CippBannerListCard } from '../../../../../components/CippCards/CippBannerListCard' +import { CippTimeAgo } from '../../../../../components/CippComponents/CippTimeAgo' +import { useEffect, useState } from 'react' +import { useCippUserActions } from '../../../../../components/CippComponents/CippUserActions' +import { EyeIcon, PencilIcon } from '@heroicons/react/24/outline' +import { CippDataTable } from '../../../../../components/CippTable/CippDataTable' +import dynamic from 'next/dynamic' +const CippMap = dynamic(() => import('../../../../../components/CippComponents/CippMap'), { ssr: false, -}); +}) -import { Button, Dialog, DialogTitle, DialogContent, IconButton } from "@mui/material"; -import { Close } from "@mui/icons-material"; -import { CippPropertyList } from "../../../../../components/CippComponents/CippPropertyList"; -import { CippCodeBlock } from "../../../../../components/CippComponents/CippCodeBlock"; -import { CippHead } from "../../../../../components/CippComponents/CippHead"; +import { Button, Dialog, DialogTitle, DialogContent, IconButton } from '@mui/material' +import { Close } from '@mui/icons-material' +import { CippPropertyList } from '../../../../../components/CippComponents/CippPropertyList' +import { CippCodeBlock } from '../../../../../components/CippComponents/CippCodeBlock' +import { CippHead } from '../../../../../components/CippComponents/CippHead' +import { usePermissions } from '../../../../../hooks/use-permissions' const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { return ( @@ -37,7 +47,7 @@ const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { @@ -47,16 +57,16 @@ const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { noCard={true} title="Sign-In Logs" simpleColumns={[ - "createdDateTime", - "status", - "ipAddress", - "clientAppUsed", - "resourceDisplayName", - "status.errorCode", - "location", + 'createdDateTime', + 'status', + 'ipAddress', + 'clientAppUsed', + 'resourceDisplayName', + 'status.errorCode', + 'location', ]} api={{ - url: "/api/ListUserSigninLogs", + url: '/api/ListUserSigninLogs', data: { UserId: userId, tenantFilter: tenantFilter, @@ -67,22 +77,24 @@ const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { /> - ); -}; + ) +} const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { userId } = router.query; - const [waiting, setWaiting] = useState(false); - const [signInLogsDialogOpen, setSignInLogsDialogOpen] = useState(false); - const userActions = useCippUserActions(); + const userSettingsDefaults = useSettings() + const router = useRouter() + const { userId } = router.query + const [waiting, setWaiting] = useState(false) + const [signInLogsDialogOpen, setSignInLogsDialogOpen] = useState(false) + const userActions = useCippUserActions() + const { checkPermissions } = usePermissions() + const canWriteRole = checkPermissions(['Identity.Role.ReadWrite']) useEffect(() => { if (userId) { - setWaiting(true); + setWaiting(true) } - }, [userId]); + }, [userId]) const userRequest = ApiGetCall({ url: `/api/ListUsers?UserId=${userId}&tenantFilter=${ @@ -90,70 +102,75 @@ const Page = () => { }`, queryKey: `ListUsers-${userId}`, waiting: waiting, - }); + }) const userBulkRequest = ApiPostCall({ urlFromData: true, - }); + }) function refreshFunction() { - const userPrincipalName = userRequest.data?.[0]?.userPrincipalName; + const userPrincipalName = userRequest.data?.[0]?.userPrincipalName const requests = [ { - id: "userMemberOf", + id: 'userMemberOf', url: `/users/${userId}/memberOf`, - method: "GET", + method: 'GET', }, { - id: "mfaDevices", + id: 'mfaDevices', url: `/users/${userId}/authentication/methods?$top=99`, - method: "GET", + method: 'GET', }, { - id: "signInLogs", + id: 'signInLogs', url: `/auditLogs/signIns?$filter=(userId eq '${userId}')&$top=1`, - method: "GET", + method: 'GET', }, - ]; + ] // Only add managedDevices request if we have the userPrincipalName if (userPrincipalName) { requests.push({ - id: "managedDevices", + id: 'managedDevices', url: `/deviceManagement/managedDevices?$filter=userPrincipalName eq '${userPrincipalName}'`, - method: "GET", - }); + method: 'GET', + }) } userBulkRequest.mutate({ - url: "/api/ListGraphBulkRequest", + url: '/api/ListGraphBulkRequest', data: { Requests: requests, tenantFilter: userSettingsDefaults.currentTenant, - noPaginateIds: ["signInLogs"], + noPaginateIds: ['signInLogs'], }, - }); + }) } useEffect(() => { - if (userId && userSettingsDefaults.currentTenant && userRequest.isSuccess && !userBulkRequest.isSuccess) { - refreshFunction(); + if ( + userId && + userSettingsDefaults.currentTenant && + userRequest.isSuccess && + !userBulkRequest.isSuccess + ) { + refreshFunction() } - }, [userId, userSettingsDefaults.currentTenant, userRequest.isSuccess, userBulkRequest.isSuccess]); + }, [userId, userSettingsDefaults.currentTenant, userRequest.isSuccess, userBulkRequest.isSuccess]) - const bulkData = userBulkRequest?.data?.data ?? []; - const signInLogsData = bulkData?.find((item) => item.id === "signInLogs"); - const userMemberOfData = bulkData?.find((item) => item.id === "userMemberOf"); - const mfaDevicesData = bulkData?.find((item) => item.id === "mfaDevices"); - const managedDevicesData = bulkData?.find((item) => item.id === "managedDevices"); + const bulkData = userBulkRequest?.data?.data ?? [] + const signInLogsData = bulkData?.find((item) => item.id === 'signInLogs') + const userMemberOfData = bulkData?.find((item) => item.id === 'userMemberOf') + const mfaDevicesData = bulkData?.find((item) => item.id === 'mfaDevices') + const managedDevicesData = bulkData?.find((item) => item.id === 'managedDevices') - const signInLogs = signInLogsData?.body?.value || []; - const userMemberOf = userMemberOfData?.body?.value || []; - const mfaDevices = mfaDevicesData?.body?.value || []; - const managedDevices = managedDevicesData?.body?.value || []; + const signInLogs = signInLogsData?.body?.value || [] + const userMemberOf = userMemberOfData?.body?.value || [] + const mfaDevices = mfaDevicesData?.body?.value || [] + const managedDevices = managedDevicesData?.body?.value || [] // Set the title and subtitle for the layout - const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading..."; + const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : 'Loading...' const subtitle = userRequest.isSuccess ? [ @@ -174,7 +191,7 @@ const Page = () => { ), }, { - icon: , + icon: , text: ( + + + ); +}; + +export default CippTutorialDialog; diff --git a/src/contexts/tutorial-context.js b/src/contexts/tutorial-context.js new file mode 100644 index 000000000000..954b641d8b0b --- /dev/null +++ b/src/contexts/tutorial-context.js @@ -0,0 +1,169 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { driver } from "driver.js"; +import { useRouter } from "next/router"; + +const STORAGE_KEY = "cipp.tutorials.completed"; + +const getCompletedTutorials = () => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } +}; + +const storeCompletedTutorial = (id) => { + try { + const completed = getCompletedTutorials(); + if (!completed.includes(id)) { + completed.push(id); + localStorage.setItem(STORAGE_KEY, JSON.stringify(completed)); + } + } catch { + // ignore + } +}; + +const resetCompletedTutorials = () => { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + // ignore + } +}; + +const TutorialContext = createContext({ + tutorials: [], + activeTutorial: null, + completedIds: [], + startTutorial: () => {}, + resetProgress: () => {}, + getTutorialsForPage: () => [], +}); + +// Load all tutorial JSON files from the data/tutorials folder at build time +const loadTutorials = () => { + const context = require.context("../data/tutorials", false, /\.json$/); + return context.keys().map((key) => { + const tutorial = context(key); + return tutorial.default || tutorial; + }); +}; + +export const TutorialProvider = ({ children }) => { + const [tutorials] = useState(() => loadTutorials()); + const [completedIds, setCompletedIds] = useState([]); + const [activeTutorial, setActiveTutorial] = useState(null); + const driverRef = useRef(null); + const router = useRouter(); + + useEffect(() => { + setCompletedIds(getCompletedTutorials()); + }, []); + + // Launch tutorial from ?tutorial=$id query param + useEffect(() => { + if (!router.isReady || activeTutorial) return; + const tutorialId = router.query.tutorial; + if (!tutorialId) return; + + const match = tutorials.find((t) => t.id === tutorialId); + if (!match) return; + + // Strip the query param so it doesn't re-trigger + const { tutorial: _, ...rest } = router.query; + router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }); + + // Delay to let the page fully render + setTimeout(() => runDriver(match), 600); + }, [router.isReady, router.query.tutorial, tutorials]); + + // Cleanup driver on unmount or route change + useEffect(() => { + return () => { + if (driverRef.current) { + driverRef.current.destroy(); + driverRef.current = null; + } + }; + }, []); + + const startTutorial = useCallback( + (tutorial) => { + if (driverRef.current) { + driverRef.current.destroy(); + } + + // If tutorial specifies pages and we're not on any of them, navigate first + if (tutorial.pages?.length && !tutorial.pages.includes(router.pathname)) { + router.push(tutorial.pages[0]).then(() => { + // Small delay to let the page render before starting the tour + setTimeout(() => runDriver(tutorial), 500); + }); + return; + } + + runDriver(tutorial); + }, + [router] + ); + + const runDriver = useCallback((tutorial) => { + setActiveTutorial(tutorial); + + const driverObj = driver({ + showProgress: true, + animate: true, + allowClose: true, + overlayColor: "rgba(0, 0, 0, 0.6)", + stagePadding: 8, + stageRadius: 8, + popoverClass: "cipp-tutorial-popover", + nextBtnText: "Next →", + prevBtnText: "← Back", + doneBtnText: "Done ✓", + progressText: "{{current}} of {{total}}", + steps: tutorial.steps, + onDestroyed: () => { + storeCompletedTutorial(tutorial.id); + setCompletedIds(getCompletedTutorials()); + setActiveTutorial(null); + driverRef.current = null; + }, + }); + + driverRef.current = driverObj; + driverObj.drive(); + }, []); + + const resetProgress = useCallback(() => { + resetCompletedTutorials(); + setCompletedIds([]); + }, []); + + const getTutorialsForPage = useCallback( + (pathname) => { + return tutorials.filter( + (t) => !t.pages || t.pages.length === 0 || t.pages.includes(pathname) + ); + }, + [tutorials] + ); + + const value = useMemo( + () => ({ + tutorials, + activeTutorial, + completedIds, + startTutorial, + resetProgress, + getTutorialsForPage, + }), + [tutorials, activeTutorial, completedIds, startTutorial, resetProgress, getTutorialsForPage] + ); + + return {children}; +}; + +export const useTutorials = () => useContext(TutorialContext); diff --git a/src/data/tutorials/dashboard-overview.json b/src/data/tutorials/dashboard-overview.json new file mode 100644 index 000000000000..df2aeca19615 --- /dev/null +++ b/src/data/tutorials/dashboard-overview.json @@ -0,0 +1,111 @@ +{ + "id": "dashboard-overview", + "title": "Dashboard Overview", + "description": "A guided tour of the CIPP Dashboard — learn what each card, chart, and control does.", + "category": "General", + "pages": ["/dashboardv2"], + "steps": [ + { + "popover": { + "title": "Welcome to the Dashboard 👋", + "description": "This tour will walk you through every section of the CIPP Dashboard so you know exactly where to find the information you need." + } + }, + { + "element": "[data-tutorial='tenant-selector']", + "popover": { + "title": "Select a Tenant", + "description": "Start here. Pick the customer tenant you want to inspect. The entire dashboard updates to show data for the selected tenant.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-portals']", + "popover": { + "title": "Quick Portal Access", + "description": "Jump straight into the Microsoft admin portals (M365, Exchange, Entra, Intune, Azure, etc.) for the selected tenant — no need to look up URLs.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-test-suite']", + "popover": { + "title": "Test Suite Selector", + "description": "Choose which test suite to run against the tenant. You can create custom test suites, refresh results, or edit existing ones from here.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-tenant-info']", + "popover": { + "title": "Tenant Information", + "description": "Quick-reference card showing the tenant's display name, tenant ID, and primary domain. Click the chips to copy values to your clipboard.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-tenant-metrics']", + "popover": { + "title": "Tenant Metrics", + "description": "At-a-glance counts for Users, Guests, Groups, Service Principals, Devices, and Managed Devices. Click any metric to drill into the full list.", + "side": "bottom", + "align": "center" + } + }, + { + "element": "[data-tutorial='dashboard-assessment']", + "popover": { + "title": "Assessment Results", + "description": "A summary of the selected test suite results broken down by category (Identity, Devices, Custom). Green = passed, Red = failed, Orange = skipped.", + "side": "bottom", + "align": "end" + } + }, + { + "element": "[data-tutorial='dashboard-secure-score']", + "popover": { + "title": "Secure Score Trend", + "description": "A line chart tracking the tenant's Microsoft Secure Score over time. The reference line shows the maximum possible score, so you can gauge progress.", + "side": "right", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-mfa']", + "popover": { + "title": "User Authentication (MFA)", + "description": "This Sankey diagram shows how many enabled users are MFA-registered vs. not, and what enforcement method protects them (Conditional Access, Security Defaults, Per-user MFA, or none). Click any node to jump to the MFA report.", + "side": "left", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-auth-methods']", + "popover": { + "title": "Auth Methods Breakdown", + "description": "See how users authenticate — single-factor vs. multi-factor, phishable vs. phish-resistant — and which methods (Phone, Authenticator, Passkey, WHfB) they use. Click a node to filter the MFA report.", + "side": "right", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-licenses']", + "popover": { + "title": "License Overview", + "description": "Shows the top licenses in the tenant with assigned vs. available counts. Use this to spot unused or over-provisioned licenses at a glance.", + "side": "left", + "align": "start" + } + }, + { + "popover": { + "title": "You're all set! 🎉", + "description": "That covers the Dashboard. Explore the other tabs at the top for more views, or check the sidebar for all CIPP modules. You can replay this tour anytime from the Tutorials menu." + } + } + ] +} diff --git a/src/data/tutorials/getting-started.json b/src/data/tutorials/getting-started.json new file mode 100644 index 000000000000..8e2c2291350a --- /dev/null +++ b/src/data/tutorials/getting-started.json @@ -0,0 +1,48 @@ +{ + "id": "getting-started", + "title": "Getting Started with CIPP", + "description": "Learn the basics of navigating CIPP and managing your tenants.", + "category": "General", + "pages": ["/"], + "steps": [ + { + "popover": { + "title": "Welcome to CIPP! 👋", + "description": "This quick tour will show you the key features of the CIPP dashboard. Let's get started!" + } + }, + { + "element": "[data-tutorial='tenant-selector']", + "popover": { + "title": "Tenant Selector", + "description": "Use the tenant selector to switch between your managed tenants. You can search by name or select 'All Tenants' for a global view.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='side-nav']", + "popover": { + "title": "Navigation Menu", + "description": "The sidebar gives you access to all CIPP modules — Identity, Endpoint, Security, Email, Teams & SharePoint, and more.", + "side": "right", + "align": "start" + } + }, + { + "element": "[data-tutorial='speed-dial']", + "popover": { + "title": "Quick Actions", + "description": "Use the help button in the bottom-right corner to report bugs, request features, join Discord, or access the documentation.", + "side": "left", + "align": "end" + } + }, + { + "popover": { + "title": "You're all set! 🎉", + "description": "You now know the basics. Explore the sidebar to discover all the tools CIPP offers. You can replay this tour anytime from the Tutorials menu." + } + } + ] +} diff --git a/src/data/tutorials/tenant-management.json b/src/data/tutorials/tenant-management.json new file mode 100644 index 000000000000..0492fd8fdac5 --- /dev/null +++ b/src/data/tutorials/tenant-management.json @@ -0,0 +1,39 @@ +{ + "id": "tenant-management", + "title": "Managing Tenants", + "description": "Learn how to view tenant details, manage tenant settings, and navigate tenant-specific pages.", + "category": "Administration", + "pages": ["/tenant/administration/tenants"], + "steps": [ + { + "popover": { + "title": "Tenant Management", + "description": "This page shows all your managed tenants. Let's walk through the key features." + } + }, + { + "element": "[data-tutorial='breadcrumb-nav']", + "popover": { + "title": "Breadcrumb Navigation", + "description": "Use the breadcrumb trail to see where you are and quickly navigate back to parent pages.", + "side": "bottom", + "align": "start" + } + }, + { + "element": ".MuiTableContainer-root", + "popover": { + "title": "Tenant List", + "description": "Here you'll find all your managed tenants. Click on any row to see detailed information about that tenant.", + "side": "top", + "align": "center" + } + }, + { + "popover": { + "title": "That's it!", + "description": "You now know how to manage your tenants. Check out other tutorials for more advanced features." + } + } + ] +} diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index 5b01ee107331..1dadfca01577 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -169,6 +169,7 @@ export const SideNav = (props) => { setHovered(true), diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index 41d5f07e0f2f..2c7e0a2507ad 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -278,7 +278,9 @@ export const TopNav = (props) => { {!mdDown && ( - + + + )} {mdDown && ( diff --git a/src/pages/_app.js b/src/pages/_app.js index aa387f0417fa..74504f43b9fa 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -11,6 +11,8 @@ import { store } from '../store' import { createTheme } from '../theme' import { createEmotionCache } from '../utils/create-emotion-cache' import '../libs/nprogress' +import 'driver.js/dist/driver.css' +import '../styles/tutorial-overrides.css' import { PrivateRoute } from '../components/PrivateRoute' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useMediaPredicate } from 'react-media-hook' @@ -52,12 +54,15 @@ import { Gavel, ClearAll as ClearAllIcon, } from '@mui/icons-material' +import { School as TutorialIcon } from '@mui/icons-material' import { SvgIcon } from '@mui/material' import React, { useEffect, useState, useRef } from 'react' import { usePathname } from 'next/navigation' import { useRouter } from 'next/router' import { persistQueryClient } from '@tanstack/react-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' +import { TutorialProvider } from '../contexts/tutorial-context' +import CippTutorialDialog from '../components/CippComponents/CippTutorialDialog' const ReactQueryDevtoolsProduction = React.lazy(() => import('@tanstack/react-query-devtools/build/modern/production.js').then((d) => ({ @@ -76,6 +81,7 @@ const App = (props) => { const pathname = usePathname() const route = useRouter() const [dateLocale, setDateLocale] = useState(enUS) + const [tutorialDialogOpen, setTutorialDialogOpen] = useState(false) useEffect(() => { if (typeof window === 'undefined') return @@ -243,6 +249,12 @@ const App = (props) => { href: `https://docs.cipp.app/user-documentation${pathname}`, onClick: () => window.open(`https://docs.cipp.app/user-documentation${pathname}`, '_blank'), }, + { + id: 'tutorials', + icon: , + name: 'Tutorials', + onClick: () => setTutorialDialogOpen(true), + }, ] return ( @@ -275,9 +287,15 @@ const App = (props) => { - - {getLayout()} - + + + {getLayout()} + + setTutorialDialogOpen(false)} + /> + diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 0ea9653e3680..dfd9d9a76528 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -194,8 +194,9 @@ const Page = () => { - + { )} - + @@ -319,12 +320,12 @@ const Page = () => { {/* Tenant Overview Section - 3 Column Layout */} {/* Column 1: Tenant Information */} - + {/* Column 2: Tenant Metrics - 2x3 Grid */} - + { {/* Column 3: Assessment Results */} - + { {/* Left Column */} - + - + { {/* Right Column */} - + - + Date: Mon, 25 May 2026 18:31:00 +0200 Subject: [PATCH 40/61] add tutorials to easy deployment of steps for Ashe. --- .../CippComponents/CippBreadcrumbNav.jsx | 514 +++++++++--------- .../CippComponents/CippSpeedDial.jsx | 156 +++--- .../CippComponents/CippTutorialDialog.jsx | 77 ++- src/contexts/tutorial-context.js | 142 +++-- src/layouts/top-nav.js | 6 +- src/pages/dashboardv2/index.js | 5 +- src/styles/tutorial-overrides.css | 2 +- 7 files changed, 455 insertions(+), 447 deletions(-) diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index 8e285d78e364..5ea88f3434cf 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -1,155 +1,155 @@ -import { useEffect, useState, useRef } from "react"; -import { useRouter } from "next/router"; -import { Breadcrumbs, Link, Typography, Box, IconButton, Tooltip } from "@mui/material"; -import { NavigateNext, History, AccountTree } from "@mui/icons-material"; -import { nativeMenuItems } from "../../layouts/config"; -import { useSettings } from "../../hooks/use-settings"; +import { useEffect, useState, useRef } from 'react' +import { useRouter } from 'next/router' +import { Breadcrumbs, Link, Typography, Box, IconButton, Tooltip } from '@mui/material' +import { NavigateNext, History, AccountTree } from '@mui/icons-material' +import { nativeMenuItems } from '../../layouts/config' +import { useSettings } from '../../hooks/use-settings' -const MAX_HISTORY_STORAGE = 20; // Maximum number of pages to keep in history -const MAX_BREADCRUMB_DISPLAY = 5; // Maximum number of breadcrumbs to display at once +const MAX_HISTORY_STORAGE = 20 // Maximum number of pages to keep in history +const MAX_BREADCRUMB_DISPLAY = 5 // Maximum number of breadcrumbs to display at once /** * Load all tabOptions.json files dynamically */ async function loadTabOptions() { const tabOptionPaths = [ - "/email/administration/exchange-retention", - "/cipp/custom-data", - "/cipp/advanced/super-admin", - "/endpoint/MEM/enrollment-profiles", - "/tenant/standards", - "/tenant/manage", - "/tenant/administration/applications", - "/tenant/administration/tenants", - "/tenant/administration/audit-logs", - "/identity/administration/users/user", - "/tenant/administration/securescore", - "/tenant/gdap-management", - "/tenant/gdap-management/relationships/relationship", - "/cipp/settings", - ]; - - const tabOptions = []; + '/email/administration/exchange-retention', + '/cipp/custom-data', + '/cipp/advanced/super-admin', + '/endpoint/MEM/enrollment-profiles', + '/tenant/standards', + '/tenant/manage', + '/tenant/administration/applications', + '/tenant/administration/tenants', + '/tenant/administration/audit-logs', + '/identity/administration/users/user', + '/tenant/administration/securescore', + '/tenant/gdap-management', + '/tenant/gdap-management/relationships/relationship', + '/cipp/settings', + ] + + const tabOptions = [] for (const basePath of tabOptionPaths) { try { - const module = await import(`../../pages${basePath}/tabOptions.json`); - const options = module.default || module; + const module = await import(`../../pages${basePath}/tabOptions.json`) + const options = module.default || module // Add each tab option with metadata options.forEach((option) => { tabOptions.push({ title: option.label, path: option.path, - type: "tab", + type: 'tab', basePath: basePath, - }); - }); + }) + }) } catch (error) { // Silently skip if file doesn't exist or can't be loaded } } - return tabOptions; + return tabOptions } export const CippBreadcrumbNav = () => { - const router = useRouter(); - const settings = useSettings(); - const [history, setHistory] = useState([]); - const [mode, setMode] = useState(settings.breadcrumbMode || "hierarchical"); - const [tabOptions, setTabOptions] = useState([]); - const lastRouteRef = useRef(null); - const titleCheckCountRef = useRef(0); - const titleCheckIntervalRef = useRef(null); + const router = useRouter() + const settings = useSettings() + const [history, setHistory] = useState([]) + const [mode, setMode] = useState(settings.breadcrumbMode || 'hierarchical') + const [tabOptions, setTabOptions] = useState([]) + const lastRouteRef = useRef(null) + const titleCheckCountRef = useRef(0) + const titleCheckIntervalRef = useRef(null) // Helper function to filter out unnecessary query parameters const getCleanQueryParams = (query) => { - const cleaned = { ...query }; + const cleaned = { ...query } // Remove tenantFilter if it's "AllTenants" or not explicitly needed - if (cleaned.tenantFilter === "AllTenants" || cleaned.tenantFilter === undefined) { - delete cleaned.tenantFilter; + if (cleaned.tenantFilter === 'AllTenants' || cleaned.tenantFilter === undefined) { + delete cleaned.tenantFilter } - return cleaned; - }; + return cleaned + } // Helper function to clean page titles const cleanPageTitle = (title) => { - if (!title) return 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(); - }; + .replace(/\s*-\s*AllTenants\s*/, '') + .replace(/AllTenants\s*-\s*/, '') + .replace(/AllTenants/, '') + .trim() + } // Load tab options on mount useEffect(() => { - loadTabOptions().then(setTabOptions); - }, []); + loadTabOptions().then(setTabOptions) + }, []) useEffect(() => { // Only update when the route actually changes, not on every render - const currentRoute = router.asPath; + const currentRoute = router.asPath // Skip if this is the same route as last time if (lastRouteRef.current === currentRoute) { - return; + return } - lastRouteRef.current = currentRoute; + lastRouteRef.current = currentRoute // Clear any existing title check interval if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } // Reset check counter - titleCheckCountRef.current = 0; + titleCheckCountRef.current = 0 // Function to check and update title const checkTitle = () => { - titleCheckCountRef.current++; + titleCheckCountRef.current++ // Stop checking after 50 attempts (5 seconds) to prevent infinite intervals if (titleCheckCountRef.current > 50) { if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - return; + return } - let pageTitle = document.title.replace(" - CIPP", "").trim(); + let pageTitle = document.title.replace(' - CIPP', '').trim() // Remove tenant domain from title (e.g., "Groups - domain.onmicrosoft.com" -> "Groups") // But only if it looks like a domain (contains a dot) - const parts = pageTitle.split(" - "); - if (parts.length > 1 && parts[parts.length - 1].includes(".")) { - pageTitle = parts.slice(0, -1).join(" - ").trim(); + const parts = pageTitle.split(' - ') + if (parts.length > 1 && parts[parts.length - 1].includes('.')) { + pageTitle = parts.slice(0, -1).join(' - ').trim() } // Clean AllTenants from title - pageTitle = cleanPageTitle(pageTitle); + pageTitle = cleanPageTitle(pageTitle) // Skip if title is empty, generic, or error page if ( !pageTitle || - pageTitle === "CIPP" || - pageTitle.toLowerCase().includes("error") || - pageTitle === "404" || - pageTitle === "500" + pageTitle === 'CIPP' || + pageTitle.toLowerCase().includes('error') || + pageTitle === '404' || + pageTitle === '500' ) { - return; + return } // Normalize URL for comparison (remove trailing slashes and query params) const normalizeUrl = (url) => { // Remove query params and trailing slashes for comparison - return url.split("?")[0].replace(/\/$/, "").toLowerCase(); - }; + return url.split('?')[0].replace(/\/$/, '').toLowerCase() + } const currentPage = { title: pageTitle, @@ -157,190 +157,190 @@ export const CippBreadcrumbNav = () => { query: { ...router.query }, fullUrl: router.asPath, timestamp: Date.now(), - }; + } - const normalizedCurrentUrl = normalizeUrl(currentPage.fullUrl); + const normalizedCurrentUrl = normalizeUrl(currentPage.fullUrl) setHistory((prevHistory) => { // Check if last entry has same title AND similar path (prevent duplicate with same content) - const lastEntry = prevHistory[prevHistory.length - 1]; + const lastEntry = prevHistory[prevHistory.length - 1] if (lastEntry) { - const sameTitle = lastEntry.title.trim() === currentPage.title.trim(); - const samePath = normalizeUrl(lastEntry.fullUrl) === normalizedCurrentUrl; + const sameTitle = lastEntry.title.trim() === currentPage.title.trim() + const samePath = normalizeUrl(lastEntry.fullUrl) === normalizedCurrentUrl if (sameTitle && samePath) { // Exact duplicate - don't add, just stop checking if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - return prevHistory; + return prevHistory } if (samePath && !sameTitle) { // Same URL but title changed - update the entry - const updated = [...prevHistory]; + const updated = [...prevHistory] updated[prevHistory.length - 1] = { ...currentPage, query: getCleanQueryParams(currentPage.query), - }; + } if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - return updated; + return updated } } // Find if this URL exists anywhere EXCEPT the last position in history const existingIndex = prevHistory.findIndex((entry, index) => { // Skip the last entry since we already checked it above - if (index === prevHistory.length - 1) return false; - return normalizeUrl(entry.fullUrl) === normalizedCurrentUrl; - }); + if (index === prevHistory.length - 1) return false + return normalizeUrl(entry.fullUrl) === normalizedCurrentUrl + }) // URL not in history (except possibly as last entry which we handled) - add as new entry if (existingIndex === -1) { const cleanedCurrentPage = { ...currentPage, query: getCleanQueryParams(currentPage.query), - }; - const newHistory = [...prevHistory, cleanedCurrentPage]; + } + const newHistory = [...prevHistory, cleanedCurrentPage] // Keep only the last MAX_HISTORY_STORAGE pages const trimmedHistory = newHistory.length > MAX_HISTORY_STORAGE ? newHistory.slice(-MAX_HISTORY_STORAGE) - : newHistory; + : newHistory // Don't stop checking yet - title might still be loading - return trimmedHistory; + return trimmedHistory } // URL exists in history but not as last entry - user navigated back // Truncate history after this point and update the entry if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - const updated = prevHistory.slice(0, existingIndex + 1); + const updated = prevHistory.slice(0, existingIndex + 1) updated[existingIndex] = { ...currentPage, query: getCleanQueryParams(currentPage.query), - }; - return updated; - }); - }; + } + return updated + }) + } // Start checking for title updates - titleCheckIntervalRef.current = setInterval(checkTitle, 100); + titleCheckIntervalRef.current = setInterval(checkTitle, 100) return () => { if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - }; - }, [router.asPath, router.pathname, router.query]); + } + }, [router.asPath, router.pathname, router.query]) const handleBreadcrumbClick = (index) => { - const page = history[index]; + const page = history[index] if (page) { - const cleanedQuery = getCleanQueryParams(page.query); + const cleanedQuery = getCleanQueryParams(page.query) router.push({ pathname: page.path, query: cleanedQuery, - }); + }) } - }; + } // State to track current page title for hierarchical mode - const [currentPageTitle, setCurrentPageTitle] = useState(null); - const hierarchicalTitleCheckRef = useRef(null); - const hierarchicalCheckCountRef = useRef(0); + const [currentPageTitle, setCurrentPageTitle] = useState(null) + const hierarchicalTitleCheckRef = useRef(null) + const hierarchicalCheckCountRef = useRef(0) // Watch for title changes to update hierarchical breadcrumbs useEffect(() => { - if (mode === "hierarchical") { + if (mode === 'hierarchical') { // Clear any existing interval if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } // Reset counter - hierarchicalCheckCountRef.current = 0; + hierarchicalCheckCountRef.current = 0 const updateTitle = () => { - hierarchicalCheckCountRef.current++; + hierarchicalCheckCountRef.current++ // Stop after 20 attempts (10 seconds) to prevent infinite checking if (hierarchicalCheckCountRef.current > 20) { if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } - return; + return } - let pageTitle = document.title.replace(" - CIPP", "").trim(); - const parts = pageTitle.split(" - "); + 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; + parts.length > 1 && parts[parts.length - 1].includes('.') + ? parts.slice(0, -1).join(' - ').trim() + : pageTitle // Clean AllTenants from title - const finalTitle = cleanPageTitle(cleanTitle); + const finalTitle = cleanPageTitle(cleanTitle) - if (finalTitle && finalTitle !== "CIPP" && !finalTitle.toLowerCase().includes("loading")) { - setCurrentPageTitle(finalTitle); + if (finalTitle && finalTitle !== 'CIPP' && !finalTitle.toLowerCase().includes('loading')) { + setCurrentPageTitle(finalTitle) // Stop checking once we have a valid title if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } } - }; + } // Initial update - updateTitle(); + updateTitle() // Only start interval if we don't have a valid title yet - if (!currentPageTitle || currentPageTitle.toLowerCase().includes("loading")) { - hierarchicalTitleCheckRef.current = setInterval(updateTitle, 500); + if (!currentPageTitle || currentPageTitle.toLowerCase().includes('loading')) { + hierarchicalTitleCheckRef.current = setInterval(updateTitle, 500) } return () => { if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } - }; + } } - }, [mode, router.pathname]); + }, [mode, router.pathname]) // Build hierarchical breadcrumbs from config.js navigation structure const buildHierarchicalBreadcrumbs = () => { - const currentPath = router.pathname; + const currentPath = router.pathname // Helper to check if paths match (handles dynamic routes) const pathsMatch = (menuPath, currentPath) => { - if (!menuPath) return false; + if (!menuPath) return false // Exact match - if (menuPath === currentPath) return true; + if (menuPath === currentPath) return true // Check if current path starts with menu path (for nested routes) // e.g., menu: "/identity/administration/users" matches "/identity/administration/users/edit" - if (currentPath.startsWith(menuPath + "/")) return true; + if (currentPath.startsWith(menuPath + '/')) return true - return false; - }; + return false + } const findPathInMenu = (items, path = []) => { for (const item of items) { - const currentBreadcrumb = [...path]; + const currentBreadcrumb = [...path] // Add current item to path if it has a title // Include all items (headers, groups, and pages) to show full hierarchy @@ -350,44 +350,44 @@ export const CippBreadcrumbNav = () => { path: item.path, type: item.type, query: {}, // Menu items don't have query params by default - }); + }) } // 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 (cleaned) if (item.path === currentPath) { - const lastItem = currentBreadcrumb[currentBreadcrumb.length - 1]; + const lastItem = currentBreadcrumb[currentBreadcrumb.length - 1] if (lastItem) { - lastItem.query = getCleanQueryParams(router.query); + lastItem.query = getCleanQueryParams(router.query) } } - return currentBreadcrumb; + return currentBreadcrumb } // Recursively search children if (item.items && item.items.length > 0) { - const result = findPathInMenu(item.items, currentBreadcrumb); + const result = findPathInMenu(item.items, currentBreadcrumb) if (result.length > 0) { - return result; + return result } } } - return []; - }; + return [] + } - let result = findPathInMenu(nativeMenuItems); + 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(/\/$/, ""); + 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; - }); + const normalizedTabPath = tab.path.replace(/\/$/, '') + return normalizedTabPath === normalizedCurrentPath + }) if (matchingTab) { // Tab matches the current path - use tab's label instead of config's @@ -396,32 +396,32 @@ export const CippBreadcrumbNav = () => { return { ...item, title: matchingTab.title, - type: "tab", - }; + type: 'tab', + } } - return item; - }); + 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(/\/$/, ""); + const normalizedCurrentPath = currentPath.replace(/\/$/, '') // Find matching tab option const matchingTab = tabOptions.find((tab) => { - const normalizedTabPath = tab.path.replace(/\/$/, ""); - return normalizedTabPath === normalizedCurrentPath; - }); + const normalizedTabPath = tab.path.replace(/\/$/, '') + return normalizedTabPath === normalizedCurrentPath + }) if (matchingTab) { // Find the base page in the menu and build full path to it - const normalizedBasePath = matchingTab.basePath?.replace(/\/$/, ""); + const normalizedBasePath = matchingTab.basePath?.replace(/\/$/, '') // Recursively find the base page and build breadcrumb path const findBasePageWithPath = (items, path = []) => { for (const item of items) { - const currentBreadcrumb = [...path]; + const currentBreadcrumb = [...path] // Add current item to path if it has a title if (item.title) { @@ -430,185 +430,188 @@ export const CippBreadcrumbNav = () => { path: item.path, type: item.type, query: {}, // Menu items don't have query params by default - }); + }) } // Check if this item matches the base path if (item.path) { - const normalizedItemPath = item.path.replace(/\/$/, ""); + const normalizedItemPath = item.path.replace(/\/$/, '') if ( normalizedItemPath === normalizedBasePath || normalizedItemPath.startsWith(normalizedBasePath) ) { - return currentBreadcrumb; + return currentBreadcrumb } } // Recursively search children if (item.items && item.items.length > 0) { - const found = findBasePageWithPath(item.items, currentBreadcrumb); + const found = findBasePageWithPath(item.items, currentBreadcrumb) if (found.length > 0) { - return found; + return found } } } - return []; - }; + return [] + } - const basePagePath = findBasePageWithPath(nativeMenuItems); + const basePagePath = findBasePageWithPath(nativeMenuItems) if (basePagePath.length > 0) { - result = basePagePath; + result = basePagePath // Add the tab as the final breadcrumb with current query params (cleaned) result.push({ title: matchingTab.title, path: matchingTab.path, - type: "tab", + type: 'tab', query: getCleanQueryParams(router.query), // Include current query params for tab page - }); + }) } } } // Check if we're on a nested page under a menu item (e.g., edit page) if (result.length > 0) { - const lastItem = result[result.length - 1]; + 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 - let tabTitle = currentPageTitle || document.title.replace(" - CIPP", "").trim(); + let tabTitle = currentPageTitle || document.title.replace(' - CIPP', '').trim() // Clean AllTenants from title - tabTitle = cleanPageTitle(tabTitle); + tabTitle = cleanPageTitle(tabTitle) // Add tab as an additional breadcrumb item if ( tabTitle && tabTitle !== lastItem.title && - !tabTitle.toLowerCase().includes("loading") + !tabTitle.toLowerCase().includes('loading') ) { result.push({ title: tabTitle, path: currentPath, - type: "tab", + type: 'tab', query: getCleanQueryParams(router.query), // Include current query params (cleaned) - }); + }) } } } - return result; - }; + 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 }; + if (!path) return { isValid: false, title: null } - const normalizedPath = path.replace(/\/$/, ""); + 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(/\/$/, ""); + const normalizedItemPath = item.path.replace(/\/$/, '') if (normalizedItemPath === normalizedPath) { - return { isValid: true, title: item.title }; + return { isValid: true, title: item.title } } } if (item.items && item.items.length > 0) { - const found = findInMenu(item.items); + const found = findInMenu(item.items) if (found.isValid) { - return found; + return found } } } - return { isValid: false, title: null }; - }; + return { isValid: false, title: null } + } // Check if path exists in navigation - const menuResult = findInMenu(nativeMenuItems); + const menuResult = findInMenu(nativeMenuItems) if (menuResult.isValid) { - return menuResult; + return menuResult } // Check if path exists in tab options - const matchingTab = tabOptions.find((tab) => tab.path.replace(/\/$/, "") === normalizedPath); + const matchingTab = tabOptions.find((tab) => tab.path.replace(/\/$/, '') === normalizedPath) if (matchingTab) { - return { isValid: true, title: matchingTab.title }; + return { isValid: true, title: matchingTab.title } } - return { isValid: false, title: null }; - }; + return { isValid: false, title: null } + } // Handle click for hierarchical breadcrumbs const handleHierarchicalClick = (path, query) => { if (path) { - const cleanedQuery = getCleanQueryParams(query); + const cleanedQuery = getCleanQueryParams(query) if (cleanedQuery && Object.keys(cleanedQuery).length > 0) { router.push({ pathname: path, query: cleanedQuery, - }); + }) } else { - router.push(path); + router.push(path) } } - }; + } // Toggle between modes const toggleMode = () => { setMode((prevMode) => { - const newMode = prevMode === "hierarchical" ? "history" : "hierarchical"; - settings.handleUpdate({ breadcrumbMode: newMode }); - return newMode; - }); - }; + const newMode = prevMode === 'hierarchical' ? 'history' : 'hierarchical' + settings.handleUpdate({ breadcrumbMode: newMode }) + return newMode + }) + } // Render based on mode - if (mode === "hierarchical") { - let breadcrumbs = buildHierarchicalBreadcrumbs(); + if (mode === 'hierarchical') { + let breadcrumbs = buildHierarchicalBreadcrumbs() // Fallback: If no breadcrumbs found in navigation config, generate from URL path if (breadcrumbs.length === 0) { - const pathSegments = router.pathname.split("/").filter((segment) => segment); + const pathSegments = router.pathname.split('/').filter((segment) => segment) if (pathSegments.length > 0) { breadcrumbs = pathSegments.map((segment, index) => { // Build the path up to this segment - const path = "/" + pathSegments.slice(0, index + 1).join("/"); + const path = '/' + pathSegments.slice(0, index + 1).join('/') // Format segment as title (replace hyphens with spaces, capitalize words) const title = segment - .split("-") + .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); + .join(' ') return { title, path, - type: "fallback", + type: 'fallback', query: index === pathSegments.length - 1 ? getCleanQueryParams(router.query) : {}, - }; - }); + } + }) // If we have a current page title from document.title, use it for the last breadcrumb if ( currentPageTitle && - currentPageTitle !== "CIPP" && - !currentPageTitle.toLowerCase().includes("loading") + currentPageTitle !== 'CIPP' && + !currentPageTitle.toLowerCase().includes('loading') ) { - breadcrumbs[breadcrumbs.length - 1].title = cleanPageTitle(currentPageTitle); + breadcrumbs[breadcrumbs.length - 1].title = cleanPageTitle(currentPageTitle) } } } // Don't show if still no breadcrumbs found if (breadcrumbs.length === 0) { - return null; + return null } return ( - + @@ -617,13 +620,13 @@ export const CippBreadcrumbNav = () => { } aria-label="page hierarchy" - sx={{ fontSize: "0.875rem", flexGrow: 1 }} + sx={{ fontSize: '0.875rem', flexGrow: 1 }} > {breadcrumbs.map((crumb, index) => { - const isLast = index === breadcrumbs.length - 1; - const pathInfo = getPathInfo(crumb.path); + 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; + const displayTitle = pathInfo.title || crumb.title // Items without paths (headers/groups) - show as text if (!crumb.path) { @@ -636,7 +639,7 @@ export const CippBreadcrumbNav = () => { > {displayTitle}
- ); + ) } // Items with valid paths are clickable @@ -649,48 +652,51 @@ export const CippBreadcrumbNav = () => { variant="subtitle2" onClick={() => handleHierarchicalClick(crumb.path, crumb.query)} sx={{ - textDecoration: "none", - color: isLast ? "text.primary" : "text.secondary", + textDecoration: 'none', + color: isLast ? 'text.primary' : 'text.secondary', fontWeight: isLast ? 500 : 400, - "&:hover": { - textDecoration: "underline", - color: "primary.main", + '&:hover': { + textDecoration: 'underline', + color: 'primary.main', }, }} > {displayTitle} - ); + ) } else { // Invalid path - show as text only return ( {displayTitle} - ); + ) } })} - ); + ) } // Default mode: history-based breadcrumbs // Don't show breadcrumbs if we have no history if (history.length === 0) { - return null; + return null } // Show only the last MAX_BREADCRUMB_DISPLAY items - const visibleHistory = history.slice(-MAX_BREADCRUMB_DISPLAY); + const visibleHistory = history.slice(-MAX_BREADCRUMB_DISPLAY) return ( - + @@ -700,12 +706,12 @@ export const CippBreadcrumbNav = () => { maxItems={MAX_BREADCRUMB_DISPLAY} separator={} aria-label="navigation history" - sx={{ fontSize: "0.875rem", flexGrow: 1 }} + sx={{ fontSize: '0.875rem', flexGrow: 1 }} > {visibleHistory.map((page, index) => { - const isLast = index === visibleHistory.length - 1; + const isLast = index === visibleHistory.length - 1 // Calculate the actual index in the full history - const actualIndex = history.length - visibleHistory.length + index; + const actualIndex = history.length - visibleHistory.length + index if (isLast) { return ( @@ -717,7 +723,7 @@ export const CippBreadcrumbNav = () => { > {page.title} - ); + ) } return ( @@ -727,19 +733,19 @@ export const CippBreadcrumbNav = () => { variant="subtitle2" onClick={() => handleBreadcrumbClick(actualIndex)} sx={{ - textDecoration: "none", - color: "text.secondary", - "&:hover": { - textDecoration: "underline", - color: "primary.main", + textDecoration: 'none', + color: 'text.secondary', + '&:hover': { + textDecoration: 'underline', + color: 'primary.main', }, }} > {page.title} - ); + ) })} - ); -}; + ) +} diff --git a/src/components/CippComponents/CippSpeedDial.jsx b/src/components/CippComponents/CippSpeedDial.jsx index 15a1b4547d9b..0a2365bc4707 100644 --- a/src/components/CippComponents/CippSpeedDial.jsx +++ b/src/components/CippComponents/CippSpeedDial.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect } from 'react' import { SpeedDial, SpeedDialAction, @@ -11,10 +11,10 @@ import { Snackbar, Alert, CircularProgress, -} from "@mui/material"; -import { Close as CloseIcon } from "@mui/icons-material"; -import { useForm } from "react-hook-form"; -import { CippFormComponent } from "../../components/CippComponents/CippFormComponent"; +} from '@mui/material' +import { Close as CloseIcon } from '@mui/icons-material' +import { useForm } from 'react-hook-form' +import { CippFormComponent } from '../../components/CippComponents/CippFormComponent' const CippSpeedDial = ({ actions = [], @@ -22,92 +22,92 @@ const CippSpeedDial = ({ icon, openIcon = , }) => { - const [openDialogs, setOpenDialogs] = useState({}); - const [loading, setLoading] = useState(false); - const [showSnackbar, setShowSnackbar] = useState(false); - const [speedDialOpen, setSpeedDialOpen] = useState(false); - const [isHovering, setIsHovering] = useState(false); - const [snackbarMessage, setSnackbarMessage] = useState(""); + const [openDialogs, setOpenDialogs] = useState({}) + const [loading, setLoading] = useState(false) + const [showSnackbar, setShowSnackbar] = useState(false) + const [speedDialOpen, setSpeedDialOpen] = useState(false) + const [isHovering, setIsHovering] = useState(false) + const [snackbarMessage, setSnackbarMessage] = useState('') const formControls = actions.reduce((acc, action) => { if (action.form) { acc[action.id] = useForm({ - mode: "onChange", + mode: 'onChange', defaultValues: action.form.defaultValues || {}, - }); + }) } - return acc; - }, {}); + return acc + }, {}) const handleSpeedDialClose = (event, reason) => { - if (reason === "toggle") { - setSpeedDialOpen(false); - setIsHovering(false); - return; + if (reason === 'toggle') { + setSpeedDialOpen(false) + setIsHovering(false) + return } if (!isHovering) { setTimeout(() => { - setSpeedDialOpen(false); - }, 200); + setSpeedDialOpen(false) + }, 200) } - }; + } const handleMouseEnter = () => { - setIsHovering(true); - setSpeedDialOpen(true); - }; + setIsHovering(true) + setSpeedDialOpen(true) + } const handleMouseLeave = () => { - setIsHovering(false); - handleSpeedDialClose(); - }; + setIsHovering(false) + handleSpeedDialClose() + } const handleDialogOpen = (actionId) => { - setOpenDialogs((prev) => ({ ...prev, [actionId]: true })); - }; + setOpenDialogs((prev) => ({ ...prev, [actionId]: true })) + } const handleDialogClose = (actionId) => { - setOpenDialogs((prev) => ({ ...prev, [actionId]: false })); - }; + setOpenDialogs((prev) => ({ ...prev, [actionId]: false })) + } const handleSubmit = async (actionId, data) => { - if (!actions.find((a) => a.id === actionId)?.onSubmit) return; + if (!actions.find((a) => a.id === actionId)?.onSubmit) return - setLoading(true); + setLoading(true) try { - const action = actions.find((a) => a.id === actionId); - const result = await action.onSubmit(data); + const action = actions.find((a) => a.id === actionId) + const result = await action.onSubmit(data) if (result.success) { - formControls[actionId]?.reset(); - handleDialogClose(actionId); + formControls[actionId]?.reset() + handleDialogClose(actionId) } - setSnackbarMessage(result.message); - setShowSnackbar(true); + setSnackbarMessage(result.message) + setShowSnackbar(true) } catch (error) { - console.error(`Error submitting ${actionId}:`, error); - setSnackbarMessage("An error occurred while submitting"); - setShowSnackbar(true); + console.error(`Error submitting ${actionId}:`, error) + setSnackbarMessage('An error occurred while submitting') + setShowSnackbar(true) } finally { - setLoading(false); + setLoading(false) } - }; + } useEffect(() => { const handleClickOutside = (event) => { if (speedDialOpen) { - const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]'); + const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]') if (speedDial && !speedDial.contains(event.target)) { - setSpeedDialOpen(false); + setSpeedDialOpen(false) } } - }; + } - document.addEventListener("click", handleClickOutside); + document.addEventListener('click', handleClickOutside) return () => { - document.removeEventListener("click", handleClickOutside); - }; - }, [speedDialOpen]); + document.removeEventListener('click', handleClickOutside) + } + }, [speedDialOpen]) return ( <> @@ -115,13 +115,13 @@ const CippSpeedDial = ({ ariaLabel="Navigation SpeedDial" data-tutorial="speed-dial" sx={{ - position: "fixed", + position: 'fixed', ...position, - "& .MuiFab-primary": { + '& .MuiFab-primary': { width: 46, height: 46, - "&:hover": { - backgroundColor: "primary.dark", + '&:hover': { + backgroundColor: 'primary.dark', }, }, }} @@ -139,27 +139,27 @@ const CippSpeedDial = ({ tooltipTitle={action.name} onClick={() => { if (action.form) { - handleDialogOpen(action.id); + handleDialogOpen(action.id) } else if (action.onClick) { - action.onClick(); + action.onClick() } - setSpeedDialOpen(false); + setSpeedDialOpen(false) }} tooltipOpen sx={{ - "&.MuiSpeedDialAction-fab": { - backgroundColor: "background.paper", - "&:hover": { - backgroundColor: "action.hover", + '&.MuiSpeedDialAction-fab': { + backgroundColor: 'background.paper', + '&:hover': { + backgroundColor: 'action.hover', }, }, - "& .MuiSpeedDialAction-staticTooltipLabel": { - cursor: "pointer", - whiteSpace: "nowrap", - marginRight: "10px", - padding: "6px 10px", - "&:hover": { - backgroundColor: "action.hover", + '& .MuiSpeedDialAction-staticTooltipLabel': { + cursor: 'pointer', + whiteSpace: 'nowrap', + marginRight: '10px', + padding: '6px 10px', + '&:hover': { + backgroundColor: 'action.hover', }, }, }} @@ -184,10 +184,10 @@ const CippSpeedDial = ({ name={action.form.fieldName} required formControl={formControls[action.id]} - style={{ minHeight: "150px" }} + style={{ minHeight: '150px' }} editorProps={{ attributes: { - style: "min-height: 150px; font-size: 1.1rem; padding: 1rem;", + style: 'min-height: 150px; font-size: 1.1rem; padding: 1rem;', }, }} /> @@ -205,7 +205,7 @@ const CippSpeedDial = ({ disabled={loading} startIcon={loading ? : null} > - {loading ? "Submitting..." : action.form.submitText || "Submit"} + {loading ? 'Submitting...' : action.form.submitText || 'Submit'} @@ -215,14 +215,14 @@ const CippSpeedDial = ({ open={showSnackbar} autoHideDuration={6000} onClose={() => setShowSnackbar(false)} - anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > - setShowSnackbar(false)} severity="success" sx={{ width: "100%" }}> + setShowSnackbar(false)} severity="success" sx={{ width: '100%' }}> {snackbarMessage} - ); -}; + ) +} -export default CippSpeedDial; +export default CippSpeedDial diff --git a/src/components/CippComponents/CippTutorialDialog.jsx b/src/components/CippComponents/CippTutorialDialog.jsx index f52c9234d6ca..016c8ba01e04 100644 --- a/src/components/CippComponents/CippTutorialDialog.jsx +++ b/src/components/CippComponents/CippTutorialDialog.jsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo } from 'react' import { Dialog, DialogTitle, @@ -17,54 +17,52 @@ import { Divider, IconButton, Tooltip, -} from "@mui/material"; +} from '@mui/material' import { PlayArrow as PlayIcon, CheckCircle as CompletedIcon, School as TutorialIcon, Search as SearchIcon, Replay as ResetIcon, -} from "@mui/icons-material"; -import { useTutorials } from "../../contexts/tutorial-context"; -import { useRouter } from "next/router"; +} from '@mui/icons-material' +import { useTutorials } from '../../contexts/tutorial-context' +import { useRouter } from 'next/router' const CippTutorialDialog = ({ open, onClose }) => { - const { tutorials, completedIds, startTutorial, resetProgress } = useTutorials(); - const [search, setSearch] = useState(""); - const router = useRouter(); + const { tutorials, completedIds, startTutorial, resetProgress } = useTutorials() + const [search, setSearch] = useState('') + const router = useRouter() const grouped = useMemo(() => { const filtered = tutorials.filter((t) => { - const q = search.toLowerCase(); + const q = search.toLowerCase() return ( t.title.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q) || t.category?.toLowerCase().includes(q) - ); - }); + ) + }) return filtered.reduce((acc, tutorial) => { - const cat = tutorial.category || "General"; - if (!acc[cat]) acc[cat] = []; - acc[cat].push(tutorial); - return acc; - }, {}); - }, [tutorials, search]); + const cat = tutorial.category || 'General' + if (!acc[cat]) acc[cat] = [] + acc[cat].push(tutorial) + return acc + }, {}) + }, [tutorials, search]) const handleStart = (tutorial) => { - onClose(); + onClose() // Small delay to let dialog close animation finish - setTimeout(() => startTutorial(tutorial), 300); - }; + setTimeout(() => startTutorial(tutorial), 300) + } - const categoryKeys = Object.keys(grouped).sort(); + const categoryKeys = Object.keys(grouped).sort() return ( - - + + Tutorials @@ -94,7 +92,7 @@ const CippTutorialDialog = ({ open, onClose }) => { /> {categoryKeys.length === 0 && ( - + No tutorials found. )} @@ -106,9 +104,8 @@ const CippTutorialDialog = ({ open, onClose }) => { {grouped[category].map((tutorial) => { - const isCompleted = completedIds.includes(tutorial.id); - const isOnPage = - !tutorial.pages?.length || tutorial.pages.includes(router.pathname); + const isCompleted = completedIds.includes(tutorial.id) + const isOnPage = !tutorial.pages?.length || tutorial.pages.includes(router.pathname) return ( { primary={tutorial.title} secondary={tutorial.description} slotProps={{ - primary: { variant: "body2", fontWeight: 500 }, - secondary: { variant: "caption" }, + primary: { variant: 'body2', fontWeight: 500 }, + secondary: { variant: 'caption' }, }} /> - - {isCompleted && } - {!isOnPage && ( - + + {isCompleted && ( + )} + {!isOnPage && } { /> - ); + ) })} @@ -151,13 +148,13 @@ const CippTutorialDialog = ({ open, onClose }) => { ))} - + {completedIds.length} of {tutorials.length} completed - ); -}; + ) +} -export default CippTutorialDialog; +export default CippTutorialDialog diff --git a/src/contexts/tutorial-context.js b/src/contexts/tutorial-context.js index 954b641d8b0b..749f645aa15f 100644 --- a/src/contexts/tutorial-context.js +++ b/src/contexts/tutorial-context.js @@ -1,37 +1,37 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { driver } from "driver.js"; -import { useRouter } from "next/router"; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { driver } from 'driver.js' +import { useRouter } from 'next/router' -const STORAGE_KEY = "cipp.tutorials.completed"; +const STORAGE_KEY = 'cipp.tutorials.completed' const getCompletedTutorials = () => { try { - const stored = localStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : []; + const stored = localStorage.getItem(STORAGE_KEY) + return stored ? JSON.parse(stored) : [] } catch { - return []; + return [] } -}; +} const storeCompletedTutorial = (id) => { try { - const completed = getCompletedTutorials(); + const completed = getCompletedTutorials() if (!completed.includes(id)) { - completed.push(id); - localStorage.setItem(STORAGE_KEY, JSON.stringify(completed)); + completed.push(id) + localStorage.setItem(STORAGE_KEY, JSON.stringify(completed)) } } catch { // ignore } -}; +} const resetCompletedTutorials = () => { try { - localStorage.removeItem(STORAGE_KEY); + localStorage.removeItem(STORAGE_KEY) } catch { // ignore } -}; +} const TutorialContext = createContext({ tutorials: [], @@ -40,116 +40,114 @@ const TutorialContext = createContext({ startTutorial: () => {}, resetProgress: () => {}, getTutorialsForPage: () => [], -}); +}) // Load all tutorial JSON files from the data/tutorials folder at build time const loadTutorials = () => { - const context = require.context("../data/tutorials", false, /\.json$/); + const context = require.context('../data/tutorials', false, /\.json$/) return context.keys().map((key) => { - const tutorial = context(key); - return tutorial.default || tutorial; - }); -}; + const tutorial = context(key) + return tutorial.default || tutorial + }) +} export const TutorialProvider = ({ children }) => { - const [tutorials] = useState(() => loadTutorials()); - const [completedIds, setCompletedIds] = useState([]); - const [activeTutorial, setActiveTutorial] = useState(null); - const driverRef = useRef(null); - const router = useRouter(); + const [tutorials] = useState(() => loadTutorials()) + const [completedIds, setCompletedIds] = useState([]) + const [activeTutorial, setActiveTutorial] = useState(null) + const driverRef = useRef(null) + const router = useRouter() useEffect(() => { - setCompletedIds(getCompletedTutorials()); - }, []); + setCompletedIds(getCompletedTutorials()) + }, []) // Launch tutorial from ?tutorial=$id query param useEffect(() => { - if (!router.isReady || activeTutorial) return; - const tutorialId = router.query.tutorial; - if (!tutorialId) return; + if (!router.isReady || activeTutorial) return + const tutorialId = router.query.tutorial + if (!tutorialId) return - const match = tutorials.find((t) => t.id === tutorialId); - if (!match) return; + const match = tutorials.find((t) => t.id === tutorialId) + if (!match) return // Strip the query param so it doesn't re-trigger - const { tutorial: _, ...rest } = router.query; - router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }); + const { tutorial: _, ...rest } = router.query + router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }) // Delay to let the page fully render - setTimeout(() => runDriver(match), 600); - }, [router.isReady, router.query.tutorial, tutorials]); + setTimeout(() => runDriver(match), 600) + }, [router.isReady, router.query.tutorial, tutorials]) // Cleanup driver on unmount or route change useEffect(() => { return () => { if (driverRef.current) { - driverRef.current.destroy(); - driverRef.current = null; + driverRef.current.destroy() + driverRef.current = null } - }; - }, []); + } + }, []) const startTutorial = useCallback( (tutorial) => { if (driverRef.current) { - driverRef.current.destroy(); + driverRef.current.destroy() } // If tutorial specifies pages and we're not on any of them, navigate first if (tutorial.pages?.length && !tutorial.pages.includes(router.pathname)) { router.push(tutorial.pages[0]).then(() => { // Small delay to let the page render before starting the tour - setTimeout(() => runDriver(tutorial), 500); - }); - return; + setTimeout(() => runDriver(tutorial), 500) + }) + return } - runDriver(tutorial); + runDriver(tutorial) }, [router] - ); + ) const runDriver = useCallback((tutorial) => { - setActiveTutorial(tutorial); + setActiveTutorial(tutorial) const driverObj = driver({ showProgress: true, animate: true, allowClose: true, - overlayColor: "rgba(0, 0, 0, 0.6)", + overlayColor: 'rgba(0, 0, 0, 0.6)', stagePadding: 8, stageRadius: 8, - popoverClass: "cipp-tutorial-popover", - nextBtnText: "Next →", - prevBtnText: "← Back", - doneBtnText: "Done ✓", - progressText: "{{current}} of {{total}}", + popoverClass: 'cipp-tutorial-popover', + nextBtnText: 'Next →', + prevBtnText: '← Back', + doneBtnText: 'Done ✓', + progressText: '{{current}} of {{total}}', steps: tutorial.steps, onDestroyed: () => { - storeCompletedTutorial(tutorial.id); - setCompletedIds(getCompletedTutorials()); - setActiveTutorial(null); - driverRef.current = null; + storeCompletedTutorial(tutorial.id) + setCompletedIds(getCompletedTutorials()) + setActiveTutorial(null) + driverRef.current = null }, - }); + }) - driverRef.current = driverObj; - driverObj.drive(); - }, []); + driverRef.current = driverObj + driverObj.drive() + }, []) const resetProgress = useCallback(() => { - resetCompletedTutorials(); - setCompletedIds([]); - }, []); + resetCompletedTutorials() + setCompletedIds([]) + }, []) const getTutorialsForPage = useCallback( (pathname) => { - return tutorials.filter( - (t) => !t.pages || t.pages.length === 0 || t.pages.includes(pathname) - ); + return tutorials.filter((t) => !t.pages || t.pages.length === 0 || t.pages.includes(pathname)) }, [tutorials] - ); + ) const value = useMemo( () => ({ @@ -161,9 +159,9 @@ export const TutorialProvider = ({ children }) => { getTutorialsForPage, }), [tutorials, activeTutorial, completedIds, startTutorial, resetProgress, getTutorialsForPage] - ); + ) - return {children}; -}; + return {children} +} -export const useTutorials = () => useContext(TutorialContext); +export const useTutorials = () => useContext(TutorialContext) diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index 2c7e0a2507ad..baa5d1ad9f9f 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -279,7 +279,11 @@ export const TopNav = (props) => { {!mdDown && ( - + )} {mdDown && ( diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index dfd9d9a76528..48e3bd3b3b62 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -194,7 +194,10 @@ const Page = () => { - + Date: Mon, 25 May 2026 20:37:52 +0200 Subject: [PATCH 41/61] demo data --- src/contexts/tutorial-context.js | 10 +++ src/data/dashboardv2-demo-data.js | 138 +++++++++++++++--------------- 2 files changed, 79 insertions(+), 69 deletions(-) diff --git a/src/contexts/tutorial-context.js b/src/contexts/tutorial-context.js index 749f645aa15f..b32ab50c8fa8 100644 --- a/src/contexts/tutorial-context.js +++ b/src/contexts/tutorial-context.js @@ -73,6 +73,16 @@ export const TutorialProvider = ({ children }) => { // Strip the query param so it doesn't re-trigger const { tutorial: _, ...rest } = router.query + + // If the tutorial has a target page and we're not on it, navigate there first + const targetPage = match.pages?.[0] + if (targetPage && router.pathname !== targetPage) { + router.replace({ pathname: targetPage, query: rest }, undefined).then(() => { + setTimeout(() => runDriver(match), 600) + }) + return + } + router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }) // Delay to let the page fully render diff --git a/src/data/dashboardv2-demo-data.js b/src/data/dashboardv2-demo-data.js index e5e23eee579a..4f0db4c12aad 100644 --- a/src/data/dashboardv2-demo-data.js +++ b/src/data/dashboardv2-demo-data.js @@ -1,8 +1,8 @@ // Demo data structure matching Zero Trust Assessment export const dashboardDemoData = { - ExecutedAt: "2025-12-16T10:00:00Z", - TenantName: "Demo Tenant", - Domain: "demo.contoso.com", + ExecutedAt: '2025-12-16T10:00:00Z', + TenantName: 'Demo Tenant', + Domain: 'demo.contoso.com', TestResultSummary: { IdentityPassed: 85, IdentityTotal: 100, @@ -13,100 +13,100 @@ export const dashboardDemoData = { }, TenantInfo: { TenantOverview: { - UserCount: 1250, - GuestCount: 85, - GroupCount: 340, - ApplicationCount: 156, - DeviceCount: 765, - ManagedDeviceCount: 733, + UserCount: 0, + GuestCount: 0, + GroupCount: 0, + ApplicationCount: 0, + DeviceCount: 0, + ManagedDeviceCount: 0, }, OverviewCaMfaAllUsers: { description: - "Over the past 30 days, 68.5% of sign-ins were protected by conditional access policies enforcing multifactor authentication.", + 'Over the past 30 days, 68.5% of sign-ins were protected by conditional access policies enforcing multifactor authentication.', nodes: [ - { source: "User sign in", target: "No CA applied", value: 394 }, - { source: "User sign in", target: "CA applied", value: 856 }, - { source: "CA applied", target: "No MFA", value: 146 }, - { source: "CA applied", target: "MFA", value: 710 }, + { source: 'User sign in', target: 'No CA applied', value: 394 }, + { source: 'User sign in', target: 'CA applied', value: 856 }, + { source: 'CA applied', target: 'No MFA', value: 146 }, + { source: 'CA applied', target: 'MFA', value: 710 }, ], }, OverviewCaDevicesAllUsers: { - description: "Over the past 30 days, 71.2% of sign-ins were from compliant devices.", + description: 'Over the past 30 days, 71.2% of sign-ins were from compliant devices.', nodes: [ - { source: "User sign in", target: "Unmanaged", value: 500 }, - { source: "User sign in", target: "Managed", value: 1150 }, - { source: "Managed", target: "Non-compliant", value: 260 }, - { source: "Managed", target: "Compliant", value: 890 }, + { source: 'User sign in', target: 'Unmanaged', value: 500 }, + { source: 'User sign in', target: 'Managed', value: 1150 }, + { source: 'Managed', target: 'Non-compliant', value: 260 }, + { source: 'Managed', target: 'Compliant', value: 890 }, ], }, OverviewAuthMethodsPrivilegedUsers: { - description: "Authentication methods used by privileged users over the past 30 days.", + description: 'Authentication methods used by privileged users over the past 30 days.', nodes: [ - { source: "Users", target: "Single factor", value: 5 }, - { source: "Users", target: "Phishable", value: 28 }, - { source: "Users", target: "Phish resistant", value: 15 }, - { source: "Phishable", target: "Phone", value: 8 }, - { source: "Phishable", target: "Authenticator", value: 20 }, - { source: "Phish resistant", target: "Passkey", value: 12 }, - { source: "Phish resistant", target: "WHfB", value: 3 }, + { source: 'Users', target: 'Single factor', value: 5 }, + { source: 'Users', target: 'Phishable', value: 28 }, + { source: 'Users', target: 'Phish resistant', value: 15 }, + { source: 'Phishable', target: 'Phone', value: 8 }, + { source: 'Phishable', target: 'Authenticator', value: 20 }, + { source: 'Phish resistant', target: 'Passkey', value: 12 }, + { source: 'Phish resistant', target: 'WHfB', value: 3 }, ], }, OverviewAuthMethodsAllUsers: { - description: "Authentication methods used by all users over the past 30 days.", + description: 'Authentication methods used by all users over the past 30 days.', nodes: [ - { source: "Users", target: "Single factor", value: 120 }, - { source: "Users", target: "Phishable", value: 580 }, - { source: "Users", target: "Phish resistant", value: 550 }, - { source: "Phishable", target: "Phone", value: 180 }, - { source: "Phishable", target: "Authenticator", value: 400 }, - { source: "Phish resistant", target: "Passkey", value: 450 }, - { source: "Phish resistant", target: "WHfB", value: 100 }, + { source: 'Users', target: 'Single factor', value: 120 }, + { source: 'Users', target: 'Phishable', value: 580 }, + { source: 'Users', target: 'Phish resistant', value: 550 }, + { source: 'Phishable', target: 'Phone', value: 180 }, + { source: 'Phishable', target: 'Authenticator', value: 400 }, + { source: 'Phish resistant', target: 'Passkey', value: 450 }, + { source: 'Phish resistant', target: 'WHfB', value: 100 }, ], }, DeviceOverview: { DesktopDevicesSummary: { - description: "Desktop devices (Windows and macOS) by join type and compliance status.", + description: 'Desktop devices (Windows and macOS) by join type and compliance status.', nodes: [ // Level 1: Desktop devices to OS - { source: "Desktop devices", target: "Windows", value: 585 }, - { source: "Desktop devices", target: "macOS", value: 75 }, + { source: 'Desktop devices', target: 'Windows', value: 585 }, + { source: 'Desktop devices', target: 'macOS', value: 75 }, // Level 2: Windows to join types - { source: "Windows", target: "Entra joined", value: 285 }, - { source: "Windows", target: "Entra registered", value: 100 }, - { source: "Windows", target: "Entra hybrid joined", value: 200 }, + { source: 'Windows', target: 'Entra joined', value: 285 }, + { source: 'Windows', target: 'Entra registered', value: 100 }, + { source: 'Windows', target: 'Entra hybrid joined', value: 200 }, // Level 3: Windows join types to compliance - { source: "Entra joined", target: "Compliant", value: 171 }, - { source: "Entra joined", target: "Non-compliant", value: 42 }, - { source: "Entra joined", target: "Unmanaged", value: 72 }, - { source: "Entra hybrid joined", target: "Compliant", value: 50 }, - { source: "Entra hybrid joined", target: "Non-compliant", value: 23 }, - { source: "Entra hybrid joined", target: "Unmanaged", value: 127 }, - { source: "Entra registered", target: "Compliant", value: 60 }, - { source: "Entra registered", target: "Non-compliant", value: 40 }, - { source: "Entra registered", target: "Unmanaged", value: 0 }, + { source: 'Entra joined', target: 'Compliant', value: 171 }, + { source: 'Entra joined', target: 'Non-compliant', value: 42 }, + { source: 'Entra joined', target: 'Unmanaged', value: 72 }, + { source: 'Entra hybrid joined', target: 'Compliant', value: 50 }, + { source: 'Entra hybrid joined', target: 'Non-compliant', value: 23 }, + { source: 'Entra hybrid joined', target: 'Unmanaged', value: 127 }, + { source: 'Entra registered', target: 'Compliant', value: 60 }, + { source: 'Entra registered', target: 'Non-compliant', value: 40 }, + { source: 'Entra registered', target: 'Unmanaged', value: 0 }, // Level 2: macOS directly to compliance - { source: "macOS", target: "Compliant", value: 56 }, - { source: "macOS", target: "Non-compliant", value: 15 }, - { source: "macOS", target: "Unmanaged", value: 4 }, + { source: 'macOS', target: 'Compliant', value: 56 }, + { source: 'macOS', target: 'Non-compliant', value: 15 }, + { source: 'macOS', target: 'Unmanaged', value: 4 }, ], }, MobileSummary: { - description: "Mobile devices by compliance status.", + description: 'Mobile devices by compliance status.', nodes: [ - { source: "Mobile devices", target: "Android", value: 105 }, - { source: "Mobile devices", target: "iOS", value: 75 }, - { source: "Android", target: "Android (Company)", value: 72 }, - { source: "Android", target: "Android (Personal)", value: 33 }, - { source: "iOS", target: "iOS (Company)", value: 58 }, - { source: "iOS", target: "iOS (Personal)", value: 17 }, - { source: "Android (Company)", target: "Compliant", value: 60 }, - { source: "Android (Company)", target: "Non-compliant", value: 12 }, - { source: "Android (Personal)", target: "Compliant", value: 10 }, - { source: "Android (Personal)", target: "Non-compliant", value: 23 }, - { source: "iOS (Company)", target: "Compliant", value: 52 }, - { source: "iOS (Company)", target: "Non-compliant", value: 6 }, - { source: "iOS (Personal)", target: "Compliant", value: 11 }, - { source: "iOS (Personal)", target: "Non-compliant", value: 6 }, + { source: 'Mobile devices', target: 'Android', value: 105 }, + { source: 'Mobile devices', target: 'iOS', value: 75 }, + { source: 'Android', target: 'Android (Company)', value: 72 }, + { source: 'Android', target: 'Android (Personal)', value: 33 }, + { source: 'iOS', target: 'iOS (Company)', value: 58 }, + { source: 'iOS', target: 'iOS (Personal)', value: 17 }, + { source: 'Android (Company)', target: 'Compliant', value: 60 }, + { source: 'Android (Company)', target: 'Non-compliant', value: 12 }, + { source: 'Android (Personal)', target: 'Compliant', value: 10 }, + { source: 'Android (Personal)', target: 'Non-compliant', value: 23 }, + { source: 'iOS (Company)', target: 'Compliant', value: 52 }, + { source: 'iOS (Company)', target: 'Non-compliant', value: 6 }, + { source: 'iOS (Personal)', target: 'Compliant', value: 11 }, + { source: 'iOS (Personal)', target: 'Non-compliant', value: 6 }, ], }, ManagedDevices: { @@ -128,4 +128,4 @@ export const dashboardDemoData = { }, }, }, -}; +} From a2d8f1918f67ed864a57e50e55f8fd3f949774a9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 21:32:56 +0200 Subject: [PATCH 42/61] react-dom --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 842abc453ddd..331476b49fa7 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "react": "19.2.6", "react-apexcharts": "2.1.0", "react-beautiful-dnd": "13.1.1", - "react-dom": "19.2.5", + "react-dom": "19.2.6", "react-dropzone": "15.0.0", "react-error-boundary": "^6.1.1", "react-hook-form": "^7.72.0", From 7d1c2096f820f505684167d28bf38487534bd5ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 23:09:50 +0000 Subject: [PATCH 43/61] Move EnrollmentProfileTabs from pages to components and update imports Agent-Logs-Url: https://github.com/KelvinTegelaar/CIPP/sessions/bc416d42-34cd-460e-b5cb-57aec78b6c58 Co-authored-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- .../EnrollmentProfileTabs.jsx | 18 +++++++++--------- .../enrollment-profiles/android-enterprise.js | 2 +- .../MEM/enrollment-profiles/apple-ade.js | 2 +- .../endpoint/MEM/enrollment-profiles/index.js | 2 +- .../enrollment-profiles/windows-autopilot.js | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename src/{pages/endpoint/MEM/enrollment-profiles => components}/EnrollmentProfileTabs.jsx (95%) diff --git a/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx b/src/components/EnrollmentProfileTabs.jsx similarity index 95% rename from src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx rename to src/components/EnrollmentProfileTabs.jsx index 73fd35519bd0..882b73b04d9e 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx +++ b/src/components/EnrollmentProfileTabs.jsx @@ -19,15 +19,15 @@ import { QrCode2, Sync, } from '@mui/icons-material' -import { CippHead } from '../../../../components/CippComponents/CippHead.jsx' -import { CippDataTable } from '../../../../components/CippTable/CippDataTable.js' -import { CippInfoBar } from '../../../../components/CippCards/CippInfoBar.jsx' -import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog.jsx' -import { CippAutopilotProfileDrawer } from '../../../../components/CippComponents/CippAutopilotProfileDrawer.jsx' -import CippJsonView from '../../../../components/CippFormPages/CippJSONView.jsx' -import { ApiGetCall } from '../../../../api/ApiCall.jsx' -import { useDialog } from '../../../../hooks/use-dialog.js' -import { useSettings } from '../../../../hooks/use-settings.js' +import { CippHead } from './CippComponents/CippHead.jsx' +import { CippDataTable } from './CippTable/CippDataTable.js' +import { CippInfoBar } from './CippCards/CippInfoBar.jsx' +import { CippApiDialog } from './CippComponents/CippApiDialog.jsx' +import { CippAutopilotProfileDrawer } from './CippComponents/CippAutopilotProfileDrawer.jsx' +import CippJsonView from './CippFormPages/CippJSONView.jsx' +import { ApiGetCall } from '../api/ApiCall.jsx' +import { useDialog } from '../hooks/use-dialog.js' +import { useSettings } from '../hooks/use-settings.js' const pageTitle = 'Enrollment Profiles' const appleADEPageTitle = 'Apple Enrollment Profiles' diff --git a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js index 826afd75f085..58245f4a548a 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AndroidEnterpriseEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { AndroidEnterpriseEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js index e6fc7bfad50d..4f4aed94df6f 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AppleADEEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { AppleADEEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/index.js b/src/pages/endpoint/MEM/enrollment-profiles/index.js index 4c072ed7302d..ec561612656d 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/index.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js index 4c072ed7302d..ec561612656d 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => From 02c7a4341ca6d9ceb016ed45d10f0e188bce6a26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 23:13:28 +0000 Subject: [PATCH 44/61] Move EnrollmentProfileTabs to CippComponents folder and update imports Agent-Logs-Url: https://github.com/KelvinTegelaar/CIPP/sessions/08f68e6a-d4d1-481c-ba38-76d0ef9af438 Co-authored-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- .../EnrollmentProfileTabs.jsx | 18 +++++++++--------- .../enrollment-profiles/android-enterprise.js | 2 +- .../MEM/enrollment-profiles/apple-ade.js | 2 +- .../endpoint/MEM/enrollment-profiles/index.js | 2 +- .../enrollment-profiles/windows-autopilot.js | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename src/components/{ => CippComponents}/EnrollmentProfileTabs.jsx (96%) diff --git a/src/components/EnrollmentProfileTabs.jsx b/src/components/CippComponents/EnrollmentProfileTabs.jsx similarity index 96% rename from src/components/EnrollmentProfileTabs.jsx rename to src/components/CippComponents/EnrollmentProfileTabs.jsx index 882b73b04d9e..4499d1c94962 100644 --- a/src/components/EnrollmentProfileTabs.jsx +++ b/src/components/CippComponents/EnrollmentProfileTabs.jsx @@ -19,15 +19,15 @@ import { QrCode2, Sync, } from '@mui/icons-material' -import { CippHead } from './CippComponents/CippHead.jsx' -import { CippDataTable } from './CippTable/CippDataTable.js' -import { CippInfoBar } from './CippCards/CippInfoBar.jsx' -import { CippApiDialog } from './CippComponents/CippApiDialog.jsx' -import { CippAutopilotProfileDrawer } from './CippComponents/CippAutopilotProfileDrawer.jsx' -import CippJsonView from './CippFormPages/CippJSONView.jsx' -import { ApiGetCall } from '../api/ApiCall.jsx' -import { useDialog } from '../hooks/use-dialog.js' -import { useSettings } from '../hooks/use-settings.js' +import { CippHead } from './CippHead.jsx' +import { CippDataTable } from '../CippTable/CippDataTable.js' +import { CippInfoBar } from '../CippCards/CippInfoBar.jsx' +import { CippApiDialog } from './CippApiDialog.jsx' +import { CippAutopilotProfileDrawer } from './CippAutopilotProfileDrawer.jsx' +import CippJsonView from '../CippFormPages/CippJSONView.jsx' +import { ApiGetCall } from '../../api/ApiCall.jsx' +import { useDialog } from '../../hooks/use-dialog.js' +import { useSettings } from '../../hooks/use-settings.js' const pageTitle = 'Enrollment Profiles' const appleADEPageTitle = 'Apple Enrollment Profiles' diff --git a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js index 58245f4a548a..88f86700374a 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AndroidEnterpriseEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { AndroidEnterpriseEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js index 4f4aed94df6f..2225de59f0a8 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AppleADEEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { AppleADEEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/index.js b/src/pages/endpoint/MEM/enrollment-profiles/index.js index ec561612656d..a2cf307e80d2 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/index.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js index ec561612656d..a2cf307e80d2 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => From 1e59d2d43fab463a709d1dac2477f45cfacdfae6 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 11:54:57 +0800 Subject: [PATCH 45/61] Update ListTests.json --- Tests/Shapes/ListTests.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Shapes/ListTests.json b/Tests/Shapes/ListTests.json index a4553bd14c70..e79c73d8eb9f 100644 --- a/Tests/Shapes/ListTests.json +++ b/Tests/Shapes/ListTests.json @@ -119,6 +119,7 @@ "ExoSafeLinksRules": "number", "ExoSharingPolicy": "number", "ExoTenantAllowBlockList": "number", + "ExoTransportConfig": "number", "ExoTransportRules": "number", "Groups": "number", "Guests": "number", From 1cd1ef7223672170bdce1fffe88d8bb4ddb903d9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 12:30:37 +0800 Subject: [PATCH 46/61] Update AuditLogTemplates.json --- src/data/AuditLogTemplates.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/data/AuditLogTemplates.json b/src/data/AuditLogTemplates.json index 63df852bd318..68f87b52bdf6 100644 --- a/src/data/AuditLogTemplates.json +++ b/src/data/AuditLogTemplates.json @@ -439,18 +439,10 @@ "label": "updated user" } }, - { - "Property": { "value": "String", "label": "SecuredAccessPassData" }, - "Operator": { "value": "ne", "label": "Not Equals to" }, - "Input": { - "value": "[]", - "label": "[]" - } - }, { "Property": { "value": "String", "label": "SecuredAccessPassData" }, "Operator": { "value": "like", "label": "Like" }, - "Input": { "value": "[*]" } + "Input": { "value": "*PassId*" } } ] } From ca150a28cfcb3922d6de42cb4db54f2afcdf6e0e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 15:08:56 +0800 Subject: [PATCH 47/61] Better display standards that are missing licenses to be able to work --- src/pages/tenant/manage/applied-standards.js | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 376507ec4017..d69b3aebc81c 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -36,6 +36,7 @@ import { Construction, Schedule, Check, + Warning, } from '@mui/icons-material' import standards from '../../../data/standards.json' import { CippApiDialog } from '../../../components/CippComponents/CippApiDialog' @@ -248,6 +249,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -372,6 +374,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -504,6 +507,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -619,6 +623,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -727,6 +732,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: { displayName }, @@ -867,6 +873,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -1035,6 +1042,11 @@ const Page = () => { } } + // If the tenant is missing the required license, treat as compliant + if (standardObject?.LicenseAvailable === false) { + isCompliant = true + } + // Determine compliance status text based on reporting flag const complianceStatus = reportingDisabled ? 'Reporting Disabled' @@ -1061,6 +1073,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: standardSettings, @@ -1155,6 +1168,7 @@ const Page = () => { TemplateId: standardObject?.TemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, }, standardValue: { templateId: itemTemplateId, @@ -1969,6 +1983,15 @@ const Page = () => { + {standard.currentTenantValue?.LicenseAvailable === false ? ( + }> + {typeof standard.currentTenantValue?.Value === 'string' && + standard.currentTenantValue.Value.startsWith('License Missing:') + ? standard.currentTenantValue.Value + : 'This tenant does not have the required licenses for this standard'} + + ) : ( + <> {/* Show Expected Configuration with property-by-property breakdown */} {standard.currentTenantValue?.ExpectedValue !== undefined ? ( @@ -2073,6 +2096,8 @@ const Page = () => { sx={{ mr: 1 }} /> + + )}
@@ -2170,6 +2195,15 @@ const Page = () => { + {standard.currentTenantValue?.LicenseAvailable === false ? ( + }> + {typeof standard.currentTenantValue?.Value === 'string' && + standard.currentTenantValue.Value.startsWith('License Missing:') + ? standard.currentTenantValue.Value + : 'This tenant does not have the required licenses for this standard'} + + ) : ( + <> {/* Existing tenant comparison content */} {typeof standard.currentTenantValue?.Value === 'object' && standard.currentTenantValue?.Value !== null ? ( @@ -2931,6 +2965,8 @@ const Page = () => { )} )} + + )} From f3c8a79e42e97ad5b0cad9889ce15e4863f6bef8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 15:09:04 +0800 Subject: [PATCH 48/61] Update yarn.lock --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6ffd82077dd1..f679e9ff64b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6558,10 +6558,10 @@ react-colorful@^5.6.1: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== -react-dom@19.2.5: - version "19.2.5" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.5.tgz#b8768b10837d0b8e9ca5b9e2d58dff3d880ea25e" - integrity sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag== +react-dom@19.2.6: + version "19.2.6" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.6.tgz#44a81b0bcca22da814c00847d09d01c8615529b7" + integrity sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g== dependencies: scheduler "^0.27.0" From d28e8ebfaa9517e5f2134bb1c339d294096dca91 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 23:33:43 +0800 Subject: [PATCH 49/61] user sync --- .../CippSettings/CippRoleAddEdit.jsx | 7 + .../CippSettings/CippUserManagement.jsx | 163 +++++++++++++++--- .../cipp/advanced/super-admin/cipp-users.js | 11 +- 3 files changed, 148 insertions(+), 33 deletions(-) diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index 757215cd0f49..75192d394004 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -41,6 +41,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { const formControl = useForm({ mode: "onChange", + defaultValues: { + allowedTenants: [], + blockedTenants: [], + BlockedEndpoints: [], + IPRange: [], + Permissions: {}, + }, }); const formState = useFormState({ control: formControl.control }); diff --git a/src/components/CippSettings/CippUserManagement.jsx b/src/components/CippSettings/CippUserManagement.jsx index ab4be74c1b2b..b1d84c2f0b70 100644 --- a/src/components/CippSettings/CippUserManagement.jsx +++ b/src/components/CippSettings/CippUserManagement.jsx @@ -26,26 +26,30 @@ import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; export const CippUserManagement = () => { const [dialogOpen, setDialogOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); + const [bulkEditUsers, setBulkEditUsers] = useState(null); const formControl = useForm({ mode: "onChange", defaultValues: { UPN: "", Roles: [] }, }); - const rolesQuery = ApiGetCall({ - url: "/api/ListCustomRole", - queryKey: "customRoleList", + const usersQuery = ApiGetCall({ + url: "/api/ListCIPPUsers", + queryKey: "cippUsersList", }); const userAction = ApiPostCall({ relatedQueryKeys: ["cippUsersList"], }); - const allRoles = Array.isArray(rolesQuery.data) ? rolesQuery.data : []; + const pageData = usersQuery.data?.pages?.[0] ?? usersQuery.data ?? {}; + const allRoles = Array.isArray(pageData.AvailableRoles) ? pageData.AvailableRoles : []; const roleOptions = allRoles.map((r) => ({ label: `${r.RoleName} (${r.Type})`, value: r.RoleName, })); + const existingUsers = Array.isArray(pageData.Users) ? pageData.Users : []; + const existingUpns = new Set(existingUsers.map((u) => u.UPN.toLowerCase())); const openAddDialog = () => { setEditingUser(null); @@ -55,33 +59,36 @@ export const CippUserManagement = () => { const openEditDialog = (row) => { setEditingUser(row); - const currentRoles = (row.Roles ?? []).map((r) => { + // Show only manual roles for editing — auto roles are managed by sync + const editableRoles = (row.ManualRoles ?? row.Roles ?? []).map((r) => { const match = roleOptions.find((opt) => opt.value === r); return match ?? { label: r, value: r }; }); - formControl.reset({ UPN: row.UPN, Roles: currentRoles }); + formControl.reset({ UPN: row.UPN, Roles: editableRoles }); setDialogOpen(true); }; const handleSaveUser = (data) => { const roles = Array.isArray(data.Roles) ? data.Roles.map((r) => r.value ?? r) : [data.Roles]; - userAction.mutate( - { + + // Bulk edit: apply same roles to all selected users + const upns = bulkEditUsers ? bulkEditUsers.map((u) => u.UPN) : [data.UPN]; + + upns.forEach((upn) => { + userAction.mutate({ url: "/api/ExecCIPPUsers", data: { Action: "AddUpdate", - UPN: data.UPN, + UPN: upn, Roles: roles, }, - }, - { - onSuccess: () => { - formControl.reset({ UPN: "", Roles: [] }); - setEditingUser(null); - setDialogOpen(false); - }, - } - ); + }); + }); + + formControl.reset({ UPN: "", Roles: [] }); + setEditingUser(null); + setBulkEditUsers(null); + setDialogOpen(false); }; const actions = [ @@ -94,6 +101,12 @@ export const CippUserManagement = () => { ), noConfirm: true, customFunction: (row) => openEditDialog(row), + customBulkHandler: ({ data, clearSelection }) => { + setBulkEditUsers(data); + setEditingUser(null); + formControl.reset({ UPN: "", Roles: [] }); + setDialogOpen(true); + }, }, { label: "Delete User", @@ -113,6 +126,19 @@ export const CippUserManagement = () => { }, ]; + const sourceLabel = (source) => { + switch (source) { + case "Auto": + return "Auto-synced from Entra groups"; + case "Both": + return "Auto-synced + Manual"; + case "Manual": + return "Manually assigned"; + default: + return source || "—"; + } + }; + const offCanvas = { children: (row) => ( @@ -123,9 +149,16 @@ export const CippUserManagement = () => { {row.UPN} + + + Source + + {sourceLabel(row.Source)} + + - Assigned Roles + Effective Roles {(row.Roles ?? []).map((role, idx) => ( @@ -133,6 +166,49 @@ export const CippUserManagement = () => { ))} + {(row.ManualRoles ?? []).length > 0 && ( + <> + + + + Manual Roles + + + {row.ManualRoles.map((role, idx) => ( + + ))} + + + + )} + {(row.AutoRoles ?? []).length > 0 && ( + <> + + + + Auto Roles (from Entra groups) + + + {row.AutoRoles.map((role, idx) => ( + + ))} + + + + )} + {row.LastSync && ( + <> + + + + Last Synced + + + {new Date(row.LastSync).toLocaleString()} + + + + )} ), }; @@ -161,7 +237,7 @@ export const CippUserManagement = () => { dataKey: "Users", }} queryKey="cippUsersList" - simpleColumns={["UPN", "Roles"]} + simpleColumns={["UPN", "Roles", "Source"]} offCanvas={offCanvas} /> @@ -169,26 +245,57 @@ export const CippUserManagement = () => { setDialogOpen(false)} + onClose={() => { + setDialogOpen(false); + setBulkEditUsers(null); + }} maxWidth="sm" fullWidth > - {editingUser ? `Edit Roles — ${editingUser.UPN}` : "Add CIPP User"} + + {bulkEditUsers + ? `Bulk Edit Roles — ${bulkEditUsers.length} users` + : editingUser + ? `Edit Roles — ${editingUser.UPN}` + : "Add CIPP User"} + - {editingUser - ? "Update the roles assigned to this user." - : "Add a user by their email address (UPN) and assign one or more roles. If the user already exists, their roles will be updated."} + {bulkEditUsers + ? `Set the manual roles for ${bulkEditUsers.length} selected users. This will replace their existing manual roles. Auto-synced roles from Entra groups will not be affected.` + : editingUser + ? "Update the manually assigned roles for this user. Auto-synced roles from Entra groups are managed separately and will not be affected." + : "Add a user by their email address (UPN) and assign one or more roles. These are stored as manual assignments and won't be overwritten by the automatic Entra group sync."} - {!editingUser && ( + {bulkEditUsers && ( + + + Selected Users + + + {bulkEditUsers.map((u, idx) => ( + + ))} + + + )} + {!editingUser && !bulkEditUsers && ( { + if (existingUpns.has(value?.trim()?.toLowerCase())) { + return "This user already exists. Use Edit Roles to update their permissions."; + } + return true; + }, + }} /> )} { - + - + + From 1b7797afd4f4c55e6a44157a87914daf8110068f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 16:29:15 +0800 Subject: [PATCH 52/61] Update standards.json --- src/data/standards.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index b0fd2eabf081..1f40d86e0234 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6822,7 +6822,7 @@ { "type": "switch", "name": "standards.DefenderCompliancePolicy.ConnectWindows", - "label": "Connect Windows 10.0.15063+ to MDE", + "label": "Connect Windows 10.0.15063+ to MDE (Note: enabling this forces 'Block Windows if partner data unavailable' to on)", "defaultValue": false }, { @@ -6834,7 +6834,7 @@ { "type": "switch", "name": "standards.DefenderCompliancePolicy.windowsDeviceBlockedOnMissingPartnerData", - "label": "Block Windows if partner data unavailable", + "label": "Block Windows if partner data unavailable (Note: Microsoft enforces this to on when Connect Windows 10.0.15063+ to MDE is on)", "defaultValue": false }, { From de70889fe1c4e157ac25eb1ef43751add88056fa Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 14:39:44 +0200 Subject: [PATCH 53/61] smart lockout standard --- src/data/standards.json | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 1f40d86e0234..d78651f45ef7 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7293,5 +7293,49 @@ "addedDate": "2026-05-25", "powershellEquivalent": "Graph API PATCH /policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2", "recommendedBy": ["CIPP"] + }, + { + "name": "standards.SmartLockout", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "**Requires Entra ID P1.** Configures the Entra ID Smart Lockout settings including lockout duration, lockout threshold, and on-premises integration mode.", + "docsDescription": "Configures the Entra ID Smart Lockout policy which protects against brute-force password attacks. Smart Lockout locks out bad actors who try to guess user passwords or use brute-force methods. It recognizes sign-ins from valid users and treats them differently from attackers. Settings include lockout duration (seconds), lockout threshold (failed attempts before lockout), and on-premises password protection mode (Audit or Enforced).", + "addedComponent": [ + { + "type": "number", + "name": "standards.SmartLockout.LockoutDurationInSeconds", + "label": "Lockout Duration (seconds)", + "default": 60, + "required": true + }, + { + "type": "number", + "name": "standards.SmartLockout.LockoutThreshold", + "label": "Lockout Threshold (failed attempts)", + "default": 10, + "required": true + }, + { + "type": "switch", + "name": "standards.SmartLockout.EnableBannedPasswordCheckOnPremises", + "label": "Enable On-Premises Password Protection" + }, + { + "type": "radio", + "name": "standards.SmartLockout.BannedPasswordCheckOnPremisesMode", + "label": "On-Premises Mode", + "options": [ + { "label": "Audit", "value": "Audit" }, + { "label": "Enforced", "value": "Enforced" } + ] + } + ], + "label": "Configure Entra ID Smart Lockout", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-05-27", + "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting", + "recommendedBy": ["CIS"], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] } ] From 0e527e50d858628dbe0943c33c0b67b18949d612 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 17:03:29 +0200 Subject: [PATCH 54/61] Sharepoint management functionality. --- src/data/standards.json | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index d78651f45ef7..cdb04909bc20 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7337,5 +7337,48 @@ "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting", "recommendedBy": ["CIS"], "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] + }, + { + "name": "standards.SPOVersionControl", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Configures SharePoint Online file versioning to either use automatic version trimming managed by Microsoft, or enforce a fixed major version limit with optional version expiration.", + "docsDescription": "Configures the SharePoint Online tenant-level file versioning policy. When automatic version trimming is enabled, Microsoft intelligently manages version cleanup. When disabled, you can set a fixed maximum number of major versions to retain and optionally expire versions after a specified number of days. This helps manage storage consumption while preserving version history as needed.", + "executiveText": "Controls how SharePoint Online manages file version history at the tenant level. Automatic trimming lets Microsoft optimize storage by cleaning up old versions intelligently. Manual limits give administrators precise control over the maximum number of versions retained and their expiration, ensuring predictable storage usage and compliance with data retention policies.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.SPOVersionControl.EnableAutoTrim", + "label": "Enable Automatic Version Trimming (Microsoft managed)" + }, + { + "type": "number", + "name": "standards.SPOVersionControl.MajorVersionLimit", + "label": "Maximum Major Versions (when auto trim is off)", + "default": 50 + }, + { + "type": "number", + "name": "standards.SPOVersionControl.ExpireVersionsAfterDays", + "label": "Expire Versions After Days (0 = never, when auto trim is off)", + "default": 0 + }, + { + "type": "switch", + "name": "standards.SPOVersionControl.ApplyToExistingSites", + "label": "Apply to all existing sites and document libraries" + } + ], + "label": "Set SharePoint File Version Limits", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-27", + "powershellEquivalent": "Set-SPOTenant -EnableAutoExpirationVersionTrim $true or Set-SPOTenant -EnableAutoExpirationVersionTrim $false -MajorVersionLimit 50 -ExpireVersionsAfterDays 365", + "recommendedBy": [], + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + } } ] From 5709f85661a047dad29216739acccf6326c9231b Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 27 May 2026 17:11:46 +0200 Subject: [PATCH 55/61] fix: update terminology from "Temporary Access Password" to "Temporary Access Pass" Fixes https://github.com/KelvinTegelaar/CIPP/issues/6060 --- src/components/CippComponents/CippUserActions.jsx | 4 ++-- src/data/standards.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index c1af17ab13e6..e635c5fa988f 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -418,7 +418,7 @@ export const useCippUserActions = () => { }, { //tested - label: 'Create Temporary Access Password', + label: 'Create Temporary Access Pass', type: 'POST', icon: , url: '/api/ExecCreateTAP', @@ -443,7 +443,7 @@ export const useCippUserActions = () => { }, ], confirmText: - 'Are you sure you want to create a Temporary Access Password for [userPrincipalName]?', + 'Are you sure you want to create a Temporary Access Pass for [userPrincipalName]?', multiPost: false, condition: () => canWriteUser, }, diff --git a/src/data/standards.json b/src/data/standards.json index cdb04909bc20..dbe64432f9d5 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -753,8 +753,8 @@ "tag": [], "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02", "ZTNA21845", "ZTNA21846"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", - "docsDescription": "Enables Temporary Password generation for the tenant.", - "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", + "docsDescription": "Enables Temporary Access Pass generation for the tenant.", + "executiveText": "Enables temporary access passes that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passs provide a secure way to restore access without compromising long-term security policies.", "addedComponent": [ { "type": "autoComplete", @@ -768,7 +768,7 @@ ] } ], - "label": "Enable Temporary Access Passwords", + "label": "Enable Temporary Access Passes (TAP)", "impact": "Low Impact", "impactColour": "info", "addedDate": "2022-03-15", From bf6056baf362471dba4ecbb599e0b1c85091b8d0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:24:47 +0200 Subject: [PATCH 56/61] Add version cleanup --- src/pages/teams-share/sharepoint/index.js | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index cf1353f52b7f..657f9b8e60cf 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -9,12 +9,15 @@ import { AdminPanelSettings, NoAccounts, Delete, + CleaningServices, } from '@mui/icons-material' import Link from 'next/link' import { Stack } from '@mui/system' import { CippDataTable } from '../../../components/CippTable/CippDataTable' import { useSettings } from '../../../hooks/use-settings' import { useCippReportDB } from '../../../components/CippComponents/CippReportDBControls' +import CippFormComponent from '../../../components/CippComponents/CippFormComponent' +import { CippFormCondition } from '../../../components/CippComponents/CippFormCondition' const Page = () => { const pageTitle = 'SharePoint Sites' @@ -202,6 +205,71 @@ const Page = () => { color: 'error', multiPost: false, }, + { + label: 'Start Version Cleanup Job', + type: 'POST', + icon: , + url: '/api/ExecSPOVersionCleanup', + data: { + SiteUrl: 'webUrl', + }, + confirmText: + 'Start a file version cleanup job for [displayName]. This will trim old file versions based on the selected mode.', + children: ({ formHook }) => ( + <> + + + + + + + + + ), + defaultvalues: { + BatchDeleteMode: '2', + }, + customDataformatter: (row, action, formData) => ({ + tenantFilter: row.Tenant ?? tenantFilter, + SiteUrl: row.webUrl, + BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10), + DeleteOlderThanDays: formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, + MajorVersionLimit: formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, + }), + multiPost: false, + }, ] const offCanvas = { From 635548afd1d988518d8c392413828aec7e39eda5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:25:01 +0200 Subject: [PATCH 57/61] Add version cleanup --- src/pages/teams-share/sharepoint/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index 657f9b8e60cf..695b2bef7684 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -224,7 +224,10 @@ const Page = () => { formControl={formHook} options={[ { label: 'Sync Policy — apply site version policy to existing versions', value: '2' }, - { label: 'Delete Older Than Days — remove versions older than a set number of days', value: '0' }, + { + label: 'Delete Older Than Days — remove versions older than a set number of days', + value: '0', + }, { label: 'Count Limits — keep a maximum number of major versions', value: '1' }, ]} /> @@ -265,8 +268,10 @@ const Page = () => { tenantFilter: row.Tenant ?? tenantFilter, SiteUrl: row.webUrl, BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10), - DeleteOlderThanDays: formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, - MajorVersionLimit: formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, + DeleteOlderThanDays: + formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, + MajorVersionLimit: + formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, }), multiPost: false, }, From 7a40854272a60617d57a28570406953a9cca913c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:37:02 +0200 Subject: [PATCH 58/61] fix query keys --- src/pages/security/compliance/dlp/index.js | 126 ++++++++--------- src/pages/security/compliance/labels/index.js | 106 ++++++++------- .../security/compliance/retention/index.js | 128 +++++++++--------- src/pages/security/compliance/sit/index.js | 74 +++++----- 4 files changed, 221 insertions(+), 213 deletions(-) diff --git a/src/pages/security/compliance/dlp/index.js b/src/pages/security/compliance/dlp/index.js index 750cfe6ade53..5e09520f5579 100644 --- a/src/pages/security/compliance/dlp/index.js +++ b/src/pages/security/compliance/dlp/index.js @@ -1,99 +1,101 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { Book, Block, Check } from "@mui/icons-material"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { Book, Block, Check } from '@mui/icons-material' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "DLP Compliance Policies"; - const apiUrl = "/api/ListDlpCompliancePolicy"; - const cardButtonPermissions = ["Security.DlpCompliancePolicy.ReadWrite"]; + const pageTitle = 'DLP Compliance Policies' + const apiUrl = '/api/ListDlpCompliancePolicy' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.DlpCompliancePolicy.ReadWrite'] const actions = [ { - label: "Create template based on policy", - type: "POST", + label: 'Create template based on policy', + type: 'POST', icon: , - url: "/api/AddDlpCompliancePolicyTemplate", + url: '/api/AddDlpCompliancePolicyTemplate', dataFunction: (data) => { - return { ...data }; + return { ...data } }, - confirmText: "Are you sure you want to create a template based on this DLP policy?", + confirmText: 'Are you sure you want to create a template based on this DLP policy?', }, { - label: "Enable Policy", - type: "POST", + label: 'Enable Policy', + type: 'POST', icon: , - url: "/api/EditDlpCompliancePolicy", + url: '/api/EditDlpCompliancePolicy', data: { - State: "!enable", - Identity: "Name", + State: '!enable', + Identity: 'Name', }, - confirmText: "Are you sure you want to enable this DLP policy?", + confirmText: 'Are you sure you want to enable this DLP policy?', condition: (row) => row.Enabled === false, }, { - label: "Disable Policy", - type: "POST", + label: 'Disable Policy', + type: 'POST', icon: , - url: "/api/EditDlpCompliancePolicy", + url: '/api/EditDlpCompliancePolicy', data: { - State: "!disable", - Identity: "Name", + State: '!disable', + Identity: 'Name', }, - confirmText: "Are you sure you want to disable this DLP policy?", + confirmText: 'Are you sure you want to disable this DLP policy?', condition: (row) => row.Enabled === true, }, { - label: "Delete Policy", - type: "POST", + label: 'Delete Policy', + type: 'POST', icon: , - url: "/api/RemoveDlpCompliancePolicy", + url: '/api/RemoveDlpCompliancePolicy', data: { - Identity: "Name", + Identity: 'Name', }, - confirmText: "Are you sure you want to delete this DLP policy?", - color: "danger", + confirmText: 'Are you sure you want to delete this DLP policy?', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "Name", - "Comment", - "Mode", - "Enabled", - "Workload", - "ExchangeLocation", - "SharePointLocation", - "OneDriveLocation", - "TeamsLocation", - "EndpointDlpLocation", - "RuleCount", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", + 'Name', + 'Comment', + 'Mode', + 'Enabled', + 'Workload', + 'ExchangeLocation', + 'SharePointLocation', + 'OneDriveLocation', + 'TeamsLocation', + 'EndpointDlpLocation', + 'RuleCount', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', ], actions: actions, - }; + } const simpleColumns = [ - "Name", - "Mode", - "Enabled", - "Workload", - "RuleCount", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", - ]; + 'Name', + 'Mode', + 'Enabled', + 'Workload', + 'RuleCount', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page diff --git a/src/pages/security/compliance/labels/index.js b/src/pages/security/compliance/labels/index.js index e35ff5942b0f..998865acdc87 100644 --- a/src/pages/security/compliance/labels/index.js +++ b/src/pages/security/compliance/labels/index.js @@ -1,78 +1,80 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { Book } from "@mui/icons-material"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { Book } from '@mui/icons-material' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "Sensitivity Labels"; - const apiUrl = "/api/ListSensitivityLabel"; - const cardButtonPermissions = ["Security.SensitivityLabel.ReadWrite"]; + const pageTitle = 'Sensitivity Labels' + const apiUrl = '/api/ListSensitivityLabel' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.SensitivityLabel.ReadWrite'] const actions = [ { - label: "Create template based on label", - type: "POST", + label: 'Create template based on label', + type: 'POST', icon: , - url: "/api/AddSensitivityLabelTemplate", + url: '/api/AddSensitivityLabelTemplate', dataFunction: (data) => { - return { ...data }; + return { ...data } }, - confirmText: "Are you sure you want to create a template based on this sensitivity label?", + confirmText: 'Are you sure you want to create a template based on this sensitivity label?', }, { - label: "Delete Label", - type: "POST", + label: 'Delete Label', + type: 'POST', icon: , - url: "/api/RemoveSensitivityLabel", + url: '/api/RemoveSensitivityLabel', data: { - Identity: "Guid", + Identity: 'Guid', }, confirmText: - "Are you sure you want to delete this sensitivity label? Labels currently published to users will be removed from policies.", - color: "danger", + 'Are you sure you want to delete this sensitivity label? Labels currently published to users will be removed from policies.', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "DisplayName", - "Name", - "Comment", - "Tooltip", - "ParentId", - "ContentType", - "EncryptionEnabled", - "EncryptionProtectionType", - "ContentMarkingHeaderEnabled", - "ContentMarkingFooterEnabled", - "ContentMarkingWatermarkEnabled", - "SiteAndGroupProtectionEnabled", - "Priority", - "Disabled", - "PublishedInPolicies", + 'DisplayName', + 'Name', + 'Comment', + 'Tooltip', + 'ParentId', + 'ContentType', + 'EncryptionEnabled', + 'EncryptionProtectionType', + 'ContentMarkingHeaderEnabled', + 'ContentMarkingFooterEnabled', + 'ContentMarkingWatermarkEnabled', + 'SiteAndGroupProtectionEnabled', + 'Priority', + 'Disabled', + 'PublishedInPolicies', ], actions: actions, - }; + } const simpleColumns = [ - "DisplayName", - "Name", - "ContentType", - "EncryptionEnabled", - "ContentMarkingHeaderEnabled", - "ContentMarkingWatermarkEnabled", - "SiteAndGroupProtectionEnabled", - "Priority", - "Disabled", - ]; + 'DisplayName', + 'Name', + 'ContentType', + 'EncryptionEnabled', + 'ContentMarkingHeaderEnabled', + 'ContentMarkingWatermarkEnabled', + 'SiteAndGroupProtectionEnabled', + 'Priority', + 'Disabled', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page diff --git a/src/pages/security/compliance/retention/index.js b/src/pages/security/compliance/retention/index.js index 962301013f29..940d88b4eb0a 100644 --- a/src/pages/security/compliance/retention/index.js +++ b/src/pages/security/compliance/retention/index.js @@ -1,98 +1,100 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { Book, Block, Check } from "@mui/icons-material"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { Book, Block, Check } from '@mui/icons-material' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "Purview Retention Policies"; - const apiUrl = "/api/ListRetentionCompliancePolicy"; - const cardButtonPermissions = ["Security.RetentionCompliancePolicy.ReadWrite"]; + const pageTitle = 'Purview Retention Policies' + const apiUrl = '/api/ListRetentionCompliancePolicy' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.RetentionCompliancePolicy.ReadWrite'] const actions = [ { - label: "Create template based on policy", - type: "POST", + label: 'Create template based on policy', + type: 'POST', icon: , - url: "/api/AddRetentionCompliancePolicyTemplate", - data: { Identity: "Name" }, - confirmText: "Are you sure you want to create a template based on this retention policy?", + url: '/api/AddRetentionCompliancePolicyTemplate', + data: { Identity: 'Name' }, + confirmText: 'Are you sure you want to create a template based on this retention policy?', }, { - label: "Enable Policy", - type: "POST", + label: 'Enable Policy', + type: 'POST', icon: , - url: "/api/EditRetentionCompliancePolicy", + url: '/api/EditRetentionCompliancePolicy', data: { - State: "!enable", - Identity: "Name", + State: '!enable', + Identity: 'Name', }, - confirmText: "Are you sure you want to enable this retention policy?", + confirmText: 'Are you sure you want to enable this retention policy?', condition: (row) => row.Enabled === false, }, { - label: "Disable Policy", - type: "POST", + label: 'Disable Policy', + type: 'POST', icon: , - url: "/api/EditRetentionCompliancePolicy", + url: '/api/EditRetentionCompliancePolicy', data: { - State: "!disable", - Identity: "Name", + State: '!disable', + Identity: 'Name', }, - confirmText: "Are you sure you want to disable this retention policy?", + confirmText: 'Are you sure you want to disable this retention policy?', condition: (row) => row.Enabled === true, }, { - label: "Delete Policy", - type: "POST", + label: 'Delete Policy', + type: 'POST', icon: , - url: "/api/RemoveRetentionCompliancePolicy", + url: '/api/RemoveRetentionCompliancePolicy', data: { - Identity: "Name", + Identity: 'Name', }, - confirmText: "Are you sure you want to delete this retention policy?", - color: "danger", + confirmText: 'Are you sure you want to delete this retention policy?', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "Name", - "Comment", - "Enabled", - "Workload", - "RestrictiveRetention", - "ExchangeLocation", - "SharePointLocation", - "OneDriveLocation", - "ModernGroupLocation", - "TeamsChannelLocation", - "TeamsChatLocation", - "RuleCount", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", + 'Name', + 'Comment', + 'Enabled', + 'Workload', + 'RestrictiveRetention', + 'ExchangeLocation', + 'SharePointLocation', + 'OneDriveLocation', + 'ModernGroupLocation', + 'TeamsChannelLocation', + 'TeamsChatLocation', + 'RuleCount', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', ], actions: actions, - }; + } const simpleColumns = [ - "Name", - "Enabled", - "Workload", - "RuleCount", - "RestrictiveRetention", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", - ]; + 'Name', + 'Enabled', + 'Workload', + 'RuleCount', + 'RestrictiveRetention', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page diff --git a/src/pages/security/compliance/sit/index.js b/src/pages/security/compliance/sit/index.js index 3101f0502218..43039cdba7fb 100644 --- a/src/pages/security/compliance/sit/index.js +++ b/src/pages/security/compliance/sit/index.js @@ -1,57 +1,59 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "Sensitive Information Types"; - const apiUrl = "/api/ListSensitiveInfoType"; - const cardButtonPermissions = ["Security.SensitiveInfoType.ReadWrite"]; + const pageTitle = 'Sensitive Information Types' + const apiUrl = '/api/ListSensitiveInfoType' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.SensitiveInfoType.ReadWrite'] const actions = [ { - label: "Delete SIT", - type: "POST", + label: 'Delete SIT', + type: 'POST', icon: , - url: "/api/RemoveSensitiveInfoType", + url: '/api/RemoveSensitiveInfoType', data: { - Identity: "Name", + Identity: 'Name', }, confirmText: - "Are you sure you want to delete this Sensitive Information Type? Built-in Microsoft types cannot be deleted.", - color: "danger", + 'Are you sure you want to delete this Sensitive Information Type? Built-in Microsoft types cannot be deleted.', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "Name", - "Description", - "Publisher", - "Recommended", - "RulePackId", - "RulePackVersion", - "State", - "Type", + 'Name', + 'Description', + 'Publisher', + 'Recommended', + 'RulePackId', + 'RulePackVersion', + 'State', + 'Type', ], actions: actions, - }; + } const simpleColumns = [ - "Name", - "Publisher", - "Description", - "Recommended", - "RulePackVersion", - "State", - ]; + 'Name', + 'Publisher', + 'Description', + 'Recommended', + 'RulePackVersion', + 'State', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page From de035243aa1090ac818cc2d02e651007a90723e1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:59:42 +0200 Subject: [PATCH 59/61] fixes #6065 --- src/pages/tenant/manage/user-defaults.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/user-defaults.js b/src/pages/tenant/manage/user-defaults.js index 8eb4f2592f61..3a5c3a150899 100644 --- a/src/pages/tenant/manage/user-defaults.js +++ b/src/pages/tenant/manage/user-defaults.js @@ -124,7 +124,7 @@ const Page = () => { labelField: (option) => `${option.License || option.skuPartNumber} (${option.AvailableUnits || 0} available)`, valueField: 'skuId', - queryKey: 'ListLicenses', + queryKey: `ListLicenses-${userSettings.currentTenant}`, }, multiple: true, creatable: false, @@ -137,7 +137,7 @@ const Page = () => { url: '/api/ListGroups', labelField: 'displayName', valueField: 'id', - queryKey: 'ListGroups', + queryKey: `ListGroups-${userSettings.currentTenant}`, addedField: { groupType: 'calculatedGroupType', }, From 072416dd95fb955625d466022ebb4a05847e76e3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 28 May 2026 00:54:29 +0200 Subject: [PATCH 60/61] new autopatch standard --- src/data/standards.json | 238 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index dbe64432f9d5..464b433e4cb6 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7235,6 +7235,244 @@ "powershellEquivalent": "Graph API - /deviceAppManagement/mobileApps", "recommendedBy": [] }, + { + "name": "standards.AutopatchGroup", + "cat": "Intune Standards", + "tag": [], + "beta": true, + "deprecated": true, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, + "helpText": "Deploys a Windows Autopatch group with configurable deployment ring settings for quality updates, feature updates, Edge, and Office.", + "docsDescription": "Creates or updates a Windows Autopatch deployment group with Test and Last deployment rings. Configures quality update deferrals, feature update targeting, Edge and Office update channels per ring. Uses the Autopatch API proxy to manage the group configuration.", + "executiveText": "Configures Windows Autopatch deployment groups to manage update delivery across devices. Autopatch automates Windows quality updates, feature updates, Edge, and Office updates using deployment rings with configurable deferrals and deadlines.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.AutopatchGroup.GroupName", + "label": "Group Name", + "required": true, + "defaultValue": "Autopatch default group" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TargetOSVersion", + "label": "Target OS Version", + "required": true, + "options": [ + { "label": "Windows 11, version 24H2", "value": "Windows 11, version 24H2" }, + { "label": "Windows 11, version 25H2", "value": "Windows 11, version 25H2" } + ], + "defaultValue": "Windows 11, version 25H2" + }, + { + "type": "switch", + "name": "standards.AutopatchGroup.EnableDriverUpdate", + "label": "Enable Driver Updates", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.AutopatchGroup.InstallWin10OnWin11Ineligible", + "label": "Install latest Windows 10 on Windows 11 ineligible devices", + "defaultValue": false + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityDeferral", + "label": "Test Ring - Quality Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityDeadline", + "label": "Test Ring - Quality Update Deadline (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityGracePeriod", + "label": "Test Ring - Quality Update Grace Period (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 7, "message": "Maximum value is 7" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestFeatureDeferral", + "label": "Test Ring - Feature Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 365, "message": "Maximum value is 365" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestFeatureDeadline", + "label": "Test Ring - Feature Update Deadline (days)", + "defaultValue": 5, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TestEdgeChannel", + "label": "Test Ring - Edge Update Channel", + "options": [ + { "label": "Stable", "value": "Stable" }, + { "label": "Beta", "value": "Beta" }, + { "label": "Dev", "value": "Dev" } + ], + "defaultValue": "Beta" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TestOfficeChannel", + "label": "Test Ring - Office Update Channel", + "options": [ + { "label": "Current", "value": "Current" }, + { "label": "Monthly Enterprise", "value": "MonthlyEnterprise" }, + { "label": "Semi-Annual Enterprise", "value": "SemiAnnual" } + ], + "defaultValue": "MonthlyEnterprise" + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestDnfDeferral", + "label": "Test Ring - Driver & Firmware Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityDeferral", + "label": "Last Ring - Quality Update Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityDeadline", + "label": "Last Ring - Quality Update Deadline (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityGracePeriod", + "label": "Last Ring - Quality Update Grace Period (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 7, "message": "Maximum value is 7" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastFeatureDeferral", + "label": "Last Ring - Feature Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 365, "message": "Maximum value is 365" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastFeatureDeadline", + "label": "Last Ring - Feature Update Deadline (days)", + "defaultValue": 5, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.LastEdgeChannel", + "label": "Last Ring - Edge Update Channel", + "options": [ + { "label": "Stable", "value": "Stable" }, + { "label": "Beta", "value": "Beta" }, + { "label": "Dev", "value": "Dev" } + ], + "defaultValue": "Stable" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.LastOfficeChannel", + "label": "Last Ring - Office Update Channel", + "options": [ + { "label": "Current", "value": "Current" }, + { "label": "Monthly Enterprise", "value": "MonthlyEnterprise" }, + { "label": "Semi-Annual Enterprise", "value": "SemiAnnual" } + ], + "defaultValue": "MonthlyEnterprise" + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastOfficeDeferral", + "label": "Last Ring - Office Update Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastOfficeDeadline", + "label": "Last Ring - Office Update Deadline (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastDnfDeferral", + "label": "Last Ring - Driver & Firmware Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + } + ], + "label": "Deploy Windows Autopatch Group", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-05-27", + "powershellEquivalent": "Autopatch API - POST /api/autoPatch", + "recommendedBy": [] + }, { "name": "standards.FIDO2PasskeyProfiles", "cat": "Entra (AAD) Standards", From b8ece5c448ee814f5400dacaaee9a3590c911cde Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 28 May 2026 13:56:39 +0800 Subject: [PATCH 61/61] Update worker-health.js --- src/pages/cipp/advanced/worker-health.js | 77 +++++++++++++++++++----- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index ccc19a335570..721f6c578799 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -33,7 +33,6 @@ import { Cancel, Delete, LowPriority, - DeleteSweep, Timeline, RocketLaunch, Pause, @@ -306,6 +305,7 @@ const CompactStatsRow = ({ snapshot }) => { const bg = snapshot.BgPool || {}; const jobs = snapshot.Jobs || {}; const limiter = snapshot.Limiter || {}; + const mem = snapshot.Memory || {}; const sections = [ { @@ -351,6 +351,18 @@ const CompactStatsRow = ({ snapshot }) => { ...(limiter.IsHttpThrottled ? [{ k: "Status", v: "Throttled", w: true }] : []), ], }, + { + label: "Memory", + color: "secondary", + stats: [ + { k: "Heap", v: `${mem.HeapMB ?? 0}MB` }, + { k: "RSS", v: `${mem.RssMB ?? 0}MB`, w: mem.UsagePct > 85 }, + { k: "Committed", v: `${mem.CommittedMB ?? 0}MB` }, + { k: "Limit", v: `${mem.ContainerLimitMB ?? 0}MB` }, + { k: "Usage", v: `${mem.UsagePct ?? 0}%`, w: mem.UsagePct > 85 }, + { k: "GC", v: `${mem.GC0 ?? 0}/${mem.GC1 ?? 0}/${mem.GC2 ?? 0}` }, + ], + }, ]; return ( @@ -427,6 +439,7 @@ const Page = () => { const [historyRange, setHistoryRange] = useState(60); const [paused, setPaused] = useState(false); const [importedData, setImportedData] = useState(null); + const [jobLimit, setJobLimit] = useState(2000); const isImported = importedData !== null; const effectivePaused = paused || isImported; @@ -573,10 +586,16 @@ const Page = () => { : `${limiter.Active ?? 0} / ${limiter.CurrentMax ?? 0} active`, color: limiter.IsHttpThrottled ? "error" : "primary", }, + { + icon: , + name: "Memory", + data: `${snapshot.Memory?.RssMB ?? 0}MB / ${snapshot.Memory?.ContainerLimitMB ?? 0}MB (${snapshot.Memory?.UsagePct ?? 0}%)`, + color: (snapshot.Memory?.UsagePct ?? 0) > 85 ? "error" : (snapshot.Memory?.UsagePct ?? 0) > 70 ? "warning" : "primary", + }, ]; }, [snapshot]); - const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "WaitSeconds", "DurationSeconds"]; + const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "QueuedUtc", "WaitSeconds", "DurationSeconds"]; const jobActions = useMemo( () => [ @@ -761,30 +780,29 @@ const Page = () => { ) : ( val !== null && setJobLimit(val)} size="small" - startIcon={} - color="warning" - onClick={() => - jobAction.mutate({ - url: "/api/ListWorkerHealth", - data: { Action: "PurgeCompleted" }, - }) - } > - Purge Completed - + {[500, 2000, 5000, 10000].map((n) => ( + + {n >= 1000 ? `${n / 1000}k` : n} + + ))} + } /> )} @@ -878,6 +896,33 @@ const Page = () => { + + } + > + {(data, t) => ( + + + + + + + + + + + )} + +