From 83f7b0ef52513848244d39c6c4c202096afdf66d Mon Sep 17 00:00:00 2001 From: Steven van Beek Date: Tue, 10 Feb 2026 11:40:24 +0100 Subject: [PATCH 001/177] Added new alerts --- src/data/alerts.json | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index 40cb347adeb3..7b07645c5408 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -47,6 +47,44 @@ "inputName": "ExcludeDisabled" } ] + }, + { + "name": "InactiveGuestUsers", + "label": "Alert on guest users that have not logged in for X days", + "recommendedRunInterval": "1d", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "number", + "inputLabel": "Days since last login (default: 90)", + "inputName": "DaysSinceLastLogin" + }, + { + "inputType": "switch", + "inputLabel": "Exclude disabled guest users?", + "inputName": "ExcludeDisabled" + } + ] + }, + { + "name": "InactiveUsers", + "label": "Alert on users that have not logged in for X days", + "recommendedRunInterval": "1d", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "number", + "inputLabel": "Days since last login (default: 90)", + "inputName": "DaysSinceLastLogin" + }, + { + "inputType": "switch", + "inputLabel": "Exclude disabled users?", + "inputName": "ExcludeDisabled" + } + ] }, { "name": "EntraConnectSyncStatus", @@ -341,6 +379,25 @@ "label": "Alert on quarantine release requests", "recommendedRunInterval": "30m", "description": "Monitors for user requests to release quarantined messages and provides a CIPP-native alternative to the external email forwarding method. This helps MSPs maintain secure configurations while getting timely notifications about quarantine activity. Links to the tenant's quarantine page are provided in alerts." + }, + { + "name": "AlertStaleEntraDevices", + "label": "Alert on Stale Entra devices that have not been active for X days", + "recommendedRunInterval": "1d", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "number", + "inputLabel": "Days since last activity (default: 90)", + "inputName": "DaysSinceLastActivity" + }, + { + "inputType": "switch", + "inputLabel": "Exclude disabled devices?", + "inputName": "ExcludeDisabled" + } + ] }, { "name": "SecureScore", From e475cd1c6399d3cffd4b66943e01f9b387ad008d Mon Sep 17 00:00:00 2001 From: Steven van Beek Date: Tue, 10 Feb 2026 11:47:05 +0100 Subject: [PATCH 002/177] Added new alert --- src/data/alerts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index 7b07645c5408..775fc54c4e7c 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -381,7 +381,7 @@ "description": "Monitors for user requests to release quarantined messages and provides a CIPP-native alternative to the external email forwarding method. This helps MSPs maintain secure configurations while getting timely notifications about quarantine activity. Links to the tenant's quarantine page are provided in alerts." }, { - "name": "AlertStaleEntraDevices", + "name": "StaleEntraDevices", "label": "Alert on Stale Entra devices that have not been active for X days", "recommendedRunInterval": "1d", "requiresInput": true, From 3ad4ec663a7226088ae82708aee157d0516656e8 Mon Sep 17 00:00:00 2001 From: Steven van Beek Date: Tue, 10 Feb 2026 13:35:22 +0100 Subject: [PATCH 003/177] added new alerts --- src/data/alerts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index 775fc54c4e7c..1e7f3cb74d01 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -382,7 +382,7 @@ }, { "name": "StaleEntraDevices", - "label": "Alert on Stale Entra devices that have not been active for X days", + "label": "Alert on stale Entra devices that have not been active for X days", "recommendedRunInterval": "1d", "requiresInput": true, "multipleInput": true, From a3e856b06b569027c6283cdd1a706285a554abe6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:46:05 +0000 Subject: [PATCH 004/177] Bump react-hook-form from 7.68.0 to 7.71.1 Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.68.0 to 7.71.1. - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.68.0...v7.71.1) --- updated-dependencies: - dependency-name: react-hook-form dependency-version: 7.71.1 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 059e1aabea30..1e62b1fca2cd 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "react-dropzone": "14.3.8", "react-error-boundary": "^6.1.0", "react-grid-layout": "^1.5.0", - "react-hook-form": "^7.53.0", + "react-hook-form": "^7.71.1", "react-hot-toast": "2.6.0", "react-html-parser": "^2.0.2", "react-i18next": "15.7.3", diff --git a/yarn.lock b/yarn.lock index 309852f41632..cdde0a057d01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6578,10 +6578,10 @@ react-grid-layout@^1.5.0: react-resizable "^3.0.5" resize-observer-polyfill "^1.5.1" -react-hook-form@^7.53.0: - version "7.68.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.68.0.tgz#733c6871fa4ec5e5bfb13e7650a3a912eafe1721" - integrity sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q== +react-hook-form@^7.71.1: + version "7.71.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.71.1.tgz#6a758958861682cf0eb22131eead684ba3618f66" + integrity sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w== react-hot-toast@2.6.0: version "2.6.0" From f0d64fa173dff3fa0b47b60af161311e50deff83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:46:18 +0000 Subject: [PATCH 005/177] Bump @react-pdf/renderer from 4.3.1 to 4.3.2 Bumps [@react-pdf/renderer](https://github.com/diegomura/react-pdf/tree/HEAD/packages/renderer) from 4.3.1 to 4.3.2. - [Release notes](https://github.com/diegomura/react-pdf/releases) - [Changelog](https://github.com/diegomura/react-pdf/blob/master/packages/renderer/CHANGELOG.md) - [Commits](https://github.com/diegomura/react-pdf/commits/@react-pdf/renderer@4.3.2/packages/renderer) --- updated-dependencies: - dependency-name: "@react-pdf/renderer" dependency-version: 4.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 114 +++++++++++++++++++++++++-------------------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index 059e1aabea30..dfcc551cc3bc 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@musement/iso-duration": "^1.0.0", "@nivo/core": "^0.99.0", "@nivo/sankey": "^0.99.0", - "@react-pdf/renderer": "^4.3.0", + "@react-pdf/renderer": "^4.3.2", "@reduxjs/toolkit": "^2.11.2", "@tanstack/query-sync-storage-persister": "^5.76.0", "@tanstack/react-query": "^5.51.11", diff --git a/yarn.lock b/yarn.lock index 309852f41632..6c0105de8f35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1678,43 +1678,43 @@ resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-3.1.2.tgz#9ce7351d9fdf1cdb6e9c6ffd6801bc65f29f991c" integrity sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g== -"@react-pdf/font@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@react-pdf/font/-/font-4.0.3.tgz#81036f4933c8d4a1468720440314c0a1c8b9c021" - integrity sha512-N1qQDZr6phXYQOp033Hvm2nkUkx2LkszjGPbmRavs9VOYzi4sp31MaccMKptL24ii6UhBh/z9yPUhnuNe/qHwA== +"@react-pdf/font@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@react-pdf/font/-/font-4.0.4.tgz#7b5bed082fb159e582f22fe4c56e9a8c46736835" + integrity sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg== dependencies: - "@react-pdf/pdfkit" "^4.0.4" - "@react-pdf/types" "^2.9.1" + "@react-pdf/pdfkit" "^4.1.0" + "@react-pdf/types" "^2.9.2" fontkit "^2.0.2" is-url "^1.2.4" -"@react-pdf/image@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@react-pdf/image/-/image-3.0.3.tgz#bfdb9e782c361c9d9e0f81c31ef98554bc4e928c" - integrity sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ== +"@react-pdf/image@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@react-pdf/image/-/image-3.0.4.tgz#ee9c8928843d9680279a512138c5f597b3aae616" + integrity sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g== dependencies: "@react-pdf/png-js" "^3.0.0" jay-peg "^1.1.1" -"@react-pdf/layout@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@react-pdf/layout/-/layout-4.4.1.tgz#50c95084f703cf2a1395e8b2e48300b57b3a74fe" - integrity sha512-GVzdlWoZWldRDzlWj3SttRXmVDxg7YfraAohwy+o9gb9hrbDJaaAV6jV3pc630Evd3K46OAzk8EFu8EgPDuVuA== +"@react-pdf/layout@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@react-pdf/layout/-/layout-4.4.2.tgz#30bde1e460ec8ead6a0aed85eca41279ed6f0ed8" + integrity sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg== dependencies: "@react-pdf/fns" "3.1.2" - "@react-pdf/image" "^3.0.3" + "@react-pdf/image" "^3.0.4" "@react-pdf/primitives" "^4.1.1" - "@react-pdf/stylesheet" "^6.1.1" - "@react-pdf/textkit" "^6.0.0" - "@react-pdf/types" "^2.9.1" + "@react-pdf/stylesheet" "^6.1.2" + "@react-pdf/textkit" "^6.1.0" + "@react-pdf/types" "^2.9.2" emoji-regex-xs "^1.0.0" queue "^6.0.1" yoga-layout "^3.2.1" -"@react-pdf/pdfkit@^4.0.4": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@react-pdf/pdfkit/-/pdfkit-4.0.4.tgz#c40d49850dafccbdbd371d4dab26d9ca07b88e0f" - integrity sha512-/nITLggsPlB66bVLnm0X7MNdKQxXelLGZG6zB5acF5cCgkFwmXHnLNyxYOUD4GMOMg1HOPShXDKWrwk2ZeHsvw== +"@react-pdf/pdfkit@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@react-pdf/pdfkit/-/pdfkit-4.1.0.tgz#2a32cb4bfa36e887747395d8c13ac425459eda0a" + integrity sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ== dependencies: "@babel/runtime" "^7.20.13" "@react-pdf/png-js" "^3.0.0" @@ -1737,79 +1737,79 @@ resolved "https://registry.yarnpkg.com/@react-pdf/primitives/-/primitives-4.1.1.tgz#c7bfb7e83173661b6ec50ada4aba8dc9e94d0563" integrity sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ== -"@react-pdf/reconciler@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@react-pdf/reconciler/-/reconciler-1.1.4.tgz#62395cf5c8786a1c3465e2cf6315562543b663c5" - integrity sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg== +"@react-pdf/reconciler@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-pdf/reconciler/-/reconciler-2.0.0.tgz#d53ba53d5418c275c1fe1b150f0e9822243b799a" + integrity sha512-7zaPRujpbHSmCpIrZ+b9HSTJHthcVZzX0Wx7RzvQGsGBUbHP4p6s5itXrAIOuQuPvDepoHGNOvf6xUuMVvdoyw== dependencies: object-assign "^4.1.1" scheduler "0.25.0-rc-603e6108-20241029" -"@react-pdf/render@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@react-pdf/render/-/render-4.3.1.tgz#76a721af66a5279727b61b200234f5f0dca034fd" - integrity sha512-v1WAaAhQShQZGcBxfjkEThGCHVH9CSuitrZ1bIOLvB5iBKM14abYK5D6djKhWCwF6FTzYeT2WRjRMVgze/ND2A== +"@react-pdf/render@^4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@react-pdf/render/-/render-4.3.2.tgz#ae24c363fc25c46eb25fe85a13b28e693ba97635" + integrity sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q== dependencies: "@babel/runtime" "^7.20.13" "@react-pdf/fns" "3.1.2" "@react-pdf/primitives" "^4.1.1" - "@react-pdf/textkit" "^6.0.0" - "@react-pdf/types" "^2.9.1" + "@react-pdf/textkit" "^6.1.0" + "@react-pdf/types" "^2.9.2" abs-svg-path "^0.1.1" color-string "^1.9.1" normalize-svg-path "^1.1.0" parse-svg-path "^0.1.2" svg-arc-to-cubic-bezier "^3.2.0" -"@react-pdf/renderer@^4.3.0": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@react-pdf/renderer/-/renderer-4.3.1.tgz#0fdc86a0c5d7f92565cda3bef393ef5d374a7d5f" - integrity sha512-dPKHiwGTaOsKqNWCHPYYrx8CDfAGsUnV4tvRsEu0VPGxuot1AOq/M+YgfN/Pb+MeXCTe2/lv6NvA8haUtj3tsA== +"@react-pdf/renderer@^4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@react-pdf/renderer/-/renderer-4.3.2.tgz#6a08d9f19cb1221ef377fb15586db4547d59434d" + integrity sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ== dependencies: "@babel/runtime" "^7.20.13" "@react-pdf/fns" "3.1.2" - "@react-pdf/font" "^4.0.3" - "@react-pdf/layout" "^4.4.1" - "@react-pdf/pdfkit" "^4.0.4" + "@react-pdf/font" "^4.0.4" + "@react-pdf/layout" "^4.4.2" + "@react-pdf/pdfkit" "^4.1.0" "@react-pdf/primitives" "^4.1.1" - "@react-pdf/reconciler" "^1.1.4" - "@react-pdf/render" "^4.3.1" - "@react-pdf/types" "^2.9.1" + "@react-pdf/reconciler" "^2.0.0" + "@react-pdf/render" "^4.3.2" + "@react-pdf/types" "^2.9.2" events "^3.3.0" object-assign "^4.1.1" prop-types "^15.6.2" queue "^6.0.1" -"@react-pdf/stylesheet@^6.1.1": - version "6.1.1" - resolved "https://registry.yarnpkg.com/@react-pdf/stylesheet/-/stylesheet-6.1.1.tgz#ade1174e43ce8fc5fe3f8f6598a9426d853050ac" - integrity sha512-Iyw0A3wRIeQLN4EkaKf8yF9MvdMxiZ8JjoyzLzDHSxnKYoOA4UGu84veCb8dT9N8MxY5x7a0BUv/avTe586Plg== +"@react-pdf/stylesheet@^6.1.2": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@react-pdf/stylesheet/-/stylesheet-6.1.2.tgz#6ef21e2851ee7c2dc30582e7c01efb14f0308525" + integrity sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw== dependencies: "@react-pdf/fns" "3.1.2" - "@react-pdf/types" "^2.9.1" + "@react-pdf/types" "^2.9.2" color-string "^1.9.1" hsl-to-hex "^1.0.0" media-engine "^1.0.3" postcss-value-parser "^4.1.0" -"@react-pdf/textkit@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@react-pdf/textkit/-/textkit-6.0.0.tgz#87cd29aba8b0d81133dbbd61c52d8647fdf11616" - integrity sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw== +"@react-pdf/textkit@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@react-pdf/textkit/-/textkit-6.1.0.tgz#ff7667b4a67c98fecefbeabff221de4bafa37979" + integrity sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ== dependencies: "@react-pdf/fns" "3.1.2" bidi-js "^1.0.2" hyphen "^1.6.4" unicode-properties "^1.4.1" -"@react-pdf/types@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.9.1.tgz#d3265c019979cd256a423dba3ff0ca72cf86eefb" - integrity sha512-5GoCgG0G5NMgpPuHbKG2xcVRQt7+E5pg3IyzVIIozKG3nLcnsXW4zy25vG1ZBQA0jmo39q34au/sOnL/0d1A4w== +"@react-pdf/types@^2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.9.2.tgz#92aefa900b25bd3d0e87bb139346af545ed1ddfc" + integrity sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g== dependencies: - "@react-pdf/font" "^4.0.3" + "@react-pdf/font" "^4.0.4" "@react-pdf/primitives" "^4.1.1" - "@react-pdf/stylesheet" "^6.1.1" + "@react-pdf/stylesheet" "^6.1.2" "@react-spring/animated@~10.0.3": version "10.0.3" From 2508999b3e988f09d4ce8c35507d348d637fe9b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:47:44 +0000 Subject: [PATCH 006/177] Bump next from 16.1.2 to 16.1.6 Bumps [next](https://github.com/vercel/next.js) from 16.1.2 to 16.1.6. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v16.1.2...v16.1.6) --- updated-dependencies: - dependency-name: next dependency-version: 16.1.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 112 +++++++++++++++++++++++++-------------------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index 58019e25bb41..227981e58469 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "material-react-table": "^3.0.1", "monaco-editor": "^0.55.1", "mui-tiptap": "^1.14.0", - "next": "^16.1.2", + "next": "^16.1.6", "nprogress": "0.2.0", "numeral": "2.0.6", "prop-types": "15.8.1", diff --git a/yarn.lock b/yarn.lock index ca973629395e..2189bfcba960 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1495,10 +1495,10 @@ "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.10.0" -"@next/env@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/env/-/env-16.1.2.tgz#449128f465309fee4999cb9cc346a0bf33de6aad" - integrity sha512-r6TpLovDTvWtzw11UubUQxEK6IduT8rSAHbGX68yeFpA/1Oq9R4ovi5nqMUMgPN0jr2SpfeyFRbTZg3Inuuv3g== +"@next/env@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/env/-/env-16.1.6.tgz#0f85979498249a94ef606ef535042a831f905e89" + integrity sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ== "@next/eslint-plugin-next@15.5.2": version "15.5.2" @@ -1507,45 +1507,45 @@ dependencies: fast-glob "3.3.1" -"@next/swc-darwin-arm64@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.2.tgz#74f81d4d4d1d6d0b7cda28fdf86c60c0a9ffa00d" - integrity sha512-0N2baysDpTXASTVxTV+DkBnD97bo9PatUj8sHlKA+oR9CyvReaPQchQyhCbH0Jm0mC/Oka5F52intN+lNOhSlA== - -"@next/swc-darwin-x64@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.2.tgz#4ab440f5dbce4147e0044d2439a4cbf5264044e0" - integrity sha512-Q0wnSK0lmeC9ps+/w/bDsMSF3iWS45WEwF1bg8dvMH3CmKB2BV4346tVrjWxAkrZq20Ro6Of3R19IgrEJkXKyw== - -"@next/swc-linux-arm64-gnu@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.2.tgz#62007c34504caa6d65d5174df6d575bd406a40a5" - integrity sha512-4twW+h7ZatGKWq+2pUQ9SDiin6kfZE/mY+D8jOhSZ0NDzKhQfAPReXqwTDWVrNjvLzHzOcDL5kYjADHfXL/b/Q== - -"@next/swc-linux-arm64-musl@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.2.tgz#7dc9db5aa7d4dc705fc5831a0bf2210bd5dc8175" - integrity sha512-Sn6LxPIZcADe5AnqqMCfwBv6vRtDikhtrjwhu+19WM6jHZe31JDRcGuPZAlJrDk6aEbNBPUUAKmySJELkBOesg== - -"@next/swc-linux-x64-gnu@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.2.tgz#588913369dac3191bb2833bd1739acc1d5c2f642" - integrity sha512-nwzesEQBfQIOOnQ7JArzB08w9qwvBQ7nC1i8gb0tiEFH94apzQM3IRpY19MlE8RBHxc9ArG26t1DEg2aaLaqVQ== - -"@next/swc-linux-x64-musl@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.2.tgz#61c1466fe6e7a9176942ef9d276e9b5838d51c5f" - integrity sha512-s60bLf16BDoICQHeKEm0lDgUNMsL1UpQCkRNZk08ZNnRpK0QUV+6TvVHuBcIA7oItzU0m7kVmXe8QjXngYxJVA== - -"@next/swc-win32-arm64-msvc@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.2.tgz#6854380d1c8456c955b5352c18ddf44e2373345d" - integrity sha512-Sq8k4SZd8Y8EokKdz304TvMO9HoiwGzo0CTacaiN1bBtbJSQ1BIwKzNFeFdxOe93SHn1YGnKXG6Mq3N+tVooyQ== - -"@next/swc-win32-x64-msvc@16.1.2": - version "16.1.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.2.tgz#5b884d3b722ccd751b1482b575daa74ce5434dd0" - integrity sha512-KQDBwspSaNX5/wwt6p7ed5oINJWIxcgpuqJdDNubAyq7dD+ZM76NuEjg8yUxNOl5R4NNgbMfqE/RyNrsbYmOKg== +"@next/swc-darwin-arm64@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz#fbe1e360efdcc9ebd0a10301518275bc59e12a91" + integrity sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw== + +"@next/swc-darwin-x64@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz#0e3781ef3abc8251c2a21addc733d9a87f44829b" + integrity sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ== + +"@next/swc-linux-arm64-gnu@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz#b24511af2c6129f2deaf5c8c04d297fe09cd40d7" + integrity sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw== + +"@next/swc-linux-arm64-musl@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz#9d4ed0565689fc6a867250f994736a5b8c542ccb" + integrity sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ== + +"@next/swc-linux-x64-gnu@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz#cc757f4384e7eab7d3dba704a97f737518bae0d2" + integrity sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ== + +"@next/swc-linux-x64-musl@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz#ef1341740f29717deea7c6ec27ae6269386e20d1" + integrity sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg== + +"@next/swc-win32-arm64-msvc@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz#fee8719242aecf9c39c3a66f1f73821f7884dd16" + integrity sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw== + +"@next/swc-win32-x64-msvc@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz#60c27323c30f35722b20fd6d62449fbb768e46d9" + integrity sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A== "@nivo/colors@0.99.0": version "0.99.0" @@ -5955,26 +5955,26 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@^16.1.2: - version "16.1.2" - resolved "https://registry.yarnpkg.com/next/-/next-16.1.2.tgz#7b12fdc499448a809c3e6fe42ac3cbbcf543982d" - integrity sha512-SVSWX7wjUUDrIDVqhl4xm/jiOrvYGMG7NzVE/dGzzgs7r3dFGm4V19ia0xn3GDNtHCKM7C9h+5BoimnJBhmt9A== +next@^16.1.6: + version "16.1.6" + resolved "https://registry.yarnpkg.com/next/-/next-16.1.6.tgz#24a861371cbe211be7760d9a89ddf2415e3824de" + integrity sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw== dependencies: - "@next/env" "16.1.2" + "@next/env" "16.1.6" "@swc/helpers" "0.5.15" baseline-browser-mapping "^2.8.3" caniuse-lite "^1.0.30001579" postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "16.1.2" - "@next/swc-darwin-x64" "16.1.2" - "@next/swc-linux-arm64-gnu" "16.1.2" - "@next/swc-linux-arm64-musl" "16.1.2" - "@next/swc-linux-x64-gnu" "16.1.2" - "@next/swc-linux-x64-musl" "16.1.2" - "@next/swc-win32-arm64-msvc" "16.1.2" - "@next/swc-win32-x64-msvc" "16.1.2" + "@next/swc-darwin-arm64" "16.1.6" + "@next/swc-darwin-x64" "16.1.6" + "@next/swc-linux-arm64-gnu" "16.1.6" + "@next/swc-linux-arm64-musl" "16.1.6" + "@next/swc-linux-x64-gnu" "16.1.6" + "@next/swc-linux-x64-musl" "16.1.6" + "@next/swc-win32-arm64-msvc" "16.1.6" + "@next/swc-win32-x64-msvc" "16.1.6" sharp "^0.34.4" no-case@^3.0.4: From 3019d5cd282bf34ba6a13745a32baa0964be0ceb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:49:38 +0000 Subject: [PATCH 007/177] Bump jspdf-autotable from 5.0.2 to 5.0.7 Bumps [jspdf-autotable](https://github.com/simonbengtsson/jsPDF-AutoTable) from 5.0.2 to 5.0.7. - [Release notes](https://github.com/simonbengtsson/jsPDF-AutoTable/releases) - [Commits](https://github.com/simonbengtsson/jsPDF-AutoTable/compare/v5.0.2...v5.0.7) --- updated-dependencies: - dependency-name: jspdf-autotable dependency-version: 5.0.7 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 58019e25bb41..97b2ca53d2c8 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "i18next": "25.5.2", "javascript-time-ago": "^2.6.2", "jspdf": "^4.1.0", - "jspdf-autotable": "^5.0.2", + "jspdf-autotable": "^5.0.7", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", "leaflet.markercluster": "^1.5.3", diff --git a/yarn.lock b/yarn.lock index ca973629395e..b4e7a4972bd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5178,10 +5178,10 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jspdf-autotable@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz#bcf7aa2ff9eb46a2db6aa8c0407ab86c0a6c7b96" - integrity sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ== +jspdf-autotable@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz#c5970646dd5ae18801d97e3e91625c95783efbe4" + integrity sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw== jspdf@^4.1.0: version "4.1.0" From a30222539cdde550e7677b6b33955edf6e086011 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:49:42 +0000 Subject: [PATCH 008/177] Bump @uiw/react-json-view from 2.0.0-alpha.39 to 2.0.0-alpha.41 Bumps [@uiw/react-json-view](https://github.com/uiwjs/react-json-view) from 2.0.0-alpha.39 to 2.0.0-alpha.41. - [Release notes](https://github.com/uiwjs/react-json-view/releases) - [Commits](https://github.com/uiwjs/react-json-view/compare/v2.0.0-alpha.39...v2.0.0-alpha.41) --- updated-dependencies: - dependency-name: "@uiw/react-json-view" dependency-version: 2.0.0-alpha.41 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 58019e25bb41..3b45a972b4ac 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@tiptap/pm": "^3.4.1", "@tiptap/react": "^3.4.1", "@tiptap/starter-kit": "^3.19.0", - "@uiw/react-json-view": "^2.0.0-alpha.30", + "@uiw/react-json-view": "^2.0.0-alpha.41", "@vvo/tzdb": "^6.198.0", "apexcharts": "5.3.5", "axios": "^1.7.2", diff --git a/yarn.lock b/yarn.lock index ca973629395e..b838ee0fe333 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2683,10 +2683,10 @@ "@typescript-eslint/types" "8.50.0" eslint-visitor-keys "^4.2.1" -"@uiw/react-json-view@^2.0.0-alpha.30": - version "2.0.0-alpha.39" - resolved "https://registry.yarnpkg.com/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.39.tgz#bc050135c2763175a21c38359aa9458c6a8046df" - integrity sha512-D9MHNan56WhtdAsmjtE9x18YLY0JSMnh0a6Ji0/2sVXCF456ZVumYLdx2II7hLQOgRMa4QMaHloytpTUHxsFRw== +"@uiw/react-json-view@^2.0.0-alpha.41": + version "2.0.0-alpha.41" + resolved "https://registry.yarnpkg.com/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.41.tgz#54425c948175df5fd2155fa22a12cfb023f98773" + integrity sha512-botRpQ5AgymYEsqXSdT2/1LefAJEYfMntvdnx1SqhTQCTW9HygeFZXx9inkYqUmiQZ3+0QlZnodjBvwnUfZhVA== "@ungap/structured-clone@^1.0.0": version "1.3.0" From 2b570c8526146fca3639d90ef8f6117b4d67cffc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 12 Feb 2026 00:34:43 -0500 Subject: [PATCH 009/177] Add simple Graph Explorer filter & UI tweaks Introduce a simplified Graph Explorer filter and several UI/behavior improvements. Key changes: - Add CippGraphExplorerSimpleFilter component to provide a compact preset selector, Run/Edit Filters toggle, and view-mode switch (table/json). - Update graph-explorer page to use the simple filter, add viewMode state, fetch JSON results for a new JSON editor view, and show loading overlay when fetching. - Enhance CippGraphExplorerFilter: support parent-driven selectedPreset, onPresetSelect callback, hideButtons flag, sync preset selection with parent, refactor footer actions, and minor layout/spacing fixes. - Modify CippOffCanvas: add contentPadding and keepMounted props, apply padding to content container and allow controlling ModalProps.keepMounted. - Update CippButtonCard to hide header/divider when no title is provided. - Update CippCodeBlock to pass code via value and set the editor to readOnly. - Wire CIPPTableToptoolbar to pass selectedPreset/onPresetSelect and off-canvas options (contentPadding, keepMounted). These changes enable a lightweight preset-driven workflow, a JSON view for debugging, better off-canvas behavior, and improved preset synchronization between components. --- src/components/CippCards/CippButtonCard.jsx | 8 +- .../CippComponents/CippCodeBlock.jsx | 3 +- .../CippComponents/CippOffCanvas.jsx | 12 +- .../CippTable/CIPPTableToptoolbar.js | 12 + .../CippTable/CippGraphExplorerFilter.js | 256 ++++++++++++------ .../CippGraphExplorerSimpleFilter.js | 197 ++++++++++++++ .../tenant/tools/graph-explorer/index.js | 84 +++++- 7 files changed, 480 insertions(+), 92 deletions(-) create mode 100644 src/components/CippTable/CippGraphExplorerSimpleFilter.js diff --git a/src/components/CippCards/CippButtonCard.jsx b/src/components/CippCards/CippButtonCard.jsx index 73f1009e1ec7..ca9429af940b 100644 --- a/src/components/CippCards/CippButtonCard.jsx +++ b/src/components/CippCards/CippButtonCard.jsx @@ -42,8 +42,12 @@ export default function CippButtonCard({ {component === "card" && ( <> - - + {title && ( + <> + + + + )} {isFetching ? : children} diff --git a/src/components/CippComponents/CippCodeBlock.jsx b/src/components/CippComponents/CippCodeBlock.jsx index 507a26667bbd..bce1d9e7d65d 100644 --- a/src/components/CippComponents/CippCodeBlock.jsx +++ b/src/components/CippComponents/CippCodeBlock.jsx @@ -48,13 +48,14 @@ export const CippCodeBlock = (props) => { {type === "editor" && ( diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index b8e5b548e94d..abbe5aa682a4 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -23,6 +23,8 @@ export const CippOffCanvas = (props) => { onNavigateDown, canNavigateUp = false, canNavigateDown = false, + contentPadding = 2, + keepMounted = false, } = props; const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); @@ -80,7 +82,7 @@ export const CippOffCanvas = (props) => { sx: { width: drawerWidth }, }} ModalProps={{ - keepMounted: false, + keepMounted: keepMounted, }} anchor={"right"} open={visible} @@ -152,7 +154,13 @@ export const CippOffCanvas = (props) => { sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }} > {/* Render children if provided, otherwise render default content */} {typeof children === "function" ? children(extendedData) : children} diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 32c28c31e84a..a1a14f3ecc40 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -1322,10 +1322,22 @@ export const CIPPTableToptoolbar = ({ title="Edit Filters" visible={filterCanvasVisible} onClose={() => setFilterCanvasVisible(!filterCanvasVisible)} + contentPadding={1} + keepMounted={true} > f.filterName === activeFilterName) + : null + } + onPresetSelect={(preset) => { + if (preset?.value && preset?.type === "graph") { + setTableFilter(preset.value, preset.type, preset.filterName); + } + }} onSubmitFilter={(filter) => { setTableFilter(filter, "graph", "Custom Filter"); if (filter?.$select) { diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index 9d296e50ef2f..d95654de1594 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import { Button, Link, Typography } from "@mui/material"; +import { Box, Button, Link, Typography } from "@mui/material"; import { Save as SaveIcon, Delete, @@ -29,6 +29,9 @@ const CippGraphExplorerFilter = ({ onPresetChange, component = "accordion", relatedQueryKeys = [], + selectedPreset = null, + onPresetSelect, + hideButtons = false, }) => { const [offCanvasOpen, setOffCanvasOpen] = useState(false); const [cardExpanded, setCardExpanded] = useState(true); @@ -123,7 +126,7 @@ const CippGraphExplorerFilter = ({ .filter( (item) => !endpointFilter || - normalizeEndpoint(item.params.endpoint) === normalizeEndpoint(endpointFilter) + normalizeEndpoint(item.params.endpoint) === normalizeEndpoint(endpointFilter), ) .forEach((item) => { presetOptionList.push({ @@ -153,7 +156,7 @@ const CippGraphExplorerFilter = ({ propertyList.refetch(); } }, 1000), - [currentEndpoint] // Dependencies that the debounce function depends on + [currentEndpoint], // Dependencies that the debounce function depends on ); useEffect(() => { @@ -180,14 +183,29 @@ const CippGraphExplorerFilter = ({ }); }; + const deletePreset = (id) => { + savePresetApi.mutate({ + url: "/api/ExecGraphExplorerPreset", + data: { action: "Delete", preset: { id: selectedPresetState } }, + }); + }; + const selectedPresets = useWatch({ control: presetControl.control, name: "reportTemplate" }); + + // Sync with parent component's selected preset + useEffect(() => { + if (selectedPreset && selectedPreset.value !== selectedPresets?.value) { + presetControl.setValue("reportTemplate", selectedPreset); + } + }, [selectedPreset?.value]); + useEffect(() => { if (selectedPresets?.addedFields?.params) { setPresetOwner(selectedPresets?.addedFields?.IsMyPreset ?? false); Object.keys(selectedPresets.addedFields.params).forEach( (key) => selectedPresets.addedFields.params[key] == null && - delete selectedPresets.addedFields.params[key] + delete selectedPresets.addedFields.params[key], ); //if $select is a blank array, set it to a string. if ( @@ -233,6 +251,11 @@ const CippGraphExplorerFilter = ({ // save last preset title setLastPresetTitle(selectedPresets.label); formControl.reset(selectedPresets?.addedFields?.params, { keepDefaultValues: true }); + + // Notify parent when preset changes in this component + if (onPresetSelect) { + onPresetSelect(selectedPresets); + } } }, [selectedPresets]); @@ -375,7 +398,7 @@ const CippGraphExplorerFilter = ({ Schedule Graph Explorer Report - + , ); setOffCanvasOpen(true); }; @@ -488,17 +511,10 @@ const CippGraphExplorerFilter = ({ }; //console.log(cardExpanded); - const deletePreset = (id) => { - savePresetApi.mutate({ - url: "/api/ExecGraphExplorerPreset", - data: { action: "Delete", preset: { id: selectedPresetState } }, - }); - }; - return (
setCardExpanded(expanded)} @@ -507,76 +523,8 @@ const CippGraphExplorerFilter = ({ height: "100%", mb: 2, }} - CardButton={ - <> - - - - - - - - - {selectedPresetState && ( - - )} - - - - - - - } > - + - + {/* Reverse Tenant Lookup Switch */} + + {/* Footer-style action section */} + {!hideButtons && ( + + + + {component === "accordion" ? ( + + + + + + + + + + + + + + + ) : ( + + + + + + + + + + + + + + + + + + + + + + + )} + + + )} - { + const [offCanvasVisible, setOffCanvasVisible] = useState(false); + const [presetOptions, setPresetOptions] = useState([]); + const [currentFilterValues, setCurrentFilterValues] = useState(null); + + const presetControl = useForm({ + mode: "onChange", + defaultValues: { + reportTemplate: null, + }, + }); + + const selectedPreset = useWatch({ control: presetControl.control, name: "reportTemplate" }); + + // API call for available presets + const presetList = ApiGetCall({ + url: "/api/ListGraphExplorerPresets", + queryKey: "ListGraphExplorerPresets", + }); + + useEffect(() => { + var presetOptionList = []; + defaultPresets.forEach((item) => { + presetOptionList.push({ + label: item.name, + value: item.id, + addedFields: item, + type: "Built-In", + }); + }); + if (presetList.isSuccess && presetList.data?.Results.length > 0) { + presetList.data.Results.forEach((item) => { + presetOptionList.push({ + label: item.name, + value: item.id, + addedFields: item, + type: "Custom", + }); + }); + } + setPresetOptions(presetOptionList); + }, [defaultPresets, presetList.isSuccess, presetList.data]); + + const handleRunPreset = () => { + if (selectedPreset?.addedFields?.params) { + const params = selectedPreset.addedFields.params; + const values = { ...params }; + + // Handle $select array/string conversion + if (values.$select && Array.isArray(values.$select) && values.$select.length > 0) { + values.$select = values.$select + .map((item) => (typeof item === "string" ? item : item.value)) + .join(","); + } else if (values.$select === "") { + delete values.$select; + } + + // Handle version conversion + if (values.version && values.version.value) { + values.version = values.version.value; + } else if (!values.version) { + values.version = "beta"; + } + + // Clean up false boolean values + if (values.ReverseTenantLookup === false) { + delete values.ReverseTenantLookup; + } + if (values.NoPagination === false) { + delete values.NoPagination; + } + if (values.$count === false) { + delete values.$count; + } + if (values.AsApp === false) { + delete values.AsApp; + } + + // Remove null/empty values + Object.keys(values).forEach((key) => { + if (values[key] === null || values[key] === "") { + delete values[key]; + } + }); + + // Update page title if callback provided + if (onPresetChange && selectedPreset.label) { + onPresetChange(`Graph Explorer - ${selectedPreset.label}`); + } + + setCurrentFilterValues(values); + onSubmitFilter(values); + } + }; + + const handleFilterSubmit = (values) => { + setCurrentFilterValues(values); + onSubmitFilter(values); + setOffCanvasVisible(false); + }; + + const handlePresetChange = (preset) => { + presetControl.setValue("reportTemplate", preset); + }; + + return ( + <> + + + option.type} + renderGroup={(params) => ( +
  • + {params.group} + {params.children} +
  • + )} + placeholder="Select a preset to run" + /> +
    + + + + {onViewModeChange && ( + + )} + +
    + + setOffCanvasVisible(false)} + contentPadding={1} + > + + + + ); +}; + +export default CippGraphExplorerSimpleFilter; diff --git a/src/pages/tenant/tools/graph-explorer/index.js b/src/pages/tenant/tools/graph-explorer/index.js index 2ba20baa9332..3efb93604479 100644 --- a/src/pages/tenant/tools/graph-explorer/index.js +++ b/src/pages/tenant/tools/graph-explorer/index.js @@ -1,22 +1,100 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import CippGraphExplorerFilter from "../../../../components/CippTable/CippGraphExplorerFilter"; +import CippGraphExplorerSimpleFilter from "../../../../components/CippTable/CippGraphExplorerSimpleFilter"; import { useState } from "react"; -import { Grid } from "@mui/system"; +import { Grid, Stack, Box, Container } from "@mui/system"; import { useSettings } from "../../../../hooks/use-settings"; +import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; +import { ApiGetCallWithPagination } from "../../../../api/ApiCall"; +import { CircularProgress, Typography, Card } from "@mui/material"; +import { CippHead } from "../../../../components/CippComponents/CippHead"; const Page = () => { const [apiFilter, setApiFilter] = useState([]); const [pageTitle, setPageTitle] = useState("Graph Explorer"); + const [viewMode, setViewMode] = useState("table"); const tenantFilter = useSettings().currentTenant; const queryKey = JSON.stringify({ apiFilter, tenantFilter }); + const apiData = ApiGetCallWithPagination({ + url: apiFilter.endpoint ? "/api/ListGraphRequest" : "/api/ListEmptyResults", + data: apiFilter, + queryKey: queryKey, + waiting: !!apiFilter.endpoint, + }); + + const jsonData = apiData?.data?.pages?.[0]?.Results || apiData?.data || {}; + + if (viewMode === "json") { + return ( + <> + + + + + + + + + + + {pageTitle} + + + {apiData.isLoading || apiData.isFetching ? ( + + + + Loading data... + + + ) : null} + + + + + + + ); + } + return ( - + } From dfc303b142331f107a3184ba515688fd2f942120 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 12 Feb 2026 00:55:26 -0500 Subject: [PATCH 010/177] Rename filter UI to query and update icon Replace filter-focused wording and icon with query-focused alternatives in CippGraphExplorerSimpleFilter. Swapped import of FilterList for ManageSearch, updated the button icon and label from "Edit Filters" to "Edit Query", and changed the off-canvas title from "Graph Explorer Filters" to "Graph Explorer Query" to reflect the new terminology. --- src/components/CippTable/CippGraphExplorerSimpleFilter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/CippTable/CippGraphExplorerSimpleFilter.js b/src/components/CippTable/CippGraphExplorerSimpleFilter.js index cac7a80868f6..c6d5c751ba06 100644 --- a/src/components/CippTable/CippGraphExplorerSimpleFilter.js +++ b/src/components/CippTable/CippGraphExplorerSimpleFilter.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Button, Stack, Box } from "@mui/material"; -import { PlayCircle, FilterList, TableChart, Code } from "@mui/icons-material"; +import { PlayCircle, ManageSearch, TableChart, Code } from "@mui/icons-material"; import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "../CippComponents/CippFormComponent"; import { ApiGetCall } from "../../api/ApiCall"; @@ -155,11 +155,11 @@ const CippGraphExplorerSimpleFilter = ({ {onViewModeChange && ( diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/index.js b/src/pages/endpoint/MEM/reusable-settings-templates/index.js index 230f214e3fca..fe6a5810e995 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/index.js +++ b/src/pages/endpoint/MEM/reusable-settings-templates/index.js @@ -1,10 +1,10 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; import { Button } from "@mui/material"; import Link from "next/link"; import { AddBox, GitHub, Delete, Edit } from "@mui/icons-material"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall } from "../../../../api/ApiCall"; const Page = () => { const pageTitle = "Reusable Settings Templates"; diff --git a/src/pages/endpoint/MEM/reusable-settings/edit.jsx b/src/pages/endpoint/MEM/reusable-settings/edit.jsx index b6dcd1825ffa..3fe089ac520d 100644 --- a/src/pages/endpoint/MEM/reusable-settings/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings/edit.jsx @@ -3,13 +3,13 @@ import { Alert, Box, Stack } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import { useRouter } from "next/router"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import CippJsonView from "/src/components/CippFormPages/CippJSONView"; -import { ApiGetCall } from "/src/api/ApiCall"; -import { useSettings } from "/src/hooks/use-settings"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "../../../../components/CippFormPages/CippFormSkeleton"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useSettings } from "../../../../hooks/use-settings"; const EditReusableSetting = () => { const router = useRouter(); @@ -72,7 +72,9 @@ const EditReusableSetting = () => { return ( { return ( } + cardButton={ + + } apiUrl="/api/ListIntuneReusableSettings" queryKey={`ListIntuneReusableSettings-${currentTenant}`} actions={actions} From f004a437035ffc2c67dd8763e424945bd985288d Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 13 Feb 2026 14:01:18 +0100 Subject: [PATCH 013/177] universal search --- .../CippCards/CippUniversalSearchV2.jsx | 165 ++++++++++++++++++ src/pages/dashboardv2/index.js | 8 + 2 files changed, 173 insertions(+) create mode 100644 src/components/CippCards/CippUniversalSearchV2.jsx diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx new file mode 100644 index 000000000000..eff93da7ddb8 --- /dev/null +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -0,0 +1,165 @@ +import React, { useState } from "react"; +import { + TextField, + Box, + Typography, + Card, + CardContent, + CardActionArea, + Skeleton, + Button, + Link, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { ApiGetCall } from "../../api/ApiCall"; +import { useSettings } from "../../hooks/use-settings"; + +export const CippUniversalSearchV2 = React.forwardRef( + ({ onConfirm = () => {}, onChange = () => {}, maxResults = 10, value = "" }, ref) => { + const [searchValue, setSearchValue] = useState(value); + const settings = useSettings(); + const { currentTenant } = settings; + + const handleChange = (event) => { + const newValue = event.target.value; + setSearchValue(newValue); + onChange(newValue); + }; + + const search = ApiGetCall({ + url: `/api/ExecUniversalSearchV2`, + data: { + tenantFilter: currentTenant, + searchTerms: searchValue, + limit: maxResults, + }, + queryKey: `searchV2-${currentTenant}-${searchValue}`, + waiting: false, + }); + + const handleKeyDown = async (event) => { + if (event.key === "Enter") { + search.refetch(); + } + }; + + return ( + + + + {search.isFetching && ( + + + + )} + {search.isSuccess && search?.data?.length > 0 ? ( + + ) : ( + search.isSuccess && searchValue.length > 0 && "No results found." + )} + + ); + } +); + +CippUniversalSearchV2.displayName = "CippUniversalSearchV2"; + +const Results = ({ items = [], searchValue }) => { + const [currentPage, setCurrentPage] = useState(1); + const resultsPerPage = 9; + const totalResults = items.length; + const totalPages = Math.ceil(totalResults / resultsPerPage); + + const startIndex = (currentPage - 1) * resultsPerPage; + const endIndex = startIndex + resultsPerPage; + const displayedResults = items.slice(startIndex, endIndex); + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1); + } + }; + + const handlePreviousPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + return ( + <> + + {totalResults} results (Page {currentPage} of {totalPages}) + + + {displayedResults.map((item, key) => ( + + + + ))} + + {totalPages > 1 && ( + + + + + )} + + ); +}; + +const ResultsRow = ({ match, searchValue }) => { + const highlightMatch = (text) => { + if (!text || !searchValue) return text; + const escapedSearch = searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const parts = text?.split(new RegExp(`(${escapedSearch})`, "gi")); + return parts?.map((part, index) => + part.toLowerCase() === searchValue.toLowerCase() ? ( + + {part} + + ) : ( + part + ) + ); + }; + + const userData = match.Data || {}; + const tenantDomain = match.Tenant || ""; + + return ( + + + + {highlightMatch(userData.displayName || "")} + + {highlightMatch(userData.userPrincipalName || "")} + + + Found in tenant {tenantDomain} + + + + + ); +}; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 1a169586fe21..0d9fb9df4be3 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -8,6 +8,7 @@ import { ApiGetCall } from "../../api/ApiCall.jsx"; import Portals from "../../data/portals"; import { BulkActionsMenu } from "../../components/bulk-actions-menu.js"; import { ExecutiveReportButton } from "../../components/ExecutiveReportButton.js"; +import { CippUniversalSearchV2 } from "../../components/CippCards/CippUniversalSearchV2.jsx"; import { TabbedLayout } from "../../layouts/TabbedLayout"; import { Layout as DashboardLayout } from "../../layouts/index.js"; import tabOptions from "./tabOptions"; @@ -203,6 +204,13 @@ const Page = () => { return ( + {/* Universal Search */} + + + + + + From 719da202733123424f6573655d9c5b58545fd74e Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 13 Feb 2026 15:16:28 +0100 Subject: [PATCH 014/177] dropdown instead --- .../CippCards/CippUniversalSearchV2.jsx | 307 +++++++++++------- 1 file changed, 196 insertions(+), 111 deletions(-) diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index eff93da7ddb8..71ad3eb524f6 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -1,31 +1,32 @@ -import React, { useState } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { TextField, Box, Typography, - Card, - CardContent, - CardActionArea, Skeleton, - Button, - Link, + MenuItem, + ListItemText, + Paper, + CircularProgress, + InputAdornment, + Portal, } from "@mui/material"; -import { Grid } from "@mui/system"; +import { Search as SearchIcon } from "@mui/icons-material"; import { ApiGetCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; +import { useRouter } from "next/router"; export const CippUniversalSearchV2 = React.forwardRef( ({ onConfirm = () => {}, onChange = () => {}, maxResults = 10, value = "" }, ref) => { const [searchValue, setSearchValue] = useState(value); + const [showDropdown, setShowDropdown] = useState(false); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); + const containerRef = useRef(null); + const textFieldRef = useRef(null); + const router = useRouter(); const settings = useSettings(); const { currentTenant } = settings; - const handleChange = (event) => { - const newValue = event.target.value; - setSearchValue(newValue); - onChange(newValue); - }; - const search = ApiGetCall({ url: `/api/ExecUniversalSearchV2`, data: { @@ -37,129 +38,213 @@ export const CippUniversalSearchV2 = React.forwardRef( waiting: false, }); - const handleKeyDown = async (event) => { - if (event.key === "Enter") { + const handleChange = (event) => { + const newValue = event.target.value; + setSearchValue(newValue); + onChange(newValue); + + if (newValue.length === 0) { + setShowDropdown(false); + } + }; + + const updateDropdownPosition = () => { + if (textFieldRef.current) { + const rect = textFieldRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + window.scrollY + 4, + left: rect.left + window.scrollX, + width: rect.width, + }); + } + }; + + const handleKeyDown = (event) => { + if (event.key === "Enter" && searchValue.length > 0) { + updateDropdownPosition(); search.refetch(); + setShowDropdown(true); } }; + const handleResultClick = (match) => { + const userData = match.Data || {}; + const tenantDomain = match.Tenant || ""; + router.push( + `/identity/administration/users/user?tenantFilter=${tenantDomain}&userId=${userData.id}` + ); + setShowDropdown(false); + }; + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target) && + !event.target.closest('[data-dropdown-portal]') + ) { + setShowDropdown(false); + } + }; + + if (showDropdown) { + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + } + }, [showDropdown]); + + // Update position on scroll/resize + useEffect(() => { + if (showDropdown) { + updateDropdownPosition(); + const handleScroll = () => updateDropdownPosition(); + const handleResize = () => updateDropdownPosition(); + window.addEventListener("scroll", handleScroll, true); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("scroll", handleScroll, true); + window.removeEventListener("resize", handleResize); + }; + } + }, [showDropdown]); + + const hasResults = Array.isArray(search?.data) && search.data.length > 0; + const shouldShowDropdown = showDropdown && searchValue.length > 0; + return ( - - - - {search.isFetching && ( - - - - )} - {search.isSuccess && search?.data?.length > 0 ? ( - - ) : ( - search.isSuccess && searchValue.length > 0 && "No results found." + <> + + { + textFieldRef.current = node; + if (typeof ref === "function") { + ref(node); + } else if (ref) { + ref.current = node; + } + }} + fullWidth + type="text" + label="Search users by UPN or Display Name..." + onKeyDown={handleKeyDown} + onChange={handleChange} + value={searchValue} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: search.isFetching ? ( + + + + ) : null, + }} + /> + + + {shouldShowDropdown && ( + + + {search.isFetching ? ( + + + + + ) : hasResults ? ( + + ) : ( + + + No results found. + + + )} + + )} - + ); } ); CippUniversalSearchV2.displayName = "CippUniversalSearchV2"; -const Results = ({ items = [], searchValue }) => { - const [currentPage, setCurrentPage] = useState(1); - const resultsPerPage = 9; - const totalResults = items.length; - const totalPages = Math.ceil(totalResults / resultsPerPage); - - const startIndex = (currentPage - 1) * resultsPerPage; - const endIndex = startIndex + resultsPerPage; - const displayedResults = items.slice(startIndex, endIndex); - - const handleNextPage = () => { - if (currentPage < totalPages) { - setCurrentPage(currentPage + 1); - } - }; - - const handlePreviousPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; - - return ( - <> - - {totalResults} results (Page {currentPage} of {totalPages}) - - - {displayedResults.map((item, key) => ( - - - - ))} - - {totalPages > 1 && ( - - - - - )} - - ); -}; - -const ResultsRow = ({ match, searchValue }) => { +const Results = ({ items = [], searchValue, onResultClick }) => { const highlightMatch = (text) => { if (!text || !searchValue) return text; const escapedSearch = searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const parts = text?.split(new RegExp(`(${escapedSearch})`, "gi")); return parts?.map((part, index) => part.toLowerCase() === searchValue.toLowerCase() ? ( - + {part} - + ) : ( part ) ); }; - const userData = match.Data || {}; - const tenantDomain = match.Tenant || ""; - return ( - - - - {highlightMatch(userData.displayName || "")} - - {highlightMatch(userData.userPrincipalName || "")} - - - Found in tenant {tenantDomain} - - - - + <> + {items.map((match, index) => { + const userData = match.Data || {}; + const tenantDomain = match.Tenant || ""; + + return ( + onResultClick(match)} + sx={{ + py: 1.5, + px: 2, + borderBottom: index < items.length - 1 ? "1px solid" : "none", + borderColor: "divider", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + {highlightMatch(userData.displayName || "")} + + } + secondary={ + + + {highlightMatch(userData.userPrincipalName || "")} + + + Tenant: {tenantDomain} + + + } + /> + + ); + })} + ); }; From 2a28b18e3c759ad15744491b62112080ac11b5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Feb 2026 18:57:13 +0100 Subject: [PATCH 015/177] feat: add DEP sync functionality and dialog component - Introduced CippApiDialog for syncing DEP tokens. --- src/pages/endpoint/MEM/devices/index.js | 73 +++++++++++++++++-------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index f2f905a968ce..9cd56f210a9f 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -1,7 +1,10 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { useSettings } from "../../../../hooks/use-settings"; +import { useDialog } from "../../../../hooks/use-dialog.js"; import { EyeIcon } from "@heroicons/react/24/outline"; +import { Box, Button } from "@mui/material"; import { Sync, RestartAlt, @@ -22,6 +25,7 @@ import { const Page = () => { const pageTitle = "Devices"; const tenantFilter = useSettings().currentTenant; + const depSyncDialog = useDialog(); const actions = [ { @@ -373,31 +377,52 @@ const Page = () => { actions: actions, }; + const simpleColumns = [ + "deviceName", + "userPrincipalName", + "complianceState", + "manufacturer", + "model", + "operatingSystem", + "osVersion", + "enrolledDateTime", + "managedDeviceOwnerType", + "deviceEnrollmentType", + "joinType", + ]; + return ( - + <> + + + + } + /> + + ); }; From cb4ef7c5d18b7f837fa8b4a596521aa87fa09726 Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:30:51 -0500 Subject: [PATCH 016/177] fix(reusable-settings): refactor add and edit templates --- ...ppAddIntuneReusableSettingTemplateForm.jsx | 54 --- .../MEM/reusable-settings-templates/add.jsx | 345 +++++++++++++++++- .../MEM/reusable-settings-templates/edit.jsx | 13 +- 3 files changed, 350 insertions(+), 62 deletions(-) delete mode 100644 src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx diff --git a/src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx b/src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx deleted file mode 100644 index ec96e59f2765..000000000000 --- a/src/components/CippFormPages/CippAddIntuneReusableSettingTemplateForm.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Grid } from "@mui/system"; -import CippFormComponent from "../CippComponents/CippFormComponent"; - -const CippAddIntuneReusableSettingTemplateForm = ({ formControl }) => { - return ( - - - - - - - - - - - - - - - - - - - ); -}; - -export default CippAddIntuneReusableSettingTemplateForm; diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx index 186389793d25..35ec60797ead 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx +++ b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx @@ -1,20 +1,255 @@ -import { Box } from "@mui/material"; -import { useForm } from "react-hook-form"; +import { + Box, + Button, + Divider, + Stack, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { useFieldArray, useForm } from "react-hook-form"; +import { useMemo } from "react"; import { useSettings } from "../../../../hooks/use-settings"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import CippAddIntuneReusableSettingTemplateForm from "../../../../components/CippFormPages/CippAddIntuneReusableSettingTemplateForm"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; const Page = () => { const userSettingsDefaults = useSettings(); + const baseSettingDefinitionId = "vendor_msft_firewall_mdmstore_dynamickeywords_addresses_{id}"; + + const generateGuid = () => { + const wrap = (val) => `{${val}}`; + if (typeof crypto !== "undefined" && crypto.randomUUID) return wrap(crypto.randomUUID()); + const s4 = () => + Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + return wrap(`${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`); + }; + + const buildInitialGroupEntry = () => ({ + children: [ + { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: `${baseSettingDefinitionId}_id`, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: generateGuid(), + }, + }, + { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance", + settingDefinitionId: `${baseSettingDefinitionId}_autoresolve`, + choiceSettingValue: { value: "", children: [] }, + }, + { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: `${baseSettingDefinitionId}_keyword`, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: "", + }, + }, + ], + }); + + const initialGroupCollection = [buildInitialGroupEntry()]; + const initialParsedRaw = { + settingDefinitionId: baseSettingDefinitionId, + settingInstance: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance", + settingDefinitionId: baseSettingDefinitionId, + groupSettingCollectionValue: [buildInitialGroupEntry()], + }, + }; + const formControl = useForm({ mode: "onChange", defaultValues: { tenantFilter: userSettingsDefaults.currentTenant, + parsedRAWJson: initialParsedRaw, + groupSettingCollectionValue: initialGroupCollection, }, }); + const customDataFormatter = useMemo(() => { + const extractValues = (obj) => { + if (obj === null || obj === undefined) return obj; + + if ( + obj && + typeof obj === "object" && + obj.hasOwnProperty("value") && + obj.hasOwnProperty("label") + ) { + return obj.value; + } + + if (Array.isArray(obj)) { + return obj.map((item) => extractValues(item)); + } + + if (typeof obj === "object") { + const result = {}; + Object.keys(obj).forEach((key) => { + result[key] = extractValues(obj[key]); + }); + return result; + } + + return obj; + }; + + return (values) => { + const processedValues = extractValues(values) || {}; + const baseRaw = processedValues.parsedRAWJson || {}; + const normalizeCollection = (collection) => { + if (!collection) return undefined; + return Array.isArray(collection) ? collection : [collection]; + }; + const ensureIdValues = (collection) => { + if (!collection) return collection; + return collection.map((entry) => { + if (!entry?.children || !Array.isArray(entry.children)) return entry; + const nextChildren = entry.children.map((child) => { + if ( + child?.settingDefinitionId?.toLowerCase().endsWith("_id") && + child?.simpleSettingValue && + !child.simpleSettingValue.value + ) { + return { + ...child, + simpleSettingValue: { + ...child.simpleSettingValue, + value: generateGuid(), + }, + }; + } + return child; + }); + return { ...entry, children: nextChildren }; + }); + }; + const deriveBaseSettingDefinitionId = (collection) => { + if (!collection?.length) return undefined; + const firstChildren = collection[0]?.children || []; + const firstDef = firstChildren.find((child) => child?.settingDefinitionId)?.settingDefinitionId; + if (!firstDef) return undefined; + return firstDef + .replace(/_id$/i, "") + .replace(/_autoresolve$/i, "") + .replace(/_keyword$/i, ""); + }; + + if (!baseRaw.settingInstance) { + baseRaw.settingInstance = {}; + } + + if (processedValues.displayName) { + baseRaw.displayName = processedValues.displayName; + } + if (processedValues.description) { + baseRaw.description = processedValues.description; + } + const normalizedCollection = normalizeCollection( + processedValues.groupSettingCollectionValue ?? + baseRaw?.settingInstance?.groupSettingCollectionValue, + ); + + const normalizedWithIds = ensureIdValues(normalizedCollection); + + if (normalizedWithIds) { + baseRaw.settingInstance.groupSettingCollectionValue = normalizedWithIds; + } + + if (baseRaw.settingInstance.groupSettingCollectionValue) { + if (!baseRaw.settingInstance["@odata.type"]) { + baseRaw.settingInstance["@odata.type"] = + "#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance"; + } + + const resolvedBaseDefinitionId = + baseRaw.settingDefinitionId || deriveBaseSettingDefinitionId(normalizedWithIds); + + if (!baseRaw.settingDefinitionId) { + baseRaw.settingDefinitionId = resolvedBaseDefinitionId; + } + + if (!baseRaw.settingInstance.settingDefinitionId) { + baseRaw.settingInstance.settingDefinitionId = resolvedBaseDefinitionId; + } + } + + return { + GUID: processedValues.GUID, + displayName: processedValues.displayName, + description: processedValues.description, + rawJSON: JSON.stringify(baseRaw, null, 2), + tenantFilter: processedValues.tenantFilter || userSettingsDefaults.currentTenant, + }; + }; + }, [userSettingsDefaults.currentTenant]); + + const { fields, append, remove } = useFieldArray({ + control: formControl.control, + name: "groupSettingCollectionValue", + }); + + const groupChildDefinitions = useMemo(() => { + const first = fields?.[0]?.children || []; + return { + idDef: first.find((c) => c.settingDefinitionId?.toLowerCase().includes("_id")) + ?.settingDefinitionId, + autoresolveDef: first.find((c) => + c.settingDefinitionId?.toLowerCase().includes("_autoresolve"), + )?.settingDefinitionId, + keywordDef: first.find((c) => c.settingDefinitionId?.toLowerCase().includes("_keyword")) + ?.settingDefinitionId, + }; + }, [fields]); + + const createEmptyEntry = () => { + const { idDef, autoresolveDef, keywordDef } = groupChildDefinitions; + const children = []; + + if (idDef) { + children.push({ + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: idDef, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: "", + }, + }); + } + + if (autoresolveDef) { + children.push({ + "@odata.type": "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance", + settingDefinitionId: autoresolveDef, + choiceSettingValue: { value: "", children: [] }, + }); + } + + if (keywordDef) { + children.push({ + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: keywordDef, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: "", + }, + }); + } + + return { children }; + }; + return ( { title="Reusable Settings Template" backButtonTitle="Reusable Settings Templates" postUrl="/api/AddIntuneReusableSettingTemplate" + backUrl="/endpoint/MEM/reusable-settings-templates" + customDataformatter={customDataFormatter} + formPageType="Add" > - + + Template + + + + + + + {fields?.length > 0 && ( + + + Group Setting Collection (Policy) + + + + + ID + Autoresolve + Keyword + Actions + + + + {fields.map((field, index) => { + const idPath = `groupSettingCollectionValue.${index}.children.0.simpleSettingValue.value`; + const autoresolvePath = `groupSettingCollectionValue.${index}.children.1.choiceSettingValue.value`; + const keywordPath = `groupSettingCollectionValue.${index}.children.2.simpleSettingValue.value`; + + const autoresolveBase = groupChildDefinitions.autoresolveDef || "autoresolve"; + const autoresolveTrue = `${autoresolveBase}_true`; + const autoresolveFalse = `${autoresolveBase}_false`; + + return ( + + + + + + + + + + + + + + + ); + })} + +
    + + + +
    + )}
    ); diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx index 40554697f4ec..001d303620a2 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx @@ -34,6 +34,11 @@ const generateGuid = () => { return wrap(`${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`); }; +const normalizeCollection = (collection) => { + if (!collection) return []; + return Array.isArray(collection) ? collection : [collection]; +}; + const EditReusableSettingsTemplate = () => { const router = useRouter(); const { id: rawId } = router.query; @@ -93,11 +98,11 @@ const EditReusableSettingsTemplate = () => { }, [parsedRaw]); const groupCollection = useMemo(() => { - return ( + const source = parsedRaw?.settingInstance?.groupSettingCollectionValue || templateData?.settingInstance?.groupSettingCollectionValue || - [] - ); + []; + return normalizeCollection(source); }, [parsedRaw, templateData]); const groupChildDefinitions = useMemo(() => { @@ -278,7 +283,7 @@ const EditReusableSettingsTemplate = () => { normalizedTemplate?.displayName || normalizedTemplate?.name || normalizedTemplate?.Displayname || - "Edit Reusable Settings Template" + "Reusable Settings Template" } formControl={formControl} queryKey={[`ReusableSettingTemplate-${normalizedId}`, "ListIntuneReusableSettingTemplates"]} From 75e329eb55d23eca7134bb3a78d960669b4be020 Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:47:16 -0500 Subject: [PATCH 017/177] refactor(reusable-settings): simplify group entry creation --- .../MEM/reusable-settings-templates/add.jsx | 116 ++++++++++-------- .../MEM/reusable-settings-templates/edit.jsx | 81 +++++++----- 2 files changed, 113 insertions(+), 84 deletions(-) diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx index 35ec60797ead..481499eb557d 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx +++ b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx @@ -32,31 +32,80 @@ const Page = () => { return wrap(`${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`); }; - const buildInitialGroupEntry = () => ({ - children: [ - { + const buildGroupEntryFromBase = (baseId, options = {}) => { + const { idValue = generateGuid(), autoresolveValue = "", keywordValue = "" } = options; + return { + children: [ + { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: `${baseId}_id`, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: idValue, + }, + }, + { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance", + settingDefinitionId: `${baseId}_autoresolve`, + choiceSettingValue: { value: autoresolveValue, children: [] }, + }, + { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: `${baseId}_keyword`, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: keywordValue, + }, + }, + ], + }; + }; + + const buildGroupEntryFromDefinitions = ({ + idDef, + autoresolveDef, + keywordDef, + idValue = "", + autoresolveValue = "", + keywordValue = "", + } = {}) => { + const children = []; + + if (idDef) { + children.push({ "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", - settingDefinitionId: `${baseSettingDefinitionId}_id`, + settingDefinitionId: idDef, simpleSettingValue: { "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", - value: generateGuid(), + value: idValue, }, - }, - { + }); + } + + if (autoresolveDef) { + children.push({ "@odata.type": "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance", - settingDefinitionId: `${baseSettingDefinitionId}_autoresolve`, - choiceSettingValue: { value: "", children: [] }, - }, - { + settingDefinitionId: autoresolveDef, + choiceSettingValue: { value: autoresolveValue, children: [] }, + }); + } + + if (keywordDef) { + children.push({ "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", - settingDefinitionId: `${baseSettingDefinitionId}_keyword`, + settingDefinitionId: keywordDef, simpleSettingValue: { "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", - value: "", + value: keywordValue, }, - }, - ], - }); + }); + } + + return { children }; + }; + + const buildInitialGroupEntry = () => + buildGroupEntryFromBase(baseSettingDefinitionId, { idValue: generateGuid() }); const initialGroupCollection = [buildInitialGroupEntry()]; const initialParsedRaw = { @@ -214,40 +263,7 @@ const Page = () => { }, [fields]); const createEmptyEntry = () => { - const { idDef, autoresolveDef, keywordDef } = groupChildDefinitions; - const children = []; - - if (idDef) { - children.push({ - "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", - settingDefinitionId: idDef, - simpleSettingValue: { - "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", - value: "", - }, - }); - } - - if (autoresolveDef) { - children.push({ - "@odata.type": "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance", - settingDefinitionId: autoresolveDef, - choiceSettingValue: { value: "", children: [] }, - }); - } - - if (keywordDef) { - children.push({ - "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", - settingDefinitionId: keywordDef, - simpleSettingValue: { - "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", - value: "", - }, - }); - } - - return { children }; + return buildGroupEntryFromDefinitions(groupChildDefinitions); }; return ( diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx index 001d303620a2..046a14a70d20 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx @@ -34,6 +34,49 @@ const generateGuid = () => { return wrap(`${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`); }; +const buildGroupEntryFromDefinitions = ({ + idDef, + autoresolveDef, + keywordDef, + idValue = "", + autoresolveValue = "", + keywordValue = "", +} = {}) => { + const children = []; + + if (idDef) { + children.push({ + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: idDef, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: idValue, + }, + }); + } + + if (autoresolveDef) { + children.push({ + "@odata.type": "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance", + settingDefinitionId: autoresolveDef, + choiceSettingValue: { value: autoresolveValue, children: [] }, + }); + } + + if (keywordDef) { + children.push({ + "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", + settingDefinitionId: keywordDef, + simpleSettingValue: { + "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", + value: keywordValue, + }, + }); + } + + return { children }; +}; + const normalizeCollection = (collection) => { if (!collection) return []; return Array.isArray(collection) ? collection : [collection]; @@ -241,40 +284,10 @@ const EditReusableSettingsTemplate = () => { }); const createEmptyEntry = () => { - const { idDef, autoresolveDef, keywordDef } = groupChildDefinitions; - const children = []; - - if (idDef) { - children.push({ - "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", - settingDefinitionId: idDef, - simpleSettingValue: { - "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", - value: generateGuid(), - }, - }); - } - - if (autoresolveDef) { - children.push({ - "@odata.type": "#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance", - settingDefinitionId: autoresolveDef, - choiceSettingValue: { value: "", children: [] }, - }); - } - - if (keywordDef) { - children.push({ - "@odata.type": "#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance", - settingDefinitionId: keywordDef, - simpleSettingValue: { - "@odata.type": "#microsoft.graph.deviceManagementConfigurationStringSettingValue", - value: "", - }, - }); - } - - return { children }; + return buildGroupEntryFromDefinitions({ + ...groupChildDefinitions, + idValue: generateGuid(), + }); }; return ( From f610dc68e90a2b9f2d3c53571018cd0686fb7cbb Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:34:44 +0100 Subject: [PATCH 018/177] feat: add assignment filter options to policy forms - Introduced assignment filter and filter type options in CippPolicyDeployDrawer, CippIntunePolicy, and various policy list pages. - Updated data formatting to include selected assignment filter and type in API requests. - Enhanced user interface with radio buttons for filter mode selection. Fixes #5344 --- .../CippComponents/CippPolicyDeployDrawer.jsx | 49 +++++++- .../CippWizard/CippIntunePolicy.jsx | 41 +++++++ .../MEM/list-appprotection-policies/index.js | 108 ++++++++++++++++++ .../MEM/list-compliance-policies/index.js | 105 +++++++++++++++++ src/pages/endpoint/MEM/list-policies/index.js | 105 +++++++++++++++++ 5 files changed, 407 insertions(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippPolicyDeployDrawer.jsx b/src/components/CippComponents/CippPolicyDeployDrawer.jsx index 619f516496cd..669ef80075fb 100644 --- a/src/components/CippComponents/CippPolicyDeployDrawer.jsx +++ b/src/components/CippComponents/CippPolicyDeployDrawer.jsx @@ -12,6 +12,11 @@ import { CippApiResults } from "./CippApiResults"; import { useSettings } from "../../hooks/use-settings"; import { CippFormTenantSelector } from "./CippFormTenantSelector"; +const assignmentFilterTypeOptions = [ + { label: "Include - Apply policy to devices matching filter", value: "include" }, + { label: "Exclude - Apply policy to devices NOT matching filter", value: "exclude" }, +]; + export const CippPolicyDeployDrawer = ({ buttonText = "Deploy Policy", requiredPermissions = [], @@ -59,6 +64,10 @@ export const CippPolicyDeployDrawer = ({ } const formData = formControl.getValues(); + const assignmentFilterName = formData?.assignmentFilter?.value || null; + const assignmentFilterType = assignmentFilterName + ? formData?.assignmentFilterType || "include" + : null; console.log("Submitting form data:", formData); deployPolicy.mutate({ url: "/api/AddPolicy", @@ -68,7 +77,11 @@ export const CippPolicyDeployDrawer = ({ "Compliance Policies", "Protection Policies", ], - data: { ...formData }, + data: { + ...formData, + AssignmentFilterName: assignmentFilterName, + AssignmentFilterType: assignmentFilterType, + }, }); }; @@ -177,6 +190,40 @@ export const CippPolicyDeployDrawer = ({ /> + + + filter.displayName, + valueField: "displayName", + }} + /> + + + + + { const { formControl, onPreviousStep, onNextStep, currentStep } = props; const values = formControl.getValues(); + const tenantFilter = useSettings()?.currentTenant; const CATemplates = ApiGetCall({ url: "/api/ListIntuneTemplates", queryKey: "IntuneTemplates" }); const [JSONData, setJSONData] = useState(); const watcher = useWatch({ control: formControl.control, name: "TemplateList" }); @@ -111,6 +118,40 @@ export const CippIntunePolicy = (props) => { /> + + + filter.displayName, + valueField: "displayName", + }} + /> + + + + + { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: row?.URLName, + platformType: "deviceAppManagement", + AssignTo: "allLicensedUsers", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users?', icon: , color: "info", @@ -79,7 +115,43 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: row?.URLName, + platformType: "deviceAppManagement", + AssignTo: "AllDevices", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', icon: , color: "info", @@ -104,7 +176,43 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: row?.URLName, + platformType: "deviceAppManagement", + AssignTo: "AllDevicesAndUsers", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', icon: , color: "info", diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js index dcb37a6eadc7..54004aef6124 100644 --- a/src/pages/endpoint/MEM/list-compliance-policies/index.js +++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js @@ -53,7 +53,42 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: "deviceCompliancePolicies", + AssignTo: "allLicensedUsers", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users?', icon: , color: "info", @@ -77,7 +112,42 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: "deviceCompliancePolicies", + AssignTo: "AllDevices", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', icon: , color: "info", @@ -101,7 +171,42 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: "deviceCompliancePolicies", + AssignTo: "AllDevicesAndUsers", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', icon: , color: "info", diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js index ec6fa08d0d08..375d4b69d1cd 100644 --- a/src/pages/endpoint/MEM/list-policies/index.js +++ b/src/pages/endpoint/MEM/list-policies/index.js @@ -53,7 +53,42 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: row?.URLName, + AssignTo: "allLicensedUsers", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users?', icon: , color: "info", @@ -77,7 +112,42 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: row?.URLName, + AssignTo: "AllDevices", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', icon: , color: "info", @@ -101,7 +171,42 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.", }, + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + type: row?.URLName, + AssignTo: "AllDevicesAndUsers", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', icon: , color: "info", From 4be16f8afa1492f6cba550214ac2f02f06495652 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:35:31 +0100 Subject: [PATCH 019/177] fixes #5309 --- src/components/CippTable/CippDataTable.js | 31 +++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 798adb751ba6..132e3f047842 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -12,7 +12,7 @@ import { import { ResourceUnavailable } from "../resource-unavailable"; import { ResourceError } from "../resource-error"; import { Scrollbar } from "../scrollbar"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useRef } from "react"; import { ApiGetCallWithPagination } from "../../api/ApiCall"; import { utilTableMode } from "./util-tablemode"; import { utilColumnsFromAPI, resolveSimpleColumnVariables } from "./util-columnsFromAPI"; @@ -124,6 +124,11 @@ export const CippDataTable = (props) => { return acc; }, {}); }, [filters]); + + // Track if initial filters have been applied + const filtersInitializedRef = useRef(false); + const previousFiltersRef = useRef(null); + const [columnVisibility, setColumnVisibility] = useState(initialColumnVisibility); const [configuredSimpleColumns, setConfiguredSimpleColumns] = useState(simpleColumns); const [usedData, setUsedData] = useState(data); @@ -151,7 +156,10 @@ export const CippDataTable = (props) => { }); useEffect(() => { - if (filters && Array.isArray(filters) && filters.length > 0) { + // Only set initial filters if they haven't been set yet OR if the filters prop has actually changed + const filtersChanged = !isEqual(filters, previousFiltersRef.current); + + if (filters && Array.isArray(filters) && filters.length > 0 && (!filtersInitializedRef.current || filtersChanged)) { // Process filters to add filterFn based on filterType const processedFilters = filters.map((filter) => { if (filter.filterType === "equal") { @@ -165,6 +173,8 @@ export const CippDataTable = (props) => { return filter; }); setColumnFilters(processedFilters); + filtersInitializedRef.current = true; + previousFiltersRef.current = filters; } }, [filters]); @@ -740,20 +750,9 @@ export const CippDataTable = (props) => { }, }); - useEffect(() => { - if (filters && Array.isArray(filters) && filters.length > 0 && memoizedColumns.length > 0) { - // Make sure the table and columns are ready - setTimeout(() => { - if (table && typeof table.setColumnFilters === "function") { - const formattedFilters = filters.map((filter) => ({ - id: filter.id || filter.columnId, - value: filter.value, - })); - table.setColumnFilters(formattedFilters); - } - }); - } - }, [filters, memoizedColumns, table]); + // Remove the useEffect that was resetting filters on table changes + // The initial filter application is now handled by the columnFilters state + // and the useEffect above that only triggers on actual filter prop changes useEffect(() => { if (onChange && table.getSelectedRowModel().rows) { From fc65fdb171cbc2c41f0aa8c2c9e64c12e877f8b7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:35:36 +0100 Subject: [PATCH 020/177] 5309 --- src/components/CippTable/CippDataTable.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 132e3f047842..4baad9dca6a9 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -124,11 +124,11 @@ export const CippDataTable = (props) => { return acc; }, {}); }, [filters]); - + // Track if initial filters have been applied const filtersInitializedRef = useRef(false); const previousFiltersRef = useRef(null); - + const [columnVisibility, setColumnVisibility] = useState(initialColumnVisibility); const [configuredSimpleColumns, setConfiguredSimpleColumns] = useState(simpleColumns); const [usedData, setUsedData] = useState(data); @@ -158,8 +158,13 @@ export const CippDataTable = (props) => { useEffect(() => { // Only set initial filters if they haven't been set yet OR if the filters prop has actually changed const filtersChanged = !isEqual(filters, previousFiltersRef.current); - - if (filters && Array.isArray(filters) && filters.length > 0 && (!filtersInitializedRef.current || filtersChanged)) { + + if ( + filters && + Array.isArray(filters) && + filters.length > 0 && + (!filtersInitializedRef.current || filtersChanged) + ) { // Process filters to add filterFn based on filterType const processedFilters = filters.map((filter) => { if (filter.filterType === "equal") { From 4dd1e8f07a85ec34b6e2c502e4264dcdad203000 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:05:41 +0100 Subject: [PATCH 021/177] Alert add --- src/data/alerts.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index b17893a7ddee..8608e813ad75 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -9,6 +9,11 @@ "label": "Alert on admins without any form of MFA", "recommendedRunInterval": "1d" }, + { + "name": "NewMFADevice", + "label": "Alert when users register new MFA devices", + "recommendedRunInterval": "1h" + }, { "name": "LicenseAssignmentErrors", "label": "Alert on license assignment errors", From b91de068b7851b2ccce28345c777360b8de310f0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:05:50 +0100 Subject: [PATCH 022/177] alert add --- 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 8608e813ad75..4d6a811dc36e 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -53,7 +53,7 @@ } ] }, - { + { "name": "InactiveGuestUsers", "label": "Alert on guest users that have not logged in for X days", "recommendedRunInterval": "1d", @@ -72,7 +72,7 @@ } ] }, - { + { "name": "InactiveUsers", "label": "Alert on users that have not logged in for X days", "recommendedRunInterval": "1d", @@ -485,7 +485,7 @@ "recommendedRunInterval": "30m", "description": "Monitors for user requests to release quarantined messages and provides a CIPP-native alternative to the external email forwarding method. This helps MSPs maintain secure configurations while getting timely notifications about quarantine activity. Links to the tenant's quarantine page are provided in alerts." }, - { + { "name": "StaleEntraDevices", "label": "Alert on stale Entra devices that have not been active for X days", "recommendedRunInterval": "1d", From 54082c982eb7ab633665300c6e459db40dcc3a3f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:00:38 +0100 Subject: [PATCH 023/177] Fixes button issue --- src/components/CippTable/CIPPTableToptoolbar.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index a1a14f3ecc40..7ea4bb463d83 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -1059,7 +1059,7 @@ export const CIPPTableToptoolbar = ({ { // Trigger CSV export - const csvButton = document.querySelector("[data-csv-export]"); + const csvButton = document.querySelector(`[data-csv-export="${title}"]`); if (csvButton) csvButton.click(); setExportAnchor(null); }} @@ -1072,7 +1072,7 @@ export const CIPPTableToptoolbar = ({ { // Trigger PDF export - const pdfButton = document.querySelector("[data-pdf-export]"); + const pdfButton = document.querySelector(`[data-pdf-export="${title}"]`); if (pdfButton) pdfButton.click(); setExportAnchor(null); }} @@ -1204,14 +1204,14 @@ export const CIPPTableToptoolbar = ({ columns={usedColumns} reportName={title} columnVisibility={columnVisibility} - data-pdf-export + data-pdf-export={title} /> From cc617727dec6bbca05027cc307902ac0c4e29f9f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 01:00:46 +0100 Subject: [PATCH 024/177] button --- src/components/CippTable/CIPPTableToptoolbar.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 7ea4bb463d83..0833ec0310ce 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -660,8 +660,8 @@ export const CIPPTableToptoolbar = ({ getRequestData?.isFetchNextPageError ? "Could not retrieve all data. Click to try again." : getRequestData?.isFetching - ? "Retrieving more data..." - : "Refresh data" + ? "Retrieving more data..." + : "Refresh data" } > - action.customFunction(row.original.original, action, {}) + action.customFunction(row.original.original, action, {}), ); } else { createDialog.handleOpen(); @@ -1329,9 +1329,7 @@ export const CIPPTableToptoolbar = ({ endpointFilter={api?.data?.Endpoint} relatedQueryKeys={[queryKey, currentEffectiveQueryKey].filter(Boolean)} selectedPreset={ - activeFilterName - ? filterList.find((f) => f.filterName === activeFilterName) - : null + activeFilterName ? filterList.find((f) => f.filterName === activeFilterName) : null } onPresetSelect={(preset) => { if (preset?.value && preset?.type === "graph") { From 782dc379f1b69f3a8d8f259496a1b20d6a6e1b2d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 14 Feb 2026 20:42:31 -0500 Subject: [PATCH 025/177] Add calendar permissions switch to offboarding Introduce a toggle to remove users' calendar permissions during offboarding. Adds a CippFormComponent switch to the offboarding default settings and the offboarding wizard (name: removeCalendarPermissions) wired to formControl so admins can opt in to revoke calendar permissions as part of the offboarding flow. --- .../CippComponents/CippOffboardingDefaultSettings.jsx | 10 ++++++++++ src/components/CippWizard/CippWizardOffboarding.jsx | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/components/CippComponents/CippOffboardingDefaultSettings.jsx b/src/components/CippComponents/CippOffboardingDefaultSettings.jsx index 9ac089dee39b..7ad68f3b74cb 100644 --- a/src/components/CippComponents/CippOffboardingDefaultSettings.jsx +++ b/src/components/CippComponents/CippOffboardingDefaultSettings.jsx @@ -115,6 +115,16 @@ export const CippOffboardingDefaultSettings = (props) => { /> ), }, + { + label: "Remove users calendar permissions", + value: ( + + ), + }, { label: "Remove all Rules", value: ( diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index 44af44afbb27..dfb412d86824 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -110,6 +110,12 @@ export const CippWizardOffboarding = (props) => { type="switch" formControl={formControl} /> + Date: Sun, 15 Feb 2026 02:43:25 +0100 Subject: [PATCH 026/177] fixes #5259 --- src/pages/tenant/manage/applied-standards.js | 111 ++++++++++++++++--- 1 file changed, 97 insertions(+), 14 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index fbd55c304e6c..bcf7a9d89d43 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -296,18 +296,43 @@ const Page = () => { const standardObject = currentTenantObj?.[standardId]; const directStandardValue = standardObject?.Value; - // Determine compliance status + // Determine compliance status - match main logic let isCompliant = false; - // For IntuneTemplate, the value is true if compliant, or an object with comparison data if not compliant - if (directStandardValue === true) { - isCompliant = true; - } else if ( - directStandardValue !== undefined && - typeof directStandardValue !== "object" + // FIRST: Check if CurrentValue and ExpectedValue exist and match + if ( + standardObject?.CurrentValue !== undefined && + standardObject?.ExpectedValue !== undefined ) { + const sortedCurrent = + typeof standardObject.CurrentValue === "object" && + standardObject.CurrentValue !== null + ? Object.keys(standardObject.CurrentValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.CurrentValue[key]; + return obj; + }, {}) + : standardObject.CurrentValue; + const sortedExpected = + typeof standardObject.ExpectedValue === "object" && + standardObject.ExpectedValue !== null + ? Object.keys(standardObject.ExpectedValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.ExpectedValue[key]; + return obj; + }, {}) + : standardObject.ExpectedValue; + isCompliant = + JSON.stringify(sortedCurrent) === JSON.stringify(sortedExpected); + } + // SECOND: Check if Value is explicitly true + else if (directStandardValue === true) { isCompliant = true; - } else if (currentTenantStandard) { + } + // THIRD: Fall back to currentTenantStandard + else if (currentTenantStandard) { isCompliant = currentTenantStandard.value === true; } @@ -533,11 +558,41 @@ const Page = () => { : null; let isCompliant = false; - // For ConditionalAccessTemplate, the value is true if compliant, or an object with comparison data if not compliant - if (directStandardValue === true) { + // FIRST: Check if CurrentValue and ExpectedValue exist and match + if ( + standardObject?.CurrentValue !== undefined && + standardObject?.ExpectedValue !== undefined + ) { + const sortedCurrent = + typeof standardObject.CurrentValue === "object" && + standardObject.CurrentValue !== null + ? Object.keys(standardObject.CurrentValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.CurrentValue[key]; + return obj; + }, {}) + : standardObject.CurrentValue; + const sortedExpected = + typeof standardObject.ExpectedValue === "object" && + standardObject.ExpectedValue !== null + ? Object.keys(standardObject.ExpectedValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.ExpectedValue[key]; + return obj; + }, {}) + : standardObject.ExpectedValue; + isCompliant = + JSON.stringify(sortedCurrent) === JSON.stringify(sortedExpected); + } + // SECOND: Check if Value is explicitly true + else if (directStandardValue === true) { isCompliant = true; - } else { - isCompliant = false; + } + // THIRD: Fall back to currentTenantStandard + else if (currentTenantStandard) { + isCompliant = currentTenantStandard.value === true; } // Create a standardValue object that contains the template settings @@ -620,8 +675,36 @@ const Page = () => { : null; let isCompliant = false; - // For GroupTemplate, the value is true if compliant - if (directStandardValue === true) { + // FIRST: Check if CurrentValue and ExpectedValue exist and match + if ( + standardObject?.CurrentValue !== undefined && + standardObject?.ExpectedValue !== undefined + ) { + const sortedCurrent = + typeof standardObject.CurrentValue === "object" && + standardObject.CurrentValue !== null + ? Object.keys(standardObject.CurrentValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.CurrentValue[key]; + return obj; + }, {}) + : standardObject.CurrentValue; + const sortedExpected = + typeof standardObject.ExpectedValue === "object" && + standardObject.ExpectedValue !== null + ? Object.keys(standardObject.ExpectedValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.ExpectedValue[key]; + return obj; + }, {}) + : standardObject.ExpectedValue; + isCompliant = + JSON.stringify(sortedCurrent) === JSON.stringify(sortedExpected); + } + // SECOND: Check if Value is explicitly true + else if (directStandardValue === true) { isCompliant = true; } else if (currentTenantStandard?.value) { isCompliant = currentTenantStandard.value === true; From 6b010c314b3bf7ec651f04e4ee81970624bc975a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:43:34 +0100 Subject: [PATCH 027/177] fixes #5259 --- src/pages/tenant/manage/applied-standards.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index bcf7a9d89d43..674ab5736122 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -700,8 +700,7 @@ const Page = () => { return obj; }, {}) : standardObject.ExpectedValue; - isCompliant = - JSON.stringify(sortedCurrent) === JSON.stringify(sortedExpected); + isCompliant = JSON.stringify(sortedCurrent) === JSON.stringify(sortedExpected); } // SECOND: Check if Value is explicitly true else if (directStandardValue === true) { From e914008444d9fa0d729e90af1d5b237057fd85fc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:09:35 +0100 Subject: [PATCH 028/177] Add group membership change alert --- src/data/alerts.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index 4d6a811dc36e..348dff167cac 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -458,6 +458,16 @@ "recommendedRunInterval": "30m", "description": "Monitors for new risky users in the tenant. Risky users are defined as users who have performed actions that are considered risky, such as password resets, MFA failures, or suspicious activity." }, + { + "name": "GroupMembershipChange", + "label": "Alert on group membership changes for monitored groups", + "recommendedRunInterval": "1h", + "requiresInput": true, + "inputType": "textField", + "inputLabel": "Group display names to monitor (comma separated, wildcards supported)", + "inputName": "InputValue", + "description": "Monitors audit logs for membership changes (add/remove) in specified groups. Supports wildcards like 'CA*' or '*Exclusion*'. Particularly useful for tracking changes to groups used in Conditional Access exclusions or other security-critical groups." + }, { "name": "LowTenantAlignment", "label": "Alert on low tenant alignment percentage", From 39ff55efda10664fb22f01bd011422eaf0f1e0dd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:54:56 +0100 Subject: [PATCH 029/177] add bec report. --- src/components/BECRemediationReportButton.js | 1503 +++++++++++++++++ .../administration/users/user/bec.jsx | 55 +- 2 files changed, 1535 insertions(+), 23 deletions(-) create mode 100644 src/components/BECRemediationReportButton.js diff --git a/src/components/BECRemediationReportButton.js b/src/components/BECRemediationReportButton.js new file mode 100644 index 000000000000..c7edd50be9d4 --- /dev/null +++ b/src/components/BECRemediationReportButton.js @@ -0,0 +1,1503 @@ +import { useState } from "react"; +import { + Button, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Typography, + IconButton, + CircularProgress, +} from "@mui/material"; +import { PictureAsPdf, Download, Close } from "@mui/icons-material"; +import { + Document, + Page, + Text, + View, + StyleSheet, + PDFViewer, + PDFDownloadLink, + Image, +} from "@react-pdf/renderer"; +import { useSettings } from "../hooks/use-settings"; + +// BEC Remediation PDF Document Component +const BECRemediationReportDocument = ({ + userData, + becData, + brandingSettings, + tenantName, + remediationData, +}) => { + const currentDate = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + const brandColor = brandingSettings?.colour || "#F77F00"; + + const styles = StyleSheet.create({ + page: { + flexDirection: "column", + backgroundColor: "#FFFFFF", + fontFamily: "Helvetica", + fontSize: 10, + lineHeight: 1.4, + color: "#2D3748", + padding: 40, + }, + + // COVER PAGE + coverPage: { + flexDirection: "column", + backgroundColor: "#FFFFFF", + fontFamily: "Helvetica", + padding: 60, + justifyContent: "space-between", + minHeight: "100%", + }, + + coverHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 80, + }, + + logoSection: { + flexDirection: "row", + alignItems: "center", + }, + + logo: { + height: 100, + marginRight: 12, + }, + + headerLogo: { + height: 30, + }, + + dateStamp: { + fontSize: 9, + color: "#000000", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + + coverHero: { + flex: 1, + justifyContent: "flex-start", + alignItems: "flex-start", + paddingTop: 40, + }, + + coverLabel: { + backgroundColor: brandColor, + color: "#FFFFFF", + fontSize: 10, + fontWeight: "bold", + textTransform: "uppercase", + letterSpacing: 1, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + marginBottom: 30, + alignSelf: "flex-start", + }, + + mainTitle: { + fontSize: 48, + fontWeight: "bold", + color: "#1A202C", + lineHeight: 1.1, + marginBottom: 20, + letterSpacing: -1, + }, + + titleAccent: { + color: brandColor, + }, + + subtitle: { + fontSize: 14, + color: "#000000", + fontWeight: "normal", + lineHeight: 1.5, + marginBottom: 40, + maxWidth: 400, + }, + + userCard: { + backgroundColor: "transparent", + padding: 0, + maxWidth: 500, + }, + + userName: { + fontSize: 18, + fontWeight: "bold", + color: "#000000", + marginBottom: 8, + }, + + userEmail: { + fontSize: 12, + color: "#333333", + marginBottom: 4, + }, + + coverFooter: { + textAlign: "center", + marginTop: 60, + }, + + confidential: { + fontSize: 9, + color: "#A0AEC0", + textTransform: "uppercase", + letterSpacing: 1, + }, + + // CONTENT PAGES + pageHeader: { + borderBottom: `1px solid ${brandColor}`, + paddingBottom: 12, + marginBottom: 24, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + }, + + pageHeaderContent: { + flex: 1, + }, + + pageTitle: { + fontSize: 20, + fontWeight: "bold", + color: "#1A202C", + marginBottom: 8, + }, + + pageSubtitle: { + fontSize: 11, + color: "#4A5568", + fontWeight: "normal", + }, + + section: { + marginBottom: 24, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + sectionTitle: { + fontSize: 14, + fontWeight: "bold", + color: brandColor, + marginBottom: 12, + }, + + sectionSubtitle: { + fontSize: 11, + fontWeight: "bold", + color: "#2D3748", + marginBottom: 8, + marginTop: 12, + }, + + bodyText: { + fontSize: 9, + color: "#2D3748", + lineHeight: 1.5, + marginBottom: 12, + textAlign: "justify", + }, + + bulletList: { + marginLeft: 12, + marginBottom: 12, + }, + + bulletItem: { + flexDirection: "row", + alignItems: "flex-start", + marginBottom: 6, + }, + + bulletPoint: { + fontSize: 8, + color: brandColor, + marginRight: 6, + fontWeight: "bold", + marginTop: 1, + }, + + bulletText: { + fontSize: 9, + color: "#2D3748", + lineHeight: 1.4, + flex: 1, + }, + + // ALERT BOXES + alertBox: { + backgroundColor: "#FFF5F5", + border: `2px solid ${brandColor}`, + borderRadius: 6, + padding: 12, + marginBottom: 16, + }, + + alertTitle: { + fontSize: 11, + fontWeight: "bold", + color: brandColor, + marginBottom: 6, + }, + + alertText: { + fontSize: 9, + color: "#2D3748", + lineHeight: 1.4, + }, + + // INFO BOXES + infoBox: { + backgroundColor: "#F7FAFC", + border: `1px solid #E2E8F0`, + borderLeft: `4px solid ${brandColor}`, + borderRadius: 4, + padding: 12, + marginBottom: 12, + }, + + infoTitle: { + fontSize: 10, + fontWeight: "bold", + color: "#2D3748", + marginBottom: 6, + }, + + infoText: { + fontSize: 8, + color: "#4A5568", + lineHeight: 1.4, + }, + + // STATS GRID + statsGrid: { + flexDirection: "row", + gap: 12, + marginBottom: 20, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + statCard: { + flex: 1, + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderRadius: 6, + padding: 16, + alignItems: "center", + borderTop: `3px solid ${brandColor}`, + }, + + statNumber: { + fontSize: 20, + fontWeight: "bold", + color: brandColor, + marginBottom: 4, + }, + + statLabel: { + fontSize: 7, + color: "#4A5568", + textTransform: "uppercase", + letterSpacing: 0.5, + textAlign: "center", + fontWeight: "bold", + }, + + // TABLES + table: { + border: `1px solid #E2E8F0`, + borderRadius: 6, + overflow: "hidden", + marginBottom: 16, + }, + + tableHeader: { + flexDirection: "row", + backgroundColor: brandColor, + paddingVertical: 10, + paddingHorizontal: 12, + }, + + tableHeaderCell: { + fontSize: 7, + fontWeight: "bold", + color: "#FFFFFF", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + + tableRow: { + flexDirection: "row", + borderBottomWidth: 1, + borderBottomColor: "#F7FAFC", + paddingVertical: 8, + paddingHorizontal: 12, + alignItems: "flex-start", + }, + + tableCell: { + fontSize: 8, + color: "#2D3748", + lineHeight: 1.3, + }, + + // FOOTER + footer: { + position: "absolute", + bottom: 20, + left: 40, + right: 40, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + borderTop: "1px solid #E2E8F0", + paddingTop: 8, + }, + + footerText: { + fontSize: 7, + color: "#718096", + }, + + pageNumber: { + fontSize: 7, + color: "#718096", + fontWeight: "bold", + }, + + // STATUS INDICATORS + statusBadge: { + fontSize: 7, + fontWeight: "bold", + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + textTransform: "uppercase", + letterSpacing: 0.3, + }, + + statusSuccess: { + backgroundColor: "#C6F6D5", + color: "#22543D", + }, + + statusWarning: { + backgroundColor: "#FEEBC8", + color: "#744210", + }, + + statusDanger: { + backgroundColor: "#FED7D7", + color: "#742A2A", + }, + + statusInfo: { + backgroundColor: "#BEE3F8", + color: "#2C5282", + }, + }); + + // Helper function to format dates + const formatDate = (dateString) => { + if (!dateString) return "N/A"; + try { + return new Date(dateString).toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + } catch { + return dateString; + } + }; + + // Calculate statistics + const stats = { + newRules: becData?.NewRules?.length || 0, + newUsers: becData?.NewUsers?.length || 0, + newApps: becData?.AddedApps?.length || 0, + permissionChanges: becData?.MailboxPermissionChanges?.length || 0, + mfaDevices: becData?.MFADevices?.length || 0, + passwordChanges: becData?.ChangedPasswords?.length || 0, + }; + + // Determine threat level + const calculateThreatLevel = () => { + let threatScore = 0; + if (stats.newRules > 0) threatScore += 3; + if (stats.permissionChanges > 0) threatScore += 2; + if (stats.newApps > 0) threatScore += 2; + if (stats.newUsers > 5) threatScore += 1; + + // Check for suspicious rules (RSS folder moves) + const hasSuspiciousRules = becData?.NewRules?.some((rule) => + rule.MoveToFolder?.includes("RSS") + ); + if (hasSuspiciousRules) threatScore += 5; + + if (threatScore >= 7) return { level: "High", color: "#742A2A" }; + if (threatScore >= 4) return { level: "Medium", color: "#744210" }; + return { level: "Low", color: "#22543D" }; + }; + + const threatLevel = calculateThreatLevel(); + + return ( + + {/* COVER PAGE */} + + + + {brandingSettings?.logo && ( + + )} + + {currentDate} + + + + SECURITY INCIDENT REPORT + + + BEC Compromise{"\n"} + Analysis + + + + Business Email Compromise Investigation Report for {tenantName || "your organization"} + + + + {userData?.displayName || "Unknown User"} + {userData?.userPrincipalName || "user@domain.com"} + + Analysis Date: {becData?.ExtractedAt ? formatDate(becData.ExtractedAt) : "N/A"} + + + + + + + Confidential & Proprietary - For Internal Use Only + + + + + {/* EXECUTIVE SUMMARY PAGE */} + + + + Executive Summary + + Overview of Business Email Compromise investigation findings + + + {brandingSettings?.logo && ( + + )} + + + + + This report documents the findings of a Business Email Compromise (BEC) investigation + performed for the user account{" "} + {userData?.userPrincipalName} within{" "} + {tenantName}. The investigation analyzed + suspicious activity indicators including mailbox rules, permission changes, new + applications, and authentication patterns over a 7-day period. + + + + Business Email Compromise is a sophisticated scam targeting organizations that regularly + perform wire transfers or have established relationships with foreign suppliers. + Attackers compromise legitimate email accounts through social engineering or computer + intrusion techniques to conduct unauthorized fund transfers, steal sensitive information, + or impersonate executives. + + + + + Investigation Overview + + + + {stats.newRules} + Mailbox Rules + + + {stats.permissionChanges} + Permission Changes + + + {stats.newApps} + New Applications + + + {stats.newUsers} + New Users + + + + + + Threat Assessment: {threatLevel.level} + + + {threatLevel.level === "High" && + "HIGH RISK: Multiple indicators of compromise detected. Immediate remediation actions are strongly recommended. This account shows patterns consistent with active Business Email Compromise attacks."} + {threatLevel.level === "Medium" && + "MEDIUM RISK: Suspicious activity patterns detected. Review findings and consider implementing recommended security measures. Some indicators suggest potential unauthorized access."} + {threatLevel.level === "Low" && + "LOW RISK: Minimal suspicious activity detected. The findings show standard user behavior with no significant indicators of compromise. Continue monitoring as a precautionary measure."} + + + + + + Data Source Information + + Audit Log Status + {becData?.ExtractResult || "Unknown"} + + + Analysis Period + + Last 7 days ending {becData?.ExtractedAt ? formatDate(becData.ExtractedAt) : "N/A"} + + + + + + + {tenantName} - BEC Analysis Report for {userData?.displayName} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + {/* UNDERSTANDING BEC PAGE */} + + + + Understanding Business Email Compromise + What is BEC and why does it matter? + + {brandingSettings?.logo && ( + + )} + + + + What is Business Email Compromise? + + Business Email Compromise (BEC) is a type of cyberattack where criminals gain + unauthorized access to a business email account. Once inside, attackers can: + + + + + • + + Monitor communications: Read sensitive + emails to learn about business operations, financial processes, and key + relationships. + + + + • + + Impersonate executives: Send fraudulent + emails appearing to come from company leadership requesting wire transfers or + sensitive data. + + + + • + + Manipulate transactions: Intercept + legitimate invoices and alter payment information to redirect funds to attacker-controlled accounts. + + + + • + + Hide their tracks: Create email rules to + automatically delete or hide messages, preventing detection. + + + + + + + Common Attack Methods + + Attackers typically gain access to email accounts through: + + + + + • + + Phishing: Deceptive emails that trick + users into providing their login credentials on fake websites. + + + + • + + Password Spraying: Automated attempts to + log in using common passwords across many accounts. + + + + • + + Credential Stuffing: Using usernames and + passwords leaked from other breached websites. + + + + • + + Malware: Software that captures + keystrokes or steals stored passwords from compromised devices. + + + + + + + Why This Investigation Was Performed + + This analysis was initiated because suspicious activity was detected or reported for this + user account. The investigation examines multiple indicators that might suggest account + compromise, including unusual mailbox rules, unexpected permission changes, new + application authorizations, and abnormal sign-in patterns. Early detection is critical to + minimize potential damage and prevent financial loss or data theft. + + + + + + {tenantName} - BEC Analysis Report for {userData?.displayName} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + {/* DETAILED FINDINGS PAGE */} + + + + Detailed Findings + Investigation results and analysis + + {brandingSettings?.logo && ( + + )} + + + {/* Check 1: Mailbox Rules */} + + Check 1: Mailbox Rules + + Why We Check This + + Attackers often create email rules to automatically forward, delete, or hide messages. + This prevents victims from seeing evidence of fraudulent activity. Suspicious rules may + move emails to obscure folders like "RSS Subscriptions" or forward them to external + addresses. + + + + {stats.newRules > 0 ? ( + <> + + ⚠ {stats.newRules} Mailbox Rule(s) Found + + The following mailbox rules were detected. Review each rule carefully to determine + if it was created by the user or by an attacker. Rules that forward emails or move + them to unusual folders are particularly suspicious. + + + + {becData.NewRules.slice(0, 10).map((rule, index) => ( + + Rule: {rule.Name || "Unnamed Rule"} + + Description: {rule.Description || "No description available"} + {"\n"} + {rule.MoveToFolder && `Moves to: ${rule.MoveToFolder}`} + {rule.ForwardTo && `\nForwards to: ${rule.ForwardTo}`} + {rule.DeleteMessage && "\nDeletes messages"} + + + ))} + {becData.NewRules.length > 10 && ( + + ... and {becData.NewRules.length - 10} more rules (see JSON export for full list) + + )} + + ) : ( + + ✓ No Suspicious Rules Found + + No mailbox rules were detected that match suspicious patterns. This is a positive + indicator. + + + )} + + + + + {tenantName} - BEC Analysis Report for {userData?.displayName} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + {/* CHECK 2: NEW USERS */} + + + + Detailed Findings (Continued) + Investigation results and analysis + + {brandingSettings?.logo && ( + + )} + + + + Check 2: Recently Created Users + + Why We Check This + + Attackers sometimes create new user accounts to maintain persistent access or to use as + staging accounts for fraudulent activities. Reviewing recently created users helps + identify unauthorized account creation. + + + + {stats.newUsers > 0 ? ( + <> + + ℹ {stats.newUsers} New User(s) Found + + The following users were created in the last 7 days. Verify that each account + creation was authorized and legitimate. + + + + {becData.NewUsers.slice(0, 8).map((user, index) => ( + + {user.displayName || "Unknown"} + + Email: {user.userPrincipalName || "N/A"} + {"\n"} + Created: {formatDate(user.createdDateTime)} + + + ))} + {becData.NewUsers.length > 8 && ( + + ... and {becData.NewUsers.length - 8} more users (see JSON export for full list) + + )} + + ) : ( + + ✓ No New Users Found + + No new user accounts were created during the analysis period. + + + )} + + + {/* Check 3: New Applications */} + + Check 3: New Applications + + Why We Check This + + Attackers may authorize malicious or suspicious third-party applications to access your + email and data. These applications can read emails, send messages, and access files + without the user's explicit knowledge. + + + + {stats.newApps > 0 ? ( + <> + + ⚠ {stats.newApps} New Application(s) Found + + New applications were granted access during the analysis period. Review each + application to ensure it was authorized and is from a trusted publisher. + + + + {becData.AddedApps.slice(0, 6).map((app, index) => ( + + {app.displayName || app.appDisplayName || "Unknown"} + + Publisher: {app.publisher || "Unknown"} + {"\n"} + App ID: {app.appId || "N/A"} + {"\n"} + Created: {formatDate(app.createdDateTime)} + + + ))} + {becData.AddedApps.length > 6 && ( + + ... and {becData.AddedApps.length - 6} more apps (see JSON export for full list) + + )} + + ) : ( + + ✓ No New Applications Found + + No new applications were authorized during the analysis period. + + + )} + + + + + {tenantName} - BEC Analysis Report for {userData?.displayName} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + {/* CHECK 4, 5, 6: PERMISSIONS, MFA, PASSWORDS */} + + + + Additional Security Checks + Permissions, authentication, and access patterns + + {brandingSettings?.logo && ( + + )} + + + {/* Check 4: Mailbox Permission Changes */} + + Check 4: Mailbox Permission Changes + + Why We Check This + + Unauthorized changes to mailbox permissions can allow attackers to grant themselves or + accomplices access to read, send, or manage emails. This is a common technique to + maintain persistent access. + + + + {stats.permissionChanges > 0 ? ( + <> + + + ⚠ {stats.permissionChanges} Permission Change(s) Found + + + Mailbox permission changes were detected. Verify that each change was authorized and + necessary for legitimate business purposes. + + + + {becData.MailboxPermissionChanges.slice(0, 5).map((change, index) => ( + + + {change.Operation || "Permission Change"} + + + User: {change.UserKey || "Unknown"} + {"\n"} + Target: {change.ObjectId || "N/A"} + {"\n"} + Permissions: {change.Permissions || "Unknown"} + + + ))} + {becData.MailboxPermissionChanges.length > 5 && ( + + ... and {becData.MailboxPermissionChanges.length - 5} more changes + + )} + + ) : ( + + + ✓ No Permission Changes Found + + + No mailbox permission changes were detected during the analysis period. + + + )} + + + {/* Check 5: MFA Devices */} + + Check 5: MFA Devices + + Why We Check This + + Multi-factor authentication (MFA) devices provide an additional layer of security. + Reviewing registered MFA methods helps identify if attackers have added unauthorized + devices to bypass security controls. + + + + {stats.mfaDevices > 0 ? ( + <> + + ℹ {stats.mfaDevices} MFA device(s) registered. Verify each device belongs to the user. + + + {becData.MFADevices.slice(0, 5).map((device, index) => ( + + + {device["@odata.type"]?.replace("#microsoft.graph.", "").replace("AuthenticationMethod", "") || "Unknown"} + + + Display Name: {device.displayName || "N/A"} + {"\n"} + Registered: {formatDate(device.createdDateTime)} + + + ))} + + ) : ( + + + ⚠ No MFA Devices Found + + + No multi-factor authentication devices are registered. MFA is highly recommended to + prevent unauthorized access. + + + )} + + + {/* Check 6: Password Changes */} + + Check 6: Recent Password Changes + + Why We Check This + + Attackers often change passwords to lock out legitimate users. Reviewing recent password + changes in the tenant helps identify if the compromised account's password was changed + or if other accounts were affected. + + + + {stats.passwordChanges > 0 ? ( + <> + + ℹ {stats.passwordChanges} password change(s) detected in the tenant during the + analysis period. + + + {becData.ChangedPasswords.slice(0, 5).map((user, index) => ( + + {user.displayName || "Unknown"} + + Email: {user.userPrincipalName || "N/A"} + {"\n"} + Last Password Change: {formatDate(user.lastPasswordChangeDateTime)} + + + ))} + + ) : ( + + ℹ No password changes detected during the analysis period. + + )} + + + + + {tenantName} - BEC Analysis Report for {userData?.displayName} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + {/* RECOMMENDATIONS PAGE */} + + + + Recommendations + + Actions to take and prevention best practices + + + {brandingSettings?.logo && ( + + )} + + + + Immediate Actions Required + + Based on the investigation findings, the following actions should be taken immediately: + + + + + 1. + + Reset Password: Change the user's + password immediately to prevent further unauthorized access. + + + + 2. + + Revoke Sessions: Sign out the user from + all active sessions to terminate any attacker access. + + + + 3. + + Remove Suspicious Rules: Delete any + mailbox rules that forward, redirect, or hide emails, especially those moving messages + to unusual folders. + + + + 4. + + Review MFA Devices: Remove any MFA devices + that the user doesn't recognize and re-register legitimate devices. + + + + 5. + + Audit Permissions: Review and revoke any + unauthorized mailbox permissions or application consents. + + + + 6. + + Monitor Account: Continue monitoring the + account for suspicious activity for at least 30 days. + + + + + + + Long-Term Prevention Strategies + + To prevent future Business Email Compromise attacks, implement these security best + practices: + + + + + • + + Enforce Multi-Factor Authentication (MFA):{" "} + Require MFA for all users, especially those with administrative privileges or access + to financial systems. + + + + • + + Implement Security Awareness Training:{" "} + Educate employees about phishing, social engineering, and how to identify suspicious + emails. Regular training significantly reduces successful attacks. + + + + • + + Enable Advanced Threat Protection: Use + email security solutions that detect and block phishing, malware, and suspicious + attachments. + + + + • + + Configure Conditional Access Policies:{" "} + Restrict access based on location, device compliance, and risk level to prevent + unauthorized sign-ins. + + + + • + + Monitor Audit Logs: Regularly review + audit logs for suspicious activities such as unusual sign-in patterns, rule creation, + or permission changes. + + + + • + + Establish Financial Controls: Implement + multi-person approval processes for wire transfers and payment changes to prevent + fraudulent transactions. + + + + + + + User Education Points + + Share these key points with the affected user to help prevent future compromises: + + + + + • + + Never click on links or open attachments in unexpected emails, even if they appear to + come from known contacts. + + + + • + + Always verify unusual requests for money transfers or sensitive information through a + separate communication channel (phone call, in person). + + + + • + + Use strong, unique passwords for each account and consider using a password manager. + + + + • + + Be cautious when authorizing new applications or granting permissions to third-party + services. + + + + • + + Report suspicious emails or activities to your IT security team immediately. + + + + + + + + {tenantName} - BEC Analysis Report for {userData?.displayName} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + {/* COMPLIANCE & DOCUMENTATION PAGE */} + + + + Compliance & Documentation + Meeting regulatory and audit requirements + + {brandingSettings?.logo && ( + + )} + + + + Compliance Considerations + + This report supports compliance and documentation requirements for various security + frameworks and regulatory standards: + + + + + • + + ISO 27001: Demonstrates incident + detection, analysis, and response procedures (Controls A.16.1.1 - A.16.1.7). + + + + • + + CMMC Level 2: Provides evidence of + security incident monitoring, analysis, and documentation (AC.L2-3.1.12, + AU.L2-3.3.1). + + + + • + + SOC 2 Type II: Documents detective and + responsive controls for security incidents (CC7.3, CC7.4). + + + + • + + NIST CSF: Aligns with Detect (DE.AE, + DE.CM) and Respond (RS.AN, RS.MI) functions. + + + + • + + GDPR: Demonstrates security breach + detection and potential data breach assessment (Articles 32, 33). + + + + + + + Audit Trail + + This investigation and resulting documentation provide an audit trail for security + incident response: + + + + Investigation Details + + Investigation Date: {formatDate(becData?.ExtractedAt)} + {"\n"} + Analyzed User: {userData?.userPrincipalName} + {"\n"} + Organization: {tenantName} + {"\n"} + Analysis Period: 7 days + {"\n"} + Audit Log Status: {becData?.ExtractResult || "Unknown"} + + + + + Findings Summary + + Threat Level: {threatLevel.level} + {"\n"} + Mailbox Rules Found: {stats.newRules} + {"\n"} + Permission Changes: {stats.permissionChanges} + {"\n"} + New Applications: {stats.newApps} + {"\n"} + New Users: {stats.newUsers} + {"\n"} + MFA Devices: {stats.mfaDevices} + {"\n"} + Password Changes: {stats.passwordChanges} + + + + + + Document Retention + + This report should be retained according to your organization's document retention + policy and regulatory requirements. Typical retention periods range from 3-7 years + depending on applicable compliance frameworks. Store this document securely with + restricted access as it contains sensitive security information. + + + + + Additional Resources + + For more information about Business Email Compromise and cybersecurity best practices: + + + + + • + FBI IC3: Internet Crime Complaint Center (ic3.gov) + + + • + + CISA: Cybersecurity & Infrastructure Security Agency (cisa.gov) + + + + • + + Microsoft Security: Business Email Compromise resources + + + + + + + + {tenantName} - BEC Analysis Report for {userData?.displayName} + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + ); +}; + +// Main Button Component +export const BECRemediationReportButton = ({ userData, becData, tenantName }) => { + const [dialogOpen, setDialogOpen] = useState(false); + const [isGenerating, setIsGenerating] = useState(false); + const userSettings = useSettings(); + + // Check if we have the necessary data + const hasData = userData && becData && !becData.Waiting; + + const brandingSettings = userSettings?.organizationSettings || { + logo: userSettings?.organizationSettings?.logo, + colour: userSettings?.organizationSettings?.colour || "#F77F00", + }; + + const handleOpenDialog = () => { + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + }; + + if (!hasData) { + return null; // Don't show button if data isn't ready + } + + return ( + <> + + + + + + + + BEC Remediation Report Preview + + + + + + + {hasData && ( + + + + )} + + + + + } + fileName={`BEC_Report_${userData?.userPrincipalName}_${new Date().toISOString().split("T")[0]}.pdf`} + style={{ textDecoration: "none" }} + > + {({ loading }) => ( + + )} + + + + + ); +}; diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index 6d305994513d..6aa3d37419f8 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -17,6 +17,7 @@ import { SvgIcon, Typography, CircularProgress, Button } from "@mui/material"; import { PropertyList } from "../../../../../components/property-list"; import { PropertyListItem } from "../../../../../components/property-list-item"; import { CippHead } from "../../../../../components/CippComponents/CippHead"; +import { BECRemediationReportButton } from "../../../../../components/BECRemediationReportButton"; const Page = () => { const userSettingsDefaults = useSettings(); @@ -567,33 +568,41 @@ const Page = () => { } > - Click this button to download a report of all the data found during this - research to perform your own analysis. + Generate a comprehensive PDF report for documentation, compliance, or end-user review. + The report includes detailed explanations suitable for non-technical users, managers, and + compliance requirements (ISO/CMMC/SOC). {/* Implement download functionality */} {becPollingCall.data && ( - + + + + )} From 1581d0f0ae798ad7459de9141423e0e0c697c44c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:54:59 +0100 Subject: [PATCH 030/177] add bec report. --- src/components/BECRemediationReportButton.js | 108 ++++++++++-------- .../administration/users/user/bec.jsx | 10 +- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/components/BECRemediationReportButton.js b/src/components/BECRemediationReportButton.js index c7edd50be9d4..c7a98231f7d3 100644 --- a/src/components/BECRemediationReportButton.js +++ b/src/components/BECRemediationReportButton.js @@ -454,7 +454,7 @@ const BECRemediationReportDocument = ({ // Check for suspicious rules (RSS folder moves) const hasSuspiciousRules = becData?.NewRules?.some((rule) => - rule.MoveToFolder?.includes("RSS") + rule.MoveToFolder?.includes("RSS"), ); if (hasSuspiciousRules) threatScore += 5; @@ -534,8 +534,8 @@ const BECRemediationReportDocument = ({ Business Email Compromise is a sophisticated scam targeting organizations that regularly perform wire transfers or have established relationships with foreign suppliers. Attackers compromise legitimate email accounts through social engineering or computer - intrusion techniques to conduct unauthorized fund transfers, steal sensitive information, - or impersonate executives. + intrusion techniques to conduct unauthorized fund transfers, steal sensitive + information, or impersonate executives. @@ -641,7 +641,8 @@ const BECRemediationReportDocument = ({ • Manipulate transactions: Intercept - legitimate invoices and alter payment information to redirect funds to attacker-controlled accounts. + legitimate invoices and alter payment information to redirect funds to + attacker-controlled accounts. @@ -695,11 +696,11 @@ const BECRemediationReportDocument = ({ Why This Investigation Was Performed - This analysis was initiated because suspicious activity was detected or reported for this - user account. The investigation examines multiple indicators that might suggest account - compromise, including unusual mailbox rules, unexpected permission changes, new - application authorizations, and abnormal sign-in patterns. Early detection is critical to - minimize potential damage and prevent financial loss or data theft. + This analysis was initiated because suspicious activity was detected or reported for + this user account. The investigation examines multiple indicators that might suggest + account compromise, including unusual mailbox rules, unexpected permission changes, new + application authorizations, and abnormal sign-in patterns. Early detection is critical + to minimize potential damage and prevent financial loss or data theft. @@ -733,9 +734,9 @@ const BECRemediationReportDocument = ({ Why We Check This Attackers often create email rules to automatically forward, delete, or hide messages. - This prevents victims from seeing evidence of fraudulent activity. Suspicious rules may - move emails to obscure folders like "RSS Subscriptions" or forward them to external - addresses. + This prevents victims from seeing evidence of fraudulent activity. Suspicious rules + may move emails to obscure folders like "RSS Subscriptions" or forward them to + external addresses. @@ -770,7 +771,9 @@ const BECRemediationReportDocument = ({ ) : ( - ✓ No Suspicious Rules Found + + ✓ No Suspicious Rules Found + No mailbox rules were detected that match suspicious patterns. This is a positive indicator. @@ -807,8 +810,8 @@ const BECRemediationReportDocument = ({ Why We Check This - Attackers sometimes create new user accounts to maintain persistent access or to use as - staging accounts for fraudulent activities. Reviewing recently created users helps + Attackers sometimes create new user accounts to maintain persistent access or to use + as staging accounts for fraudulent activities. Reviewing recently created users helps identify unauthorized account creation. @@ -855,9 +858,9 @@ const BECRemediationReportDocument = ({ Why We Check This - Attackers may authorize malicious or suspicious third-party applications to access your - email and data. These applications can read emails, send messages, and access files - without the user's explicit knowledge. + Attackers may authorize malicious or suspicious third-party applications to access + your email and data. These applications can read emails, send messages, and access + files without the user's explicit knowledge. @@ -873,7 +876,9 @@ const BECRemediationReportDocument = ({ {becData.AddedApps.slice(0, 6).map((app, index) => ( - {app.displayName || app.appDisplayName || "Unknown"} + + {app.displayName || app.appDisplayName || "Unknown"} + Publisher: {app.publisher || "Unknown"} {"\n"} @@ -891,7 +896,9 @@ const BECRemediationReportDocument = ({ ) : ( - ✓ No New Applications Found + + ✓ No New Applications Found + No new applications were authorized during the analysis period. @@ -915,7 +922,9 @@ const BECRemediationReportDocument = ({ Additional Security Checks - Permissions, authentication, and access patterns + + Permissions, authentication, and access patterns + {brandingSettings?.logo && ( @@ -941,16 +950,14 @@ const BECRemediationReportDocument = ({ ⚠ {stats.permissionChanges} Permission Change(s) Found - Mailbox permission changes were detected. Verify that each change was authorized and - necessary for legitimate business purposes. + Mailbox permission changes were detected. Verify that each change was authorized + and necessary for legitimate business purposes. {becData.MailboxPermissionChanges.slice(0, 5).map((change, index) => ( - - {change.Operation || "Permission Change"} - + {change.Operation || "Permission Change"} User: {change.UserKey || "Unknown"} {"\n"} @@ -993,13 +1000,16 @@ const BECRemediationReportDocument = ({ {stats.mfaDevices > 0 ? ( <> - ℹ {stats.mfaDevices} MFA device(s) registered. Verify each device belongs to the user. + ℹ {stats.mfaDevices} MFA device(s) registered. Verify each device belongs to the + user. {becData.MFADevices.slice(0, 5).map((device, index) => ( - {device["@odata.type"]?.replace("#microsoft.graph.", "").replace("AuthenticationMethod", "") || "Unknown"} + {device["@odata.type"] + ?.replace("#microsoft.graph.", "") + .replace("AuthenticationMethod", "") || "Unknown"} Display Name: {device.displayName || "N/A"} @@ -1011,9 +1021,7 @@ const BECRemediationReportDocument = ({ ) : ( - - ⚠ No MFA Devices Found - + ⚠ No MFA Devices Found No multi-factor authentication devices are registered. MFA is highly recommended to prevent unauthorized access. @@ -1028,9 +1036,9 @@ const BECRemediationReportDocument = ({ Why We Check This - Attackers often change passwords to lock out legitimate users. Reviewing recent password - changes in the tenant helps identify if the compromised account's password was changed - or if other accounts were affected. + Attackers often change passwords to lock out legitimate users. Reviewing recent + password changes in the tenant helps identify if the compromised account's password + was changed or if other accounts were affected. @@ -1075,9 +1083,7 @@ const BECRemediationReportDocument = ({ Recommendations - - Actions to take and prevention best practices - + Actions to take and prevention best practices {brandingSettings?.logo && ( @@ -1109,15 +1115,15 @@ const BECRemediationReportDocument = ({ 3. Remove Suspicious Rules: Delete any - mailbox rules that forward, redirect, or hide emails, especially those moving messages - to unusual folders. + mailbox rules that forward, redirect, or hide emails, especially those moving + messages to unusual folders. 4. - Review MFA Devices: Remove any MFA devices - that the user doesn't recognize and re-register legitimate devices. + Review MFA Devices: Remove any MFA + devices that the user doesn't recognize and re-register legitimate devices. @@ -1148,7 +1154,9 @@ const BECRemediationReportDocument = ({ • - Enforce Multi-Factor Authentication (MFA):{" "} + + Enforce Multi-Factor Authentication (MFA): + {" "} Require MFA for all users, especially those with administrative privileges or access to financial systems. @@ -1181,8 +1189,8 @@ const BECRemediationReportDocument = ({ • Monitor Audit Logs: Regularly review - audit logs for suspicious activities such as unusual sign-in patterns, rule creation, - or permission changes. + audit logs for suspicious activities such as unusual sign-in patterns, rule + creation, or permission changes. @@ -1206,15 +1214,15 @@ const BECRemediationReportDocument = ({ • - Never click on links or open attachments in unexpected emails, even if they appear to - come from known contacts. + Never click on links or open attachments in unexpected emails, even if they appear + to come from known contacts. • - Always verify unusual requests for money transfers or sensitive information through a - separate communication channel (phone call, in person). + Always verify unusual requests for money transfers or sensitive information through + a separate communication channel (phone call, in person). @@ -1370,7 +1378,9 @@ const BECRemediationReportDocument = ({ • - FBI IC3: Internet Crime Complaint Center (ic3.gov) + + FBI IC3: Internet Crime Complaint Center (ic3.gov) + • diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index 6aa3d37419f8..a5b44d5a7893 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -103,7 +103,7 @@ const Page = () => { if (becPollingCall.data.NewRules && becPollingCall.data.NewRules.length > 0) { // Example condition to check for potential breach const hasPotentialBreach = becPollingCall.data.NewRules.some((rule) => - rule.MoveToFolder?.includes("RSS") + rule.MoveToFolder?.includes("RSS"), ); if (hasPotentialBreach) { return "Potential Breach found. The rules for this user contain classic signs of a breach."; @@ -126,7 +126,7 @@ const Page = () => { if (becPollingCall.data.AddedApps && becPollingCall.data.AddedApps.length > 0) { // Example condition to check for potential breach const hasPotentialBreach = becPollingCall.data.AddedApps.some( - (app) => /* your condition here */ false + (app) => /* your condition here */ false, ); if (hasPotentialBreach) { return "Potential Breach found."; @@ -568,9 +568,9 @@ const Page = () => { } > - Generate a comprehensive PDF report for documentation, compliance, or end-user review. - The report includes detailed explanations suitable for non-technical users, managers, and - compliance requirements (ISO/CMMC/SOC). + Generate a comprehensive PDF report for documentation, compliance, or end-user + review. The report includes detailed explanations suitable for non-technical + users, managers, and compliance requirements (ISO/CMMC/SOC). {/* Implement download functionality */} {becPollingCall.data && ( From 9b6dac4f2cdaaf08761a990620af2c04b77e3ab4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:26:29 +0100 Subject: [PATCH 031/177] search input adornement --- .../CippCards/CippUniversalSearchV2.jsx | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 71ad3eb524f6..6df30e77e2eb 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -42,7 +42,7 @@ export const CippUniversalSearchV2 = React.forwardRef( const newValue = event.target.value; setSearchValue(newValue); onChange(newValue); - + if (newValue.length === 0) { setShowDropdown(false); } @@ -71,7 +71,7 @@ export const CippUniversalSearchV2 = React.forwardRef( const userData = match.Data || {}; const tenantDomain = match.Tenant || ""; router.push( - `/identity/administration/users/user?tenantFilter=${tenantDomain}&userId=${userData.id}` + `/identity/administration/users/user?tenantFilter=${tenantDomain}&userId=${userData.id}`, ); setShowDropdown(false); }; @@ -82,7 +82,7 @@ export const CippUniversalSearchV2 = React.forwardRef( if ( containerRef.current && !containerRef.current.contains(event.target) && - !event.target.closest('[data-dropdown-portal]') + !event.target.closest("[data-dropdown-portal]") ) { setShowDropdown(false); } @@ -128,14 +128,14 @@ export const CippUniversalSearchV2 = React.forwardRef( }} fullWidth type="text" - label="Search users by UPN or Display Name..." + label="Search users by UPN or Display Name" onKeyDown={handleKeyDown} onChange={handleChange} value={searchValue} InputProps={{ startAdornment: ( - - + + ), endAdornment: search.isFetching ? ( @@ -143,6 +143,12 @@ export const CippUniversalSearchV2 = React.forwardRef( ) : null, + sx: { + '& .MuiInputAdornment-root': { + marginTop: '0 !important', + alignSelf: 'center' + } + } }} /> @@ -171,7 +177,11 @@ export const CippUniversalSearchV2 = React.forwardRef( ) : hasResults ? ( - + ) : ( @@ -184,7 +194,7 @@ export const CippUniversalSearchV2 = React.forwardRef( )} ); - } + }, ); CippUniversalSearchV2.displayName = "CippUniversalSearchV2"; @@ -201,7 +211,7 @@ const Results = ({ items = [], searchValue, onResultClick }) => { ) : ( part - ) + ), ); }; @@ -236,7 +246,11 @@ const Results = ({ items = [], searchValue, onResultClick }) => { {highlightMatch(userData.userPrincipalName || "")} - + Tenant: {tenantDomain} From 5c19918ef189d44a56e9c8136b2eb83049406718 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:26:35 +0100 Subject: [PATCH 032/177] input --- .../CippCards/CippUniversalSearchV2.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 6df30e77e2eb..4b4437cecc8b 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -134,7 +134,10 @@ export const CippUniversalSearchV2 = React.forwardRef( value={searchValue} InputProps={{ startAdornment: ( - + ), @@ -144,11 +147,11 @@ export const CippUniversalSearchV2 = React.forwardRef( ) : null, sx: { - '& .MuiInputAdornment-root': { - marginTop: '0 !important', - alignSelf: 'center' - } - } + "& .MuiInputAdornment-root": { + marginTop: "0 !important", + alignSelf: "center", + }, + }, }} /> From d7f1de8cfd40f7dbcb8ae6a767ddad073ad7cd40 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 20:51:56 +0100 Subject: [PATCH 033/177] Add CIPPDB cache as an action to app settings tenants menu --- src/components/CippWizard/CippTenantTable.jsx | 38 +- src/data/CIPPDBCacheTypes.json | 332 ++++++++++++++++++ 2 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 src/data/CIPPDBCacheTypes.json diff --git a/src/components/CippWizard/CippTenantTable.jsx b/src/components/CippWizard/CippTenantTable.jsx index 8494aa558e19..1bed4a9d067b 100644 --- a/src/components/CippWizard/CippTenantTable.jsx +++ b/src/components/CippWizard/CippTenantTable.jsx @@ -1,8 +1,9 @@ import { Button, SvgIcon } from "@mui/material"; import { CippTablePage } from "../CippComponents/CippTablePage.jsx"; -import { Sync, Block, PlayArrow, RestartAlt, Delete, Add } from "@mui/icons-material"; +import { Sync, Block, PlayArrow, RestartAlt, Delete, Add, Refresh } from "@mui/icons-material"; import { useDialog } from "../../hooks/use-dialog"; import { CippApiDialog } from "../CippComponents/CippApiDialog"; +import cacheTypes from "../../data/CIPPDBCacheTypes.json"; export const CippTenantTable = ({ title = "Tenants", @@ -70,6 +71,41 @@ export const CippTenantTable = ({ multiPost: false, condition: (row) => row.displayName !== "*Partner Tenant", }, + { + label: "Refresh CIPPDB Cache", + type: "GET", + url: `/api/ExecCIPPDBCache`, + icon: , + data: { Name: "Name", TenantFilter: "customerId" }, + confirmText: "Select the cache type to refresh for [displayName]:", + multiPost: false, + hideBulk: true, + fields: [ + { + type: "autoComplete", + name: "Name", + label: "Cache Type", + placeholder: "Select a cache type", + options: cacheTypes.map((cacheType) => ({ + label: cacheType.friendlyName, + value: cacheType.type, + description: cacheType.description, + })), + multiple: false, + creatable: false, + required: true, + }, + ], + customDataformatter: (rowData, actionData, formData) => { + const tenantFilter = rowData?.customerId || rowData?.defaultDomainName || ""; + // Extract value from autoComplete object (which returns { label, value } or just value) + const cacheTypeName = formData.Name?.value || formData.Name || ""; + return { + Name: cacheTypeName, + TenantFilter: tenantFilter, + }; + }, + }, ]; // Offcanvas details diff --git a/src/data/CIPPDBCacheTypes.json b/src/data/CIPPDBCacheTypes.json new file mode 100644 index 000000000000..28fac3a6fde8 --- /dev/null +++ b/src/data/CIPPDBCacheTypes.json @@ -0,0 +1,332 @@ +[ + { + "type": "Users", + "friendlyName": "Users", + "description": "All Azure AD users with sign-in activity" + }, + { + "type": "Groups", + "friendlyName": "Groups", + "description": "All Azure AD groups with members" + }, + { + "type": "Guests", + "friendlyName": "Guest Users", + "description": "All guest users in the tenant" + }, + { + "type": "ServicePrincipals", + "friendlyName": "Service Principals", + "description": "All service principals (applications)" + }, + { + "type": "Apps", + "friendlyName": "Application Registrations", + "description": "All application registrations with owners" + }, + { + "type": "Devices", + "friendlyName": "Azure AD Devices", + "description": "All Azure AD registered devices" + }, + { + "type": "Organization", + "friendlyName": "Organization", + "description": "Tenant organization information" + }, + { + "type": "Roles", + "friendlyName": "Directory Roles", + "description": "All Azure AD directory roles with members" + }, + { + "type": "AdminConsentRequestPolicy", + "friendlyName": "Admin Consent Request Policy", + "description": "Admin consent request policy settings" + }, + { + "type": "AuthorizationPolicy", + "friendlyName": "Authorization Policy", + "description": "Tenant authorization policy" + }, + { + "type": "AuthenticationMethodsPolicy", + "friendlyName": "Authentication Methods Policy", + "description": "Authentication methods policy configuration" + }, + { + "type": "DeviceSettings", + "friendlyName": "Device Settings", + "description": "Device management settings" + }, + { + "type": "DirectoryRecommendations", + "friendlyName": "Directory Recommendations", + "description": "Azure AD directory recommendations" + }, + { + "type": "CrossTenantAccessPolicy", + "friendlyName": "Cross-Tenant Access Policy", + "description": "Cross-tenant access policy configuration" + }, + { + "type": "DefaultAppManagementPolicy", + "friendlyName": "Default App Management Policy", + "description": "Default application management policy" + }, + { + "type": "Settings", + "friendlyName": "Directory Settings", + "description": "Directory settings configuration" + }, + { + "type": "SecureScore", + "friendlyName": "Secure Score", + "description": "Microsoft Secure Score and control profiles" + }, + { + "type": "PIMSettings", + "friendlyName": "PIM Settings", + "description": "Privileged Identity Management settings and assignments" + }, + { + "type": "Domains", + "friendlyName": "Domains", + "description": "All verified and unverified domains" + }, + { + "type": "RoleEligibilitySchedules", + "friendlyName": "Role Eligibility Schedules", + "description": "PIM role eligibility schedules" + }, + { + "type": "RoleManagementPolicies", + "friendlyName": "Role Management Policies", + "description": "Role management policies" + }, + { + "type": "RoleAssignmentScheduleInstances", + "friendlyName": "Role Assignment Schedule Instances", + "description": "Active role assignment instances" + }, + { + "type": "B2BManagementPolicy", + "friendlyName": "B2B Management Policy", + "description": "B2B collaboration policy settings" + }, + { + "type": "AuthenticationFlowsPolicy", + "friendlyName": "Authentication Flows Policy", + "description": "Authentication flows policy configuration" + }, + { + "type": "DeviceRegistrationPolicy", + "friendlyName": "Device Registration Policy", + "description": "Device registration policy settings" + }, + { + "type": "CredentialUserRegistrationDetails", + "friendlyName": "Credential User Registration Details", + "description": "User credential registration details" + }, + { + "type": "UserRegistrationDetails", + "friendlyName": "User Registration Details", + "description": "MFA registration details for users" + }, + { + "type": "OAuth2PermissionGrants", + "friendlyName": "OAuth2 Permission Grants", + "description": "OAuth2 permission grants" + }, + { + "type": "AppRoleAssignments", + "friendlyName": "App Role Assignments", + "description": "Application role assignments" + }, + { + "type": "LicenseOverview", + "friendlyName": "License Overview", + "description": "License usage overview" + }, + { + "type": "MFAState", + "friendlyName": "MFA State", + "description": "Multi-factor authentication state" + }, + { + "type": "ExoAntiPhishPolicies", + "friendlyName": "Exchange Anti-Phish Policies", + "description": "Exchange Online anti-phishing policies" + }, + { + "type": "ExoMalwareFilterPolicies", + "friendlyName": "Exchange Malware Filter Policies", + "description": "Exchange Online malware filter policies" + }, + { + "type": "ExoSafeLinksPolicies", + "friendlyName": "Exchange Safe Links Policies", + "description": "Exchange Online Safe Links policies" + }, + { + "type": "ExoSafeAttachmentPolicies", + "friendlyName": "Exchange Safe Attachment Policies", + "description": "Exchange Online Safe Attachment policies" + }, + { + "type": "ExoTransportRules", + "friendlyName": "Exchange Transport Rules", + "description": "Exchange Online transport rules" + }, + { + "type": "ExoDkimSigningConfig", + "friendlyName": "Exchange DKIM Signing Config", + "description": "Exchange Online DKIM signing configuration" + }, + { + "type": "ExoOrganizationConfig", + "friendlyName": "Exchange Organization Config", + "description": "Exchange Online organization configuration" + }, + { + "type": "ExoAcceptedDomains", + "friendlyName": "Exchange Accepted Domains", + "description": "Exchange Online accepted domains" + }, + { + "type": "ExoHostedContentFilterPolicy", + "friendlyName": "Exchange Hosted Content Filter Policy", + "description": "Exchange Online hosted content filter policy" + }, + { + "type": "ExoHostedOutboundSpamFilterPolicy", + "friendlyName": "Exchange Hosted Outbound Spam Filter Policy", + "description": "Exchange Online hosted outbound spam filter policy" + }, + { + "type": "ExoAntiPhishPolicy", + "friendlyName": "Exchange Anti-Phish Policy", + "description": "Exchange Online anti-phishing policy" + }, + { + "type": "ExoSafeLinksPolicy", + "friendlyName": "Exchange Safe Links Policy", + "description": "Exchange Online Safe Links policy" + }, + { + "type": "ExoSafeAttachmentPolicy", + "friendlyName": "Exchange Safe Attachment Policy", + "description": "Exchange Online Safe Attachment policy" + }, + { + "type": "ExoMalwareFilterPolicy", + "friendlyName": "Exchange Malware Filter Policy", + "description": "Exchange Online malware filter policy" + }, + { + "type": "ExoAtpPolicyForO365", + "friendlyName": "Exchange ATP Policy for O365", + "description": "Exchange Online Advanced Threat Protection policy" + }, + { + "type": "ExoQuarantinePolicy", + "friendlyName": "Exchange Quarantine Policy", + "description": "Exchange Online quarantine policy" + }, + { + "type": "ExoRemoteDomain", + "friendlyName": "Exchange Remote Domain", + "description": "Exchange Online remote domain configuration" + }, + { + "type": "ExoSharingPolicy", + "friendlyName": "Exchange Sharing Policy", + "description": "Exchange Online sharing policies" + }, + { + "type": "ExoAdminAuditLogConfig", + "friendlyName": "Exchange Admin Audit Log Config", + "description": "Exchange Online admin audit log configuration" + }, + { + "type": "ExoPresetSecurityPolicy", + "friendlyName": "Exchange Preset Security Policy", + "description": "Exchange Online preset security policy" + }, + { + "type": "ExoTenantAllowBlockList", + "friendlyName": "Exchange Tenant Allow/Block List", + "description": "Exchange Online tenant allow/block list" + }, + { + "type": "Mailboxes", + "friendlyName": "Mailboxes", + "description": "All Exchange Online mailboxes" + }, + { + "type": "CASMailboxes", + "friendlyName": "CAS Mailboxes", + "description": "Client Access Server mailbox settings" + }, + { + "type": "MailboxUsage", + "friendlyName": "Mailbox Usage", + "description": "Exchange Online mailbox usage statistics" + }, + { + "type": "OneDriveUsage", + "friendlyName": "OneDrive Usage", + "description": "OneDrive usage statistics" + }, + { + "type": "ConditionalAccessPolicies", + "friendlyName": "Conditional Access Policies", + "description": "Azure AD Conditional Access policies" + }, + { + "type": "RiskyUsers", + "friendlyName": "Risky Users", + "description": "Users flagged as risky by Identity Protection" + }, + { + "type": "RiskyServicePrincipals", + "friendlyName": "Risky Service Principals", + "description": "Service principals flagged as risky by Identity Protection" + }, + { + "type": "ServicePrincipalRiskDetections", + "friendlyName": "Service Principal Risk Detections", + "description": "Risk detections for service principals" + }, + { + "type": "RiskDetections", + "friendlyName": "Risk Detections", + "description": "Identity Protection risk detections" + }, + { + "type": "ManagedDevices", + "friendlyName": "Managed Devices", + "description": "Intune managed devices" + }, + { + "type": "IntunePolicies", + "friendlyName": "Intune Policies", + "description": "All Intune policies including compliance, configuration, and app protection" + }, + { + "type": "ManagedDeviceEncryptionStates", + "friendlyName": "Managed Device Encryption States", + "description": "BitLocker encryption states for managed devices" + }, + { + "type": "IntuneAppProtectionPolicies", + "friendlyName": "Intune App Protection Policies", + "description": "Intune app protection policies for iOS and Android" + }, + { + "type": "DetectedApps", + "friendlyName": "Detected Apps", + "description": "All detected applications with devices where each app is installed" + } +] From 01ad232362a4202434212ccd83db5d1cd06250e5 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 21:21:27 +0100 Subject: [PATCH 034/177] boilerplate devices page --- .../endpoint/MEM/devices/device/index.jsx | 907 ++++++++++++++++++ .../MEM/devices/device/tabOptions.json | 6 + src/pages/endpoint/MEM/devices/index.js | 7 + 3 files changed, 920 insertions(+) create mode 100644 src/pages/endpoint/MEM/devices/device/index.jsx create mode 100644 src/pages/endpoint/MEM/devices/device/tabOptions.json diff --git a/src/pages/endpoint/MEM/devices/device/index.jsx b/src/pages/endpoint/MEM/devices/device/index.jsx new file mode 100644 index 000000000000..8e3707e383cc --- /dev/null +++ b/src/pages/endpoint/MEM/devices/device/index.jsx @@ -0,0 +1,907 @@ +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 { + PhoneAndroid, + Computer, + PhoneIphone, + Laptop, + Launch, + Security, + CheckCircle, + Warning, + Sync, + RestartAlt, + LocationOn, + Password, + PasswordOutlined, + Key, + Edit, + FindInPage, + Shield, + Archive, + AutoMode, + Recycling, + ManageAccounts, +} 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 { SvgIcon, Typography, Card, CardHeader, Divider } from "@mui/material"; +import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; +import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; +import { useEffect, useState } from "react"; +import { PropertyList } from "../../../../../components/property-list"; +import { PropertyListItem } from "../../../../../components/property-list-item"; +import { CippDataTable } from "../../../../../components/CippTable/CippDataTable"; +import { CippHead } from "../../../../../components/CippComponents/CippHead"; +import { Button } from "@mui/material"; +import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { deviceId } = router.query; + const [waiting, setWaiting] = useState(false); + + useEffect(() => { + if (deviceId) { + setWaiting(true); + } + }, [deviceId]); + + const deviceRequest = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `deviceManagement/managedDevices/${deviceId}`, + tenantFilter: router.query.tenantFilter ?? userSettingsDefaults.currentTenant, + }, + queryKey: `ManagedDevice-${deviceId}`, + waiting: waiting, + }); + + const deviceBulkRequest = ApiPostCall({ + urlFromData: true, + }); + + function refreshFunction() { + if (!deviceId) return; + deviceBulkRequest.mutate({ + url: "/api/ListGraphBulkRequest", + data: { + Requests: [ + { + id: "deviceCompliance", + url: `/deviceManagement/managedDevices/${deviceId}/deviceCompliancePolicyStates`, + method: "GET", + }, + { + id: "deviceConfiguration", + url: `/deviceManagement/managedDevices/${deviceId}/deviceConfigurationStates`, + method: "GET", + }, + { + id: "detectedApps", + url: `/deviceManagement/managedDevices/${deviceId}/detectedApps`, + method: "GET", + }, + { + id: "users", + url: `/deviceManagement/managedDevices/${deviceId}/users`, + method: "GET", + }, + ], + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + } + + useEffect(() => { + if (deviceId && userSettingsDefaults.currentTenant && !deviceBulkRequest.isSuccess) { + refreshFunction(); + } + }, [deviceId, userSettingsDefaults.currentTenant, deviceBulkRequest.isSuccess]); + + const bulkData = deviceBulkRequest?.data?.data ?? []; + const deviceComplianceData = bulkData?.find((item) => item.id === "deviceCompliance"); + const deviceConfigurationData = bulkData?.find((item) => item.id === "deviceConfiguration"); + const detectedAppsData = bulkData?.find((item) => item.id === "detectedApps"); + const usersData = bulkData?.find((item) => item.id === "users"); + + const deviceCompliance = deviceComplianceData?.body?.value || []; + const deviceConfiguration = deviceConfigurationData?.body?.value || []; + const detectedApps = detectedAppsData?.body?.value || []; + const users = usersData?.body?.value || []; + + // Handle response structure - ListGraphRequest may wrap single items in Results array + // Try Results array first, then Results as object, then data directly + let deviceData = null; + if (deviceRequest.isSuccess && deviceRequest.data) { + if (Array.isArray(deviceRequest.data.Results)) { + deviceData = deviceRequest.data.Results[0]; + } else if (deviceRequest.data.Results) { + deviceData = deviceRequest.data.Results; + } else { + deviceData = deviceRequest.data; + } + } + + // Set the title and subtitle for the layout + const title = deviceRequest.isSuccess ? deviceData?.deviceName : "Loading..."; + + const subtitle = deviceRequest.isSuccess + ? [ + { + icon: , + text: , + }, + { + icon: , + text: , + }, + { + icon: , + text: ( + <> + Enrolled: + + ), + }, + { + icon: , + text: ( + + ), + }, + ] + : []; + + const data = deviceData; + + // Device actions from the devices table page + const deviceActions = [ + { + label: "View in Intune", + link: `https://intune.microsoft.com/${userSettingsDefaults.currentTenant}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/${deviceId}`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, + { + label: "Change Primary User", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "!users", + }, + fields: [ + { + type: "autoComplete", + name: "user", + label: "Select User", + multiple: false, + creatable: false, + api: { + url: "/api/ListGraphRequest", + data: { + Endpoint: "users", + $select: "id,displayName,userPrincipalName", + $top: 999, + $count: true, + }, + queryKey: "ListUsersAutoComplete", + dataKey: "Results", + labelField: (user) => `${user.displayName} (${user.userPrincipalName})`, + valueField: "id", + addedField: { + userPrincipalName: "userPrincipalName", + }, + showRefresh: true, + }, + }, + ], + confirmText: "Select the User to set as the primary user for [deviceName]", + }, + { + label: "Rename Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "setDeviceName", + }, + confirmText: "Enter the new name for the device", + fields: [ + { + type: "textField", + name: "input", + label: "New Device Name", + required: true, + }, + ], + }, + { + label: "Sync Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "syncDevice", + }, + confirmText: "Are you sure you want to sync [deviceName]?", + }, + { + label: "Reboot Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "rebootNow", + }, + confirmText: "Are you sure you want to reboot [deviceName]?", + }, + { + label: "Locate Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "locateDevice", + }, + confirmText: "Are you sure you want to locate [deviceName]?", + }, + { + label: "Retrieve LAPS password", + type: "POST", + icon: , + url: "/api/ExecGetLocalAdminPassword", + data: { + GUID: "azureADDeviceId", + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to retrieve the local admin password for [deviceName]?", + }, + { + label: "Rotate Local Admin Password", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "RotateLocalAdminPassword", + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to rotate the password for [deviceName]?", + }, + { + label: "Retrieve BitLocker Keys", + type: "POST", + icon: , + url: "/api/ExecGetRecoveryKey", + data: { + GUID: "azureADDeviceId", + RecoveryKeyType: "!BitLocker", + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to retrieve the BitLocker keys for [deviceName]?", + }, + { + label: "Retrieve FileVault Key", + type: "POST", + icon: , + url: "/api/ExecGetRecoveryKey", + data: { + GUID: "id", + RecoveryKeyType: "!FileVault", + }, + condition: (row) => row.operatingSystem === "macOS", + confirmText: "Are you sure you want to retrieve the FileVault key for [deviceName]?", + }, + { + label: "Reset Passcode", + type: "POST", + icon: , + url: "/api/ExecDevicePasscodeAction", + data: { + GUID: "id", + Action: "resetPasscode", + }, + condition: (row) => row.operatingSystem === "Android", + confirmText: + "Are you sure you want to reset the passcode for [deviceName]? A new passcode will be generated and displayed.", + }, + { + label: "Remove Passcode", + type: "POST", + icon: , + url: "/api/ExecDevicePasscodeAction", + data: { + GUID: "id", + Action: "resetPasscode", + }, + condition: (row) => row.operatingSystem === "iOS", + confirmText: + "Are you sure you want to remove the passcode from [deviceName]? This will remove the device passcode requirement.", + }, + { + label: "Windows Defender Full Scan", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "WindowsDefenderScan", + quickScan: false, + }, + confirmText: "Are you sure you want to perform a full scan on [deviceName]?", + }, + { + label: "Windows Defender Quick Scan", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "WindowsDefenderScan", + quickScan: true, + }, + confirmText: "Are you sure you want to perform a quick scan on [deviceName]?", + }, + { + label: "Update Windows Defender", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "windowsDefenderUpdateSignatures", + }, + confirmText: + "Are you sure you want to update the Windows Defender signatures for [deviceName]?", + }, + { + label: "Generate logs and ship to MEM", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "createDeviceLogCollectionRequest", + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: + "Are you sure you want to generate logs for device [deviceName] and ship these to MEM?", + }, + { + label: "Fresh Start (Remove user data)", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to Fresh Start [deviceName]?", + }, + { + label: "Fresh Start (Do not remove user data)", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: true, + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to Fresh Start [deviceName]?", + }, + { + label: "Wipe Device, keep enrollment data", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + keepEnrollmentData: true, + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to wipe [deviceName], and retain enrollment data?", + }, + { + label: "Wipe Device, remove enrollment data", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + keepEnrollmentData: false, + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to wipe [deviceName], and remove enrollment data?", + }, + { + label: "Wipe Device, keep enrollment data, and continue at powerloss", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepEnrollmentData: true, + keepUserData: false, + useProtectedWipe: true, + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: + "Are you sure you want to wipe [deviceName]? This will retain enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", + }, + { + label: "Wipe Device, remove enrollment data, and continue at powerloss", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepEnrollmentData: false, + keepUserData: false, + useProtectedWipe: true, + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: + "Are you sure you want to wipe [deviceName]? This will also remove enrollment data. Continuing at powerloss may cause boot issues if wipe is interrupted.", + }, + { + label: "Autopilot Reset", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "wipe", + keepUserData: "false", + keepEnrollmentData: "true", + }, + condition: (row) => row.operatingSystem === "Windows", + confirmText: "Are you sure you want to Autopilot Reset [deviceName]?", + }, + { + label: "Delete device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "delete", + }, + confirmText: "Are you sure you want to delete [deviceName]?", + }, + { + label: "Retire device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "retire", + }, + confirmText: "Are you sure you want to retire [deviceName]?", + }, + ]; + + // Get device icon based on OS + const getDeviceIcon = () => { + if (!data?.operatingSystem) return ; + const os = data.operatingSystem.toLowerCase(); + if (os.includes("android")) return ; + if (os.includes("ios") || os.includes("iphone") || os.includes("ipad")) return ; + if (os.includes("windows") || os.includes("macos")) return ; + return ; + }; + + // Prepare compliance policy items + let compliancePolicyItems = []; + if (deviceCompliance.length > 0) { + compliancePolicyItems = deviceCompliance.map((policy, index) => ({ + id: index, + cardLabelBox: { + cardLabelBoxHeader: policy.complianceState === "compliant" ? : , + }, + text: policy.displayName || "Unknown Policy", + subtext: `State: ${policy.complianceState || "Unknown"}`, + statusColor: policy.complianceState === "compliant" ? "success.main" : "warning.main", + statusText: policy.complianceState || "Unknown", + propertyItems: [ + { + label: "Setting Count", + value: policy.settingCount || "N/A", + }, + { + label: "Setting States", + value: policy.settingStates?.length || 0, + }, + ], + })); + } else if (deviceComplianceData?.status !== 200) { + compliancePolicyItems = [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading compliance policies", + subtext: deviceComplianceData?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ]; + } else { + compliancePolicyItems = [ + { + id: 1, + cardLabelBox: "-", + text: "No compliance policies available", + subtext: "This device does not have any compliance policies assigned.", + statusColor: "warning.main", + statusText: "No Policies", + propertyItems: [], + }, + ]; + } + + // Prepare configuration policy items + let configurationPolicyItems = []; + if (deviceConfiguration.length > 0) { + configurationPolicyItems = deviceConfiguration.map((policy, index) => ({ + id: index, + cardLabelBox: { + cardLabelBoxHeader: policy.state === "compliant" ? : , + }, + text: policy.displayName || "Unknown Policy", + subtext: `State: ${policy.state || "Unknown"}`, + statusColor: policy.state === "compliant" ? "success.main" : "warning.main", + statusText: policy.state || "Unknown", + propertyItems: [ + { + label: "Setting Count", + value: policy.settingCount || "N/A", + }, + { + label: "Setting States", + value: policy.settingStates?.length || 0, + }, + ], + })); + } else if (deviceConfigurationData?.status !== 200) { + configurationPolicyItems = [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading configuration policies", + subtext: deviceConfigurationData?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ]; + } else { + configurationPolicyItems = [ + { + id: 1, + cardLabelBox: "-", + text: "No configuration policies available", + subtext: "This device does not have any configuration policies assigned.", + statusColor: "warning.main", + statusText: "No Policies", + propertyItems: [], + }, + ]; + } + + // Prepare detected apps items + let detectedAppsItems = []; + if (detectedApps.length > 0) { + detectedAppsItems = [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Detected Applications", + subtext: `${detectedApps.length} application(s) detected`, + statusText: `${detectedApps.length} App(s)`, + statusColor: "info.main", + table: { + title: "Detected Applications", + hideTitle: true, + data: detectedApps, + simpleColumns: ["displayName", "version", "platform"], + refreshFunction: refreshFunction, + }, + }, + ]; + } else if (detectedAppsData?.status !== 200) { + detectedAppsItems = [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading detected applications", + subtext: detectedAppsData?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ]; + } else { + detectedAppsItems = [ + { + id: 1, + cardLabelBox: "-", + text: "No detected applications", + subtext: "No applications have been detected on this device.", + statusColor: "warning.main", + statusText: "No Apps", + propertyItems: [], + }, + ]; + } + + // Prepare users items + let usersItems = []; + if (users.length > 0) { + usersItems = [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Device Users", + subtext: `${users.length} user(s) associated with this device`, + statusText: `${users.length} User(s)`, + statusColor: "info.main", + table: { + title: "Device Users", + hideTitle: true, + data: users, + simpleColumns: ["displayName", "userPrincipalName", "mail"], + refreshFunction: refreshFunction, + }, + }, + ]; + } else if (usersData?.status !== 200) { + usersItems = [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading device users", + subtext: usersData?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ]; + } else { + usersItems = [ + { + id: 1, + cardLabelBox: "-", + text: "No users associated", + subtext: "No users are currently associated with this device.", + statusColor: "warning.main", + statusText: "No Users", + propertyItems: [], + }, + ]; + } + + return ( + + {deviceRequest.isLoading && } + {deviceRequest.isSuccess && ( + + + + + + + + + + {getDeviceIcon()} + {data?.deviceName || "N/A"} + + {data?.manufacturer} {data?.model} + + + } + /> + + + + Device Name: + + + {getCippFormatting(data?.deviceName, "deviceName") || "N/A"} + + + + + Device ID: + + + {getCippFormatting(data?.id, "id") || "N/A"} + + + + + Operating System: + + + {data?.operatingSystem || "N/A"} {data?.osVersion || ""} + + + + + Manufacturer: + + {data?.manufacturer || "N/A"} + + + + Model: + + {data?.model || "N/A"} + + + + Serial Number: + + {data?.serialNumber || "N/A"} + + + + Compliance State: + + + {getCippFormatting(data?.complianceState, "complianceState") || "N/A"} + + + + + Enrolled Date: + + + {data?.enrolledDateTime + ? new Date(data.enrolledDateTime).toLocaleString() + : "N/A"} + + + + + Last Sync: + + + {data?.lastSyncDateTime + ? new Date(data.lastSyncDateTime).toLocaleString() + : "N/A"} + + + + + Owner Type: + + + {getCippFormatting(data?.managedDeviceOwnerType, "managedDeviceOwnerType") || + "N/A"} + + + + + Enrollment Type: + + + {getCippFormatting(data?.deviceEnrollmentType, "deviceEnrollmentType") || + "N/A"} + + + {data?.userPrincipalName && ( + + + Primary User: + + + {getCippFormatting(data?.userPrincipalName, "userPrincipalName") || "N/A"} + + + )} + + } + /> + + + + + + Compliance Policies + 0} + /> + Configuration Policies + 0} + /> + Detected Applications + + Associated Users + + + + + + )} + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/devices/device/tabOptions.json b/src/pages/endpoint/MEM/devices/device/tabOptions.json new file mode 100644 index 000000000000..e5e134f5566a --- /dev/null +++ b/src/pages/endpoint/MEM/devices/device/tabOptions.json @@ -0,0 +1,6 @@ +[ + { + "label": "View Device", + "path": "/endpoint/MEM/devices/device" + } +] diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 9cd56f210a9f..e8e34e9338d5 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -28,6 +28,13 @@ const Page = () => { const depSyncDialog = useDialog(); const actions = [ + { + label: "View Device", + link: `/endpoint/MEM/devices/device?deviceId=[id]`, + color: "info", + icon: , + multiPost: false, + }, { label: "View in Intune", link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, From 339d0f6057122bb3e1f8f10ca87506c8a49cfd49 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 21:42:26 +0100 Subject: [PATCH 035/177] more device boilerplate --- .../endpoint/MEM/devices/device/defender.jsx | 320 ++++++++++++++++++ .../endpoint/MEM/devices/device/index.jsx | 30 +- .../MEM/devices/device/tabOptions.json | 4 + 3 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 src/pages/endpoint/MEM/devices/device/defender.jsx diff --git a/src/pages/endpoint/MEM/devices/device/defender.jsx b/src/pages/endpoint/MEM/devices/device/defender.jsx new file mode 100644 index 000000000000..814124772390 --- /dev/null +++ b/src/pages/endpoint/MEM/devices/device/defender.jsx @@ -0,0 +1,320 @@ +import { Layout as DashboardLayout } from "../../../../../layouts/index.js"; +import { useSettings } from "../../../../../hooks/use-settings"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../../api/ApiCall"; +import CippFormSkeleton from "../../../../../components/CippFormPages/CippFormSkeleton"; +import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; +import { + PhoneAndroid, + Computer, + PhoneIphone, + Laptop, + Launch, + Security, + CheckCircle, + Warning, + Fingerprint, +} 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 { Typography, Card, CardHeader, Divider, Chip } from "@mui/material"; +import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; +import { useEffect, useState } from "react"; +import { PropertyList } from "../../../../../components/property-list"; +import { PropertyListItem } from "../../../../../components/property-list-item"; +import { CippHead } from "../../../../../components/CippComponents/CippHead"; +import { Button } from "@mui/material"; +import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { deviceId } = router.query; + const [waiting, setWaiting] = useState(false); + + useEffect(() => { + if (deviceId) { + setWaiting(true); + } + }, [deviceId]); + + // Get device info for title/subtitle + const deviceRequest = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `deviceManagement/managedDevices/${deviceId}`, + tenantFilter: router.query.tenantFilter ?? userSettingsDefaults.currentTenant, + }, + queryKey: `ManagedDevice-${deviceId}`, + waiting: waiting, + }); + + // Get Defender state + const defenderRequest = ApiGetCall({ + url: "/api/ListDefenderState", + data: { + DeviceID: deviceId, + TenantFilter: router.query.tenantFilter ?? userSettingsDefaults.currentTenant, + }, + queryKey: `DefenderState-${deviceId}`, + waiting: waiting, + }); + + // Handle response structure - ListGraphRequest may wrap single items in Results array + let deviceData = null; + if (deviceRequest.isSuccess && deviceRequest.data) { + if (Array.isArray(deviceRequest.data.Results)) { + deviceData = deviceRequest.data.Results[0]; + } else if (deviceRequest.data.Results) { + deviceData = deviceRequest.data.Results; + } else { + deviceData = deviceRequest.data; + } + } + + // Handle Defender state response - API returns array + const defenderData = defenderRequest.isSuccess && defenderRequest.data?.[0] ? defenderRequest.data[0] : null; + const protectionState = defenderData?.windowsProtectionState; + + // Set the title and subtitle for the layout + const title = deviceRequest.isSuccess ? deviceData?.deviceName : "Loading..."; + + const subtitle = deviceRequest.isSuccess + ? [ + { + icon: , + text: , + }, + { + icon: , + text: , + }, + { + icon: , + text: ( + <> + Last Sync: + + ), + }, + { + icon: , + text: ( + + ), + }, + ] + : []; + + // Helper to format boolean values + const formatBoolean = (value) => { + if (value === null || value === undefined) return "Unknown"; + return value ? "Enabled" : "Disabled"; + }; + + // Helper to get status color + const getStatusColor = (value) => { + if (value === null || value === undefined) return "default"; + return value ? "success" : "error"; + }; + + // Helper to format device state + const formatDeviceState = (state) => { + if (!state) return "Unknown"; + return state.charAt(0).toUpperCase() + state.slice(1); + }; + + return ( + + {(deviceRequest.isLoading || defenderRequest.isLoading) && ( + + )} + {deviceRequest.isSuccess && ( + + + {defenderRequest.isSuccess && defenderData ? ( + + + + + + + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + {protectionState?.lastReportedDateTime && ( + + {new Date(protectionState.lastReportedDateTime).toLocaleString()} + {" ("} + + {")"} + + } + /> + )} + + + + + ) : defenderRequest.isSuccess && !defenderData ? ( + + + + + + + + No Defender state information available for this device. This may indicate: + +
      +
    • The device is not a Windows device
    • +
    • Windows Defender is not enabled on this device
    • +
    • The device has not reported its Defender state yet
    • +
    +
    +
    +
    +
    + ) : ( + + + + + + + + Error loading Defender state information. Please try refreshing the page. + + + + + + )} + + )} + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/devices/device/index.jsx b/src/pages/endpoint/MEM/devices/device/index.jsx index 8e3707e383cc..d77b74791bcb 100644 --- a/src/pages/endpoint/MEM/devices/device/index.jsx +++ b/src/pages/endpoint/MEM/devices/device/index.jsx @@ -26,6 +26,7 @@ import { AutoMode, Recycling, ManageAccounts, + Fingerprint, } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; @@ -131,6 +132,13 @@ const Page = () => { } } + // Helper function to format bytes to GB (matching getCippFormatting pattern) + const formatBytesToGB = (bytes) => { + if (!bytes || bytes === 0) return "N/A"; + const gb = bytes / 1024 / 1024 / 1024; + return `${gb.toFixed(2)} GB`; + }; + // Set the title and subtitle for the layout const title = deviceRequest.isSuccess ? deviceData?.deviceName : "Loading..."; @@ -141,14 +149,14 @@ const Page = () => { text: , }, { - icon: , + icon: , text: , }, { icon: , text: ( <> - Enrolled: + Last Sync: ), }, @@ -861,6 +869,24 @@ const Page = () => { )} + {data?.totalStorageSpaceInBytes && ( + + + Storage: + + + {formatBytesToGB(data.freeStorageSpaceInBytes || 0)} free of{" "} + {formatBytesToGB(data.totalStorageSpaceInBytes)} + {data.freeStorageSpaceInBytes && + data.totalStorageSpaceInBytes && + ` (${Math.round( + ((data.totalStorageSpaceInBytes - data.freeStorageSpaceInBytes) / + data.totalStorageSpaceInBytes) * + 100, + )}% used)`} + + + )} } /> diff --git a/src/pages/endpoint/MEM/devices/device/tabOptions.json b/src/pages/endpoint/MEM/devices/device/tabOptions.json index e5e134f5566a..2fbbc5e490bf 100644 --- a/src/pages/endpoint/MEM/devices/device/tabOptions.json +++ b/src/pages/endpoint/MEM/devices/device/tabOptions.json @@ -2,5 +2,9 @@ { "label": "View Device", "path": "/endpoint/MEM/devices/device" + }, + { + "label": "Windows Defender", + "path": "/endpoint/MEM/devices/device/defender" } ] From e26b2ec487f099c2207af9d3133c454c9e6ea2ad Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 22:17:36 +0100 Subject: [PATCH 036/177] minor edits --- .../endpoint/MEM/devices/device/index.jsx | 146 +++++++++++++----- 1 file changed, 110 insertions(+), 36 deletions(-) diff --git a/src/pages/endpoint/MEM/devices/device/index.jsx b/src/pages/endpoint/MEM/devices/device/index.jsx index d77b74791bcb..1834646c9d9d 100644 --- a/src/pages/endpoint/MEM/devices/device/index.jsx +++ b/src/pages/endpoint/MEM/devices/device/index.jsx @@ -27,6 +27,7 @@ import { Recycling, ManageAccounts, Fingerprint, + Group, } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; @@ -36,13 +37,14 @@ import { Grid } from "@mui/system"; import { SvgIcon, Typography, Card, CardHeader, Divider } from "@mui/material"; import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { PropertyList } from "../../../../../components/property-list"; import { PropertyListItem } from "../../../../../components/property-list-item"; import { CippDataTable } from "../../../../../components/CippTable/CippDataTable"; import { CippHead } from "../../../../../components/CippComponents/CippHead"; import { Button } from "@mui/material"; import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; +import { PencilIcon } from "@heroicons/react/24/outline"; const Page = () => { const userSettingsDefaults = useSettings(); @@ -70,33 +72,53 @@ const Page = () => { urlFromData: true, }); + // Handle response structure - ListGraphRequest may wrap single items in Results array + // Try Results array first, then Results as object, then data directly + let deviceData = null; + if (deviceRequest.isSuccess && deviceRequest.data) { + if (Array.isArray(deviceRequest.data.Results)) { + deviceData = deviceRequest.data.Results[0]; + } else if (deviceRequest.data.Results) { + deviceData = deviceRequest.data.Results; + } else { + deviceData = deviceRequest.data; + } + } + function refreshFunction() { if (!deviceId) return; + const requests = [ + { + id: "deviceCompliance", + url: `/deviceManagement/managedDevices/${deviceId}/deviceCompliancePolicyStates`, + method: "GET", + }, + { + id: "deviceConfiguration", + url: `/deviceManagement/managedDevices/${deviceId}/deviceConfigurationStates`, + method: "GET", + }, + { + id: "detectedApps", + url: `/deviceManagement/managedDevices/${deviceId}/detectedApps`, + method: "GET", + }, + { + id: "users", + url: `/deviceManagement/managedDevices/${deviceId}/users`, + method: "GET", + }, + { + id: "deviceMemberOf", + url: `/devices/${deviceId}/transitiveMemberOf/microsoft.graph.group`, + method: "GET", + }, + ]; + deviceBulkRequest.mutate({ url: "/api/ListGraphBulkRequest", data: { - Requests: [ - { - id: "deviceCompliance", - url: `/deviceManagement/managedDevices/${deviceId}/deviceCompliancePolicyStates`, - method: "GET", - }, - { - id: "deviceConfiguration", - url: `/deviceManagement/managedDevices/${deviceId}/deviceConfigurationStates`, - method: "GET", - }, - { - id: "detectedApps", - url: `/deviceManagement/managedDevices/${deviceId}/detectedApps`, - method: "GET", - }, - { - id: "users", - url: `/deviceManagement/managedDevices/${deviceId}/users`, - method: "GET", - }, - ], + Requests: requests, tenantFilter: userSettingsDefaults.currentTenant, }, }); @@ -113,24 +135,13 @@ const Page = () => { const deviceConfigurationData = bulkData?.find((item) => item.id === "deviceConfiguration"); const detectedAppsData = bulkData?.find((item) => item.id === "detectedApps"); const usersData = bulkData?.find((item) => item.id === "users"); + const deviceMemberOfData = bulkData?.find((item) => item.id === "deviceMemberOf"); const deviceCompliance = deviceComplianceData?.body?.value || []; const deviceConfiguration = deviceConfigurationData?.body?.value || []; const detectedApps = detectedAppsData?.body?.value || []; const users = usersData?.body?.value || []; - - // Handle response structure - ListGraphRequest may wrap single items in Results array - // Try Results array first, then Results as object, then data directly - let deviceData = null; - if (deviceRequest.isSuccess && deviceRequest.data) { - if (Array.isArray(deviceRequest.data.Results)) { - deviceData = deviceRequest.data.Results[0]; - } else if (deviceRequest.data.Results) { - deviceData = deviceRequest.data.Results; - } else { - deviceData = deviceRequest.data; - } - } + const deviceMemberOf = deviceMemberOfData?.body?.value || []; // Helper function to format bytes to GB (matching getCippFormatting pattern) const formatBytesToGB = (bytes) => { @@ -730,6 +741,63 @@ const Page = () => { ]; } + // Prepare group membership items + const groupMembershipItems = deviceMemberOf.length > 0 + ? [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Groups", + subtext: "List of groups the device is a member of", + statusText: ` ${ + deviceMemberOf?.filter((item) => item?.["@odata.type"] === "#microsoft.graph.group") + .length + } Group(s)`, + statusColor: "info.main", + table: { + title: "Group Memberships", + hideTitle: true, + actions: [ + { + icon: , + label: "Edit Group", + link: "/identity/administration/groups/edit?groupId=[id]&groupType=[calculatedGroupType]", + }, + ], + data: deviceMemberOf?.filter( + (item) => item?.["@odata.type"] === "#microsoft.graph.group", + ), + refreshFunction: refreshFunction, + simpleColumns: ["displayName", "groupTypes", "securityEnabled", "mailEnabled"], + }, + }, + ] + : deviceMemberOfData?.status !== 200 + ? [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading device group memberships", + subtext: deviceMemberOfData?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ] + : [ + { + id: 1, + cardLabelBox: "-", + text: "No group memberships", + subtext: "This device is not a member of any groups.", + statusColor: "warning.main", + statusText: "No Groups", + propertyItems: [], + }, + ]; + return ( { items={usersItems} isCollapsible={true} /> + Memberships + From 23ecf820421819dda3b4119a405431eda49a5262 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 22:28:22 +0100 Subject: [PATCH 037/177] remove defender tab for now --- .../endpoint/MEM/devices/device/defender.jsx | 320 ------------------ .../MEM/devices/device/tabOptions.json | 4 - 2 files changed, 324 deletions(-) delete mode 100644 src/pages/endpoint/MEM/devices/device/defender.jsx diff --git a/src/pages/endpoint/MEM/devices/device/defender.jsx b/src/pages/endpoint/MEM/devices/device/defender.jsx deleted file mode 100644 index 814124772390..000000000000 --- a/src/pages/endpoint/MEM/devices/device/defender.jsx +++ /dev/null @@ -1,320 +0,0 @@ -import { Layout as DashboardLayout } from "../../../../../layouts/index.js"; -import { useSettings } from "../../../../../hooks/use-settings"; -import { useRouter } from "next/router"; -import { ApiGetCall } from "../../../../../api/ApiCall"; -import CippFormSkeleton from "../../../../../components/CippFormPages/CippFormSkeleton"; -import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { - PhoneAndroid, - Computer, - PhoneIphone, - Laptop, - Launch, - Security, - CheckCircle, - Warning, - Fingerprint, -} 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 { Typography, Card, CardHeader, Divider, Chip } from "@mui/material"; -import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; -import { useEffect, useState } from "react"; -import { PropertyList } from "../../../../../components/property-list"; -import { PropertyListItem } from "../../../../../components/property-list-item"; -import { CippHead } from "../../../../../components/CippComponents/CippHead"; -import { Button } from "@mui/material"; -import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { deviceId } = router.query; - const [waiting, setWaiting] = useState(false); - - useEffect(() => { - if (deviceId) { - setWaiting(true); - } - }, [deviceId]); - - // Get device info for title/subtitle - const deviceRequest = ApiGetCall({ - url: "/api/ListGraphRequest", - data: { - Endpoint: `deviceManagement/managedDevices/${deviceId}`, - tenantFilter: router.query.tenantFilter ?? userSettingsDefaults.currentTenant, - }, - queryKey: `ManagedDevice-${deviceId}`, - waiting: waiting, - }); - - // Get Defender state - const defenderRequest = ApiGetCall({ - url: "/api/ListDefenderState", - data: { - DeviceID: deviceId, - TenantFilter: router.query.tenantFilter ?? userSettingsDefaults.currentTenant, - }, - queryKey: `DefenderState-${deviceId}`, - waiting: waiting, - }); - - // Handle response structure - ListGraphRequest may wrap single items in Results array - let deviceData = null; - if (deviceRequest.isSuccess && deviceRequest.data) { - if (Array.isArray(deviceRequest.data.Results)) { - deviceData = deviceRequest.data.Results[0]; - } else if (deviceRequest.data.Results) { - deviceData = deviceRequest.data.Results; - } else { - deviceData = deviceRequest.data; - } - } - - // Handle Defender state response - API returns array - const defenderData = defenderRequest.isSuccess && defenderRequest.data?.[0] ? defenderRequest.data[0] : null; - const protectionState = defenderData?.windowsProtectionState; - - // Set the title and subtitle for the layout - const title = deviceRequest.isSuccess ? deviceData?.deviceName : "Loading..."; - - const subtitle = deviceRequest.isSuccess - ? [ - { - icon: , - text: , - }, - { - icon: , - text: , - }, - { - icon: , - text: ( - <> - Last Sync: - - ), - }, - { - icon: , - text: ( - - ), - }, - ] - : []; - - // Helper to format boolean values - const formatBoolean = (value) => { - if (value === null || value === undefined) return "Unknown"; - return value ? "Enabled" : "Disabled"; - }; - - // Helper to get status color - const getStatusColor = (value) => { - if (value === null || value === undefined) return "default"; - return value ? "success" : "error"; - }; - - // Helper to format device state - const formatDeviceState = (state) => { - if (!state) return "Unknown"; - return state.charAt(0).toUpperCase() + state.slice(1); - }; - - return ( - - {(deviceRequest.isLoading || defenderRequest.isLoading) && ( - - )} - {deviceRequest.isSuccess && ( - - - {defenderRequest.isSuccess && defenderData ? ( - - - - - - - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - {protectionState?.lastReportedDateTime && ( - - {new Date(protectionState.lastReportedDateTime).toLocaleString()} - {" ("} - - {")"} - - } - /> - )} - - - - - ) : defenderRequest.isSuccess && !defenderData ? ( - - - - - - - - No Defender state information available for this device. This may indicate: - -
      -
    • The device is not a Windows device
    • -
    • Windows Defender is not enabled on this device
    • -
    • The device has not reported its Defender state yet
    • -
    -
    -
    -
    -
    - ) : ( - - - - - - - - Error loading Defender state information. Please try refreshing the page. - - - - - - )} -
    - )} -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/endpoint/MEM/devices/device/tabOptions.json b/src/pages/endpoint/MEM/devices/device/tabOptions.json index 2fbbc5e490bf..e5e134f5566a 100644 --- a/src/pages/endpoint/MEM/devices/device/tabOptions.json +++ b/src/pages/endpoint/MEM/devices/device/tabOptions.json @@ -2,9 +2,5 @@ { "label": "View Device", "path": "/endpoint/MEM/devices/device" - }, - { - "label": "Windows Defender", - "path": "/endpoint/MEM/devices/device/defender" } ] From b1b1ca985334572ab1c4feb13d284ca4331eb7ad Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 23:04:32 +0100 Subject: [PATCH 038/177] Add devices to user page --- .../administration/users/user/index.jsx | 111 ++++++++++++++---- 1 file changed, 91 insertions(+), 20 deletions(-) diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 7bd6c01af1ff..95adbe3d4fed 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -4,7 +4,7 @@ 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 } from "@mui/icons-material"; +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"; @@ -97,26 +97,38 @@ const Page = () => { }); function refreshFunction() { + const userPrincipalName = userRequest.data?.[0]?.userPrincipalName; + const requests = [ + { + id: "userMemberOf", + url: `/users/${userId}/memberOf`, + method: "GET", + }, + { + id: "mfaDevices", + url: `/users/${userId}/authentication/methods?$top=99`, + method: "GET", + }, + { + id: "signInLogs", + url: `/auditLogs/signIns?$filter=(userId eq '${userId}')&$top=1`, + method: "GET", + }, + ]; + + // Only add managedDevices request if we have the userPrincipalName + if (userPrincipalName) { + requests.push({ + id: "managedDevices", + url: `/deviceManagement/managedDevices?$filter=userPrincipalName eq '${userPrincipalName}'`, + method: "GET", + }); + } + userBulkRequest.mutate({ url: "/api/ListGraphBulkRequest", data: { - Requests: [ - { - id: "userMemberOf", - url: `/users/${userId}/memberOf`, - method: "GET", - }, - { - id: "mfaDevices", - url: `/users/${userId}/authentication/methods?$top=99`, - method: "GET", - }, - { - id: "signInLogs", - url: `/auditLogs/signIns?$filter=(userId eq '${userId}')&$top=1`, - method: "GET", - }, - ], + Requests: requests, tenantFilter: userSettingsDefaults.currentTenant, noPaginateIds: ["signInLogs"], }, @@ -124,19 +136,21 @@ const Page = () => { } useEffect(() => { - if (userId && userSettingsDefaults.currentTenant && !userBulkRequest.isSuccess) { + if (userId && userSettingsDefaults.currentTenant && userRequest.isSuccess && !userBulkRequest.isSuccess) { refreshFunction(); } - }, [userId, userSettingsDefaults.currentTenant, 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 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..."; @@ -577,6 +591,57 @@ const Page = () => { ] : []; + const ownedDevicesItems = managedDevices.length > 0 + ? [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Managed Devices", + subtext: "List of devices managed for this user", + statusText: `${managedDevices.length} Device(s)`, + statusColor: "info.main", + table: { + title: "Managed Devices", + hideTitle: true, + data: managedDevices, + refreshFunction: refreshFunction, + simpleColumns: ["deviceName", "operatingSystem", "osVersion", "managementType"], + actions: [ + { + icon: , + label: "View Device", + link: `/endpoint/MEM/devices/device?deviceId=[id]&tenantFilter=${userSettingsDefaults.currentTenant}`, + }, + ], + }, + }, + ] + : managedDevicesData?.status !== 200 + ? [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading devices", + subtext: managedDevicesData?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ] + : [ + { + id: 1, + cardLabelBox: "-", + text: "No devices", + subtext: "This user does not have any managed devices.", + statusColor: "warning.main", + statusText: "No Devices", + propertyItems: [], + }, + ]; + return ( { items={roleMembershipItems} isCollapsible={true} /> + Managed Devices + From 029b896933b09ba4af43fce8f57d5105db5b2880 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 23:09:59 +0100 Subject: [PATCH 039/177] Add view user to device users --- src/pages/endpoint/MEM/devices/device/index.jsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/endpoint/MEM/devices/device/index.jsx b/src/pages/endpoint/MEM/devices/device/index.jsx index 1834646c9d9d..6caa775a2616 100644 --- a/src/pages/endpoint/MEM/devices/device/index.jsx +++ b/src/pages/endpoint/MEM/devices/device/index.jsx @@ -44,7 +44,7 @@ import { CippDataTable } from "../../../../../components/CippTable/CippDataTable import { CippHead } from "../../../../../components/CippComponents/CippHead"; import { Button } from "@mui/material"; import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; -import { PencilIcon } from "@heroicons/react/24/outline"; +import { PencilIcon, EyeIcon } from "@heroicons/react/24/outline"; const Page = () => { const userSettingsDefaults = useSettings(); @@ -712,6 +712,13 @@ const Page = () => { data: users, simpleColumns: ["displayName", "userPrincipalName", "mail"], refreshFunction: refreshFunction, + actions: [ + { + icon: , + label: "View User", + link: `/identity/administration/users/user?userId=[id]&tenantFilter=${userSettingsDefaults.currentTenant}`, + }, + ], }, }, ]; From 427351531be12cb9ddca04c0291546a764904006 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 10:55:23 +0100 Subject: [PATCH 040/177] Add groups support for universalsearch --- .../CippCards/CippUniversalSearchV2.jsx | 84 +++++++++++++++---- src/components/bulk-actions-menu.js | 14 +++- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 4b4437cecc8b..c190516cd59b 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -15,10 +15,12 @@ import { Search as SearchIcon } from "@mui/icons-material"; import { ApiGetCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; import { useRouter } from "next/router"; +import { BulkActionsMenu } from "../bulk-actions-menu"; export const CippUniversalSearchV2 = React.forwardRef( ({ onConfirm = () => {}, onChange = () => {}, maxResults = 10, value = "" }, ref) => { const [searchValue, setSearchValue] = useState(value); + const [searchType, setSearchType] = useState("Users"); const [showDropdown, setShowDropdown] = useState(false); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); const containerRef = useRef(null); @@ -30,11 +32,11 @@ export const CippUniversalSearchV2 = React.forwardRef( const search = ApiGetCall({ url: `/api/ExecUniversalSearchV2`, data: { - tenantFilter: currentTenant, searchTerms: searchValue, limit: maxResults, + type: searchType, }, - queryKey: `searchV2-${currentTenant}-${searchValue}`, + queryKey: `searchV2-${searchType}-${searchValue}`, waiting: false, }); @@ -68,14 +70,38 @@ export const CippUniversalSearchV2 = React.forwardRef( }; const handleResultClick = (match) => { - const userData = match.Data || {}; + const itemData = match.Data || {}; const tenantDomain = match.Tenant || ""; - router.push( - `/identity/administration/users/user?tenantFilter=${tenantDomain}&userId=${userData.id}`, - ); + if (searchType === "Users") { + router.push( + `/identity/administration/users/user?tenantFilter=${tenantDomain}&userId=${itemData.id}`, + ); + } else if (searchType === "Groups") { + router.push( + `/identity/administration/groups/edit?tenantFilter=${tenantDomain}&groupId=${itemData.id}`, + ); + } + setShowDropdown(false); + }; + + const handleTypeChange = (type) => { + setSearchType(type); setShowDropdown(false); }; + const typeMenuActions = [ + { + label: "Users", + icon: "UsersIcon", + onClick: () => handleTypeChange("Users"), + }, + { + label: "Groups", + icon: "Group", + onClick: () => handleTypeChange("Groups"), + }, + ]; + // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event) => { @@ -114,9 +140,22 @@ export const CippUniversalSearchV2 = React.forwardRef( const hasResults = Array.isArray(search?.data) && search.data.length > 0; const shouldShowDropdown = showDropdown && searchValue.length > 0; + const getLabel = () => { + if (searchType === "Users") { + return "Search users by UPN or Display Name"; + } else if (searchType === "Groups") { + return "Search groups by Display Name"; + } + return "Search"; + }; + return ( <> - + + { textFieldRef.current = node; @@ -128,7 +167,7 @@ export const CippUniversalSearchV2 = React.forwardRef( }} fullWidth type="text" - label="Search users by UPN or Display Name" + label={getLabel()} onKeyDown={handleKeyDown} onChange={handleChange} value={searchValue} @@ -184,6 +223,7 @@ export const CippUniversalSearchV2 = React.forwardRef( items={search.data} searchValue={searchValue} onResultClick={handleResultClick} + searchType={searchType} /> ) : ( @@ -202,7 +242,7 @@ export const CippUniversalSearchV2 = React.forwardRef( CippUniversalSearchV2.displayName = "CippUniversalSearchV2"; -const Results = ({ items = [], searchValue, onResultClick }) => { +const Results = ({ items = [], searchValue, onResultClick, searchType = "Users" }) => { const highlightMatch = (text) => { if (!text || !searchValue) return text; const escapedSearch = searchValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -221,7 +261,7 @@ const Results = ({ items = [], searchValue, onResultClick }) => { return ( <> {items.map((match, index) => { - const userData = match.Data || {}; + const itemData = match.Data || {}; const tenantDomain = match.Tenant || ""; return ( @@ -241,14 +281,30 @@ const Results = ({ items = [], searchValue, onResultClick }) => { - {highlightMatch(userData.displayName || "")} + {highlightMatch(itemData.displayName || "")} } secondary={ - - {highlightMatch(userData.userPrincipalName || "")} - + {searchType === "Users" && ( + + {highlightMatch(itemData.userPrincipalName || "")} + + )} + {searchType === "Groups" && ( + <> + {itemData.mail && ( + + {highlightMatch(itemData.mail || "")} + + )} + {itemData.description && ( + + {highlightMatch(itemData.description || "")} + + )} + + )} ; case "BarChart": return ; + case "Group": + return ; default: return null; } @@ -92,7 +94,15 @@ export const BulkActionsMenu = (props) => { ); } else { return ( - + { + if (action.onClick) { + action.onClick(); + } + popover.handleClose(); + }} + > {getIconByName(action.icon)} From 065eabd1c207de27dccdee6c454b8a7c282a0c87 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 11:12:28 +0100 Subject: [PATCH 041/177] Add a view group page --- .../administration/groups/group/index.jsx | 813 ++++++++++++++++++ .../groups/group/tabOptions.json | 6 + .../identity/administration/groups/index.js | 9 +- 3 files changed, 827 insertions(+), 1 deletion(-) create mode 100644 src/pages/identity/administration/groups/group/index.jsx create mode 100644 src/pages/identity/administration/groups/group/tabOptions.json diff --git a/src/pages/identity/administration/groups/group/index.jsx b/src/pages/identity/administration/groups/group/index.jsx new file mode 100644 index 000000000000..03f7881828fa --- /dev/null +++ b/src/pages/identity/administration/groups/group/index.jsx @@ -0,0 +1,813 @@ +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 { + Group, + Mail, + Fingerprint, + Launch, + Person, + AdminPanelSettings, + Visibility, + Lock, + LockOpen, + CloudSync, + GroupSharp, + GroupAdd, +} 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 { SvgIcon, Typography, Card, CardHeader, Divider } from "@mui/material"; +import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; +import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; +import { useEffect, useState } from "react"; +import { EyeIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { CippDataTable } from "../../../../../components/CippTable/CippDataTable"; +import { PropertyList } from "../../../../../components/property-list"; +import { PropertyListItem } from "../../../../../components/property-list-item"; +import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; +import { CippHead } from "../../../../../components/CippComponents/CippHead"; +import { Button } from "@mui/material"; +import { Edit } from "@mui/icons-material"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { groupId } = router.query; + const [waiting, setWaiting] = useState(false); + + useEffect(() => { + if (groupId) { + setWaiting(true); + } + }, [groupId]); + + const groupRequest = ApiGetCall({ + url: `/api/ListGraphRequest`, + data: { + Endpoint: `groups/${groupId}`, + tenantFilter: router.query.tenantFilter ?? userSettingsDefaults.currentTenant, + }, + queryKey: `Group-${groupId}`, + waiting: waiting, + }); + + const groupBulkRequest = ApiPostCall({ + urlFromData: true, + }); + + function refreshFunction() { + if (!groupId) return; + const requests = [ + { + id: "groupMembers", + url: `/groups/${groupId}/members`, + method: "GET", + }, + { + id: "groupOwners", + url: `/groups/${groupId}/owners`, + method: "GET", + }, + { + id: "groupMemberOf", + url: `/groups/${groupId}/memberOf`, + method: "GET", + }, + ]; + + groupBulkRequest.mutate({ + url: "/api/ListGraphBulkRequest", + data: { + Requests: requests, + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + } + + useEffect(() => { + if ( + groupId && + userSettingsDefaults.currentTenant && + groupRequest.isSuccess && + !groupBulkRequest.isSuccess + ) { + refreshFunction(); + } + }, [groupId, userSettingsDefaults.currentTenant, groupRequest.isSuccess, groupBulkRequest.isSuccess]); + + // Handle response structure - ListGraphRequest may wrap single items in Results array + let groupData = null; + if (groupRequest.isSuccess && groupRequest.data) { + if (Array.isArray(groupRequest.data.Results)) { + groupData = groupRequest.data.Results[0]; + } else if (groupRequest.data.Results) { + groupData = groupRequest.data.Results; + } else { + groupData = groupRequest.data; + } + } + + const bulkData = groupBulkRequest?.data?.data ?? []; + const groupMembersData = bulkData?.find((item) => item.id === "groupMembers"); + const groupOwnersData = bulkData?.find((item) => item.id === "groupOwners"); + const groupMemberOfData = bulkData?.find((item) => item.id === "groupMemberOf"); + + const groupMembers = groupMembersData?.body?.value || []; + const groupOwners = groupOwnersData?.body?.value || []; + const groupMemberOf = groupMemberOfData?.body?.value || []; + + // Set the title and subtitle for the layout + const title = groupRequest.isSuccess ? groupData?.displayName : "Loading..."; + + const subtitle = groupRequest.isSuccess + ? [ + { + icon: , + text: , + }, + { + icon: , + text: , + }, + { + icon: , + text: ( + <> + Created: + + ), + }, + { + icon: , + text: ( + + ), + }, + ] + : []; + + // Calculate groupType and calculatedGroupType for actions + const calculateGroupType = (group) => { + if (!group) return { groupType: null, calculatedGroupType: null }; + let groupType = null; + let calculatedGroupType = null; + + if (group.groupTypes?.includes("Unified")) { + groupType = "Microsoft 365"; + calculatedGroupType = "m365"; + } else if (!group.mailEnabled && group.securityEnabled) { + groupType = "Security"; + calculatedGroupType = "security"; + } else if (group.mailEnabled && group.securityEnabled) { + groupType = "Mail-Enabled Security"; + calculatedGroupType = "mailenabledsecurity"; + } else if ( + (!group.groupTypes || group.groupTypes.length === 0) && + group.mailEnabled && + !group.securityEnabled + ) { + groupType = "Distribution List"; + calculatedGroupType = "distribution"; + } + + return { groupType, calculatedGroupType }; + }; + + // Calculate group type and add to data for actions + const { groupType, calculatedGroupType } = calculateGroupType(groupData); + const data = groupData + ? { + ...groupData, + groupType: groupType, + calculatedGroupType: calculatedGroupType, + } + : null; + + // Calculate group type for display + const getGroupType = () => { + if (!groupData) return "N/A"; + if (groupData.groupTypes?.includes("Unified")) { + return "Microsoft 365"; + } + if (!groupData.mailEnabled && groupData.securityEnabled) { + return "Security"; + } + if (groupData.mailEnabled && groupData.securityEnabled) { + return "Mail-Enabled Security"; + } + if ( + (!groupData.groupTypes || groupData.groupTypes.length === 0) && + groupData.mailEnabled && + !groupData.securityEnabled + ) { + return "Distribution List"; + } + return "N/A"; + }; + + // Get group actions + const getGroupActions = () => { + if (!groupData) return []; + const { groupType, calculatedGroupType } = calculateGroupType(groupData); + + return [ + { + //tested + label: "Edit Group", + link: "/identity/administration/groups/edit?groupId=[id]&groupType=[groupType]", + multiPost: false, + icon: , + color: "success", + showInActionsMenu: true, + }, + { + label: "Set Global Address List Visibility", + type: "POST", + url: "/api/ExecGroupsHideFromGAL", + icon: , + data: { + ID: "mail", + GroupType: "groupType", + }, + fields: [ + { + type: "radio", + name: "HidefromGAL", + label: "Global Address List Visibility", + options: [ + { label: "Hidden", value: true }, + { label: "Shown", value: false }, + ], + validators: { required: "Please select a visibility option" }, + }, + ], + confirmText: + "Are you sure you want to hide this group from the global address list? Remember this will not work if the group is AD Synched.", + multiPost: false, + }, + { + label: "Only allow messages from people inside the organisation", + type: "POST", + url: "/api/ExecGroupsDeliveryManagement", + icon: , + data: { + ID: "mail", + GroupType: "groupType", + OnlyAllowInternal: true, + }, + confirmText: + "Are you sure you want to only allow messages from people inside the organisation? Remember this will not work if the group is AD Synched.", + multiPost: false, + }, + { + label: "Allow messages from people inside and outside the organisation", + type: "POST", + icon: , + url: "/api/ExecGroupsDeliveryManagement", + data: { + ID: "mail", + GroupType: "groupType", + OnlyAllowInternal: false, + }, + confirmText: + "Are you sure you want to allow messages from people inside and outside the organisation? Remember this will not work if the group is AD Synched.", + multiPost: false, + }, + { + label: "Set Source of Authority", + type: "POST", + url: "/api/ExecSetCloudManaged", + icon: , + data: { + ID: "id", + displayName: "displayName", + type: "!Group", + }, + fields: [ + { + type: "radio", + name: "isCloudManaged", + label: "Source of Authority", + options: [ + { label: "Cloud Managed", value: true }, + { label: "On-Premises Managed", value: false }, + ], + validators: { required: "Please select a source of authority" }, + }, + ], + confirmText: + "Are you sure you want to change the source of authority for '[displayName]'? Setting it to On-Premises Managed will take until the next sync cycle to show the change.", + multiPost: false, + }, + { + label: "Create template based on group", + type: "POST", + url: "/api/AddGroupTemplate", + icon: , + data: { + displayName: "displayName", + description: "description", + groupType: "calculatedGroupType", + membershipRules: "membershipRule", + allowExternal: "allowExternal", + username: "mailNickname", + }, + confirmText: "Are you sure you want to create a template based on this group?", + multiPost: false, + }, + { + label: "Create Team from Group", + type: "POST", + url: "/api/AddGroupTeam", + icon: , + data: { + GroupId: "id", + }, + confirmText: + "Are you sure you want to create a Team from this group? Note: The group must be at least 15 minutes old for this to work.", + multiPost: false, + defaultvalues: { + TeamSettings: { + memberSettings: { + allowCreatePrivateChannels: false, + allowCreateUpdateChannels: true, + allowDeleteChannels: false, + allowAddRemoveApps: false, + allowCreateUpdateRemoveTabs: false, + allowCreateUpdateRemoveConnectors: false, + }, + messagingSettings: { + allowUserEditMessages: true, + allowUserDeleteMessages: true, + allowOwnerDeleteMessages: false, + allowTeamMentions: false, + allowChannelMentions: false, + }, + funSettings: { + allowGiphy: true, + giphyContentRating: "strict", + allowStickersAndMemes: false, + allowCustomMemes: false, + }, + }, + }, + fields: [ + { + type: "heading", + name: "memberSettingsHeading", + label: "Member Settings", + }, + { + type: "switch", + name: "TeamSettings.memberSettings.allowCreatePrivateChannels", + label: "Allow members to create private channels", + }, + { + type: "switch", + name: "TeamSettings.memberSettings.allowCreateUpdateChannels", + label: "Allow members to create and update channels", + }, + { + type: "switch", + name: "TeamSettings.memberSettings.allowDeleteChannels", + label: "Allow members to delete channels", + }, + { + type: "switch", + name: "TeamSettings.memberSettings.allowAddRemoveApps", + label: "Allow members to add and remove apps", + }, + { + type: "switch", + name: "TeamSettings.memberSettings.allowCreateUpdateRemoveTabs", + label: "Allow members to create, update and remove tabs", + }, + { + type: "switch", + name: "TeamSettings.memberSettings.allowCreateUpdateRemoveConnectors", + label: "Allow members to create, update and remove connectors", + }, + { + type: "heading", + name: "messagingSettingsHeading", + label: "Messaging Settings", + }, + { + type: "switch", + name: "TeamSettings.messagingSettings.allowUserEditMessages", + label: "Allow users to edit their messages", + }, + { + type: "switch", + name: "TeamSettings.messagingSettings.allowUserDeleteMessages", + label: "Allow users to delete their messages", + }, + { + type: "switch", + name: "TeamSettings.messagingSettings.allowOwnerDeleteMessages", + label: "Allow owners to delete messages", + }, + { + type: "switch", + name: "TeamSettings.messagingSettings.allowTeamMentions", + label: "Allow @team mentions", + }, + { + type: "switch", + name: "TeamSettings.messagingSettings.allowChannelMentions", + label: "Allow @channel mentions", + }, + { + type: "heading", + name: "funSettingsHeading", + label: "Fun Settings", + }, + { + type: "switch", + name: "TeamSettings.funSettings.allowGiphy", + label: "Allow Giphy", + }, + { + type: "select", + name: "TeamSettings.funSettings.giphyContentRating", + label: "Giphy content rating", + options: [ + { value: "strict", label: "Strict" }, + { value: "moderate", label: "Moderate" }, + ], + }, + { + type: "switch", + name: "TeamSettings.funSettings.allowStickersAndMemes", + label: "Allow stickers and memes", + }, + { + type: "switch", + name: "TeamSettings.funSettings.allowCustomMemes", + label: "Allow custom memes", + }, + ], + condition: (row) => row?.calculatedGroupType === "m365", + }, + { + label: "Delete Group", + type: "POST", + url: "/api/ExecGroupsDelete", + icon: , + data: { + ID: "id", + GroupType: "groupType", + DisplayName: "displayName", + }, + confirmText: "Are you sure you want to delete this group.", + multiPost: false, + }, + ]; + }; + + const groupActions = groupData ? getGroupActions() : []; + + // Prepare members items + const membersItems = + groupMembers.length > 0 + ? [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Members", + subtext: "List of members in this group", + statusText: ` ${groupMembers.length} Member(s)`, + statusColor: "info.main", + table: { + title: "Members", + hideTitle: true, + actions: [ + { + icon: , + label: "View User", + link: `/identity/administration/users/user?userId=[id]&tenantFilter=${userSettingsDefaults.currentTenant}`, + condition: (row) => row["@odata.type"] === "#microsoft.graph.user", + }, + ], + data: groupMembers, + refreshFunction: refreshFunction, + simpleColumns: ["displayName", "userPrincipalName", "mail", "@odata.type"], + }, + }, + ] + : groupMembersData?.status !== 200 + ? [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading members", + subtext: groupMembersData?.body?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ] + : [ + { + id: 1, + cardLabelBox: "-", + text: "No members", + subtext: "This group has no members.", + statusColor: "warning.main", + statusText: "No Members", + propertyItems: [], + }, + ]; + + // Prepare owners items + const ownersItems = + groupOwners.length > 0 + ? [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Owners", + subtext: "List of owners of this group", + statusText: ` ${groupOwners.length} Owner(s)`, + statusColor: "info.main", + table: { + title: "Owners", + hideTitle: true, + actions: [ + { + icon: , + label: "View User", + link: `/identity/administration/users/user?userId=[id]&tenantFilter=${userSettingsDefaults.currentTenant}`, + condition: (row) => row["@odata.type"] === "#microsoft.graph.user", + }, + ], + data: groupOwners, + refreshFunction: refreshFunction, + simpleColumns: ["displayName", "userPrincipalName", "mail", "@odata.type"], + }, + }, + ] + : groupOwnersData?.status !== 200 + ? [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading owners", + subtext: groupOwnersData?.body?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ] + : [ + { + id: 1, + cardLabelBox: "-", + text: "No owners", + subtext: "This group has no owners.", + statusColor: "warning.main", + statusText: "No Owners", + propertyItems: [], + }, + ]; + + // Prepare group memberships items + const groupMembershipItems = + groupMemberOf.length > 0 + ? [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: , + }, + text: "Group Memberships", + subtext: "List of groups this group is a member of", + statusText: ` ${ + groupMemberOf?.filter((item) => item?.["@odata.type"] === "#microsoft.graph.group").length + } Group(s)`, + statusColor: "info.main", + table: { + title: "Group Memberships", + hideTitle: true, + actions: [ + { + icon: , + label: "View Group", + link: `/identity/administration/groups/group?groupId=[id]&tenantFilter=${userSettingsDefaults.currentTenant}`, + condition: (row) => row["@odata.type"] === "#microsoft.graph.group", + }, + { + icon: , + label: "Edit Group", + link: "/identity/administration/groups/edit?groupId=[id]&groupType=[calculatedGroupType]", + condition: (row) => row["@odata.type"] === "#microsoft.graph.group", + }, + ], + data: groupMemberOf?.filter((item) => item?.["@odata.type"] === "#microsoft.graph.group"), + refreshFunction: refreshFunction, + simpleColumns: ["displayName", "groupTypes", "securityEnabled", "mailEnabled"], + }, + }, + ] + : groupMemberOfData?.status !== 200 + ? [ + { + id: 1, + cardLabelBox: "!", + text: "Error loading group memberships", + subtext: groupMemberOfData?.body?.error?.message || "Unknown error", + statusColor: "error.main", + statusText: "Error", + propertyItems: [], + }, + ] + : [ + { + id: 1, + cardLabelBox: "-", + text: "No group memberships", + subtext: "This group is not a member of any other groups.", + statusColor: "warning.main", + statusText: "No Groups", + propertyItems: [], + }, + ]; + + return ( + + {groupRequest.isLoading && } + {groupRequest.isSuccess && ( + + + + + + + + + + + + + {data?.displayName || "N/A"} + + {getGroupType()} + + + } + /> + + + + Display Name: + + + {getCippFormatting(data?.displayName, "displayName") || "N/A"} + + + + + Group ID: + + + {getCippFormatting(data?.id, "id") || "N/A"} + + + {data?.mail && ( + + + Email Address: + + {data.mail || "N/A"} + + )} + {data?.description && ( + + + Description: + + {data.description || "N/A"} + + )} + + + Group Type: + + {getGroupType()} + + + + Mail Enabled: + + + {getCippFormatting(data?.mailEnabled, "mailEnabled") || "N/A"} + + + + + Security Enabled: + + + {getCippFormatting(data?.securityEnabled, "securityEnabled") || "N/A"} + + + {data?.createdDateTime && ( + + + Created Date: + + + {data.createdDateTime + ? new Date(data.createdDateTime).toLocaleString() + : "N/A"} + + + )} + {data?.onPremisesSyncEnabled && ( + + + Synced from AD: + + + {getCippFormatting(data?.onPremisesSyncEnabled, "onPremisesSyncEnabled") || + "N/A"} + + + )} + + } + /> + + + + + + Members + + Owners + + Memberships + + + + + + )} + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/groups/group/tabOptions.json b/src/pages/identity/administration/groups/group/tabOptions.json new file mode 100644 index 000000000000..f092f4cb37d6 --- /dev/null +++ b/src/pages/identity/administration/groups/group/tabOptions.json @@ -0,0 +1,6 @@ +[ + { + "label": "View Group", + "path": "/identity/administration/groups/group" + } +] diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index bb5de39a09a2..1a05309ee7d2 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -2,7 +2,7 @@ import { Button } from "@mui/material"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import Link from "next/link"; -import { TrashIcon } from "@heroicons/react/24/outline"; +import { TrashIcon, EyeIcon } from "@heroicons/react/24/outline"; import { Visibility, VisibilityOff, @@ -26,6 +26,13 @@ const Page = () => { setShowMembers(!showMembers); }; const actions = [ + { + label: "View Group", + link: `/identity/administration/groups/group?groupId=[id]&tenantFilter=${currentTenant}`, + color: "info", + icon: , + multiPost: false, + }, { //tested label: "Edit Group", From 6eac4994e2e85ef048c3d0694753967dfad592e7 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 11:29:11 +0100 Subject: [PATCH 042/177] add view group to universalsearchv2 --- src/components/CippCards/CippUniversalSearchV2.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index c190516cd59b..82dfefc5824d 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -78,7 +78,7 @@ export const CippUniversalSearchV2 = React.forwardRef( ); } else if (searchType === "Groups") { router.push( - `/identity/administration/groups/edit?tenantFilter=${tenantDomain}&groupId=${itemData.id}`, + `/identity/administration/groups/group?groupId=${itemData.id}&tenantFilter=${tenantDomain}`, ); } setShowDropdown(false); From 9dd4c076e7742a6f3789474f28d0a4fe6faad7d9 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 11:34:20 +0100 Subject: [PATCH 043/177] Add a search button --- .../CippCards/CippUniversalSearchV2.jsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 82dfefc5824d..bc6fe4055dbb 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -16,6 +16,7 @@ import { ApiGetCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; import { useRouter } from "next/router"; import { BulkActionsMenu } from "../bulk-actions-menu"; +import { Button } from "@mui/material"; export const CippUniversalSearchV2 = React.forwardRef( ({ onConfirm = () => {}, onChange = () => {}, maxResults = 10, value = "" }, ref) => { @@ -63,6 +64,12 @@ export const CippUniversalSearchV2 = React.forwardRef( const handleKeyDown = (event) => { if (event.key === "Enter" && searchValue.length > 0) { + handleSearch(); + } + }; + + const handleSearch = () => { + if (searchValue.length > 0) { updateDropdownPosition(); search.refetch(); setShowDropdown(true); @@ -193,6 +200,15 @@ export const CippUniversalSearchV2 = React.forwardRef( }, }} /> + {shouldShowDropdown && ( From adce00b14550e6e0e4f26fd7284b9d9b5fe523e1 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 13:00:20 +0100 Subject: [PATCH 044/177] add log retention section --- .../CippSettings/CippLogRetentionSettings.jsx | 106 ++++++++++++++++++ src/pages/cipp/settings/index.js | 4 + 2 files changed, 110 insertions(+) create mode 100644 src/components/CippSettings/CippLogRetentionSettings.jsx diff --git a/src/components/CippSettings/CippLogRetentionSettings.jsx b/src/components/CippSettings/CippLogRetentionSettings.jsx new file mode 100644 index 000000000000..11767026aba4 --- /dev/null +++ b/src/components/CippSettings/CippLogRetentionSettings.jsx @@ -0,0 +1,106 @@ +import { Button, Typography, TextField, Box } from "@mui/material"; +import CippButtonCard from "../CippCards/CippButtonCard"; +import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; +import { useState, useEffect } from "react"; + +const CippLogRetentionSettings = () => { + const retentionSetting = ApiGetCall({ + url: "/api/ExecLogRetentionConfig?List=true", + queryKey: "LogRetentionSettings", + }); + + const retentionChange = ApiPostCall({ + datafromUrl: true, + relatedQueryKeys: "LogRetentionSettings", + }); + + const [retentionDays, setRetentionDays] = useState(90); + const [error, setError] = useState(""); + + useEffect(() => { + if (retentionSetting?.data?.Results?.RetentionDays) { + setRetentionDays(retentionSetting.data.Results.RetentionDays); + } + }, [retentionSetting.data]); + + const handleRetentionChange = () => { + const days = parseInt(retentionDays); + + if (isNaN(days) || days < 7) { + setError("Retention must be at least 7 days"); + return; + } + + if (days > 365) { + setError("Retention must be at most 365 days"); + return; + } + + setError(""); + retentionChange.mutate({ + url: "/api/ExecLogRetentionConfig", + data: { RetentionDays: days }, + queryKey: "LogRetentionPost", + }); + }; + + const handleInputChange = (e) => { + const value = e.target.value; + setRetentionDays(value); + + const days = parseInt(value); + if (!isNaN(days) && days < 7) { + setError("Retention must be at least 7 days"); + } else if (!isNaN(days) && days > 365) { + setError("Retention must be at most 365 days"); + } else if (isNaN(days) && value !== "") { + setError("Please enter a valid number"); + } else { + setError(""); + } + }; + + const RetentionControls = () => { + return ( + + + + + ); + }; + + return ( + } + > + + Configure how long to keep CIPP log entries. Logs will be automatically deleted after this + period. Minimum retention is 7 days, maximum is 365 days, default is 90 days. + + + ); +}; + +export default CippLogRetentionSettings; diff --git a/src/pages/cipp/settings/index.js b/src/pages/cipp/settings/index.js index 07f6cd63a0fe..c845a7a32c6b 100644 --- a/src/pages/cipp/settings/index.js +++ b/src/pages/cipp/settings/index.js @@ -11,6 +11,7 @@ import CippCacheSettings from "../../../components/CippSettings/CippCacheSetting import CippBackupSettings from "../../../components/CippSettings/CippBackupSettings"; import CippBrandingSettings from "../../../components/CippSettings/CippBrandingSettings"; import CippBackupRetentionSettings from "../../../components/CippSettings/CippBackupRetentionSettings"; +import CippLogRetentionSettings from "../../../components/CippSettings/CippLogRetentionSettings"; import CippJitAdminSettings from "../../../components/CippSettings/CippJitAdminSettings"; const Page = () => { return ( @@ -34,6 +35,9 @@ const Page = () => { + + + From 1e17ed083db523d173653fef4c1341843e66cb6f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 16 Feb 2026 13:26:31 -0500 Subject: [PATCH 045/177] Handle array forwarding addresses, add type checks Improve Exchange info card and user page safety: parse forwardingAddress as an array or string, strip case-insensitive "smtp:" prefixes, join multiple addresses, and infer External vs Internal targets with a safe fallback to String(). Add a typeof check for group.displayName before using startsWith to avoid runtime errors. Also include minor formatting/trailing-comma cleanup in a few calls. --- .../CippCards/CippExchangeInfoCard.jsx | 45 +++++++++++++++---- .../administration/users/user/exchange.jsx | 1 + 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index 60e7ac7ef063..6a00f53c0248 100644 --- a/src/components/CippCards/CippExchangeInfoCard.jsx +++ b/src/components/CippCards/CippExchangeInfoCard.jsx @@ -89,7 +89,7 @@ export const CippExchangeInfoCard = (props) => { {getCippFormatting( exchangeData?.HiddenFromAddressLists, - "HiddenFromAddressLists" + "HiddenFromAddressLists", )} @@ -124,13 +124,13 @@ export const CippExchangeInfoCard = (props) => { sx={{ width: "100%" }} variant="determinate" addedLabel={`(${Math.round(exchangeData.TotalItemSize)} GB of ${Math.round( - exchangeData?.ProhibitSendReceiveQuota + exchangeData?.ProhibitSendReceiveQuota, )}GB)`} value={ Math.round( (exchangeData?.TotalItemSize / exchangeData?.ProhibitSendReceiveQuota) * 100 * - 100 + 100, ) / 100 } /> @@ -154,12 +154,39 @@ export const CippExchangeInfoCard = (props) => { let cleanAddress = ""; if (forwardingAddress) { - if (forwardingAddress.startsWith("smtp:")) { - forwardingType = "External"; - cleanAddress = forwardingAddress.replace("smtp:", ""); - } else { + // Handle array of forwarding addresses + if (Array.isArray(forwardingAddress)) { + cleanAddress = forwardingAddress + .map((addr) => + typeof addr === "string" ? addr.replace(/^smtp:/i, "") : String(addr), + ) + .join(", "); + // Check if any address has smtp: prefix (external) or contains @ (external email) + forwardingType = forwardingAddress.some( + (addr) => + (typeof addr === "string" && addr.toLowerCase().startsWith("smtp:")) || + (typeof addr === "string" && addr.includes("@")), + ) + ? "External" + : "Internal"; + } + // Handle single string address + else if (typeof forwardingAddress === "string") { + if (forwardingAddress.startsWith("smtp:")) { + forwardingType = "External"; + cleanAddress = forwardingAddress.replace(/^smtp:/i, ""); + } else if (forwardingAddress.includes("@")) { + forwardingType = "External"; + cleanAddress = forwardingAddress; + } else { + forwardingType = "Internal"; + cleanAddress = forwardingAddress; + } + } + // Fallback for other types + else { forwardingType = "Internal"; - cleanAddress = forwardingAddress; + cleanAddress = String(forwardingAddress); } } @@ -225,7 +252,7 @@ export const CippExchangeInfoCard = (props) => { {getCippFormatting( exchangeData?.AutoExpandingArchive, - "AutoExpandingArchive" + "AutoExpandingArchive", )} diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index f91a6831426d..1d01f699c5a7 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -164,6 +164,7 @@ const Page = () => { (group.displayName && group.displayName === userIdentifier) || // Partial match - permission identifier starts with group display name (handles timestamps) (group.displayName && + typeof group.displayName === "string" && typeof userIdentifier === "string" && userIdentifier.startsWith(group.displayName)) ); From 197addfe29a63141f6fd43706d5e3c170bb4ce76 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 16 Feb 2026 15:51:30 -0500 Subject: [PATCH 046/177] fix tenantFilter casing in drift --- src/pages/tenant/manage/drift.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 26c4374585bf..02fe6ffb77d1 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -74,7 +74,7 @@ const ManageDriftPage = () => { const driftApi = ApiGetCall({ url: "/api/listTenantDrift", data: { - TenantFilter: tenantFilter, + tenantFilter: tenantFilter, }, queryKey: `TenantDrift-${tenantFilter}`, }); @@ -99,7 +99,7 @@ const ManageDriftPage = () => { url: "/api/ListStandardsCompare", data: { TemplateId: templateId, - TenantFilter: tenantFilter, + tenantFilter: tenantFilter, CompareToStandard: true, }, queryKey: `StandardsCompare-${templateId}-${tenantFilter}`, @@ -1109,7 +1109,7 @@ const ManageDriftPage = () => { receivedValue: deviation.receivedValue, }, ], - TenantFilter: tenantFilter, + tenantFilter: tenantFilter, }, action: { text: actionText, @@ -1162,7 +1162,7 @@ const ManageDriftPage = () => { receivedValue: deviation.receivedValue, }, ], - TenantFilter: tenantFilter, + tenantFilter: tenantFilter, }, action: { text: actionText, @@ -1239,7 +1239,7 @@ const ManageDriftPage = () => { setActionData({ data: { deviations: deviations, - TenantFilter: tenantFilter, + tenantFilter: tenantFilter, receivedValues: deviations.map((d) => d.receivedValue), }, action: { @@ -1259,7 +1259,7 @@ const ManageDriftPage = () => { setActionData({ data: { RemoveDriftCustomization: true, - TenantFilter: tenantFilter, + tenantFilter: tenantFilter, }, action: { text: "remove all drift customizations", From dae6d28d88183a66010d91aa07e24e1f7ef88366 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 16 Feb 2026 15:52:03 -0500 Subject: [PATCH 047/177] Require tenant in drift mode; code cleanup Enforce tenant selection for drift templates by removing the previous skip: step2 now always checks tenantFilter, and isSaveDisabled requires a non-empty tenantFilter and currentStep >= 4 for both drift and non-drift flows (also preserves drift conflict check). Also includes non-functional cleanups: reformat lazy import, add trailing commas, fix filter/search predicate punctuation, and adjust some JSX ternary indentation to satisfy linting/style. --- .../tenant/standards/templates/template.jsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx index f7dd53861b02..9a8d54692d11 100644 --- a/src/pages/tenant/standards/templates/template.jsx +++ b/src/pages/tenant/standards/templates/template.jsx @@ -8,8 +8,8 @@ import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } fro import standards from "../../../../data/standards"; import CippStandardAccordion from "../../../../components/CippStandards/CippStandardAccordion"; // Lazy load the dialog to improve initial page load performance -const CippStandardDialog = lazy(() => - import("../../../../components/CippStandards/CippStandardDialog") +const CippStandardDialog = lazy( + () => import("../../../../components/CippStandards/CippStandardDialog"), ); import CippStandardsSideBar from "../../../../components/CippStandards/CippStandardsSideBar"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; @@ -62,7 +62,7 @@ const Page = () => { useEffect(() => { const stepsStatus = { step1: !!_.get(watchForm, "templateName"), - step2: isDriftMode || _.get(watchForm, "tenantFilter", []).length > 0, // Skip tenant requirement for drift mode + step2: _.get(watchForm, "tenantFilter", []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: _.get(watchForm, "standards") && @@ -84,7 +84,7 @@ const Page = () => { (url) => { if (hasUnsavedChanges) { const confirmLeave = window.confirm( - "You have unsaved changes. Are you sure you want to leave this page?" + "You have unsaved changes. Are you sure you want to leave this page?", ); if (!confirmLeave) { router.events.emit("routeChangeError"); @@ -92,7 +92,7 @@ const Page = () => { } } }, - [hasUnsavedChanges, router] + [hasUnsavedChanges, router], ); // Handle browser back/forward navigation or tab close @@ -144,7 +144,7 @@ const Page = () => { Object.keys(apiData.standards).forEach((key) => { if (Array.isArray(apiData.standards[key])) { apiData.standards[key] = apiData.standards[key].filter( - (value) => value !== null && value !== undefined + (value) => value !== null && value !== undefined, ); } }); @@ -207,7 +207,7 @@ const Page = () => { standard.label.toLowerCase().includes(searchQuery.toLowerCase()) || standard.helpText.toLowerCase().includes(searchQuery.toLowerCase()) || (standard.tag && - standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))) + standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))), ); const handleToggleStandard = (standardName) => { @@ -269,10 +269,13 @@ const Page = () => { // Determine if save button should be disabled based on configuration const isSaveDisabled = isDriftMode - ? currentStep < 3 || hasDriftConflict // For drift mode, only require steps 1, 3, and 4 (skip tenant requirement) and no drift conflicts + ? !_.get(watchForm, "tenantFilter") || + !_.get(watchForm, "tenantFilter").length || + currentStep < 4 || + hasDriftConflict // For drift mode, require all steps and no drift conflicts : !_.get(watchForm, "tenantFilter") || !_.get(watchForm, "tenantFilter").length || - currentStep < 3; + currentStep < 4; // Create drift management actions (excluding refresh) const driftActions = useMemo(() => { @@ -300,7 +303,7 @@ const Page = () => { const handleSafeNavigation = (url) => { if (hasUnsavedChanges) { const confirmLeave = window.confirm( - "You have unsaved changes. Are you sure you want to leave this page?" + "You have unsaved changes. Are you sure you want to leave this page?", ); if (confirmLeave) { router.push(url); @@ -319,8 +322,8 @@ const Page = () => { ? "Edit Drift Template" : "Edit Standards Template" : isDriftMode - ? "Add Drift Template" - : "Add Standards Template" + ? "Add Drift Template" + : "Add Standards Template" } /> @@ -338,8 +341,8 @@ const Page = () => { ? "Edit Drift Template" : "Edit Standards Template" : isDriftMode - ? "Add Drift Template" - : "Add Standards Template"} + ? "Add Drift Template" + : "Add Standards Template"} + + )} + + {currentOnboarding && ( + + + + + Onboarding Status: {getCippTranslation(currentOnboarding?.Status)} + + + Updated {getCippFormatting(currentOnboarding?.Timestamp, "Timestamp", "date")} + + + {currentOnboarding?.Logs && currentOnboarding.Logs.length > 0 && ( + + + + )} + ({ + title: step.Title, + description: step.Message, + error: step.Status === "failed", + loading: step.Status === "running", + })) ?? [] + } + /> + {(currentOnboarding?.Status === "failed" || + currentOnboarding?.Status === "succeeded") && ( + + + + )} + + + )} + + )} + + + + ); +}; + +export default CippGDAPTenantOnboarding; diff --git a/src/components/CippWizard/CippGDAPTenantSetup.jsx b/src/components/CippWizard/CippGDAPTenantSetup.jsx new file mode 100644 index 000000000000..d64b2bd4f702 --- /dev/null +++ b/src/components/CippWizard/CippGDAPTenantSetup.jsx @@ -0,0 +1,281 @@ +import { useEffect, useState } from "react"; +import { + Stack, + Box, + Typography, + Link, + Alert, + Button, + Checkbox, + FormControlLabel, + Accordion, + AccordionSummary, + AccordionDetails, + SvgIcon, +} from "@mui/material"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiPostCall, ApiGetCall } from "../../api/ApiCall"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { useWatch } from "react-hook-form"; +import { CippPropertyList } from "../CippComponents/CippPropertyList"; +import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { PlusIcon } from "@heroicons/react/24/outline"; + +export const CippGDAPTenantSetup = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + const [inviteGenerated, setInviteGenerated] = useState(false); + const [inviteAccepted, setInviteAccepted] = useState(false); + const [inviteData, setInviteData] = useState(null); + const [createDefaults, setCreateDefaults] = useState(false); + + formControl.register("GDAPInviteAccepted", { + required: true, + }); + + formControl.register("GDAPRelationshipId", { + required: true, + }); + + const createCippDefaults = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ListGDAPRoleTemplatesAutocomplete-wizard", "ListGDAPRoleTemplates-wizard"], + }); + + const selectedTemplate = useWatch({ + control: formControl.control, + name: "gdapTemplate", + }); + + const templateList = ApiGetCall({ + url: "/api/ExecGDAPRoleTemplate", + queryKey: "ListGDAPRoleTemplates-wizard", + }); + + const generateInvite = ApiPostCall({ + urlFromData: true, + }); + + useEffect(() => { + if (templateList?.data?.Results?.length === 0) { + setCreateDefaults(true); + } else { + setCreateDefaults(false); + } + }, [templateList.isSuccess, templateList.data]); + + const handleGenerateInvite = () => { + if (!selectedTemplate) { + return; + } + + const inviteData = { + roleMappings: selectedTemplate.value, + Reference: "Created via Setup Wizard", + }; + + generateInvite.mutate({ + url: "/api/ExecGDAPInvite", + data: inviteData, + }); + }; + + useEffect(() => { + if (generateInvite.isSuccess && generateInvite.data) { + const invite = generateInvite.data?.data?.Invite || generateInvite.data?.Invite; + if (invite) { + setInviteGenerated(true); + setInviteData(invite); + // Store the relationship ID for the next step + formControl.setValue("GDAPRelationshipId", invite.RowKey); + } + } + }, [generateInvite.isSuccess, generateInvite.data, formControl]); + + useEffect(() => { + formControl.setValue("GDAPInviteAccepted", inviteAccepted); + formControl.trigger("GDAPInviteAccepted"); + }, [inviteAccepted, formControl]); + + return ( + + + + GDAP Tenant Setup + + + This process will help you set up a new GDAP relationship with a customer tenant. You'll + generate an invite that the customer needs to accept before completing onboarding. For + more information about GDAP setup, visit the{" "} + + GDAP documentation + + . + + + + {!inviteGenerated && ( + <> + {createDefaults && ( + + + The CIPP Defaults template is missing from the GDAP Role Templates. Create it now? + + + + + )} + + option.TemplateId, + valueField: (option) => option.RoleMappings, + }} + multiple={false} + creatable={false} + required={true} + /> + + + {selectedTemplate?.value && ( + + + }> + Selected Role Mappings + + + { + return { + label: `${role.RoleName}`, + value: `Mapped to '${role.GroupName}'`, + }; + })} + /> + + + + )} + + + + + + + + )} + + {inviteGenerated && inviteData && ( + <> + + + Invite generated successfully! Send the invite link below to your customer's Global + Administrator to accept. + + + + + + }> + Invite Details + + + + + {inviteData.InviteUrl} + + + + ), + }, + { + label: "Reference", + value: inviteData.Reference || "N/A", + }, + ]} + /> + + + + + + + + The customer must accept this invite as a Global Administrator before you can + proceed with onboarding. + + + setInviteAccepted(e.target.checked)} + /> + } + label="This invite has been accepted in the customer tenant, and we're ready to proceed with onboarding." + /> + + + )} + + + + ); +}; + +export default CippGDAPTenantSetup; diff --git a/src/components/CippWizard/CippWizardStepButtons.jsx b/src/components/CippWizard/CippWizardStepButtons.jsx index fd80d7c872fa..bf0e7f3a7918 100644 --- a/src/components/CippWizard/CippWizardStepButtons.jsx +++ b/src/components/CippWizard/CippWizardStepButtons.jsx @@ -13,6 +13,7 @@ export const CippWizardStepButtons = (props) => { formControl, noNextButton = false, noSubmitButton = false, + nextButtonDisabled = false, replacementBehaviour, queryKeys, ...other @@ -50,7 +51,7 @@ export const CippWizardStepButtons = (props) => { {!noNextButton && currentStep !== lastStep && ( } - title="JIT Admin Table" + title="JIT Admins" apiUrl="/api/ListJITAdmin" apiDataKey="Results" simpleColumns={simpleColumns} diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 405139ae6e93..cf72f6e4747e 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -185,7 +185,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr "reviewedDate", // App Consent Requests ]; - const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration|[Tt]imestamp)/; + const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration|[Tt]imestamp|[sS]tart[Dd]ate)/; if (timeAgoArray.includes(cellName) || matchDateTime.test(cellName)) { return isText && canReceive === false ? ( new Date(data).toLocaleString() // This runs if canReceive is false and isText is true From e29511d4c8233c381152d8d5b1c17d26a97867e1 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Tue, 17 Feb 2026 23:52:23 +0100 Subject: [PATCH 053/177] fix: update enable/disable actions to use ruleId Fixes #5379 --- src/pages/email/transport/list-rules/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/email/transport/list-rules/index.js b/src/pages/email/transport/list-rules/index.js index 3ff3e24d4dd8..75e3f66366b5 100644 --- a/src/pages/email/transport/list-rules/index.js +++ b/src/pages/email/transport/list-rules/index.js @@ -35,7 +35,7 @@ const Page = () => { url: "/api/AddEditTransportRule", data: { Enabled: "!Enabled", - Identity: "Guid", + ruleId: "Guid", Name: "Name", }, condition: (row) => row.State === "Disabled", @@ -44,7 +44,7 @@ const Page = () => { }, { label: "Edit Rule", - customComponent: (row, {drawerVisible, setDrawerVisible}) => ( + customComponent: (row, { drawerVisible, setDrawerVisible }) => ( { url: "/api/AddEditTransportRule", data: { Enabled: "!Disabled", - Identity: "Guid", + ruleId: "Guid", Name: "Name", }, condition: (row) => row.State === "Enabled", @@ -125,7 +125,7 @@ const Page = () => { title={pageTitle} apiUrl="/api/ListTransportRules" apiDataKey="Results" - queryKey= {`Transport Rules - ${currentTenant}`} + queryKey={`Transport Rules - ${currentTenant}`} actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} From ad0b9c083592a03e83865b846dd7fede655de9d7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Feb 2026 17:40:31 -0500 Subject: [PATCH 054/177] Enable SAM servicePrincipal lock and fix formatting Add servicePrincipalLockConfiguration to the app manifest when appDisplayName === "CIPP-SAM" (isEnabled: true, allProperties: true) so SAM gets a service principal lock applied. Also apply minor formatting and lint cleanup across CippAppPermissionBuilder.jsx (trailing commas, callback/parameter fixes and small code-style adjustments) with no other functional changes. --- .../CippAppPermissionBuilder.jsx | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx index 3ff6635eec32..da386b770f91 100644 --- a/src/components/CippComponents/CippAppPermissionBuilder.jsx +++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx @@ -125,7 +125,7 @@ const CippAppPermissionBuilder = ({ return prevPermissions; }); }, - [selectedApp, newPermissions, removePermissionConfirm, removePermissionDialog] + [selectedApp, newPermissions, removePermissionConfirm, removePermissionDialog], ); const createServicePrincipal = ApiPostCall({ @@ -165,7 +165,7 @@ const CippAppPermissionBuilder = ({ const savePermissionChanges = ( servicePrincipal, applicationPermissions, - delegatedPermissions + delegatedPermissions, ) => { setNewPermissions((prevPermissions) => { const updatedPermissions = { @@ -209,6 +209,14 @@ const CippAppPermissionBuilder = ({ requiredResourceAccess: [], }; + if (appDisplayName === "CIPP-SAM") { + // add servicePrincipalLockConfiguration to SAM manifest + manifest.servicePrincipalLockConfiguration = { + isEnabled: true, + allProperties: true, + }; + } + var newAdditionalPermissions = []; selectedApp.map((sp) => { @@ -366,7 +374,7 @@ const CippAppPermissionBuilder = ({ if (selectedApp.length === 0 && initialAppIds.length === 0) { var microsoftGraph = servicePrincipals?.Results?.find( - (sp) => sp?.appId === "00000003-0000-0000-c000-000000000000" + (sp) => sp?.appId === "00000003-0000-0000-c000-000000000000", ); if (microsoftGraph) { setSelectedApp([microsoftGraph]); // Ensure this does not trigger a loop @@ -387,7 +395,7 @@ const CippAppPermissionBuilder = ({ setPermissionsImported(false); } else if (initialAppIds.length > 0 && !permissionsImported) { const newApps = servicePrincipals?.Results?.filter((sp) => - initialAppIds.includes(sp.appId) + initialAppIds.includes(sp.appId), )?.sort((a, b) => a.displayName.localeCompare(b.displayName)); if (!_.isEqual(selectedApp, newApps)) { @@ -460,7 +468,7 @@ const CippAppPermissionBuilder = ({ value: perm.value, description: spInfo?.Results?.appRoles.find((role) => role.id === perm.id) ?.description, - })) + })), ); } if (delegatedTable !== undefined && delegatedTable.length === 0) { @@ -473,7 +481,7 @@ const CippAppPermissionBuilder = ({ description: spInfo?.Results?.publishedPermissionScopes.find((scope) => scope.id === perm.id) ?.userConsentDescription ?? "Manually added", - })) + })), ); } setSpInitialized(true); @@ -514,7 +522,7 @@ const CippAppPermissionBuilder = ({ id: permission.value, value: permission.label, description: spInfo?.Results?.publishedPermissionScopes.find( - (scope) => scope.id === permission.value + (scope) => scope.id === permission.value, )?.userConsentDescription, }; setDelegatedTable([...(delegatedTable ?? []), newDelegatedPermission]); @@ -528,7 +536,7 @@ const CippAppPermissionBuilder = ({ setAppTable((prevAppTable) => prevAppTable.filter((perm) => perm.id !== permission.id)); } else { setDelegatedTable((prevDelegatedTable) => - prevDelegatedTable.filter((perm) => perm.id !== permission.id) + prevDelegatedTable.filter((perm) => perm.id !== permission.id), ); } } @@ -538,7 +546,7 @@ const CippAppPermissionBuilder = ({ savePermissionChanges( servicePrincipal.appId, appTable?.map((perm) => ({ id: perm.id, value: perm.value })) ?? [], - delegatedTable?.map((perm) => ({ id: perm.id, value: perm.value })) ?? [] + delegatedTable?.map((perm) => ({ id: perm.id, value: perm.value })) ?? [], ); }; @@ -761,7 +769,7 @@ const CippAppPermissionBuilder = ({ setSelectedApp([ ...selectedApp, servicePrincipals?.Results?.find( - (sp) => sp.appId === currentSelectedSp.value + (sp) => sp.appId === currentSelectedSp.value, ), ]); formControl.setValue("servicePrincipal", null); @@ -943,7 +951,7 @@ const CippAppPermissionBuilder = ({ newPermissions?.MissingPermissions[perm][type].map((p) => { updatedPermissions.Permissions[perm][type].push(p); }); - } + }, ); }); updatedPermissions.MissingPermissions = {}; From c269c296c194b09a99b2953e65cdd554a9189827 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:43:43 +0000 Subject: [PATCH 055/177] Bump formik from 2.4.6 to 2.4.9 Bumps [formik](https://github.com/jaredpalmer/formik) from 2.4.6 to 2.4.9. - [Release notes](https://github.com/jaredpalmer/formik/releases) - [Commits](https://github.com/jaredpalmer/formik/compare/formik@2.4.6...formik@2.4.9) --- updated-dependencies: - dependency-name: formik dependency-version: 2.4.9 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 6a0aaeb23d0a..e4e863528568 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "date-fns": "4.1.0", "eml-parse-js": "^1.2.0-beta.0", "export-to-csv": "^1.3.0", - "formik": "2.4.6", + "formik": "2.4.9", "gray-matter": "4.0.3", "i18next": "25.5.2", "javascript-time-ago": "^2.6.2", diff --git a/yarn.lock b/yarn.lock index 6348acb6ffc3..f26e81a4f972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4395,10 +4395,10 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== -formik@2.4.6: - version "2.4.6" - resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.6.tgz#4da75ca80f1a827ab35b08fd98d5a76e928c9686" - integrity sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g== +formik@2.4.9: + version "2.4.9" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.9.tgz#7e5b81e9c9e215d0ce2ac8fed808cf7fba0cd204" + integrity sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og== dependencies: "@types/hoist-non-react-statics" "^3.3.1" deepmerge "^2.1.1" From cf4f5b60fa0f9de4e92c2297e7316f9eaa56f59d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:44:01 +0000 Subject: [PATCH 056/177] Bump simplebar from 6.3.2 to 6.3.3 Bumps [simplebar](https://github.com/grsmto/simplebar/tree/HEAD/packages/simplebar) from 6.3.2 to 6.3.3. - [Release notes](https://github.com/grsmto/simplebar/releases) - [Changelog](https://github.com/Grsmto/simplebar/blob/master/packages/simplebar/CHANGELOG.md) - [Commits](https://github.com/grsmto/simplebar/commits/simplebar@6.3.3/packages/simplebar) --- updated-dependencies: - dependency-name: simplebar dependency-version: 6.3.3 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 6a0aaeb23d0a..fe502bcb7a35 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "redux-thunk": "3.1.0", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.0", - "simplebar": "6.3.2", + "simplebar": "6.3.3", "simplebar-react": "3.3.2", "stylis-plugin-rtl": "2.1.1", "typescript": "5.9.2", diff --git a/yarn.lock b/yarn.lock index 6348acb6ffc3..a094ef419718 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7253,10 +7253,10 @@ simplebar-react@3.3.2: dependencies: simplebar-core "^1.3.2" -simplebar@6.3.2: - version "6.3.2" - resolved "https://registry.yarnpkg.com/simplebar/-/simplebar-6.3.2.tgz#df27f47836c126736b38f9703fdcaa50ab0ae077" - integrity sha512-l4P1Oma0nply0g+pkrkwfC1SF5WDnIHrgiQDXSDzIdjngUDLkPgZcPGKrOvuFeXoSensfKijjIjDlUJSEp+mLQ== +simplebar@6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/simplebar/-/simplebar-6.3.3.tgz#08b4763e2a8e2b121f444fba613797a82ac33481" + integrity sha512-ni9cIiA8GRitiaenV7A/gh8nTStiVqO3gLxrfCMmq6CSy/pB+fz+QxqvJLo142QHb2Tvgvr8ILwZxf3qzAq7pg== dependencies: simplebar-core "^1.3.2" From c1a2e703b5f18cf5000cccb3ebf11a94c3bd0b36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:44:17 +0000 Subject: [PATCH 057/177] Bump @tiptap/extension-table from 3.13.0 to 3.19.0 Bumps [@tiptap/extension-table](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-table) from 3.13.0 to 3.19.0. - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-table/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.19.0/packages/extension-table) --- updated-dependencies: - dependency-name: "@tiptap/extension-table" dependency-version: 3.19.0 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 6a0aaeb23d0a..9600d51b564b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@tiptap/core": "^3.4.1", "@tiptap/extension-heading": "^3.4.1", "@tiptap/extension-image": "^3.4.1", - "@tiptap/extension-table": "^3.4.1", + "@tiptap/extension-table": "^3.19.0", "@tiptap/pm": "^3.4.1", "@tiptap/react": "^3.4.1", "@tiptap/starter-kit": "^3.19.0", diff --git a/yarn.lock b/yarn.lock index 6348acb6ffc3..346035ea8fab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2253,10 +2253,10 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.19.0.tgz#eac7712cc791488f4c1c48baf3aed1a8d95f398c" integrity sha512-xYpabHsv7PccLUBQaP8AYiFCnYbx6P93RHPd0lgNwhdOjYFd931Zy38RyoxPHAgbYVmhf1iyx7lpuLtBnhS5dA== -"@tiptap/extension-table@^3.4.1": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.13.0.tgz#83283bc818582e621cefabf173beeb37fe6f30ba" - integrity sha512-LcH9KE4QBUJ6IPwt1Uo5iU7zatFjUUvXbctIu2fKQ9nqJ7nNSFxRhkNyporVFkTWYH7/rb0qMoF1VxSUGefG5w== +"@tiptap/extension-table@^3.19.0": + version "3.19.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.19.0.tgz#a5f9be88e319f60dc7b8df1321f95a31b20fe991" + integrity sha512-Lg8DlkkDUMYE/CcGOxoCWF98B2i7VWh+AGgqlF+XWrHjhlKHfENLRXm1a0vWuyyP3NknRYILoaaZ1s7QzmXKRA== "@tiptap/extension-text@^3.19.0": version "3.19.0" From c0a47d0344b172456a24737df4a93a1ed486bb34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:44:36 +0000 Subject: [PATCH 058/177] Bump eslint-config-next from 15.5.2 to 16.1.6 Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.5.2 to 16.1.6. - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/commits/v16.1.6/packages/eslint-config-next) --- updated-dependencies: - dependency-name: eslint-config-next dependency-version: 16.1.6 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 414 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 292 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index 6a0aaeb23d0a..61329cc56066 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,6 @@ "devDependencies": { "@svgr/webpack": "8.1.0", "eslint": "9.39.2", - "eslint-config-next": "15.5.2" + "eslint-config-next": "16.1.6" } } diff --git a/yarn.lock b/yarn.lock index 6348acb6ffc3..00c3b3b19b52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,11 +11,25 @@ js-tokens "^4.0.0" picocolors "^1.1.1" +"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== +"@babel/compat-data@^7.28.6": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + "@babel/core@^7.21.3": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" @@ -37,6 +51,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.24.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" @@ -48,6 +83,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.29.0": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": version "7.27.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" @@ -66,6 +112,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3", "@babel/helper-create-class-features-plugin@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46" @@ -120,6 +177,14 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + "@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" @@ -129,6 +194,15 @@ "@babel/helper-validator-identifier" "^7.27.1" "@babel/traverse" "^7.28.3" +"@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + "@babel/helper-optimise-call-expression@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" @@ -199,6 +273,21 @@ "@babel/template" "^7.27.2" "@babel/types" "^7.28.4" +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/parser@^7.24.4", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + "@babel/parser@^7.27.2", "@babel/parser@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" @@ -856,6 +945,15 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" @@ -869,6 +967,19 @@ "@babel/types" "^7.28.5" debug "^4.3.1" +"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.4.4": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" @@ -877,6 +988,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@babel/types@^7.28.6", "@babel/types@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@emnapi/core@^1.4.3": version "1.7.1" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.1.tgz#3a79a02dbc84f45884a1806ebb98e5746bdfaac4" @@ -1016,14 +1135,21 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== -"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": +"@eslint-community/eslint-utils@^4.8.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": +"@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2": version "4.12.2" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== @@ -1500,10 +1626,10 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-16.1.6.tgz#0f85979498249a94ef606ef535042a831f905e89" integrity sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ== -"@next/eslint-plugin-next@15.5.2": - version "15.5.2" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.2.tgz#6fa6b78687dbbb6f5726acd81bcdfd87dc26b6f3" - integrity sha512-lkLrRVxcftuOsJNhWatf1P2hNVfh98k/omQHrCEPPriUypR6RcS13IvLdIrEvkm9AH2Nu2YpR5vLqBuy6twH3Q== +"@next/eslint-plugin-next@16.1.6": + version "16.1.6" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz#73b56b01c9db506998bd1e2d303c2605b0a1b7b0" + integrity sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ== dependencies: fast-glob "3.3.1" @@ -1878,11 +2004,6 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@rushstack/eslint-patch@^1.10.3": - version "1.15.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz#8184bcb37791e6d3c3c13a9bfbe4af263f66665f" - integrity sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw== - "@sinonjs/text-encoding@^0.7.2": version "0.7.3" resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f" @@ -2587,101 +2708,101 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== -"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz#a6ce899690542e2affa9543306d2d3935740abb7" - integrity sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.50.0" - "@typescript-eslint/type-utils" "8.50.0" - "@typescript-eslint/utils" "8.50.0" - "@typescript-eslint/visitor-keys" "8.50.0" - ignore "^7.0.0" +"@typescript-eslint/eslint-plugin@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz#5aec3db807a6b8437ea5d5ebf7bd16b4119aba8d" + integrity sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw== + dependencies: + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.56.0" + "@typescript-eslint/type-utils" "8.56.0" + "@typescript-eslint/utils" "8.56.0" + "@typescript-eslint/visitor-keys" "8.56.0" + ignore "^7.0.5" natural-compare "^1.4.0" - ts-api-utils "^2.1.0" - -"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.50.0.tgz#c35b28f686dbe08e81b9d6208ebc08912549f4ba" - integrity sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q== - dependencies: - "@typescript-eslint/scope-manager" "8.50.0" - "@typescript-eslint/types" "8.50.0" - "@typescript-eslint/typescript-estree" "8.50.0" - "@typescript-eslint/visitor-keys" "8.50.0" - debug "^4.3.4" - -"@typescript-eslint/project-service@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.50.0.tgz#1422366b7cc11fef8c6d87770884e608093423a4" - integrity sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ== - dependencies: - "@typescript-eslint/tsconfig-utils" "^8.50.0" - "@typescript-eslint/types" "^8.50.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz#e0d6c838dc9044bc679724611b138cb34c81bddf" - integrity sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A== - dependencies: - "@typescript-eslint/types" "8.50.0" - "@typescript-eslint/visitor-keys" "8.50.0" - -"@typescript-eslint/tsconfig-utils@8.50.0", "@typescript-eslint/tsconfig-utils@^8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz#5c17537ad4c8a13bf6d7393035edaf91a1e13191" - integrity sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w== - -"@typescript-eslint/type-utils@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz#feb6f54f876980a258b14f1cb033f54fc545d37b" - integrity sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw== - dependencies: - "@typescript-eslint/types" "8.50.0" - "@typescript-eslint/typescript-estree" "8.50.0" - "@typescript-eslint/utils" "8.50.0" - debug "^4.3.4" - ts-api-utils "^2.1.0" - -"@typescript-eslint/types@8.50.0", "@typescript-eslint/types@^8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.50.0.tgz#ad8f1ad88ae0096f548c9cdf60da9b92832db96e" - integrity sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w== - -"@typescript-eslint/typescript-estree@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz#2871d36617f81a127db905fa91b16d1a0251411b" - integrity sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ== - dependencies: - "@typescript-eslint/project-service" "8.50.0" - "@typescript-eslint/tsconfig-utils" "8.50.0" - "@typescript-eslint/types" "8.50.0" - "@typescript-eslint/visitor-keys" "8.50.0" - debug "^4.3.4" - minimatch "^9.0.4" - semver "^7.6.0" + ts-api-utils "^2.4.0" + +"@typescript-eslint/parser@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.0.tgz#8ecff1678b8b1a742d29c446ccf5eeea7f971d72" + integrity sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg== + dependencies: + "@typescript-eslint/scope-manager" "8.56.0" + "@typescript-eslint/types" "8.56.0" + "@typescript-eslint/typescript-estree" "8.56.0" + "@typescript-eslint/visitor-keys" "8.56.0" + debug "^4.4.3" + +"@typescript-eslint/project-service@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.0.tgz#bb8562fecd8f7922e676fc6a1189c20dd7991d73" + integrity sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.56.0" + "@typescript-eslint/types" "^8.56.0" + debug "^4.4.3" + +"@typescript-eslint/scope-manager@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz#604030a4c6433df3728effdd441d47f45a86edb4" + integrity sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w== + dependencies: + "@typescript-eslint/types" "8.56.0" + "@typescript-eslint/visitor-keys" "8.56.0" + +"@typescript-eslint/tsconfig-utils@8.56.0", "@typescript-eslint/tsconfig-utils@^8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz#2538ce83cbc376e685487960cbb24b65fe2abc4e" + integrity sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg== + +"@typescript-eslint/type-utils@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz#72b4edc1fc73988998f1632b3ec99c2a66eaac6e" + integrity sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA== + dependencies: + "@typescript-eslint/types" "8.56.0" + "@typescript-eslint/typescript-estree" "8.56.0" + "@typescript-eslint/utils" "8.56.0" + debug "^4.4.3" + ts-api-utils "^2.4.0" + +"@typescript-eslint/types@8.56.0", "@typescript-eslint/types@^8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.0.tgz#a2444011b9a98ca13d70411d2cbfed5443b3526a" + integrity sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ== + +"@typescript-eslint/typescript-estree@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz#fadbc74c14c5bac947db04980ff58bb178701c2e" + integrity sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q== + dependencies: + "@typescript-eslint/project-service" "8.56.0" + "@typescript-eslint/tsconfig-utils" "8.56.0" + "@typescript-eslint/types" "8.56.0" + "@typescript-eslint/visitor-keys" "8.56.0" + debug "^4.4.3" + minimatch "^9.0.5" + semver "^7.7.3" tinyglobby "^0.2.15" - ts-api-utils "^2.1.0" + ts-api-utils "^2.4.0" -"@typescript-eslint/utils@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.50.0.tgz#107f20a5747eab5db988c5f6ad462b59851cdd1f" - integrity sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg== +"@typescript-eslint/utils@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.0.tgz#063ce6f702ec603de1b83ee795ed5e877d6f7841" + integrity sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ== dependencies: - "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/scope-manager" "8.50.0" - "@typescript-eslint/types" "8.50.0" - "@typescript-eslint/typescript-estree" "8.50.0" + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.56.0" + "@typescript-eslint/types" "8.56.0" + "@typescript-eslint/typescript-estree" "8.56.0" -"@typescript-eslint/visitor-keys@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz#79d1c95474e08f844dbe13370715cfb9b7e21363" - integrity sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q== +"@typescript-eslint/visitor-keys@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz#7d6592ab001827d3ce052155edf7ecad19688d7d" + integrity sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg== dependencies: - "@typescript-eslint/types" "8.50.0" - eslint-visitor-keys "^4.2.1" + "@typescript-eslint/types" "8.56.0" + eslint-visitor-keys "^5.0.0" "@uiw/react-json-view@^2.0.0-alpha.41": version "2.0.0-alpha.41" @@ -3576,7 +3697,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.1: +debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -3978,21 +4099,20 @@ escape-string-regexp@^5.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== -eslint-config-next@15.5.2: - version "15.5.2" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.5.2.tgz#9629ed1deaa131e8e80cbae20acf631c8595ca3e" - integrity sha512-3hPZghsLupMxxZ2ggjIIrat/bPniM2yRpsVPVM40rp8ZMzKWOJp2CGWn7+EzoV2ddkUr5fxNfHpF+wU1hGt/3g== +eslint-config-next@16.1.6: + version "16.1.6" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-16.1.6.tgz#75dd33bf32eb34b1e5be59f546e3c808429bab41" + integrity sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA== dependencies: - "@next/eslint-plugin-next" "15.5.2" - "@rushstack/eslint-patch" "^1.10.3" - "@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" - "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "@next/eslint-plugin-next" "16.1.6" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.31.0" + eslint-plugin-import "^2.32.0" eslint-plugin-jsx-a11y "^6.10.0" eslint-plugin-react "^7.37.0" - eslint-plugin-react-hooks "^5.0.0" + eslint-plugin-react-hooks "^7.0.0" + globals "16.4.0" + typescript-eslint "^8.46.0" eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -4023,7 +4143,7 @@ eslint-module-utils@^2.12.1: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.31.0: +eslint-plugin-import@^2.32.0: version "2.32.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== @@ -4069,10 +4189,16 @@ eslint-plugin-jsx-a11y@^6.10.0: safe-regex-test "^1.0.3" string.prototype.includes "^2.0.1" -eslint-plugin-react-hooks@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" - integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== +eslint-plugin-react-hooks@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169" + integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA== + dependencies: + "@babel/core" "^7.24.4" + "@babel/parser" "^7.24.4" + hermes-parser "^0.25.1" + zod "^3.25.0 || ^4.0.0" + zod-validation-error "^3.5.0 || ^4.0.0" eslint-plugin-react@^7.37.0: version "7.37.5" @@ -4116,6 +4242,11 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== +eslint-visitor-keys@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz#b9aa1a74aa48c44b3ae46c1597ce7171246a94a9" + integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q== + eslint@9.39.2: version "9.39.2" resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" @@ -4495,6 +4626,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +globals@16.4.0: + version "16.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-16.4.0.tgz#574bc7e72993d40cf27cf6c241f324ee77808e51" + integrity sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw== + globals@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" @@ -4663,6 +4799,18 @@ hastscript@^9.0.0: property-information "^7.0.0" space-separated-tokens "^2.0.0" +hermes-estree@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480" + integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw== + +hermes-parser@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1" + integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== + dependencies: + hermes-estree "0.25.1" + highlight-words@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-2.0.0.tgz#06853d68f1f7c8e59d6ef2dd072fe2f64fc93936" @@ -4762,7 +4910,7 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -ignore@^7.0.0: +ignore@^7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== @@ -5897,7 +6045,7 @@ minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.4: +minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -7109,7 +7257,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.6.0, semver@^7.7.1, semver@^7.7.3: +semver@^7.7.1, semver@^7.7.3: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== @@ -7569,10 +7717,10 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== -ts-api-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" - integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== +ts-api-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" + integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== tsconfig-paths@^3.15.0: version "3.15.0" @@ -7646,6 +7794,16 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" +typescript-eslint@^8.46.0: + version "8.56.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.56.0.tgz#f4686ccaaf2fb86daf0133820da40ca5961a2236" + integrity sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg== + dependencies: + "@typescript-eslint/eslint-plugin" "8.56.0" + "@typescript-eslint/parser" "8.56.0" + "@typescript-eslint/typescript-estree" "8.56.0" + "@typescript-eslint/utils" "8.56.0" + typescript@5.9.2: version "5.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" @@ -8000,6 +8158,16 @@ yup@1.7.1: toposort "^2.0.2" type-fest "^2.19.0" +"zod-validation-error@^3.5.0 || ^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918" + integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== + +"zod@^3.25.0 || ^4.0.0": + version "4.3.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a" + integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg== + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" From a20b3edd7f66cc30ac24daa7b31d3e795ec8fc43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:44:45 +0000 Subject: [PATCH 059/177] Bump @mui/x-date-pickers from 8.25.0 to 8.27.0 Bumps [@mui/x-date-pickers](https://github.com/mui/mui-x/tree/HEAD/packages/x-date-pickers) from 8.25.0 to 8.27.0. - [Release notes](https://github.com/mui/mui-x/releases) - [Changelog](https://github.com/mui/mui-x/blob/master/CHANGELOG.md) - [Commits](https://github.com/mui/mui-x/commits/v8.27.0/packages/x-date-pickers) --- updated-dependencies: - dependency-name: "@mui/x-date-pickers" dependency-version: 8.27.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6a0aaeb23d0a..449f85c618b3 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@mui/lab": "7.0.0-beta.17", "@mui/material": "7.3.7", "@mui/system": "7.3.2", - "@mui/x-date-pickers": "^8.25.0", + "@mui/x-date-pickers": "^8.27.0", "@musement/iso-duration": "^1.0.0", "@nivo/core": "^0.99.0", "@nivo/sankey": "^0.99.0", diff --git a/yarn.lock b/yarn.lock index 6348acb6ffc3..2308a24354e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1458,23 +1458,23 @@ prop-types "^15.8.1" react-is "^19.2.3" -"@mui/x-date-pickers@^8.25.0": - version "8.25.0" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-8.25.0.tgz#6e35a740969eeacf3f532e24ffd8960c761bdd10" - integrity sha512-XmLQwlo9C9gPWY9OeFbQka4TGi3MXrW/jJ+E4LV1wdfg/ebOklq6KKKTbvRgTVWlMcIoQwqPbalFxcwQSUUbDw== +"@mui/x-date-pickers@^8.27.0": + version "8.27.0" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-8.27.0.tgz#fdc2f4a116cab28e86b981105e902455c5b4432c" + integrity sha512-mw47IgelP5qFSBANqxUhqDEly2XO9RT/BcNKwgumy8BmmdosrGAmTev8dgFMoWg20iPHxEczlpBdDGyV6ht0jg== dependencies: "@babel/runtime" "^7.28.4" "@mui/utils" "^7.3.5" - "@mui/x-internals" "8.25.0" + "@mui/x-internals" "8.26.0" "@types/react-transition-group" "^4.4.12" clsx "^2.1.1" prop-types "^15.8.1" react-transition-group "^4.4.5" -"@mui/x-internals@8.25.0": - version "8.25.0" - resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-8.25.0.tgz#93849275bfb3a4e5e0130ae91449bb4f8517a1d7" - integrity sha512-RKexkVaK3xvAeLBNeLAw6oJCsQrXkx7TYSRoSUmmJveydqOqoBbimv+nbc8PmL4UL0ShVNkaFL1YWY7kYCCXUA== +"@mui/x-internals@8.26.0": + version "8.26.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-8.26.0.tgz#49caacac954c29a1b10425c67418310ceb9c8cfa" + integrity sha512-B9OZau5IQUvIxwpJZhoFJKqRpmWf5r0yMmSXjQuqb5WuqM755EuzWJOenY48denGoENzMLT8hQpA0hRTeU2IPA== dependencies: "@babel/runtime" "^7.28.4" "@mui/utils" "^7.3.5" From 04d5fd02ab0cf207ddc133d7a3f252f8eb365d1f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:23:32 +0100 Subject: [PATCH 060/177] api results --- src/components/CippSettings/CippBackupRetentionSettings.jsx | 2 ++ src/components/CippSettings/CippLogRetentionSettings.jsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/components/CippSettings/CippBackupRetentionSettings.jsx b/src/components/CippSettings/CippBackupRetentionSettings.jsx index 38cd78ed2ceb..b49ad30cf556 100644 --- a/src/components/CippSettings/CippBackupRetentionSettings.jsx +++ b/src/components/CippSettings/CippBackupRetentionSettings.jsx @@ -1,6 +1,7 @@ import { Button, ButtonGroup, SvgIcon, Typography, TextField, Box } from "@mui/material"; import CippButtonCard from "../CippCards/CippButtonCard"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; import { History } from "@mui/icons-material"; import { useState, useEffect } from "react"; @@ -94,6 +95,7 @@ const CippBackupRetentionSettings = () => { automatically deleted after this period. Minimum retention is 7 days, default is 30 days. Cleanup runs daily at 2:00 AM. + ); }; diff --git a/src/components/CippSettings/CippLogRetentionSettings.jsx b/src/components/CippSettings/CippLogRetentionSettings.jsx index 11767026aba4..a45b0c45bea3 100644 --- a/src/components/CippSettings/CippLogRetentionSettings.jsx +++ b/src/components/CippSettings/CippLogRetentionSettings.jsx @@ -1,6 +1,7 @@ import { Button, Typography, TextField, Box } from "@mui/material"; import CippButtonCard from "../CippCards/CippButtonCard"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; import { useState, useEffect } from "react"; const CippLogRetentionSettings = () => { @@ -99,6 +100,7 @@ const CippLogRetentionSettings = () => { Configure how long to keep CIPP log entries. Logs will be automatically deleted after this period. Minimum retention is 7 days, maximum is 365 days, default is 90 days. + ); }; From 23da89997944ac104acd892f8963f95b97e7faa2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:10:12 +0100 Subject: [PATCH 061/177] every 4 hours --- .../CippStandards/CippStandardsSideBar.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 8fde6343fe41..16307ece2ff0 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -161,7 +161,7 @@ const CippStandardsSideBar = ({ // Filter for drift templates only and group by standardId const driftTemplates = existingTemplates.filter( - (template) => template.standardType === "drift" + (template) => template.standardType === "drift", ); const uniqueTemplates = {}; @@ -200,8 +200,8 @@ const CippStandardsSideBar = ({ if (conflicts.length > 0) { setDriftError( `This template has tenants that are assigned to another Drift Template. You can only assign one Drift Template to each tenant. Please check the ${conflicts.join( - ", " - )} template.` + ", ", + )} template.`, ); onDriftConflictChange?.(true); } else { @@ -240,7 +240,7 @@ const CippStandardsSideBar = ({ const hasRequiredComponents = standard?.addedComponent && standard.addedComponent.some( - (comp) => comp.type !== "switch" && comp.required !== false + (comp) => comp.type !== "switch" && comp.required !== false, ); const actionRequired = standard?.disabledFeatures !== undefined || hasRequiredComponents; // Always require an action value which should be an array with at least one element @@ -379,7 +379,7 @@ const CippStandardsSideBar = ({ {isDriftMode && driftError && {driftError}} {(watchForm.tenantFilter?.some( - (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" + (tenant) => tenant.value === "AllTenants" || tenant.type === "Group", ) || (watchForm.excludedTenants && watchForm.excludedTenants.length > 0)) && ( <> @@ -518,8 +518,8 @@ const CippStandardsSideBar = ({ confirmText: isDriftMode ? "This template will automatically every 12 hours to detect drift. Are you sure you want to apply this Drift Template?" : watchForm.runManually - ? "Are you sure you want to apply this standard? This template has been set to never run on a schedule. After saving the template you will have to run it manually." - : "Are you sure you want to apply this standard? This will apply the template and run every 3 hours.", + ? "Are you sure you want to apply this standard? This template has been set to never run on a schedule. After saving the template you will have to run it manually." + : "Are you sure you want to apply this standard? This will apply the template and run every 4 hours.", url: "/api/AddStandardsTemplate", type: "POST", replacementBehaviour: "removeNulls", @@ -566,7 +566,7 @@ CippStandardsSideBar.propTypes = { label: PropTypes.string.isRequired, handler: PropTypes.func.isRequired, icon: PropTypes.element.isRequired, - }) + }), ).isRequired, updatedAt: PropTypes.string, formControl: PropTypes.object.isRequired, From 08e6811d23cc225352867d3c04f0fd7def91dfe1 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:26:07 +0100 Subject: [PATCH 062/177] feat: enhance SendFromAlias standard to be able to disable too Fixes #5399 --- src/data/standards.json | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 4e89eadb5656..0ec03581c049 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2256,11 +2256,23 @@ "name": "standards.SendFromAlias", "cat": "Exchange Standards", "tag": [], - "helpText": "Enables the ability for users to send from their alias addresses.", + "helpText": "Enables or disables the ability for users to send from their alias addresses.", "docsDescription": "Allows users to change the 'from' address to any set in their Azure AD Profile.", "executiveText": "Allows employees to send emails from their alternative email addresses (aliases) rather than just their primary address. This is useful for employees who manage multiple roles or departments, enabling them to send emails from the most appropriate address for the context.", - "addedComponent": [], - "label": "Allow users to send from their alias addresses", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Select value", + "name": "standards.SendFromAlias.state", + "options": [ + { "label": "Enabled", "value": true }, + { "label": "Disabled", "value": false } + ] + } + ], + "label": "Set Send from alias state", "impact": "Medium Impact", "impactColour": "warning", "addedDate": "2022-05-25", From d45f5f840ba73ea804cb88e54fe06db22fcc7d43 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 12:24:46 -0500 Subject: [PATCH 063/177] Highlight matching tenant values vs expected Compare each CurrentValue property with ExpectedValue (case-insensitive for strings, deep-compare via JSON.stringify for objects) and mark matches. UI changes: show the property key as a subtitle, render value blocks with success styling (background, border, color) and a check badge when matched, otherwise keep warning/neutral styling. Applied the same comparison and presentation logic to both value-list sections; missing ExpectedValue entries are treated as non-matching. --- src/pages/tenant/manage/applied-standards.js | 242 ++++++++++++++----- 1 file changed, 180 insertions(+), 62 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 674ab5736122..bf424f07ddd8 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -2070,43 +2070,105 @@ const Page = () => { {Object.entries( standard.currentTenantValue.CurrentValue, - ).map(([key, val]) => ( - - - {key} - - + ).map(([key, val]) => { + // Compare with expected value for this property + const expectedVal = + standard.currentTenantValue?.ExpectedValue?.[key]; + const isMatch = (() => { + if (expectedVal === undefined) return false; + // Deep comparison handling nested objects and case-insensitive strings + const compareDeep = (v1, v2) => { + if ( + typeof v1 === "string" && + typeof v2 === "string" + ) { + return v1.toLowerCase() === v2.toLowerCase(); + } + if ( + typeof v1 === "object" && + v1 !== null && + typeof v2 === "object" && + v2 !== null + ) { + return ( + JSON.stringify(v1) === JSON.stringify(v2) + ); + } + return ( + JSON.stringify(v1) === JSON.stringify(v2) + ); + }; + return compareDeep(val, expectedVal); + })(); + + return ( + - {val !== undefined - ? JSON.stringify(val, null, 2) - : "Not set"} + {key} + + {isMatch && ( + + + + )} + + {val !== undefined + ? JSON.stringify(val, null, 2) + : "Not set"} + + - - ))} + ); + })} ) : ( { {Object.entries( standard.currentTenantValue.CurrentValue, - ).map(([key, val]) => ( - - - {key} - - + ).map(([key, val]) => { + // Compare with expected value for this property + const expectedVal = + standard.currentTenantValue?.ExpectedValue?.[key]; + const isMatch = (() => { + if (expectedVal === undefined) return false; + // Deep comparison handling nested objects and case-insensitive strings + const compareDeep = (v1, v2) => { + if ( + typeof v1 === "string" && + typeof v2 === "string" + ) { + return v1.toLowerCase() === v2.toLowerCase(); + } + if ( + typeof v1 === "object" && + v1 !== null && + typeof v2 === "object" && + v2 !== null + ) { + return JSON.stringify(v1) === JSON.stringify(v2); + } + return JSON.stringify(v1) === JSON.stringify(v2); + }; + return compareDeep(val, expectedVal); + })(); + + return ( + - {val !== undefined - ? JSON.stringify(val, null, 2) - : "Not set"} + {key} + + {isMatch && ( + + + + )} + + {val !== undefined + ? JSON.stringify(val, null, 2) + : "Not set"} + + - - ))} + ); + })} ) : ( Date: Thu, 19 Feb 2026 15:32:25 -0500 Subject: [PATCH 064/177] Add error alerts and null-safety in Cipp settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add user-facing error Alerts and improve null-safety across Cipp settings components. Imported MUI Alert and display error messages when executeCheck.isError in CippGDAPResults and CippPermissionResults/PermissionCheck. Added optional chaining (e.g. executeCheck?.isFetching, Results?...., array?.map/filter) and small filter/map fixes to avoid runtime crashes when data is missing. Files changed: CippGDAPResults.jsx, CippPermissionCheck.jsx, CippPermissionResults.jsx — improves UX on failed fetches and makes result handling more robust. --- .../CippSettings/CippGDAPResults.jsx | 36 +++++++++++-------- .../CippSettings/CippPermissionCheck.jsx | 5 +++ .../CippSettings/CippPermissionResults.jsx | 35 ++++++++++-------- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/components/CippSettings/CippGDAPResults.jsx b/src/components/CippSettings/CippGDAPResults.jsx index 89897fd278d0..d1542bacdd96 100644 --- a/src/components/CippSettings/CippGDAPResults.jsx +++ b/src/components/CippSettings/CippGDAPResults.jsx @@ -1,4 +1,4 @@ -import { List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Alert, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material"; import { Cancel, CheckCircle, Warning } from "@mui/icons-material"; import { CippPropertyList } from "../CippComponents/CippPropertyList"; import { XMarkIcon } from "@heroicons/react/24/outline"; @@ -28,10 +28,10 @@ export const CippGDAPResults = (props) => { const GdapIssueValue = ({ results, type, match }) => { var issues = []; - if (type) issues = results?.Results?.GDAPIssues.filter((issue) => issue.Type === type)?.length; + if (type) issues = results?.Results?.GDAPIssues?.filter((issue) => issue.Type === type)?.length; if (match) - issues = results?.Results?.GDAPIssues.filter((issue) => - new RegExp(match).test(issue.Issue) + issues = results?.Results?.GDAPIssues?.filter((issue) => + new RegExp(match).test(issue.Issue), )?.length; return ( <> @@ -110,14 +110,19 @@ export const CippGDAPResults = (props) => { /> )} - {!importReport && executeCheck.isFetching ? ( + {!importReport && executeCheck?.isFetching ? ( + ) : !importReport && executeCheck?.isError ? ( + + Failed to load GDAP check results. Please try refreshing or contact support if the issue + persists. + ) : ( <> - {gdapTests.map((test, index) => { + {gdapTests?.map((test, index) => { var matchedResults = results?.Results?.[test.resultProperty]?.filter((item) => - new RegExp(test.match)?.test(item?.[test.matchProperty]) + new RegExp(test.match)?.test(item?.[test.matchProperty]), ); var testResult = false; @@ -153,7 +158,7 @@ export const CippGDAPResults = (props) => { <> { <> { )} {results?.Results?.Memberships?.filter( - (membership) => membership?.["@odata.type"] === "#microsoft.graph.group" + (membership) => membership?.["@odata.type"] === "#microsoft.graph.group", ).length > 0 && ( <> membership?.["@odata.type"] === "#microsoft.graph.group" + (membership) => membership?.["@odata.type"] === "#microsoft.graph.group", )} simpleColumns={["displayName"]} /> @@ -190,15 +195,16 @@ export const CippGDAPResults = (props) => { )} {results?.Results?.Memberships?.filter( - (membership) => membership?.["@odata.type"] === "#microsoft.graph.directoryRole" + (membership) => membership?.["@odata.type"] === "#microsoft.graph.directoryRole", ).length > 0 && ( <> membership?.["@odata.type"] === "#microsoft.graph.directoryRole" + (membership) => + membership?.["@odata.type"] === "#microsoft.graph.directoryRole", )} simpleColumns={["displayName"]} /> diff --git a/src/components/CippSettings/CippPermissionCheck.jsx b/src/components/CippSettings/CippPermissionCheck.jsx index c38f8aa49d5a..575f95cae5e1 100644 --- a/src/components/CippSettings/CippPermissionCheck.jsx +++ b/src/components/CippSettings/CippPermissionCheck.jsx @@ -139,6 +139,11 @@ const CippPermissionCheck = (props) => { }} CardButton={} > + {executeCheck.isError && !importReport && ( + + Failed to load {type} check. Please try refreshing the page. + + )} {(executeCheck.isSuccess || executeCheck.isLoading) && ( <> {executeCheck.data?.Metadata?.AlertMessage && ( diff --git a/src/components/CippSettings/CippPermissionResults.jsx b/src/components/CippSettings/CippPermissionResults.jsx index 375dca112f2a..95c1d8739458 100644 --- a/src/components/CippSettings/CippPermissionResults.jsx +++ b/src/components/CippSettings/CippPermissionResults.jsx @@ -1,4 +1,4 @@ -import { Button, Link, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Alert, Button, Link, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material"; import { Cancel, CheckCircle } from "@mui/icons-material"; import { CippPropertyList } from "../CippComponents/CippPropertyList"; import { WrenchIcon, XMarkIcon } from "@heroicons/react/24/outline"; @@ -91,18 +91,23 @@ export const CippPermissionResults = (props) => { <> {propertyItems.length > 0 && ( )} - {!importReport && executeCheck.isFetching ? ( + {!importReport && executeCheck?.isFetching ? ( + ) : !importReport && executeCheck?.isError ? ( + + Failed to load permission check results. Please try refreshing or contact support if the + issue persists. + ) : ( <> - {results?.Results?.Messages.map((message, index) => ( + {results?.Results?.Messages?.map((message, index) => ( @@ -112,7 +117,7 @@ export const CippPermissionResults = (props) => { ))} - {results?.Results?.ErrorMessages.map((error, index) => ( + {results?.Results?.ErrorMessages?.map((error, index) => ( @@ -122,7 +127,7 @@ export const CippPermissionResults = (props) => { ))} - {results?.Results?.MissingPermissions.length > 0 && ( + {results?.Results?.MissingPermissions?.length > 0 && ( @@ -143,12 +148,12 @@ export const CippPermissionResults = (props) => { }} extendedInfo={[]} > - {results?.Results?.Links.length > 0 && ( + {results?.Results?.Links?.length > 0 && ( { + propertyItems={results?.Results?.Links?.map((link) => { return { value: ( @@ -161,11 +166,11 @@ export const CippPermissionResults = (props) => { /> )} - {results?.Results?.MissingPermissions.length > 0 && ( + {results?.Results?.MissingPermissions?.length > 0 && ( <> { Refresh CPV } - isFetching={!importReport && executeCheck.isFetching} + isFetching={!importReport && executeCheck?.isFetching} refreshFunction={executeCheck} data={results?.Results?.CPVRefreshList} simpleColumns={["DisplayName", "DefaultDomainName", "LastRefresh"]} @@ -217,9 +222,9 @@ export const CippPermissionResults = (props) => { <> { + data={results?.Results?.AccessTokenDetails?.Scope?.map((scope) => { return { Scope: scope, }; @@ -232,9 +237,9 @@ export const CippPermissionResults = (props) => { <> { + data={results?.Results?.ApplicationTokenDetails?.Roles?.map((role) => { return { Role: role, }; From 061702c2053452123bf574ba25559fc0438c8830 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 16:26:00 -0500 Subject: [PATCH 065/177] Add helperText for litigation hold days Add a helperText to the standards.EnableLitigationHold.days field explaining that the value represents the number of days to apply litigation hold and that leaving it blank or setting it to 'Unlimited' will apply the hold indefinitely. This clarifies expected input and default behavior for users. --- src/data/standards.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/standards.json b/src/data/standards.json index 0ec03581c049..66dcc2705b50 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -1564,7 +1564,8 @@ "type": "textField", "name": "standards.EnableLitigationHold.days", "required": false, - "label": "Days to apply for litigation hold" + "label": "Days to apply for litigation hold", + "helperText": "Number of days to apply litigation hold for. If left blank or set to Unlimited, litigation hold will be applied indefinitely." } ], "label": "Enable Litigation Hold for all users", From 23f9528bb9f569e6aef947aa2d6f3ef0c6fafb0c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 22:09:49 -0500 Subject: [PATCH 066/177] Add customAction to CippAutocomplete and use in drift Introduce a new customAction prop to CippAutoComplete to allow an action button inside or outside the input. renderInput was updated to inject a customizable IconButton (with tooltip, click handler, placement and styling) while preserving existing refresh and template-view logic. The component now calls customAction.onClick with the current value/internalValue and prevents event propagation. Minor formatting and lint fixes (comma placements, memo deps) applied. Updated tenant manage drift page to import the Edit icon and pass a customAction that navigates to the template edit page (also set disableClearable). --- .../CippComponents/CippAutocomplete.jsx | 255 +++++++++++------- src/pages/tenant/manage/drift.js | 15 ++ 2 files changed, 175 insertions(+), 95 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index ea5d9811bda8..c31f1656e82d 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -80,6 +80,7 @@ export const CippAutoComplete = (props) => { preselectedValue, groupBy, renderGroup, + customAction, ...other } = props; @@ -183,10 +184,10 @@ export const CippAutoComplete = (props) => { typeof api?.labelField === "function" ? api.labelField(option) : option[api?.labelField] - ? option[api?.labelField] - : option[api?.altLabelField] || - option[api?.valueField] || - "No label found - Are you missing a labelField?", + ? option[api?.labelField] + : option[api?.altLabelField] || + option[api?.valueField] || + "No label found - Are you missing a labelField?", value: typeof api?.valueField === "function" ? api.valueField(option) @@ -195,8 +196,8 @@ export const CippAutoComplete = (props) => { typeof api?.descriptionField === "function" ? api.descriptionField(option) : api?.descriptionField - ? option[api?.descriptionField] - : undefined, + ? option[api?.descriptionField] + : undefined, addedFields, rawData: option, // Store the full original object }; @@ -299,7 +300,7 @@ export const CippAutoComplete = (props) => { const foundOption = memoizedOptions.find((option) => option.value === value); return foundOption || { label: value, value: value }; }, - [memoizedOptions] + [memoizedOptions], ); return ( @@ -336,7 +337,7 @@ export const CippAutoComplete = (props) => { const isExisting = options?.length > 0 && options.some( - (option) => params.inputValue === option.value || params.inputValue === option.label + (option) => params.inputValue === option.value || params.inputValue === option.label, ); if (params.inputValue !== "" && creatable && !isExisting) { const newOption = { @@ -355,13 +356,13 @@ export const CippAutoComplete = (props) => { defaultValue={ Array.isArray(defaultValue) ? defaultValue.map((item) => - typeof item === "string" ? lookupOptionByValue(item) : item + typeof item === "string" ? lookupOptionByValue(item) : item, ) : typeof defaultValue === "object" && multiple - ? [defaultValue] - : typeof defaultValue === "string" - ? lookupOptionByValue(defaultValue) - : defaultValue + ? [defaultValue] + : typeof defaultValue === "string" + ? lookupOptionByValue(defaultValue) + : defaultValue } name={name} onChange={(event, newValue) => { @@ -381,7 +382,7 @@ export const CippAutoComplete = (props) => { }); newValue = newValue.filter( (item) => - item.value && item.value !== "" && item.value !== "error" && item.value !== -1 + item.value && item.value !== "" && item.value !== "error" && item.value !== -1, ); } else { if (newValue?.manual || !newValue?.label) { @@ -433,7 +434,7 @@ export const CippAutoComplete = (props) => { // Fallback for any edge cases return option.label || option.value || ""; }, - [api] + [api], )} onKeyDown={(event) => { // Handle Tab key to select highlighted option @@ -461,96 +462,160 @@ export const CippAutoComplete = (props) => { } }} sx={sx} - renderInput={(params) => ( - - - {api?.url && api?.showRefresh && ( - - { - actionGetRequest.refetch(); - }} - > - - - - )} - {api?.templateView && ( - - { - // Use internalValue if value prop is not available - const currentValue = value || internalValue; - - // Get the full object from the selected value - if (multiple) { - // For multiple selection, get all full objects - const fullObjects = currentValue - .map((v) => { - const valueToFind = v?.value || v; - const found = usedOptions.find((opt) => opt.value === valueToFind); - let rawData = found?.rawData; - - // If property is specified, extract and parse JSON from that property - if (rawData && api?.templateView?.property) { - try { - const propertyValue = rawData[api.templateView.property]; - if (typeof propertyValue === "string") { - rawData = JSON.parse(propertyValue); - } else { - rawData = propertyValue; + renderInput={(params) => { + // Handle custom action button inside the TextField + const { InputProps, ...otherParams } = params; + const modifiedInputProps = + customAction && customAction.position === "inside" + ? { + ...InputProps, + endAdornment: ( + <> + {customAction && ( + + { + e.stopPropagation(); + if (customAction.onClick) { + customAction.onClick(value || internalValue); + } + }} + sx={{ + opacity: 0, + transition: "all 0.2s", + p: "4px", + mr: "-4px", + mt: -1, + cursor: "pointer", + "&:hover": { + opacity: 1, + backgroundColor: "action.hover", + }, + ".MuiAutocomplete-root:hover &": { + opacity: 0.6, + }, + ".MuiAutocomplete-root:hover &:hover": { + opacity: 1, + backgroundColor: "action.hover", + }, + }} + > + {customAction.icon} + + + )} + {InputProps?.endAdornment} + + ), + } + : InputProps; + + return ( + + + {api?.url && api?.showRefresh && ( + + { + actionGetRequest.refetch(); + }} + > + + + + )} + {api?.templateView && ( + + { + // Use internalValue if value prop is not available + const currentValue = value || internalValue; + + // Get the full object from the selected value + if (multiple) { + // For multiple selection, get all full objects + const fullObjects = currentValue + .map((v) => { + const valueToFind = v?.value || v; + const found = usedOptions.find((opt) => opt.value === valueToFind); + let rawData = found?.rawData; + + // If property is specified, extract and parse JSON from that property + if (rawData && api?.templateView?.property) { + try { + const propertyValue = rawData[api.templateView.property]; + if (typeof propertyValue === "string") { + rawData = JSON.parse(propertyValue); + } else { + rawData = propertyValue; + } + } catch (e) { + console.error("Failed to parse JSON from property:", e); + // Keep original rawData if parsing fails } - } catch (e) { - console.error("Failed to parse JSON from property:", e); - // Keep original rawData if parsing fails } - } - return rawData; - }) - .filter(Boolean); - setFullObject(fullObjects); - } else { - // For single selection, get the full object - const valueToFind = currentValue?.value || currentValue; - const selectedOption = usedOptions.find((opt) => opt.value === valueToFind); - let rawData = selectedOption?.rawData || null; - - // If property is specified, extract and parse JSON from that property - if (rawData && api?.templateView?.property) { - try { - const propertyValue = rawData[api.templateView.property]; - if (typeof propertyValue === "string") { - rawData = JSON.parse(propertyValue); - } else { - rawData = propertyValue; + return rawData; + }) + .filter(Boolean); + setFullObject(fullObjects); + } else { + // For single selection, get the full object + const valueToFind = currentValue?.value || currentValue; + const selectedOption = usedOptions.find((opt) => opt.value === valueToFind); + let rawData = selectedOption?.rawData || null; + + // If property is specified, extract and parse JSON from that property + if (rawData && api?.templateView?.property) { + try { + const propertyValue = rawData[api.templateView.property]; + if (typeof propertyValue === "string") { + rawData = JSON.parse(propertyValue); + } else { + rawData = propertyValue; + } + } catch (e) { + console.error("Failed to parse JSON from property:", e); + // Keep original rawData if parsing fails } - } catch (e) { - console.error("Failed to parse JSON from property:", e); - // Keep original rawData if parsing fails } - } - setFullObject(rawData); + setFullObject(rawData); + } + setOffCanvasVisible(true); + }} + title={api?.templateView.title || "View details"} + > + + + + )} + {customAction && customAction.position === "outside" && ( + { + e.stopPropagation(); + if (customAction.onClick) { + customAction.onClick(value || internalValue); } - setOffCanvasVisible(true); }} - title={api?.templateView.title || "View details"} + title={customAction.tooltip || ""} > - + {customAction.icon} - - )} - - )} + )} + + ); + }} groupBy={groupBy} renderGroup={renderGroup} renderOption={(props, option) => { diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 02fe6ffb77d1..2e34d3236aca 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -13,6 +13,7 @@ import { Info, FactCheck, Search, + Edit, } from "@mui/icons-material"; import { Box, @@ -1720,6 +1721,20 @@ const ManageDriftPage = () => { ); }} placeholder="Select a drift template..." + disableClearable={true} + customAction={{ + icon: , + onClick: (currentValue) => { + if (currentValue?.value) { + // Navigate to edit page for the selected template + router.push( + `/tenant/standards/templates/template?id=${currentValue.value}&type=drift`, + ); + } + }, + tooltip: "Edit Template", + position: "inside", + }} /> Date: Thu, 19 Feb 2026 22:21:07 -0500 Subject: [PATCH 067/177] Support linkable custom action in CippAutocomplete Make customAction in CippAutoComplete render as a real link when a URL is provided. Imported next/link and switch IconButton's component to Link with href when customAction.link exists; adjust onClick to only call customAction.onClick for non-link buttons and always stop event propagation. Add styling tweaks (inherit color, remove text decoration). Update drift page to pass a link instead of an onClick router push and simplify the icon prop. This enables navigation via the autocomplete action while preserving click handling for button-only actions. --- .../CippComponents/CippAutocomplete.jsx | 19 +++++++++++++------ src/pages/tenant/manage/drift.js | 13 ++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index c31f1656e82d..ab869bf1cc55 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -9,6 +9,7 @@ import { Box, Typography, } from "@mui/material"; +import Link from "next/link"; import { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; @@ -474,13 +475,17 @@ export const CippAutoComplete = (props) => { {customAction && ( { - e.stopPropagation(); - if (customAction.onClick) { - customAction.onClick(value || internalValue); - } - }} + onClick={ + customAction.onClick && !customAction.link + ? (e) => { + e.stopPropagation(); + customAction.onClick(value || internalValue); + } + : (e) => e.stopPropagation() + } sx={{ opacity: 0, transition: "all 0.2s", @@ -488,6 +493,8 @@ export const CippAutoComplete = (props) => { mr: "-4px", mt: -1, cursor: "pointer", + color: "inherit", + textDecoration: "none", "&:hover": { opacity: 1, backgroundColor: "action.hover", diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 2e34d3236aca..85ddc2871f88 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -1723,15 +1723,10 @@ const ManageDriftPage = () => { placeholder="Select a drift template..." disableClearable={true} customAction={{ - icon: , - onClick: (currentValue) => { - if (currentValue?.value) { - // Navigate to edit page for the selected template - router.push( - `/tenant/standards/templates/template?id=${currentValue.value}&type=drift`, - ); - } - }, + icon: , + link: selectedTemplateOption?.value + ? `/tenant/standards/templates/template?id=${selectedTemplateOption.value}&type=drift` + : undefined, tooltip: "Edit Template", position: "inside", }} From a648a4a1a50248d79a870f170cc3bff40d845bf6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:39:57 +0100 Subject: [PATCH 068/177] Remove helpertext --- src/components/CippFormPages/CippAddGroupTemplateForm.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/CippFormPages/CippAddGroupTemplateForm.jsx b/src/components/CippFormPages/CippAddGroupTemplateForm.jsx index 36747ee70f32..363205c64eac 100644 --- a/src/components/CippFormPages/CippAddGroupTemplateForm.jsx +++ b/src/components/CippFormPages/CippAddGroupTemplateForm.jsx @@ -41,7 +41,6 @@ const CippAddGroupTemplateForm = (props) => { Date: Fri, 20 Feb 2026 09:53:43 +0100 Subject: [PATCH 069/177] Keep loading state --- .../CippApplicationDeployDrawer.jsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/CippComponents/CippApplicationDeployDrawer.jsx b/src/components/CippComponents/CippApplicationDeployDrawer.jsx index 6b51f4889e57..ddc11982fd55 100644 --- a/src/components/CippComponents/CippApplicationDeployDrawer.jsx +++ b/src/components/CippComponents/CippApplicationDeployDrawer.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useCallback, useState } from "react"; -import { Divider, Button, Alert } from "@mui/material"; +import { Divider, Button, Alert, CircularProgress } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Add } from "@mui/icons-material"; @@ -47,7 +47,7 @@ export const CippApplicationDeployDrawer = ({ : null; } }, - [formControl.setValue] + [formControl.setValue], ); useEffect(() => { @@ -138,8 +138,8 @@ export const CippApplicationDeployDrawer = ({ {deployApplication.isLoading ? "Deploying..." : deployApplication.isSuccess - ? "Deploy Another" - : "Deploy Application"} + ? "Deploy Another" + : "Deploy Application"} @@ -446,8 +447,8 @@ export const CippApplicationDeployDrawer = ({ } multiple={false} formControl={formControl} - disabled={winGetSearchResults.isLoading} - isFetching={winGetSearchResults.isLoading} + disabled={winGetSearchResults.isPending} + isFetching={winGetSearchResults.isPending} /> @@ -541,6 +542,7 @@ export const CippApplicationDeployDrawer = ({ onClick={() => { searchApp(formControl.getValues("searchQuery"), "choco"); }} + disabled={ChocosearchResults.isPending} > Search @@ -561,7 +563,7 @@ export const CippApplicationDeployDrawer = ({ } multiple={false} formControl={formControl} - isFetching={ChocosearchResults.isLoading} + isFetching={ChocosearchResults.isPending} /> From 533605eb7999e3cc9ddc9ad41bc366cb1dd25a9e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:03:04 +0100 Subject: [PATCH 070/177] fixes tenantFilter allowed --- src/components/CippComponents/CippApplicationDeployDrawer.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CippComponents/CippApplicationDeployDrawer.jsx b/src/components/CippComponents/CippApplicationDeployDrawer.jsx index ddc11982fd55..42853df40ca1 100644 --- a/src/components/CippComponents/CippApplicationDeployDrawer.jsx +++ b/src/components/CippComponents/CippApplicationDeployDrawer.jsx @@ -96,6 +96,7 @@ export const CippApplicationDeployDrawer = ({ const handleSubmit = () => { const formData = formControl.getValues(); const formattedData = { ...formData }; + formattedData.tenantFilter = "allTenants"; //added to prevent issues with location check. temp fix formattedData.selectedTenants = selectedTenants.map((tenant) => ({ defaultDomainName: tenant.value, customerId: tenant.addedFields.customerId, From 518f6a1d1c30540b814f930b16377d16fa9c5ead Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 09:55:07 -0500 Subject: [PATCH 071/177] fix query key bug with user offboarding --- .../CippWizard/CippWizardOffboarding.jsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index dfb412d86824..d17e2fca4c0f 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -35,11 +35,11 @@ export const CippWizardOffboarding = (props) => { useEffect(() => { const currentTenantId = currentTenant?.value; const appliedDefaultsForTenant = formControl.getValues("HIDDEN_appliedDefaultsForTenant"); - + // Only apply defaults if we haven't applied them for this tenant yet if (currentTenantId && appliedDefaultsForTenant !== currentTenantId) { const tenantDefaults = currentTenant?.addedFields?.offboardingDefaults; - + if (tenantDefaults) { // Apply tenant defaults Object.entries(tenantDefaults).forEach(([key, value]) => { @@ -55,7 +55,7 @@ export const CippWizardOffboarding = (props) => { // Set the source indicator formControl.setValue("HIDDEN_defaultsSource", "user"); } - + // Mark that we've applied defaults for this tenant formControl.setValue("HIDDEN_appliedDefaultsForTenant", currentTenantId); } @@ -80,9 +80,9 @@ export const CippWizardOffboarding = (props) => { - {getDefaultsSource() === "tenant" ? "Using Tenant Defaults" : "Using User Defaults"} @@ -208,7 +208,7 @@ export const CippWizardOffboarding = (props) => { dataKey: "Results", labelField: (option) => `${option.displayName} (${option.userPrincipalName})`, valueField: "id", - queryKey: "Offboarding-Users", + queryKey: `Offboarding-Users-${currentTenant ? currentTenant.value : "default"}`, data: { Endpoint: "users", manualPagination: true, @@ -233,7 +233,7 @@ export const CippWizardOffboarding = (props) => { url: "/api/ListGraphRequest", dataKey: "Results", tenantFilter: currentTenant ? currentTenant.value : undefined, - queryKey: "Offboarding-Users", + queryKey: `Offboarding-Users-${currentTenant ? currentTenant.value : "default"}`, data: { Endpoint: "users", manualPagination: true, @@ -258,7 +258,7 @@ export const CippWizardOffboarding = (props) => { valueField: "id", url: "/api/ListGraphRequest", dataKey: "Results", - queryKey: "Offboarding-Users", + queryKey: `Offboarding-Users-${currentTenant ? currentTenant.value : "default"}`, data: { Endpoint: "users", manualPagination: true, @@ -300,7 +300,7 @@ export const CippWizardOffboarding = (props) => { valueField: "id", url: "/api/ListGraphRequest", dataKey: "Results", - queryKey: "Offboarding-Users", + queryKey: `Offboarding-Users-${currentTenant ? currentTenant.value : "default"}`, data: { Endpoint: "users", manualPagination: true, From d28cb90fe588f4117514ae723c3a95442a60124a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:44:45 +0100 Subject: [PATCH 072/177] custom apps --- .../CippApplicationDeployDrawer.jsx | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/src/components/CippComponents/CippApplicationDeployDrawer.jsx b/src/components/CippComponents/CippApplicationDeployDrawer.jsx index 42853df40ca1..410be49561c0 100644 --- a/src/components/CippComponents/CippApplicationDeployDrawer.jsx +++ b/src/components/CippComponents/CippApplicationDeployDrawer.jsx @@ -60,6 +60,7 @@ export const CippApplicationDeployDrawer = ({ winGetApp: "/api/AddwinGetApp", chocolateyApp: "/api/AddChocoApp", officeApp: "/api/AddOfficeApp", + win32ScriptApp: "/api/AddWin32ScriptApp", }; const ChocosearchResults = ApiPostCall({ @@ -160,6 +161,7 @@ export const CippApplicationDeployDrawer = ({ // uncomment after release { label: "WinGet App", value: "winGetApp" }, { label: "Chocolatey App", value: "chocolateyApp" }, { label: "Microsoft Office", value: "officeApp" }, + { label: "Custom Application", value: "win32ScriptApp" }, ]} multiple={false} formControl={formControl} @@ -814,6 +816,144 @@ export const CippApplicationDeployDrawer = ({
    + {/* Win32 Script App Section */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Assign To Options */} + + + + + + + + + +
    From 643333def3156ba9ed96f914f666797d193607a6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 11:29:22 -0500 Subject: [PATCH 073/177] Add backup download and refresh controls Add ability to download scheduled backup JSONs and refresh backup/configuration lists. Introduces ApiPostCall-based downloadAction and handleDownloadBackup to fetch backup data, create a Blob and trigger a file download. Adds IconButton refresh controls (Sync) for configuration and backup history, a Download button (CloudDownload) per backup, and updates the restore button label to "Restore". Also includes necessary imports (IconButton, Sync, CloudDownload, ApiPostCall) and minor UI adjustments (layout for tenant selector). Removes rendering of backup tag chips. --- .../tenant/manage/configuration-backup.js | 108 +++++++++++++----- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js index cda8f505d56f..8ddd21cbd4ce 100644 --- a/src/pages/tenant/manage/configuration-backup.js +++ b/src/pages/tenant/manage/configuration-backup.js @@ -10,6 +10,7 @@ import { AlertTitle, Card, CardContent, + IconButton, Stack, Skeleton, Chip, @@ -25,9 +26,11 @@ import { CheckCircle, Cancel, Delete, + Sync, + CloudDownload, } from "@mui/icons-material"; import { useSettings } from "../../../hooks/use-settings"; -import { ApiGetCall } from "../../../api/ApiCall"; +import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; import { CippPropertyListCard } from "../../../components/CippCards/CippPropertyListCard"; import { CippBackupScheduleDrawer } from "../../../components/CippComponents/CippBackupScheduleDrawer"; import { CippRestoreBackupDrawer } from "../../../components/CippComponents/CippRestoreBackupDrawer"; @@ -50,6 +53,10 @@ const Page = () => { // Prioritize URL query parameter, then fall back to settings const currentTenant = router.query.tenantFilter || settings.currentTenant; + const downloadAction = ApiPostCall({ + urlFromData: true, + }); + // API call to get backup files const backupList = ApiGetCall({ url: "/api/ExecListBackup", @@ -84,6 +91,36 @@ const Page = () => { return ["Configuration"]; }; + const handleDownloadBackup = (backup) => { + downloadAction.mutate( + { + url: `/api/ExecListBackup?BackupName=${backup.name}&Type=Scheduled`, + data: { + tenantFilter: backup.tenantSource, + }, + }, + { + onSuccess: (data) => { + const jsonString = data?.data?.[0]?.Backup; + if (!jsonString) { + console.error("No backup data returned"); + return; + } + + const blob = new Blob([jsonString], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${backup.name}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, + }, + ); + }; + // Filter backup data by selected tenant if in AllTenants view const tenantFilteredBackupData = settings.currentTenant === "AllTenants" && @@ -104,7 +141,7 @@ const Page = () => { const currentConfig = Array.isArray(existingBackupConfig.data) ? existingBackupConfig.data.find( (tenant) => - tenant.Tenant.value === settings.currentTenant || tenant.Tenant.value === "AllTenants" + tenant.Tenant.value === settings.currentTenant || tenant.Tenant.value === "AllTenants", ) : null; const hasExistingConfig = currentConfig && currentConfig.Parameters?.ScheduledBackupValues; @@ -254,6 +291,15 @@ const Page = () => { title="Backup Schedule Details" propertyItems={configPropertyItems} isFetching={existingBackupConfig.isFetching} + actionButton={ + + + + } /> @@ -308,19 +354,28 @@ const Page = () => { Backup History - {settings.currentTenant === "AllTenants" && ( - - - - )} + + {settings.currentTenant === "AllTenants" && ( + + + + )} + + + + @@ -358,7 +413,7 @@ const Page = () => { {(() => { const match = backup.name.match( - /.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/ + /.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/, ); return match ? `${match[1]} @ ${match[2]}:${match[3]}` @@ -378,8 +433,16 @@ const Page = () => { )} + { /> - - {backup.tags.map((tag, idx) => ( - - ))} - From d757754c2c8e52139994fc22397d3101d22baca2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:38:05 +0100 Subject: [PATCH 074/177] version up --- package.json | 2 +- public/version.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 982b96b85086..3a21cb75f840 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.0.9", + "version": "10.1.0", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index ad15f02cceba..af189d5af279 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.0.9" -} \ No newline at end of file + "version": "10.1.0" +} From 6e2f92f539851ca696a4e12b9dc2bc5c6f5f93a8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 11:39:22 -0500 Subject: [PATCH 075/177] Add backup preview drawer and JSON viewer Add a 'Preview' button to the backup list that opens a right-side Drawer to preview configuration backup contents. Implements state and handlers (selectedBackup, backupContent, isLoadingBackup, handleOpenBackupPreview, handleCloseDrawer), requests backup data via the existing downloadAction/ExecListBackup API, parses JSON, and shows a CircularProgress while loading, CippJsonView for the parsed content, and an Alert on failure. Also adds necessary imports (Drawer, CircularProgress, Visibility, Close, CippJsonView) and a close button in the drawer. --- .../tenant/manage/configuration-backup.js | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js index 8ddd21cbd4ce..15154dc5e258 100644 --- a/src/pages/tenant/manage/configuration-backup.js +++ b/src/pages/tenant/manage/configuration-backup.js @@ -14,6 +14,8 @@ import { Stack, Skeleton, Chip, + CircularProgress, + Drawer, } from "@mui/material"; import { Grid } from "@mui/system"; import { @@ -28,6 +30,8 @@ import { Delete, Sync, CloudDownload, + Visibility, + Close, } from "@mui/icons-material"; import { useSettings } from "../../../hooks/use-settings"; import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; @@ -37,6 +41,7 @@ import { CippRestoreBackupDrawer } from "../../../components/CippComponents/Cipp import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; import { CippTimeAgo } from "../../../components/CippComponents/CippTimeAgo"; import { CippFormTenantSelector } from "../../../components/CippComponents/CippFormTenantSelector"; +import CippJsonView from "../../../components/CippFormPages/CippJSONView"; import { useDialog } from "../../../hooks/use-dialog"; import ReactTimeAgo from "react-time-ago"; import tabOptions from "./tabOptions.json"; @@ -57,6 +62,12 @@ const Page = () => { urlFromData: true, }); + // State to track drawer and backup preview data + const [drawerOpen, setDrawerOpen] = useState(false); + const [selectedBackup, setSelectedBackup] = useState(null); + const [backupContent, setBackupContent] = useState(null); + const [isLoadingBackup, setIsLoadingBackup] = useState(false); + // API call to get backup files const backupList = ApiGetCall({ url: "/api/ExecListBackup", @@ -121,6 +132,46 @@ const Page = () => { ); }; + const handleOpenBackupPreview = (backup) => { + setSelectedBackup(backup); + setDrawerOpen(true); + setIsLoadingBackup(true); + setBackupContent(null); + + // Load backup data + downloadAction.mutate( + { + url: `/api/ExecListBackup?BackupName=${backup.name}&Type=Scheduled`, + data: { + tenantFilter: backup.tenantSource, + }, + }, + { + onSuccess: (data) => { + const jsonString = data?.data?.[0]?.Backup; + if (jsonString) { + try { + const parsedData = JSON.parse(jsonString); + setBackupContent(parsedData); + } catch (error) { + console.error("Failed to parse backup data:", error); + } + } + setIsLoadingBackup(false); + }, + onError: () => { + setIsLoadingBackup(false); + }, + }, + ); + }; + + const handleCloseDrawer = () => { + setDrawerOpen(false); + setSelectedBackup(null); + setBackupContent(null); + }; + // Filter backup data by selected tenant if in AllTenants view const tenantFilteredBackupData = settings.currentTenant === "AllTenants" && @@ -433,6 +484,14 @@ const Page = () => { )} + + + + + {/* Results Section */} + {getTenant.isFetching ? ( + + + + Fetching Results + + + + + + ) : tenantData ? ( + <> + + + + + {/* Preview Container with Illustration Background */} + + + {/* Overlay Content */} + + {/* Tenant Details Container */} + + + Tenant Information + + + + + Tenant Name + + + + {domain || "Not Available"} + + {domain && } + + + + + Default Domain Name + + + + {graphData?.defaultDomainName || "Not Available"} + + {graphData?.defaultDomainName && } + + + + + Tenant ID + + + + {graphData?.tenantId || "Not Available"} + + {graphData?.tenantId && } + + + + + Tenant Region + + {openIdData?.tenant_region_scope ? ( + + ) : ( + + Not Available + + )} + + + + + {/* Tile Logo Container */} + {(brandingData?.TileLogo || brandingData?.TileDarkLogo) && ( + + + Tenant Logo + + {tileLogoUrl ? ( + { + e.target.style.display = "none"; + }} + /> + ) : ( + + )} + + )} + + + + + ) : null} + + + ); +}; + +export default CippTenantLookup; diff --git a/src/pages/tenant/tools/tenantlookup/index.js b/src/pages/tenant/tools/tenantlookup/index.js index aea206a02e98..0f178081e8cb 100644 --- a/src/pages/tenant/tools/tenantlookup/index.js +++ b/src/pages/tenant/tools/tenantlookup/index.js @@ -1,22 +1,8 @@ -import { Box, Button, Container, Typography, Skeleton, Link } from "@mui/material"; -import { Grid } from "@mui/system"; +import { Box, Container } from "@mui/material"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { useForm, useWatch } from "react-hook-form"; -import CippButtonCard from "../../../../components/CippCards/CippButtonCard"; -import { Search } from "@mui/icons-material"; -import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; -import { ApiGetCall } from "../../../../api/ApiCall"; +import CippTenantLookup from "../../../../components/CippComponents/CippTenantLookup"; const Page = () => { - const formControl = useForm({ mode: "onBlur" }); - const domain = useWatch({ control: formControl.control, name: "domain" }); - const getTenant = ApiGetCall({ - url: "/api/ListExternalTenantInfo", - data: { tenant: domain }, - queryKey: `tenant-${domain}`, - waiting: false, - }); - return ( { }} > - - - - - - - - - - - - - - - {/* Results Card */} - {getTenant.isFetching ? ( - - - - - - - - - - ) : getTenant.data ? ( - - - - - - Tenant Name: {domain} - - - Tenant Id: {getTenant.data?.GraphRequest?.tenantId} - - - Default Domain Name:{" "} - {getTenant.data?.GraphRequest?.defaultDomainName} - - - Tenant Brand Name :{" "} - {getTenant.data?.GraphRequest?.federationBrandName - ? getTenant.data?.GraphRequest?.federationBrandName - : "Not Specified"} - - - Tenant Region:{" "} - {getTenant.data?.OpenIdConfig?.tenant_region_scope} - - - - - - ) : null} - + ); From b6281de83cf3912ebffe957af94712d22a2da1ed Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:00:38 +0800 Subject: [PATCH 095/177] Add device local-admin standard Introduce a new standard entry (standards.intuneDeviceRegLocalAdmins) to control local administrator behavior for Microsoft Entra joined device registrations. Adds two UI switches (disableRegisteringUsers, enableGlobalAdmins) with default true values, metadata (label, impact: Medium, addedDate: 2026-02-23), and a PowerShell equivalent (Update-MgBetaPolicyDeviceRegistrationPolicy). This provides a configurable policy to prevent registering users from becoming local admins and to optionally allow Global Administrators to remain local admins. --- src/data/standards.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 66dcc2705b50..54c4f402f4ed 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -3988,6 +3988,34 @@ "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", "recommendedBy": [] }, + { + "name": "standards.intuneDeviceRegLocalAdmins", + "cat": "Entra (AAD) Standards", + "tag": [], + "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.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.intuneDeviceRegLocalAdmins.disableRegisteringUsers", + "label": "Disable registering users as local administrators", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.intuneDeviceRegLocalAdmins.enableGlobalAdmins", + "label": "Allow Global Administrators to be local administrators", + "defaultValue": true + } + ], + "label": "Configure local administrator rights for users joining devices", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-02-23", + "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", + "recommendedBy": [] + }, { "name": "standards.intuneRequireMFA", "cat": "Intune Standards", From 8bdaa859a458b65da9fc0d98e9a3e0f7f1ace3af Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:20:18 +0100 Subject: [PATCH 096/177] length issues permissions check --- src/components/CippSettings/CippPermissionResults.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippSettings/CippPermissionResults.jsx b/src/components/CippSettings/CippPermissionResults.jsx index 95c1d8739458..bd8e37000c0e 100644 --- a/src/components/CippSettings/CippPermissionResults.jsx +++ b/src/components/CippSettings/CippPermissionResults.jsx @@ -218,7 +218,7 @@ export const CippPermissionResults = (props) => { /> )} - {results?.Results?.AccessTokenDetails?.Scope.length > 0 && ( + {results?.Results?.AccessTokenDetails?.Scope?.length > 0 && ( <> { /> )} - {results?.Results?.ApplicationTokenDetails?.Roles.length > 0 && ( + {results?.Results?.ApplicationTokenDetails?.Roles?.length > 0 && ( <> Date: Tue, 24 Feb 2026 07:47:49 -0600 Subject: [PATCH 097/177] fix: Update applied-standards.js to refer to Actions dropdown Replaced text referring to old Report button with new Actions dropdown reference Signed-off-by: Brian Simpson <50429915+bmsimp@users.noreply.github.com> --- src/pages/tenant/manage/applied-standards.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index bf424f07ddd8..c17dc52e67cb 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -1779,8 +1779,7 @@ const Page = () => { ) : ( - This data has not yet been collected. Collect the data by pressing - the report button on the top of the page. + This data has not yet been collected. Collect the data by selecting Refresh Data from the Actions dropdown on the top of the page. )} From eac8c88fe6cbcb024760e7e8b8ec65efa6752e76 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:09:22 +0100 Subject: [PATCH 098/177] mandatory fields and tenant allow ist. --- .../CippAddTenantAllowBlockListDrawer.jsx | 12 +----------- src/pages/cipp/preferences.js | 4 ++-- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx index 23fbe252ae55..5916604a9c4c 100644 --- a/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx +++ b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Button, Divider } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm, useFormState, useWatch } from "react-hook-form"; @@ -86,16 +86,6 @@ export const CippAddTenantAllowBlockListDrawer = ({ formControl, ]); - useEffect(() => { - if (addEntry.isSuccess) { - const currentTenants = formControl.getValues("tenantID"); - formControl.reset({ - ...defaultValues, - tenantID: currentTenants, - }); - } - }, [addEntry.isSuccess, formControl]); - const validateEntries = (value) => { if (!value) return true; diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js index bd15fbf9ae70..fd99bf3a10a7 100644 --- a/src/pages/cipp/preferences.js +++ b/src/pages/cipp/preferences.js @@ -254,7 +254,7 @@ const Page = () => { title="General Settings" propertyItems={[ { - label: "Default usage location for users", + label: "Default usage location for users *", value: ( { ), }, { - label: "Default Page Size", + label: "Default Page Size *", value: ( Date: Tue, 24 Feb 2026 17:09:28 +0100 Subject: [PATCH 099/177] mandatory fields and tenant allow ist. --- .../CippAddTenantAllowBlockListDrawer.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx index 5916604a9c4c..182f897966aa 100644 --- a/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx +++ b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx @@ -67,7 +67,7 @@ export const CippAddTenantAllowBlockListDrawer = ({ formControl.setValue( "listMethod", { label: "Block", value: "Block" }, - { shouldValidate: true } + { shouldValidate: true }, ); } @@ -263,12 +263,12 @@ export const CippAddTenantAllowBlockListDrawer = ({ listType?.value === "FileHash" ? "Enter SHA256 hash values separated by commas or semicolons (e.g., 768a813668695ef2483b2bde7cf5d1b2db0423a0d3e63e498f3ab6f2eb13ea3e)" : listType?.value === "Url" - ? "Enter URLs, IPv4, or IPv6 addresses with optional wildcards separated by commas or semicolons" - : listType?.value === "Sender" - ? "Enter domains or email addresses separated by commas or semicolons (e.g., contoso.com,user@example.com)" - : listType?.value === "IP" - ? "Enter IPv6 addresses only in colon-hexadecimal format or CIDR notation" - : "" + ? "Enter URLs, IPv4, or IPv6 addresses with optional wildcards separated by commas or semicolons" + : listType?.value === "Sender" + ? "Enter domains or email addresses separated by commas or semicolons (e.g., contoso.com,user@example.com)" + : listType?.value === "IP" + ? "Enter IPv6 addresses only in colon-hexadecimal format or CIDR notation" + : "" } /> From 877bbae76f806a4d3624eaaf02c9892dcd0dfc6f Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:48:57 +0100 Subject: [PATCH 100/177] fix: readd Start-CippDevInstallation into Start-CippDevEmulators.ps1 - Done to ensure dependencies are up to date before starting the emulators --- Tools/Start-CippDevEmulators.ps1 | 3 +++ Tools/Start-CippDevInstallation.ps1 | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Tools/Start-CippDevEmulators.ps1 b/Tools/Start-CippDevEmulators.ps1 index dc56337ebf8c..e0d0146f3ab6 100644 --- a/Tools/Start-CippDevEmulators.ps1 +++ b/Tools/Start-CippDevEmulators.ps1 @@ -8,6 +8,9 @@ Get-Process node -ErrorAction SilentlyContinue | Stop-Process -ErrorAction Silen # Get paths $Path = (Get-Item $PSScriptRoot).Parent.Parent.FullName + +# Run installation script to ensure dependencies are installed and updated before starting emulators +pwsh -File (Join-Path $PSScriptRoot 'Start-CippDevInstallation.ps1') $ApiPath = Join-Path -Path $Path -ChildPath 'CIPP-API' $FrontendPath = Join-Path -Path $Path -ChildPath 'CIPP' diff --git a/Tools/Start-CippDevInstallation.ps1 b/Tools/Start-CippDevInstallation.ps1 index 2d00a12c6a26..0cc8c07f8633 100644 --- a/Tools/Start-CippDevInstallation.ps1 +++ b/Tools/Start-CippDevInstallation.ps1 @@ -29,4 +29,5 @@ if (-not(yarn global list | Select-String -Pattern 'next')) { yarn global add 'next' } -yarn install --cwd (Join-Path $Path "CIPP") --network-timeout 500000 +Write-Host 'Running yarn install for CIPP frontend...' -ForegroundColor Cyan +yarn install --cwd (Join-Path $Path 'CIPP') --network-timeout 500000 From 3b55cb3df3140e59bfbc00b0e79d9a200e1f730f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 24 Feb 2026 13:17:06 -0500 Subject: [PATCH 101/177] bump version --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 646a9e8e14c5..d575cae8d4cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.1.0", + "version": "10.1.1", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index af189d5af279..eb5d53d8afba 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.1.0" + "version": "10.1.1" } From 46464361f7be9b9104f48fd22757b5a48fa961b3 Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:41:22 -0500 Subject: [PATCH 102/177] fix(edit): trigger form validation on room info update --- src/pages/email/resources/management/list-rooms/edit.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/email/resources/management/list-rooms/edit.jsx b/src/pages/email/resources/management/list-rooms/edit.jsx index a8125a3f8be8..9505c7e4c19b 100644 --- a/src/pages/email/resources/management/list-rooms/edit.jsx +++ b/src/pages/email/resources/management/list-rooms/edit.jsx @@ -115,6 +115,7 @@ const EditRoomMailbox = () => { } : null, }); + void formControl.trigger(); } }, [roomInfo.isSuccess, roomInfo.data]); From 08a63ae276620c886a9d1d7264ac80958e8566de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:44:20 +0000 Subject: [PATCH 103/177] Bump @tiptap/starter-kit from 3.19.0 to 3.20.0 Bumps [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) from 3.19.0 to 3.20.0. - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/starter-kit/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.20.0/packages/starter-kit) --- updated-dependencies: - dependency-name: "@tiptap/starter-kit" dependency-version: 3.20.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 250 +++++++++++++++++++++++++-------------------------- 2 files changed, 126 insertions(+), 126 deletions(-) diff --git a/package.json b/package.json index d575cae8d4cb..cc533cb63980 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@tiptap/extension-table": "^3.19.0", "@tiptap/pm": "^3.4.1", "@tiptap/react": "^3.4.1", - "@tiptap/starter-kit": "^3.19.0", + "@tiptap/starter-kit": "^3.20.0", "@uiw/react-json-view": "^2.0.0-alpha.41", "@vvo/tzdb": "^6.198.0", "apexcharts": "5.3.5", diff --git a/yarn.lock b/yarn.lock index 435c9b238443..d91465507c1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2255,20 +2255,20 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== -"@tiptap/core@^3.19.0", "@tiptap/core@^3.4.1": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.19.0.tgz#dca483b50e1b8a596f695aecde387a79fe7da717" - integrity sha512-bpqELwPW+DG8gWiD8iiFtSl4vIBooG5uVJod92Qxn3rA9nFatyXRr4kNbMJmOZ66ezUvmCjXVe/5/G4i5cyzKA== +"@tiptap/core@^3.20.0", "@tiptap/core@^3.4.1": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.20.0.tgz#dac72894d83829f2fbbabee2e90a748d7c1479ee" + integrity sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ== -"@tiptap/extension-blockquote@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-3.19.0.tgz#86c52e8e3b6d1e072ae0d9c895723034a1e37096" - integrity sha512-y3UfqY9KD5XwWz3ndiiJ089Ij2QKeiXy/g1/tlAN/F1AaWsnkHEHMLxCP1BIqmMpwsX7rZjMLN7G5Lp7c9682A== +"@tiptap/extension-blockquote@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-3.20.0.tgz#92e4a8ed00cf4fcab056766b848fc0a551847e5b" + integrity sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA== -"@tiptap/extension-bold@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.19.0.tgz#ef0ddfd9b242ef9c25e3348aef9bf2dc681cdc19" - integrity sha512-UZgb1d0XK4J/JRIZ7jW+s4S6KjuEDT2z1PPM6ugcgofgJkWQvRZelCPbmtSFd3kwsD+zr9UPVgTh9YIuGQ8t+Q== +"@tiptap/extension-bold@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-3.20.0.tgz#4298d24cb6c7759f6233eec5e24e9fd7c7efdf38" + integrity sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg== "@tiptap/extension-bubble-menu@^3.13.0": version "3.13.0" @@ -2277,127 +2277,127 @@ dependencies: "@floating-ui/dom" "^1.0.0" -"@tiptap/extension-bullet-list@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.19.0.tgz#acf12e952b6a5873dc20b58530f2f524807bbd6f" - integrity sha512-F9uNnqd0xkJbMmRxVI5RuVxwB9JaCH/xtRqOUNQZnRBt7IdAElCY+Dvb4hMCtiNv+enGM/RFGJuFHR9TxmI7rw== +"@tiptap/extension-bullet-list@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz#57bcba5988990f39cb71c7901173da9b4979523b" + integrity sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw== -"@tiptap/extension-code-block@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-3.19.0.tgz#71a7a362b3fa68c1789c8b9ac224ca89eb410630" - integrity sha512-b/2qR+tMn8MQb+eaFYgVk4qXnLNkkRYmwELQ8LEtEDQPxa5Vl7J3eu8+4OyoIFhZrNDZvvoEp80kHMCP8sI6rg== +"@tiptap/extension-code-block@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz#5c00e8ae017c32ff4dd629447635a25fcb9d0f52" + integrity sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ== -"@tiptap/extension-code@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.19.0.tgz#15d53c139ad64d1debcc08c7ca5afbcc8e531f0b" - integrity sha512-2kqqQIXBXj2Or+4qeY3WoE7msK+XaHKL6EKOcKlOP2BW8eYqNTPzNSL+PfBDQ3snA7ljZQkTs/j4GYDj90vR1A== +"@tiptap/extension-code@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-3.20.0.tgz#6aa18bc21ed8a3a6a899653c48add1cecc64783d" + integrity sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ== -"@tiptap/extension-document@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.19.0.tgz#dfa6889cff748d489e0bc1028918bf4571372ba5" - integrity sha512-AOf0kHKSFO0ymjVgYSYDncRXTITdTcrj1tqxVazrmO60KNl1Rc2dAggDvIVTEBy5NvceF0scc7q3sE/5ZtVV7A== +"@tiptap/extension-document@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-3.20.0.tgz#e10f92139c97354ab917f3095d4011d6703528f6" + integrity sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w== -"@tiptap/extension-dropcursor@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.19.0.tgz#fbef441944842f23fe0a35154b519103166a4848" - integrity sha512-sf3dEZXiLvsGqVK2maUIzXY6qtYYCvBumag7+VPTMGQ0D4hiZ1X/4ukt4+6VXDg5R2WP1CoIt/QvUetUjWNhbQ== +"@tiptap/extension-dropcursor@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.0.tgz#459d6c5d7e5f4dc1152246901387e000596fbb40" + integrity sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ== "@tiptap/extension-floating-menu@^3.13.0": version "3.13.0" resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.13.0.tgz#03d03292add49d1b380cdb1ff3890b2956d4e3f5" integrity sha512-OsezV2cMofZM4c13gvgi93IEYBUzZgnu8BXTYZQiQYekz4bX4uulBmLa1KOA9EN71FzS+SoLkXHU0YzlbLjlxA== -"@tiptap/extension-gapcursor@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.19.0.tgz#64e5462a4ab2f0bd110738410dcbf3597d76349f" - integrity sha512-w7DACS4oSZaDWjz7gropZHPc9oXqC9yERZTcjWxyORuuIh1JFf0TRYspleK+OK28plK/IftojD/yUDn1MTRhvA== +"@tiptap/extension-gapcursor@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.0.tgz#df89dd19417c020c6e9529e2db5077d08353e4a1" + integrity sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w== -"@tiptap/extension-hard-break@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.19.0.tgz#7120524cec9ed4b957963693cb4c57cbecbaecf8" - integrity sha512-lAmQraYhPS5hafvCl74xDB5+bLuNwBKIEsVoim35I0sDJj5nTrfhaZgMJ91VamMvT+6FF5f1dvBlxBxAWa8jew== +"@tiptap/extension-hard-break@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.20.0.tgz#fff1553d3b41ad32d3979d618f0f894cee926f46" + integrity sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA== -"@tiptap/extension-heading@^3.19.0", "@tiptap/extension-heading@^3.4.1": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.19.0.tgz#d0bc93426c01a2ed36b9124c1a8205ab3945e77a" - integrity sha512-uLpLlfyp086WYNOc0ekm1gIZNlEDfmzOhKzB0Hbyi6jDagTS+p9mxUNYeYOn9jPUxpFov43+Wm/4E24oY6B+TQ== +"@tiptap/extension-heading@^3.20.0", "@tiptap/extension-heading@^3.4.1": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.20.0.tgz#a16dbb625d91556399912fa110b04b2ad2dfd2e3" + integrity sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA== -"@tiptap/extension-horizontal-rule@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz#0e77078fcd53beca786277ce83d259e2103cc361" - integrity sha512-iqUHmgMGhMgYGwG6L/4JdelVQ5Mstb4qHcgTGd/4dkcUOepILvhdxajPle7OEdf9sRgjQO6uoAU5BVZVC26+ng== +"@tiptap/extension-horizontal-rule@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.0.tgz#53ffe2f9b9627f27f85b02d75878583dfb044107" + integrity sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA== "@tiptap/extension-image@^3.4.1": version "3.13.0" resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-3.13.0.tgz#55edb952e86c2ebed436cd53def8b2e743d71d7e" integrity sha512-223uzLUkIa1rkK7aQK3AcIXe6LbCtmnpVb7sY5OEp+LpSaSPyXwyrZ4A0EO1o98qXG68/0B2OqMntFtA9c5Fbw== -"@tiptap/extension-italic@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz#af2a9c095ec846e379041f3e17e1dd101a5a4bf8" - integrity sha512-6GffxOnS/tWyCbDkirWNZITiXRta9wrCmrfa4rh+v32wfaOL1RRQNyqo9qN6Wjyl1R42Js+yXTzTTzZsOaLMYA== +"@tiptap/extension-italic@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.20.0.tgz#176c2080a75082d296797618c2ed84e9defc7ef9" + integrity sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw== -"@tiptap/extension-link@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.19.0.tgz#e8e656735bda6ca1d4b6577821e06274ab0ff6c8" - integrity sha512-HEGDJnnCPfr7KWu7Dsq+eRRe/mBCsv6DuI+7fhOCLDJjjKzNgrX2abbo/zG3D/4lCVFaVb+qawgJubgqXR/Smw== +"@tiptap/extension-link@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-3.20.0.tgz#b3f2d89aabb88ed1eb66925e59fe82d1d2b866e9" + integrity sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA== dependencies: linkifyjs "^4.3.2" -"@tiptap/extension-list-item@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-3.19.0.tgz#b2218ff6be694b581fd7d817810a33ee1c218311" - integrity sha512-VsSKuJz4/Tb6ZmFkXqWpDYkRzmaLTyE6dNSEpNmUpmZ32sMqo58mt11/huADNwfBFB0Ve7siH/VnFNIJYY3xvg== +"@tiptap/extension-list-item@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-3.20.0.tgz#60ec36a3326d3bf28982b653b63f6cab5c7d9d9f" + integrity sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA== -"@tiptap/extension-list-keymap@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.19.0.tgz#41b87b154560aad92e779bff5c6e32e125b792ea" - integrity sha512-bxgmAgA3RzBGA0GyTwS2CC1c+QjkJJq9hC+S6PSOWELGRiTbwDN3MANksFXLjntkTa0N5fOnL27vBHtMStURqw== +"@tiptap/extension-list-keymap@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.0.tgz#3e49292b0cdf0a7b6d8f08ae5acdd7014c15cdc1" + integrity sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA== -"@tiptap/extension-list@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-list/-/extension-list-3.19.0.tgz#737dcb56ba9838a4431c1afb035bd622fab46d21" - integrity sha512-N6nKbFB2VwMsPlCw67RlAtYSK48TAsAUgjnD+vd3ieSlIufdQnLXDFUP6hFKx9mwoUVUgZGz02RA6bkxOdYyTw== +"@tiptap/extension-list@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list/-/extension-list-3.20.0.tgz#17d379fe34e09a9b42ab620f7ff571826485c7d5" + integrity sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ== -"@tiptap/extension-ordered-list@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.19.0.tgz#f6f8bfe41d3429c505b44764b473b6dfd7bcd2a1" - integrity sha512-cxGsINquwHYE1kmhAcLNLHAofmoDEG6jbesR5ybl7tU5JwtKVO7S/xZatll2DU1dsDAXWPWEeeMl4e/9svYjCg== +"@tiptap/extension-ordered-list@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.0.tgz#ec449716854d496ef7c1ea9243c2c467fa5d3cb1" + integrity sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA== -"@tiptap/extension-paragraph@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.19.0.tgz#91adde189aabf13a2bfbb2d961833d3bc2bc055f" - integrity sha512-xWa6gj82l5+AzdYyrSk9P4ynySaDzg/SlR1FarXE5yPXibYzpS95IWaVR0m2Qaz7Rrk+IiYOTGxGRxcHLOelNg== +"@tiptap/extension-paragraph@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-3.20.0.tgz#b1778746021ac38894287d62d9429ac309a632ef" + integrity sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ== -"@tiptap/extension-strike@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.19.0.tgz#eac7712cc791488f4c1c48baf3aed1a8d95f398c" - integrity sha512-xYpabHsv7PccLUBQaP8AYiFCnYbx6P93RHPd0lgNwhdOjYFd931Zy38RyoxPHAgbYVmhf1iyx7lpuLtBnhS5dA== +"@tiptap/extension-strike@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.20.0.tgz#a1ec09a1a56aad98d6e7dc493d77547cad4403ee" + integrity sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ== "@tiptap/extension-table@^3.19.0": version "3.19.0" resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.19.0.tgz#a5f9be88e319f60dc7b8df1321f95a31b20fe991" integrity sha512-Lg8DlkkDUMYE/CcGOxoCWF98B2i7VWh+AGgqlF+XWrHjhlKHfENLRXm1a0vWuyyP3NknRYILoaaZ1s7QzmXKRA== -"@tiptap/extension-text@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.19.0.tgz#353278c97bd8f5bdc29f06942fbd1e856bdb5b18" - integrity sha512-K95+SnbZy0h6hNFtfy23n8t/nOcTFEf69In9TSFVVmwn/Nwlke+IfiESAkqbt1/7sKJeegRXYO7WzFEmFl9Q/g== +"@tiptap/extension-text@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-3.20.0.tgz#bdf0ac6e0c638c9dbb99a5a2b5a07db2b8cba1de" + integrity sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ== -"@tiptap/extension-underline@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.19.0.tgz#bbc81d085725981d256127ab416f91d0802ec2a4" - integrity sha512-800MGEWfG49j10wQzAFiW/ele1HT04MamcL8iyuPNu7ZbjbGN2yknvdrJlRy7hZlzIrVkZMr/1tz62KN33VHIw== +"@tiptap/extension-underline@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-3.20.0.tgz#535aebafc9e30da51df3be2c2065c49539535672" + integrity sha512-LzNXuy2jwR/y+ymoUqC72TiGzbOCjioIjsDu0MNYpHuHqTWPK5aV9Mh0nbZcYFy/7fPlV1q0W139EbJeYBZEAQ== -"@tiptap/extensions@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.19.0.tgz#5747c0ebf460b9669e8b4362561872448f66abfe" - integrity sha512-ZmGUhLbMWaGqnJh2Bry+6V4M6gMpUDYo4D1xNux5Gng/E/eYtc+PMxMZ/6F7tNTAuujLBOQKj6D+4SsSm457jw== +"@tiptap/extensions@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/extensions/-/extensions-3.20.0.tgz#22bad09a1d861446e17e0d9732439940d80ed808" + integrity sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg== -"@tiptap/pm@^3.19.0", "@tiptap/pm@^3.4.1": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.19.0.tgz#5cb499c7b2603ec6550d0c7a70b924f27fdb7692" - integrity sha512-789zcnM4a8OWzvbD2DL31d0wbSm9BVeO/R7PLQwLIGysDI3qzrcclyZ8yhqOEVuvPitRRwYLq+mY14jz7kY4cw== +"@tiptap/pm@^3.20.0", "@tiptap/pm@^3.4.1": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-3.20.0.tgz#d9a1b92a1cb061059977952e6ec2afe8dff67857" + integrity sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A== dependencies: prosemirror-changeset "^2.3.0" prosemirror-collab "^1.3.1" @@ -2430,35 +2430,35 @@ "@tiptap/extension-bubble-menu" "^3.13.0" "@tiptap/extension-floating-menu" "^3.13.0" -"@tiptap/starter-kit@^3.19.0": - version "3.19.0" - resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-3.19.0.tgz#312440bd18c3cce379ea8eab3fe174b8141dd313" - integrity sha512-dTCkHEz+Y8ADxX7h+xvl6caAj+3nII/wMB1rTQchSuNKqJTOrzyUsCWm094+IoZmLT738wANE0fRIgziNHs/ug== - dependencies: - "@tiptap/core" "^3.19.0" - "@tiptap/extension-blockquote" "^3.19.0" - "@tiptap/extension-bold" "^3.19.0" - "@tiptap/extension-bullet-list" "^3.19.0" - "@tiptap/extension-code" "^3.19.0" - "@tiptap/extension-code-block" "^3.19.0" - "@tiptap/extension-document" "^3.19.0" - "@tiptap/extension-dropcursor" "^3.19.0" - "@tiptap/extension-gapcursor" "^3.19.0" - "@tiptap/extension-hard-break" "^3.19.0" - "@tiptap/extension-heading" "^3.19.0" - "@tiptap/extension-horizontal-rule" "^3.19.0" - "@tiptap/extension-italic" "^3.19.0" - "@tiptap/extension-link" "^3.19.0" - "@tiptap/extension-list" "^3.19.0" - "@tiptap/extension-list-item" "^3.19.0" - "@tiptap/extension-list-keymap" "^3.19.0" - "@tiptap/extension-ordered-list" "^3.19.0" - "@tiptap/extension-paragraph" "^3.19.0" - "@tiptap/extension-strike" "^3.19.0" - "@tiptap/extension-text" "^3.19.0" - "@tiptap/extension-underline" "^3.19.0" - "@tiptap/extensions" "^3.19.0" - "@tiptap/pm" "^3.19.0" +"@tiptap/starter-kit@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-3.20.0.tgz#d356f15632c52f90e8eea8b5c912cab3b01b0866" + integrity sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw== + dependencies: + "@tiptap/core" "^3.20.0" + "@tiptap/extension-blockquote" "^3.20.0" + "@tiptap/extension-bold" "^3.20.0" + "@tiptap/extension-bullet-list" "^3.20.0" + "@tiptap/extension-code" "^3.20.0" + "@tiptap/extension-code-block" "^3.20.0" + "@tiptap/extension-document" "^3.20.0" + "@tiptap/extension-dropcursor" "^3.20.0" + "@tiptap/extension-gapcursor" "^3.20.0" + "@tiptap/extension-hard-break" "^3.20.0" + "@tiptap/extension-heading" "^3.20.0" + "@tiptap/extension-horizontal-rule" "^3.20.0" + "@tiptap/extension-italic" "^3.20.0" + "@tiptap/extension-link" "^3.20.0" + "@tiptap/extension-list" "^3.20.0" + "@tiptap/extension-list-item" "^3.20.0" + "@tiptap/extension-list-keymap" "^3.20.0" + "@tiptap/extension-ordered-list" "^3.20.0" + "@tiptap/extension-paragraph" "^3.20.0" + "@tiptap/extension-strike" "^3.20.0" + "@tiptap/extension-text" "^3.20.0" + "@tiptap/extension-underline" "^3.20.0" + "@tiptap/extensions" "^3.20.0" + "@tiptap/pm" "^3.20.0" "@trysound/sax@0.2.0": version "0.2.0" From 85673f6556c43ea91506af9c4335b895ec42e127 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:44:39 +0000 Subject: [PATCH 104/177] Bump i18next from 25.5.2 to 25.8.13 Bumps [i18next](https://github.com/i18next/i18next) from 25.5.2 to 25.8.13. - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.5.2...v25.8.13) --- updated-dependencies: - dependency-name: i18next dependency-version: 25.8.13 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d575cae8d4cb..e9b3b606b37b 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "export-to-csv": "^1.3.0", "formik": "2.4.9", "gray-matter": "4.0.3", - "i18next": "25.5.2", + "i18next": "25.8.13", "javascript-time-ago": "^2.6.2", "jspdf": "^4.1.0", "jspdf-autotable": "^5.0.7", diff --git a/yarn.lock b/yarn.lock index 435c9b238443..668cf1e54e8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4898,12 +4898,12 @@ hyphen@^1.6.4: resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274" integrity sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw== -i18next@25.5.2: - version "25.5.2" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.5.2.tgz#16efa309e154d46dac7583e6a315ccb47e3e3a10" - integrity sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw== +i18next@25.8.13: + version "25.8.13" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.8.13.tgz#1f9df59329f1706f02b2b58b5d1f75196ddb6e4a" + integrity sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA== dependencies: - "@babel/runtime" "^7.27.6" + "@babel/runtime" "^7.28.4" ignore@^5.2.0: version "5.3.2" From ff82c7427406e7698fb0aae81ffa415fcd8e4389 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:45:25 +0000 Subject: [PATCH 105/177] Bump bhermann/issue-volunteer from 0.1.12 to 0.1.20 Bumps [bhermann/issue-volunteer](https://github.com/bhermann/issue-volunteer) from 0.1.12 to 0.1.20. - [Release notes](https://github.com/bhermann/issue-volunteer/releases) - [Commits](https://github.com/bhermann/issue-volunteer/compare/v0.1.12...v0.1.20) --- updated-dependencies: - dependency-name: bhermann/issue-volunteer dependency-version: 0.1.20 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/Assign_Issue_Volunteer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Assign_Issue_Volunteer.yml b/.github/workflows/Assign_Issue_Volunteer.yml index 23ef1d16ffc0..fe199038b2e5 100644 --- a/.github/workflows/Assign_Issue_Volunteer.yml +++ b/.github/workflows/Assign_Issue_Volunteer.yml @@ -5,6 +5,6 @@ jobs: build: runs-on: ubuntu-slim steps: - - uses: bhermann/issue-volunteer@v0.1.12 + - uses: bhermann/issue-volunteer@v0.1.20 with: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From 61a9e9da57199fcd2dad9f1ff9a50e7d0488cfb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:45:30 +0000 Subject: [PATCH 106/177] Bump JasonEtco/is-sponsor-label-action from 1.2.0 to 2.0.0 Bumps [JasonEtco/is-sponsor-label-action](https://github.com/jasonetco/is-sponsor-label-action) from 1.2.0 to 2.0.0. - [Release notes](https://github.com/jasonetco/is-sponsor-label-action/releases) - [Commits](https://github.com/jasonetco/is-sponsor-label-action/compare/v1.2.0...v2.0.0) --- updated-dependencies: - dependency-name: JasonEtco/is-sponsor-label-action dependency-version: 2.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/label_sponsor_requests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/label_sponsor_requests.yml b/.github/workflows/label_sponsor_requests.yml index bb9d6a31b427..28b84ea4e9b6 100644 --- a/.github/workflows/label_sponsor_requests.yml +++ b/.github/workflows/label_sponsor_requests.yml @@ -11,7 +11,7 @@ jobs: issues: write steps: - name: Sponsor Labels - uses: JasonEtco/is-sponsor-label-action@v1.2.0 + uses: JasonEtco/is-sponsor-label-action@v2.0.0 with: label: "Sponsor Priority" env: From 83b3e802dbad0924cffd8c7543d297c0e4d198c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:45:35 +0000 Subject: [PATCH 107/177] Bump peter-evans/create-or-update-comment from 3 to 5 Bumps [peter-evans/create-or-update-comment](https://github.com/peter-evans/create-or-update-comment) from 3 to 5. - [Release notes](https://github.com/peter-evans/create-or-update-comment/releases) - [Commits](https://github.com/peter-evans/create-or-update-comment/compare/v3...v5) --- updated-dependencies: - dependency-name: peter-evans/create-or-update-comment dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/Comment_on_Issues.yml | 2 +- .github/workflows/auto_comments.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Comment_on_Issues.yml b/.github/workflows/Comment_on_Issues.yml index c408d8b38c84..6a6014f38dcd 100644 --- a/.github/workflows/Comment_on_Issues.yml +++ b/.github/workflows/Comment_on_Issues.yml @@ -12,7 +12,7 @@ jobs: issues: write steps: - name: Add Comment - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v5 with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/.github/workflows/auto_comments.yml b/.github/workflows/auto_comments.yml index 6cd003a36ae8..5955dfd28169 100644 --- a/.github/workflows/auto_comments.yml +++ b/.github/workflows/auto_comments.yml @@ -30,7 +30,7 @@ jobs: # 2) Post a sponsor-specific reply - name: Reply to !notasponsor if: contains(github.event.comment.body, '!notasponsor') - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v5 with: issue-number: ${{ github.event.issue.number }} body: | @@ -51,7 +51,7 @@ jobs: # 3) If the comment includes '!support', classify as a support request - name: Reply to !support if: contains(github.event.comment.body, '!support') - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v5 with: issue-number: ${{ github.event.issue.number }} body: | @@ -69,7 +69,7 @@ jobs: # 4) If the comment includes '!incomplete', note the bug or feature request is incomplete - name: Reply to !incomplete if: contains(github.event.comment.body, '!incomplete') - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v5 with: issue-number: ${{ github.event.issue.number }} body: | From 03e9cf4b5f5c79a3798052c8446d1cf31122b546 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:45:41 +0000 Subject: [PATCH 108/177] Bump actions/github-script from 6 to 8 Bumps [actions/github-script](https://github.com/actions/github-script) from 6 to 8. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v6...v8) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/auto_comments.yml | 2 +- .github/workflows/pr_check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto_comments.yml b/.github/workflows/auto_comments.yml index 6cd003a36ae8..ddb5fb4445b6 100644 --- a/.github/workflows/auto_comments.yml +++ b/.github/workflows/auto_comments.yml @@ -17,7 +17,7 @@ jobs: # 1) If the comment includes '!notasponsor', delete it using GitHub Script - name: Delete !notasponsor comment if: contains(github.event.comment.body, '!notasponsor') - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 08cce1de130b..15b52ecd4e82 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -25,7 +25,7 @@ jobs: github.event.pull_request.head.repo.fork == true && ((github.event.pull_request.head.ref == 'main' || github.event.pull_request.head.ref == 'master') || (github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'master')) - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 119f7a4745046fd310f94512208f4e42810ca403 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:45:46 +0000 Subject: [PATCH 109/177] Bump LanceMcCarthy/Action-AzureBlobUpload from 3.3.1 to 3.7.0 Bumps [LanceMcCarthy/Action-AzureBlobUpload](https://github.com/lancemccarthy/action-azureblobupload) from 3.3.1 to 3.7.0. - [Release notes](https://github.com/lancemccarthy/action-azureblobupload/releases) - [Commits](https://github.com/lancemccarthy/action-azureblobupload/compare/v3.3.1...v3.7.0) --- updated-dependencies: - dependency-name: LanceMcCarthy/Action-AzureBlobUpload dependency-version: 3.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/cipp_dev_build.yml | 2 +- .github/workflows/cipp_frontend_build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml index 31d0f846d016..432d9363cade 100644 --- a/.github/workflows/cipp_dev_build.yml +++ b/.github/workflows/cipp_dev_build.yml @@ -47,7 +47,7 @@ jobs: # Upload to Azure Blob Storage - name: Azure Blob Upload - uses: LanceMcCarthy/Action-AzureBlobUpload@v3.3.1 + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.7.0 with: connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} container_name: cipp diff --git a/.github/workflows/cipp_frontend_build.yml b/.github/workflows/cipp_frontend_build.yml index d6df65f2320e..5db059b438a8 100644 --- a/.github/workflows/cipp_frontend_build.yml +++ b/.github/workflows/cipp_frontend_build.yml @@ -47,7 +47,7 @@ jobs: # Upload to Azure Blob Storage - name: Azure Blob Upload - uses: LanceMcCarthy/Action-AzureBlobUpload@v3.3.1 + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.7.0 with: connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} container_name: cipp From 35400890e5c6360e90895df09efcb079f8acaf28 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:12:11 +0800 Subject: [PATCH 110/177] Send defaultDomainName in offboarding updates Include tenant defaultDomainName in offboarding update payloads and use tenantDetails.data?.id as the customerId fallback. Add an updateOffboardingDefaults.mutate call on reset to clear offboardingDefaults on the server, and disable the Reset button while an update is pending or tenant details are fetching to avoid concurrent actions. --- src/pages/tenant/manage/edit.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/manage/edit.js b/src/pages/tenant/manage/edit.js index fdc18281a1f8..b96994fe7069 100644 --- a/src/pages/tenant/manage/edit.js +++ b/src/pages/tenant/manage/edit.js @@ -136,6 +136,15 @@ const Page = () => { }; offboardingFormControl.reset({ offboardingDefaults: defaultOffboardingValues }); + + updateOffboardingDefaults.mutate({ + url: "/api/EditTenantOffboardingDefaults", + data: { + customerId: tenantDetails.data?.id || currentTenant, + defaultDomainName: tenantDetails.data?.defaultDomainName || currentTenant, + offboardingDefaults: null, + }, + }); }; const title = "Manage Tenant"; @@ -275,7 +284,8 @@ const Page = () => { onClick={offboardingFormControl.handleSubmit((values) => { const offboardingSettings = values.offboardingDefaults || values; const formattedValues = { - customerId: currentTenant, + customerId: tenantDetails.data?.id || currentTenant, + defaultDomainName: tenantDetails.data?.defaultDomainName || currentTenant, offboardingDefaults: offboardingSettings, }; updateOffboardingDefaults.mutate({ @@ -309,6 +319,7 @@ const Page = () => { + + } + /> + { + if (response?.QueueId) { + setSyncQueueId(response.QueueId); + } + }, + }} + /> + ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; From c1787b7cc735dc587f33b60793c4d9927cff4712 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 26 Feb 2026 17:44:56 -0500 Subject: [PATCH 121/177] Use report DB for ListMailboxes calls Add data: { UseReportDB: true } to /api/ListMailboxes requests so mailbox listings are fetched from the reporting database. Applied to CippMailboxRestoreDrawer and the mailbox-restore add form to ensure consistent mailbox results. --- src/components/CippComponents/CippMailboxRestoreDrawer.jsx | 1 + src/pages/email/tools/mailbox-restores/add.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/CippComponents/CippMailboxRestoreDrawer.jsx b/src/components/CippComponents/CippMailboxRestoreDrawer.jsx index 81e03bb4c659..ab8923e2c1b7 100644 --- a/src/components/CippComponents/CippMailboxRestoreDrawer.jsx +++ b/src/components/CippComponents/CippMailboxRestoreDrawer.jsx @@ -275,6 +275,7 @@ export const CippMailboxRestoreDrawer = ({ ItemCount: "ItemCount", }, url: "/api/ListMailboxes", + data: { UseReportDB: true }, showRefresh: true, }} validators={{ diff --git a/src/pages/email/tools/mailbox-restores/add.jsx b/src/pages/email/tools/mailbox-restores/add.jsx index a238c0c29b1a..0d9a74ba686a 100644 --- a/src/pages/email/tools/mailbox-restores/add.jsx +++ b/src/pages/email/tools/mailbox-restores/add.jsx @@ -126,6 +126,7 @@ const MailboxRestoreForm = () => { valueField: "UPN", addedField: { displayName: "displayName", ExchangeGuid: "ExchangeGuid" }, url: "/api/ListMailboxes", + data: { UseReportDB: true }, }} validators={{ validate: (value) => (value ? true : "Please select a target mailbox.") }} /> From 43104af427caac02cb3aece44b783882698d56c7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 26 Feb 2026 18:00:36 -0500 Subject: [PATCH 122/177] set required fields --- src/components/CippFormPages/CippAddEditUser.jsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 87e8088861a0..4f3029976390 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -76,7 +76,7 @@ const CippAddEditUser = (props) => { const tenantGroupsList = tenantGroups?.data || []; return tenantGroupsList.filter( - (tenantGroup) => !userGroups?.data?.some((userGroup) => userGroup.id === tenantGroup.id) + (tenantGroup) => !userGroups?.data?.some((userGroup) => userGroup.id === tenantGroup.id), ); } return []; @@ -139,7 +139,7 @@ const CippAddEditUser = (props) => { const generatedUsername = generateUsername( formatString, watcher.givenName, - watcher.surname + watcher.surname, ); if (generatedUsername) { formControl.setValue("username", generatedUsername); @@ -153,7 +153,7 @@ const CippAddEditUser = (props) => { useEffect(() => { if (formType === "add" && userTemplates.isSuccess && !watcher.userTemplate) { const defaultTemplate = userTemplates.data?.find( - (template) => template.defaultForTenant === true + (template) => template.defaultForTenant === true, ); if (defaultTemplate) { formControl.setValue("userTemplate", { @@ -307,6 +307,7 @@ const CippAddEditUser = (props) => { onChange={(e) => { setDisplayNameManuallySet(true); }} + required={true} /> @@ -322,6 +323,7 @@ const CippAddEditUser = (props) => { onChange={(e) => { setUsernameManuallySet(true); }} + required={true} /> From 7656bc18caa21d874f88e29fd6526852c35612cc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 26 Feb 2026 18:08:37 -0500 Subject: [PATCH 123/177] add validators --- src/components/CippFormPages/CippAddEditUser.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 4f3029976390..db64e6b2cf00 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -308,6 +308,7 @@ const CippAddEditUser = (props) => { setDisplayNameManuallySet(true); }} required={true} + validators={{ required: "Display Name is required" }} /> @@ -324,6 +325,7 @@ const CippAddEditUser = (props) => { setUsernameManuallySet(true); }} required={true} + validators={{ required: "Username is required" }} /> From 56e0df7a092ed5fc869d7a51976ce36c10d44dea Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 26 Feb 2026 18:10:32 -0500 Subject: [PATCH 124/177] add shouldDirty --- src/components/CippFormPages/CippAddEditUser.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index db64e6b2cf00..1273be90a360 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -124,7 +124,7 @@ const CippAddEditUser = (props) => { displayName += selectedTemplate.displayName; } - formControl.setValue("displayName", displayName); + formControl.setValue("displayName", displayName, { shouldDirty: true }); } // Auto-generate username if template has usernameFormat @@ -142,7 +142,7 @@ const CippAddEditUser = (props) => { watcher.surname, ); if (generatedUsername) { - formControl.setValue("username", generatedUsername); + formControl.setValue("username", generatedUsername, { shouldDirty: true }); } } } From a3d6eb6393b61d6a9c2d94eb220065930872086f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 26 Feb 2026 18:22:53 -0500 Subject: [PATCH 125/177] Preserve userTemplate on form resets Keep the currently selected userTemplate across form resets and copy operations. Updates in CippAddUserDrawer.jsx and add.jsx ensure the form retains userTemplate when copying properties. When a user is created or the add drawer is closed, the reset preserves the tenant default template (if current template has addedFields.defaultForTenant). In CippAddEditUser.jsx, add an effect to clear manual display/username flags and deselect the selected template when the add form fields are emptied, but only clear non-default templates. Also includes a minor JSX formatting adjustment for the create button label. --- .../CippComponents/CippAddUserDrawer.jsx | 34 +++++++++++++++---- .../CippFormPages/CippAddEditUser.jsx | 12 +++++++ .../identity/administration/users/add.jsx | 6 ++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/components/CippComponents/CippAddUserDrawer.jsx b/src/components/CippComponents/CippAddUserDrawer.jsx index 6e8e333b317f..970ac9ca8333 100644 --- a/src/components/CippComponents/CippAddUserDrawer.jsx +++ b/src/components/CippComponents/CippAddUserDrawer.jsx @@ -52,16 +52,30 @@ export const CippAddUserDrawer = ({ } newFields.tenantFilter = userSettingsDefaults.currentTenant; + // Preserve the currently selected template when copying properties + const currentTemplate = formControl.getValues("userTemplate"); + if (currentTemplate) { + newFields.userTemplate = currentTemplate; + } + formControl.reset(newFields); } }, [formValues]); useEffect(() => { if (createUser.isSuccess) { - formControl.reset({ + const resetValues = { tenantFilter: userSettingsDefaults.currentTenant, usageLocation: userSettingsDefaults.usageLocation, - }); + }; + + // Preserve the default template if it exists + const currentTemplate = formControl.getValues("userTemplate"); + if (currentTemplate?.addedFields?.defaultForTenant) { + resetValues.userTemplate = currentTemplate; + } + + formControl.reset(resetValues); } }, [createUser.isSuccess]); @@ -84,10 +98,18 @@ export const CippAddUserDrawer = ({ const handleCloseDrawer = () => { setDrawerVisible(false); - formControl.reset({ + const resetValues = { tenantFilter: userSettingsDefaults.currentTenant, usageLocation: userSettingsDefaults.usageLocation, - }); + }; + + // Preserve the default template if it exists + const currentTemplate = formControl.getValues("userTemplate"); + if (currentTemplate?.addedFields?.defaultForTenant) { + resetValues.userTemplate = currentTemplate; + } + + formControl.reset(resetValues); }; return ( @@ -117,8 +139,8 @@ export const CippAddUserDrawer = ({ {createUser.isPending ? "Creating User..." : createUser.isSuccess - ? "Create Another User" - : "Create User"} + ? "Create Another User" + : "Create User"} + + + + {/* Results Section */} + {getBitlockerKeys.isFetching ? ( + + + + Searching... + + + + ) : getBitlockerKeys.isSuccess ? ( + <> + + + + + {results.length === 0 ? ( + + }> + No BitLocker keys found matching your search criteria. + + + ) : ( + + + Found {results.length} BitLocker Key{results.length !== 1 ? "s" : ""} + + + {results.map((result, index) => ( + + + {/* BitLocker Key Information */} + + + + BitLocker Key Information + + + + + + Key ID + + + + {result.keyId || "N/A"} + + {result.keyId && } + + + + + + Volume Type + + + + + + + Created + + + {result.createdDateTime + ? new Date(result.createdDateTime).toLocaleString() + : "N/A"} + + + + + + Tenant + + {result.tenant || "N/A"} + + + {/* Device Information */} + {result.deviceFound && ( + <> + + + + + Device Information + + + + + + Device Name + + {result.deviceName || "N/A"} + + + + + Device ID + + + + {result.deviceId || "N/A"} + + {result.deviceId && } + + + + + + Operating System + + + {result.operatingSystem || "N/A"} + {result.osVersion && ` (${result.osVersion})`} + + + + + + Account Status + + + ) : ( + + ) + } + label={result.accountEnabled ? "Enabled" : "Disabled"} + size="small" + color={result.accountEnabled ? "success" : "default"} + /> + + + + + Trust Type + + {result.trustType || "N/A"} + + + + + Last Sign In + + + {result.lastSignIn + ? new Date(result.lastSignIn).toLocaleString() + : "N/A"} + + + + )} + + {!result.deviceFound && ( + + }> + Device information not found in cache. The device may have been deleted + or not yet synced. + + + )} + + + ))} + + )} + + ) : getBitlockerKeys.isError ? ( + + + + Error searching for BitLocker keys: {getBitlockerKeys.error?.message} + + + ) : null} + + + ); +}; + +export default CippBitlockerKeySearch; diff --git a/src/layouts/config.js b/src/layouts/config.js index 3bfc34eeb508..14020757c07f 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -801,7 +801,6 @@ export const nativeMenuItems = [ path: "/tenant/tools/tenantlookup", permissions: ["Tenant.Administration.*"], }, - { title: "IP Database", path: "/tenant/tools/geoiplookup", @@ -813,6 +812,11 @@ export const nativeMenuItems = [ path: "/tenant/tools/individual-domains", permissions: ["Tenant.DomainAnalyser.*"], }, + { + title: "BitLocker Key Search", + path: "/tenant/tools/bitlocker-search", + permissions: ["Endpoint.Device.Read"], + }, ], }, { diff --git a/src/pages/tenant/tools/bitlocker-search/index.js b/src/pages/tenant/tools/bitlocker-search/index.js new file mode 100644 index 000000000000..d445b0b0632e --- /dev/null +++ b/src/pages/tenant/tools/bitlocker-search/index.js @@ -0,0 +1,21 @@ +import { Box, Container } from "@mui/material"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import CippBitlockerKeySearch from "../../../../components/CippComponents/CippBitlockerKeySearch"; + +const Page = () => { + return ( + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 64471a1bd4f39d350b13c04db768a40d4421ecc4 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:23:15 +0100 Subject: [PATCH 138/177] fix: prevent HTML escaping of URLs in action links getNestedValue was HTML-encoding values used in window.open(), converting & to & in URLs. This consent URLs with query parameters, causing Microsoft OAuth to reject them with AADSTS900144. --- src/components/CippComponents/CippApiDialog.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index be3ce1cb2166..4ba2060a9410 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -269,10 +269,14 @@ export const CippApiDialog = (props) => { return div.innerHTML; }; - const getNestedValue = (obj, path) => { - const value = path + const getRawNestedValue = (obj, path) => { + return path .split(".") .reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj); + }; + + const getNestedValue = (obj, path) => { + const value = getRawNestedValue(obj, path); return typeof value === "string" ? escapeHtml(value) : value; }; @@ -288,7 +292,7 @@ export const CippApiDialog = (props) => { linkOpenedRef.current = true; const linkWithData = api.link.replace( /\[([^\]]+)\]/g, - (_, key) => getNestedValue(row, key) || `[${key}]`, + (_, key) => getRawNestedValue(row, key) || `[${key}]`, ); if (linkWithData.startsWith("/") && !api?.external) { router.push(linkWithData, undefined, { shallow: true }); From b68dc3b9d6b84b38f610e4292c52eb74265098f5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:23:04 +0800 Subject: [PATCH 139/177] 12 Hour Update --- src/components/CippStandards/CippStandardsSideBar.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index 16307ece2ff0..ab4c4513ab2d 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -519,7 +519,7 @@ const CippStandardsSideBar = ({ ? "This template will automatically every 12 hours to detect drift. Are you sure you want to apply this Drift Template?" : watchForm.runManually ? "Are you sure you want to apply this standard? This template has been set to never run on a schedule. After saving the template you will have to run it manually." - : "Are you sure you want to apply this standard? This will apply the template and run every 4 hours.", + : "Are you sure you want to apply this standard? This will apply the template and run every 12 hours.", url: "/api/AddStandardsTemplate", type: "POST", replacementBehaviour: "removeNulls", From 25d3417407129acc597e3a0b3dbb50b4852fc894 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 28 Feb 2026 14:34:13 -0500 Subject: [PATCH 140/177] Add BitLocker recovery key retrieval UI Add client-side support to retrieve and display BitLocker recovery keys per search result. Introduces ApiPostCall usage (ExecGetRecoveryKey), recoveryKeys and loadingKeys state, and handleRetrieveKey to fetch and store keys. UI changes add a "Retrieve Key" button with a CircularProgress indicator and Key icon, display retrieved key in monospace with a copy-to-clipboard action, and disable the button while loading or when identifiers are missing. Also import updates for icons and API call helpers; errors are logged to console on failure. Removed inline copy buttons for keyId/deviceId in the results. --- .../CippComponents/CippBitlockerKeySearch.jsx | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/components/CippComponents/CippBitlockerKeySearch.jsx b/src/components/CippComponents/CippBitlockerKeySearch.jsx index c0af1f7723d6..77ff210146de 100644 --- a/src/components/CippComponents/CippBitlockerKeySearch.jsx +++ b/src/components/CippComponents/CippBitlockerKeySearch.jsx @@ -10,13 +10,15 @@ import { Chip, Alert, ButtonGroup, + CircularProgress, } from "@mui/material"; -import { Search, VpnKey, Computer, CheckCircle, Cancel, Info } from "@mui/icons-material"; +import { Search, VpnKey, Computer, CheckCircle, Cancel, Info, Key } from "@mui/icons-material"; import { useForm, useWatch } from "react-hook-form"; import CippButtonCard from "../CippCards/CippButtonCard"; -import { ApiGetCall } from "../../api/ApiCall"; +import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; import CippFormComponent from "./CippFormComponent"; +import { useSettings } from "../../hooks/use-settings"; const getVolumeTypeLabel = (volumeType) => { const types = { @@ -38,6 +40,36 @@ export const CippBitlockerKeySearch = () => { const searchTerm = useWatch({ control: formControl.control, name: "searchTerm" }); const searchType = useWatch({ control: formControl.control, name: "searchType" }) || "keyId"; + // State to store retrieved recovery keys by keyId + const [recoveryKeys, setRecoveryKeys] = useState({}); + const [loadingKeys, setLoadingKeys] = useState({}); + + const retrieveKeyMutation = ApiPostCall({}); + + const handleRetrieveKey = async (keyId, deviceId, tenant) => { + setLoadingKeys((prev) => ({ ...prev, [keyId]: true })); + + try { + const response = await retrieveKeyMutation.mutateAsync({ + url: "/api/ExecGetRecoveryKey", + data: { + GUID: deviceId, + RecoveryKeyType: "BitLocker", + tenantFilter: tenant, + }, + }); + + // Extract the key from the response + if (response?.data?.Results?.copyField) { + setRecoveryKeys((prev) => ({ ...prev, [keyId]: response.data.Results.copyField })); + } + } catch (error) { + console.error("Failed to retrieve key:", error); + } finally { + setLoadingKeys((prev) => ({ ...prev, [keyId]: false })); + } + }; + const getBitlockerKeys = ApiGetCall({ url: "/api/ExecBitlockerSearch", data: { [searchType]: searchTerm }, @@ -175,7 +207,6 @@ export const CippBitlockerKeySearch = () => { > {result.keyId || "N/A"} - {result.keyId && } @@ -208,6 +239,41 @@ export const CippBitlockerKeySearch = () => { {result.tenant || "N/A"} + + + Recovery Key + + + {recoveryKeys[result.keyId] ? ( + <> + + {recoveryKeys[result.keyId]} + + + + ) : ( + + )} + + + {/* Device Information */} {result.deviceFound && ( <> @@ -237,7 +303,6 @@ export const CippBitlockerKeySearch = () => { > {result.deviceId || "N/A"} - {result.deviceId && } From c9efb18de0b16a7f2d984013be067ecc3a3c8fdf Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:04:22 +0800 Subject: [PATCH 141/177] Dead page, replaced with draw on index page --- .../administration/tenants/groups/add.js | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 src/pages/tenant/administration/tenants/groups/add.js diff --git a/src/pages/tenant/administration/tenants/groups/add.js b/src/pages/tenant/administration/tenants/groups/add.js deleted file mode 100644 index 229313fb75ad..000000000000 --- a/src/pages/tenant/administration/tenants/groups/add.js +++ /dev/null @@ -1,53 +0,0 @@ -import { Layout as DashboardLayout } from "../../../../../layouts/index.js"; -import { useForm } from "react-hook-form"; -import { ApiPostCall } from "../../../../../api/ApiCall"; -import { Box } from "@mui/material"; -import { Grid } from "@mui/system"; -import CippPageCard from "../../../../../components/CippCards/CippPageCard"; -import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; -import CippAddEditTenantGroups from "../../../../../components/CippComponents/CippAddEditTenantGroups"; - -const Page = () => { - const formControl = useForm({ - mode: "onChange", - }); - - const addGroupApi = ApiPostCall({ - urlFromData: true, - relatedQueryKeys: ["TenantGroupListPage"], - }); - - const handleAddGroup = (data) => { - addGroupApi.mutate({ - url: "/api/EditTenantGroup", - data: { - Action: "AddEdit", - groupName: data.groupName, - groupDescription: data.groupDescription, - }, - }); - }; - - return ( - - - - - - - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; From 023e5fddb796187a9a14abd313a1e0cf9b2f2ff1 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:46:32 +0800 Subject: [PATCH 142/177] What even is this, old dead page - add domain to cipp --- src/pages/domains.js | 149 ------------------------------------------- 1 file changed, 149 deletions(-) delete mode 100644 src/pages/domains.js diff --git a/src/pages/domains.js b/src/pages/domains.js deleted file mode 100644 index c70d96f5364b..000000000000 --- a/src/pages/domains.js +++ /dev/null @@ -1,149 +0,0 @@ -import Head from "next/head"; -import { useRef } from "react"; -import { - Alert, - Box, - Button, - Card, - CircularProgress, - Container, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - Stack, - TextField, - Typography, -} from "@mui/material"; -import { useDialog } from "../hooks/use-dialog"; -import { Layout as DashboardLayout } from "../layouts/index.js"; -import { CippDataTable } from "../components/CippTable/CippDataTable"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { ApiPostCall } from "../api/ApiCall"; -import { useFormik } from "formik"; - -const Page = () => { - const ref = useRef(); - const createDialog = useDialog(); - const domainPostRequest = ApiPostCall({ - urlFromData: true, - relatedQueryKeys: "users", - }); - - const formik = useFormik({ - initialValues: { - domainName: "", - }, - onSubmit: async (values, helpers) => { - try { - domainPostRequest.mutate({ url: "/api/AddCustomDomain", ...values }); - helpers.resetForm(); - helpers.setStatus({ success: true }); - helpers.setSubmitting(false); - } catch (err) { - helpers.setStatus({ success: false }); - helpers.setErrors({ submit: err.message }); - helpers.setSubmitting(false); - } - }, - }); - - return ( - <> - - Devices - - - - - - - - - Add Domain} - actions={[ - { - label: "Delete domain", - type: "GET", - url: "api/DeleteCustomDomain", - data: { domain: "Domain" }, - icon: , - }, - ]} - simple={false} - api={{ url: "api/ListCustomDomains" }} - columns={[ - { - header: "Domain", - accessorKey: "Domain", - }, - { - header: "Status", - accessorKey: "Status", - }, - ]} - /> - - - - - - - Add Domain - - - To add a domain to your instance, set your preferred CNAME to your CIPP default - domain, then add the domain here. - - - - - - - {domainPostRequest.isPending && ( - - Adding domain... - - )} - {domainPostRequest.isError && ( - - Error adding domain: {domainPostRequest.error.response.data} - - )} - - - - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; From 2a7e6789763e7e16087864ee51417a6529442714 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:07:00 +0800 Subject: [PATCH 143/177] Another dead page, looks to be replaced with a draw --- src/pages/tenant/manage/recover-policies.js | 200 -------------------- 1 file changed, 200 deletions(-) delete mode 100644 src/pages/tenant/manage/recover-policies.js diff --git a/src/pages/tenant/manage/recover-policies.js b/src/pages/tenant/manage/recover-policies.js deleted file mode 100644 index 16a68a469186..000000000000 --- a/src/pages/tenant/manage/recover-policies.js +++ /dev/null @@ -1,200 +0,0 @@ -import { Layout as DashboardLayout } from "../../../layouts/index.js"; -import { useRouter } from "next/router"; -import { Policy, Restore, ExpandMore } from "@mui/icons-material"; -import { - Box, - Stack, - Typography, - Accordion, - AccordionSummary, - AccordionDetails, - Chip, - Button, -} from "@mui/material"; -import { Grid } from "@mui/system"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { HeaderedTabbedLayout } from "../../../layouts/HeaderedTabbedLayout"; -import tabOptions from "./tabOptions.json"; -import { CippDataTable } from "../../../components/CippTable/CippDataTable"; -import { CippHead } from "../../../components/CippComponents/CippHead"; -import { CippFormComponent } from "../../../components/CippComponents/CippFormComponent"; -import { ApiPostCall } from "../../../api/ApiCall"; -import { CippApiResults } from "../../../components/CippComponents/CippApiResults"; -import { createDriftManagementActions } from "./driftManagementActions"; -import { useSettings } from "../../../hooks/use-settings"; - -const RecoverPoliciesPage = () => { - const router = useRouter(); - const { templateId } = router.query; - const [selectedPolicies, setSelectedPolicies] = useState([]); - const userSettings = useSettings(); - // Prioritize URL query parameter, then fall back to settings - const currentTenant = router.query.tenantFilter || userSettings.currentTenant; - - const formControl = useForm({ mode: "onChange" }); - - const selectedBackup = formControl.watch("backupDateTime"); - - // Mock data for policies in selected backup - replace with actual API call - const backupPolicies = [ - { - id: 1, - name: "Multi-Factor Authentication Policy", - type: "Conditional Access", - lastModified: "2024-01-15", - settings: "Require MFA for all users", - }, - { - id: 2, - name: "Password Policy Standard", - type: "Security Standard", - lastModified: "2024-01-10", - settings: "14 character minimum, complexity required", - }, - { - id: 3, - name: "Device Compliance Policy", - type: "Intune Policy", - lastModified: "2024-01-08", - settings: "Require encryption, PIN/Password", - }, - ]; - - // Recovery API call - const recoverApi = ApiPostCall({ - relatedQueryKeys: ["ListBackupPolicies", "ListPolicyBackups"], - }); - - const handleRecover = () => { - if (selectedPolicies.length === 0 || !selectedBackup) { - return; - } - - recoverApi.mutate({ - url: "/api/RecoverPolicies", - data: { - templateId, - backupDateTime: selectedBackup, - policyIds: selectedPolicies.map((policy) => policy.id), - }, - }); - }; - - // Actions for the ActionsMenu - const actions = createDriftManagementActions({ - templateId, - onRefresh: () => { - // Refresh any relevant data here - }, - currentTenant, - }); - - const title = "Manage Drift"; - const subtitle = [ - { - icon: , - text: `Template ID: ${templateId || "Loading..."}`, - }, - ]; - - return ( - - - - - {/* Backup Date Selection */} - - }> - - - Select Backup Date & Time - - - - - - { - const date = new Date(option.dateTime); - return `${date.toLocaleDateString()} @ ${date.toLocaleTimeString()} (${ - option.policyCount - } policies)`; - }, - valueField: "dateTime", - }} - required={true} - validators={{ - validate: (value) => !!value || "Please select a backup date & time", - }} - /> - - - - - - {/* Recovery Results */} - - - {/* Backup Policies Section */} - {selectedBackup && ( - - }> - - - Policies in Selected Backup - - - - - - - - Select policies to recover from backup:{" "} - {new Date(selectedBackup).toLocaleString()} - - - - setSelectedPolicies(selectedRows)} - /> - - - - )} - - - - ); -}; - -RecoverPoliciesPage.getLayout = (page) => {page}; - -export default RecoverPoliciesPage; From 121ba7831ff4693d0aa2bc222d70f9138041ed21 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 2 Mar 2026 13:14:35 -0500 Subject: [PATCH 144/177] fix queue tracker --- src/pages/email/administration/mailboxes/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index 19913b56cf8b..2be07f7bcde8 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -127,8 +127,8 @@ const Page = () => { Name: "Mailboxes", }, onSuccess: (response) => { - if (response?.QueueId) { - setSyncQueueId(response.QueueId); + if (response?.Metadata?.QueueId) { + setSyncQueueId(response.Metadata.QueueId); } }, }} From 811b248d7302446bf948c69ced1fb54be8c939ea Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 2 Mar 2026 13:19:37 -0500 Subject: [PATCH 145/177] Add Types property to Mailboxes data Insert a Types: "None" field into the Mailboxes to not request additional data --- src/pages/email/administration/mailboxes/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index 2be07f7bcde8..3a7af2fa1223 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -125,6 +125,7 @@ const Page = () => { relatedQueryKeys: [`ListMailboxes-${currentTenant}`], data: { Name: "Mailboxes", + Types: "None", }, onSuccess: (response) => { if (response?.Metadata?.QueueId) { From 195a81430bffec8b9ad03c3942c54231d9a8540c Mon Sep 17 00:00:00 2001 From: "Brad M.K." Date: Mon, 2 Mar 2026 20:44:37 +0000 Subject: [PATCH 146/177] Add CloudFlare Tunnel option for PWPush extension Introduce a switch field (PWPush.CFEnabled) to enable connecting to PWPush through CloudFlare Tunnel using Service Account credentials. The field is conditionally displayed when CFZTNA.Enabled is true. --- src/data/Extensions.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 21471f5e295c..30da7d2638f7 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -779,6 +779,16 @@ "compareValue": true, "action": "disable" } + }, + { + "type": "switch", + "name": "PWPush.CFEnabled", + "label": "Connect to PWPush through CloudFlare Tunnel with the Service Account credentials.", + "condition": { + "field": "CFZTNA.Enabled", + "compareType": "is", + "compareValue": true + } } ], "mappingRequired": false From d15b4df9d1e453c4645f73f4c280ac6164144ebd Mon Sep 17 00:00:00 2001 From: Woody <2997336+MWG-Logan@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:13:53 -0500 Subject: [PATCH 147/177] fix(reusable-settings): normalize RawJSON casing in templates --- .../CippReusableSettingsDeployDrawer.jsx | 5 ++++- .../endpoint/MEM/reusable-settings-templates/edit.jsx | 2 +- src/pages/endpoint/MEM/reusable-settings/edit.jsx | 10 +++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippReusableSettingsDeployDrawer.jsx b/src/components/CippComponents/CippReusableSettingsDeployDrawer.jsx index 06365e32d50c..007de36b32f1 100644 --- a/src/components/CippComponents/CippReusableSettingsDeployDrawer.jsx +++ b/src/components/CippComponents/CippReusableSettingsDeployDrawer.jsx @@ -27,11 +27,14 @@ export const CippReusableSettingsDeployDrawer = ({ const templates = ApiGetCall({ url: "/api/ListIntuneReusableSettingTemplates", queryKey: "ListIntuneReusableSettingTemplates" }); + const getRawJson = (source) => source?.RawJSON ?? source?.RAWJson ?? source?.rawJSON ?? ""; + useEffect(() => { if (templates.isSuccess && selectedTemplate?.value) { const match = templates.data?.find((t) => t.GUID === selectedTemplate.value); if (match) { - formControl.setValue("rawJSON", match.RawJSON || ""); + const rawJsonValue = getRawJson(match); + formControl.setValue("rawJSON", rawJsonValue); formControl.setValue("TemplateId", match.GUID); } } diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx index 046a14a70d20..82baa233ba27 100644 --- a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx @@ -117,7 +117,7 @@ const EditReusableSettingsTemplate = () => { return { ...templateData, // Normalize all known casing variants to the canonical RawJSON property - RawJSON: templateData.RawJSON ?? templateData.RAWJson ?? templateData.RAWJSON, + RawJSON: templateData.RawJSON ?? templateData.RAWJson ?? templateData.rawJSON, }; }, [templateData]); diff --git a/src/pages/endpoint/MEM/reusable-settings/edit.jsx b/src/pages/endpoint/MEM/reusable-settings/edit.jsx index 3fe089ac520d..ac1285e02f42 100644 --- a/src/pages/endpoint/MEM/reusable-settings/edit.jsx +++ b/src/pages/endpoint/MEM/reusable-settings/edit.jsx @@ -36,22 +36,26 @@ const EditReusableSetting = () => { const record = Array.isArray(settingQuery.data) ? settingQuery.data[0] : settingQuery.data; + const getRawJson = (source) => source?.RawJSON ?? ""; + useEffect(() => { if (record) { + const rawJsonValue = getRawJson(record); reset({ tenantFilter: effectiveTenant, ID: record.id, displayName: record.displayName, description: record.description, - rawJSON: record.RawJSON, + rawJSON: rawJsonValue, }); } }, [record, effectiveTenant, reset]); const safeJson = () => { - if (!record?.RawJSON) return null; + const rawJsonValue = getRawJson(record); + if (!rawJsonValue) return null; try { - return JSON.parse(record.RawJSON); + return JSON.parse(rawJsonValue); } catch (e) { console.error("Failed to parse RawJSON for reusable setting preview", { error: e, From 047e9c0a1e7d1e952aa936d4f5557883bae852cb Mon Sep 17 00:00:00 2001 From: "Brad M.K." Date: Tue, 3 Mar 2026 00:49:03 +0000 Subject: [PATCH 148/177] Fix PWPush email field condition logic Change condition from "is false" to "isNot true" for PWPush.Email field visibility to ensure proper conditional rendering when bearer authentication is disabled. --- src/data/Extensions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 30da7d2638f7..6d92649f60dc 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -710,8 +710,8 @@ "placeholder": "Enter your email address for PWPush. (Email & API Key auth)", "condition": { "field": "PWPush.UseBearerAuth", - "compareType": "is", - "compareValue": false + "compareType": "isNot", + "compareValue": true } }, { From be8d5d40dde9a0ba77fb719d5c2ec3111653aed3 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:12:00 +0800 Subject: [PATCH 149/177] Bitlocker search improvements :) --- .../CippCards/CippUniversalSearchV2.jsx | 160 +++++- .../CippComponents/CippBitlockerKeySearch.jsx | 490 +++++++----------- .../tenant/tools/bitlocker-search/index.js | 21 - 3 files changed, 341 insertions(+), 330 deletions(-) delete mode 100644 src/pages/tenant/tools/bitlocker-search/index.js diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index bc6fe4055dbb..0f0d6e88ad8b 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -10,27 +10,32 @@ import { CircularProgress, InputAdornment, Portal, + Button, } from "@mui/material"; import { Search as SearchIcon } from "@mui/icons-material"; import { ApiGetCall } from "../../api/ApiCall"; -import { useSettings } from "../../hooks/use-settings"; import { useRouter } from "next/router"; import { BulkActionsMenu } from "../bulk-actions-menu"; -import { Button } from "@mui/material"; +import { CippOffCanvas } from "../CippComponents/CippOffCanvas"; +import { CippBitlockerKeySearch } from "../CippComponents/CippBitlockerKeySearch"; export const CippUniversalSearchV2 = React.forwardRef( ({ onConfirm = () => {}, onChange = () => {}, maxResults = 10, value = "" }, ref) => { const [searchValue, setSearchValue] = useState(value); const [searchType, setSearchType] = useState("Users"); + const [bitlockerLookupType, setBitlockerLookupType] = useState("keyId"); const [showDropdown, setShowDropdown] = useState(false); + const [bitlockerDrawerVisible, setBitlockerDrawerVisible] = useState(false); + const [bitlockerDrawerDefaults, setBitlockerDrawerDefaults] = useState({ + searchTerm: "", + searchType: "keyId", + }); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); const containerRef = useRef(null); const textFieldRef = useRef(null); const router = useRouter(); - const settings = useSettings(); - const { currentTenant } = settings; - const search = ApiGetCall({ + const universalSearch = ApiGetCall({ url: `/api/ExecUniversalSearchV2`, data: { searchTerms: searchValue, @@ -41,6 +46,17 @@ export const CippUniversalSearchV2 = React.forwardRef( waiting: false, }); + const bitlockerSearch = ApiGetCall({ + url: "/api/ExecBitlockerSearch", + data: { + [bitlockerLookupType]: searchValue, + }, + queryKey: `bitlocker-universal-${bitlockerLookupType}-${searchValue}`, + waiting: false, + }); + + const activeSearch = searchType === "BitLocker" ? bitlockerSearch : universalSearch; + const handleChange = (event) => { const newValue = event.target.value; setSearchValue(newValue); @@ -71,7 +87,7 @@ export const CippUniversalSearchV2 = React.forwardRef( const handleSearch = () => { if (searchValue.length > 0) { updateDropdownPosition(); - search.refetch(); + activeSearch.refetch(); setShowDropdown(true); } }; @@ -93,6 +109,21 @@ export const CippUniversalSearchV2 = React.forwardRef( const handleTypeChange = (type) => { setSearchType(type); + if (type === "BitLocker") { + setBitlockerLookupType("keyId"); + } + setShowDropdown(false); + }; + + const handleBitlockerResultClick = (match) => { + setBitlockerDrawerDefaults({ + searchTerm: + bitlockerLookupType === "deviceId" + ? match?.deviceId || searchValue + : match?.keyId || searchValue, + searchType: bitlockerLookupType, + }); + setBitlockerDrawerVisible(true); setShowDropdown(false); }; @@ -107,6 +138,24 @@ export const CippUniversalSearchV2 = React.forwardRef( icon: "Group", onClick: () => handleTypeChange("Groups"), }, + { + label: "BitLocker", + icon: "FilePresent", + onClick: () => handleTypeChange("BitLocker"), + }, + ]; + + const bitlockerLookupActions = [ + { + label: "Key ID", + icon: "FilePresent", + onClick: () => setBitlockerLookupType("keyId"), + }, + { + label: "Device ID", + icon: "Laptop", + onClick: () => setBitlockerLookupType("deviceId"), + }, ]; // Close dropdown when clicking outside @@ -144,7 +193,12 @@ export const CippUniversalSearchV2 = React.forwardRef( } }, [showDropdown]); - const hasResults = Array.isArray(search?.data) && search.data.length > 0; + const bitlockerResults = Array.isArray(bitlockerSearch?.data?.Results) + ? bitlockerSearch.data.Results + : []; + const universalResults = Array.isArray(universalSearch?.data) ? universalSearch.data : []; + const hasResults = + searchType === "BitLocker" ? bitlockerResults.length > 0 : universalResults.length > 0; const shouldShowDropdown = showDropdown && searchValue.length > 0; const getLabel = () => { @@ -152,6 +206,10 @@ export const CippUniversalSearchV2 = React.forwardRef( return "Search users by UPN or Display Name"; } else if (searchType === "Groups") { return "Search groups by Display Name"; + } else if (searchType === "BitLocker") { + return bitlockerLookupType === "deviceId" + ? "Search BitLocker by Device ID" + : "Search BitLocker by Recovery Key ID"; } return "Search"; }; @@ -163,6 +221,12 @@ export const CippUniversalSearchV2 = React.forwardRef( buttonName={searchType} actions={typeMenuActions} /> + {searchType === "BitLocker" && ( + + )} { textFieldRef.current = node; @@ -187,7 +251,7 @@ export const CippUniversalSearchV2 = React.forwardRef( ), - endAdornment: search.isFetching ? ( + endAdornment: activeSearch.isFetching ? ( @@ -203,7 +267,7 @@ export const CippUniversalSearchV2 = React.forwardRef( -
    + + )} - {/* Results Section */} - {getBitlockerKeys.isFetching ? ( + {isSuccess && ( + <> - - - Searching... - - - - ) : getBitlockerKeys.isSuccess ? ( - <> - - - - - {results.length === 0 ? ( - - }> - No BitLocker keys found matching your search criteria. - - - ) : ( - - - Found {results.length} BitLocker Key{results.length !== 1 ? "s" : ""} - - - {results.map((result, index) => ( - - - {/* BitLocker Key Information */} - - - - BitLocker Key Information - - + {results.map((result, index) => ( + + + {/* BitLocker Key Information */} + + + + BitLocker Key Information + + - - - Key ID + + + Key ID + + + + {result.keyId || "N/A"} - - - {result.keyId || "N/A"} - - - + + - - - Volume Type - - - + + + Volume Type + + + - - - Created - - - {result.createdDateTime - ? new Date(result.createdDateTime).toLocaleString() - : "N/A"} - - + + + Created + + + {result.createdDateTime + ? new Date(result.createdDateTime).toLocaleString() + : "N/A"} + + - - - Tenant - - {result.tenant || "N/A"} - + + + Tenant + + {result.tenant || "N/A"} + - - - Recovery Key - - - {recoveryKeys[result.keyId] ? ( - <> - - {recoveryKeys[result.keyId]} - - - - ) : ( - - )} - - - - {/* Device Information */} - {result.deviceFound && ( - <> - - - - - Device Information + {recoveryKeys[result.keyId]} - + + + ) : ( + + )} + + - - - Device Name - - {result.deviceName || "N/A"} - + {/* Device Information */} + {result.deviceFound && ( + <> + + + + + Device Information + + - - - Device ID - - - - {result.deviceId || "N/A"} - - - + + + Device Name + + {result.deviceName || "N/A"} + - - - Operating System - - - {result.operatingSystem || "N/A"} - {result.osVersion && ` (${result.osVersion})`} + + + Device ID + + + + {result.deviceId || "N/A"} - + + - - - Account Status - - - ) : ( - - ) - } - label={result.accountEnabled ? "Enabled" : "Disabled"} - size="small" - color={result.accountEnabled ? "success" : "default"} - /> - + + + Operating System + + + {result.operatingSystem || "N/A"} + {result.osVersion && ` (${result.osVersion})`} + + - - - Trust Type - - {result.trustType || "N/A"} - + + + Account Status + + + ) : ( + + ) + } + label={result.accountEnabled ? "Enabled" : "Disabled"} + size="small" + color={result.accountEnabled ? "success" : "default"} + /> + - - - Last Sign In - - - {result.lastSignIn - ? new Date(result.lastSignIn).toLocaleString() - : "N/A"} - - - - )} + + + Trust Type + + {result.trustType || "N/A"} + - {!result.deviceFound && ( - - }> - Device information not found in cache. The device may have been deleted - or not yet synced. - + + + Last Sign In + + + {result.lastSignIn ? new Date(result.lastSignIn).toLocaleString() : "N/A"} + - )} - - - ))} - - )} - - ) : getBitlockerKeys.isError ? ( - - - - Error searching for BitLocker keys: {getBitlockerKeys.error?.message} - + + )} + + {!result.deviceFound && ( + + }> + Device information not found in cache. The device may have been deleted or + not yet synced. + + + )} + + + ))} - ) : null} - - + + )} + ); + return content; }; export default CippBitlockerKeySearch; diff --git a/src/pages/tenant/tools/bitlocker-search/index.js b/src/pages/tenant/tools/bitlocker-search/index.js deleted file mode 100644 index d445b0b0632e..000000000000 --- a/src/pages/tenant/tools/bitlocker-search/index.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Box, Container } from "@mui/material"; -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import CippBitlockerKeySearch from "../../../../components/CippComponents/CippBitlockerKeySearch"; - -const Page = () => { - return ( - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; From d1626766ae65593648f6cc6616fd32a872666263 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:08:10 -0600 Subject: [PATCH 150/177] Update authentication link in CippDirectTenantDeploy --- src/components/CippWizard/CippDirectTenantDeploy.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippWizard/CippDirectTenantDeploy.jsx b/src/components/CippWizard/CippDirectTenantDeploy.jsx index 5c34e8e76806..b4a24d77f3fa 100644 --- a/src/components/CippWizard/CippDirectTenantDeploy.jsx +++ b/src/components/CippWizard/CippDirectTenantDeploy.jsx @@ -35,7 +35,7 @@ export const CippDirectTenantDeploy = (props) => { You can authenticate to multiple tenants by repeating this step for each tenant you want to add. More information about per-tenant authentication can be found in the{" "} From e4e484bdfbd1e458c300d1d3143c73d1bcf5e390 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:09:09 -0600 Subject: [PATCH 151/177] Update GDAP documentation link in CippGDAPTenantSetup --- src/components/CippWizard/CippGDAPTenantSetup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippWizard/CippGDAPTenantSetup.jsx b/src/components/CippWizard/CippGDAPTenantSetup.jsx index d64b2bd4f702..d326bff26729 100644 --- a/src/components/CippWizard/CippGDAPTenantSetup.jsx +++ b/src/components/CippWizard/CippGDAPTenantSetup.jsx @@ -108,7 +108,7 @@ export const CippGDAPTenantSetup = (props) => { This process will help you set up a new GDAP relationship with a customer tenant. You'll generate an invite that the customer needs to accept before completing onboarding. For more information about GDAP setup, visit the{" "} - + GDAP documentation . From c702e0efb51fb4e4fae584ef7942e399d3648878 Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:09:36 -0600 Subject: [PATCH 152/177] Update service account documentation link --- src/components/CippWizard/CippTenantModeDeploy.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index aae77e6ab265..c14bb0aa573b 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -77,7 +77,7 @@ export const CippTenantModeDeploy = (props) => { Please remember to log onto a service account dedicated for CIPP. More info? Check out the{" "} From 8368043e3c86116f1ef3541a5b41b7adbf0b766d Mon Sep 17 00:00:00 2001 From: Brian Simpson <50429915+bmsimp@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:10:48 -0600 Subject: [PATCH 153/177] Update link to recommended roles documentation --- .../tenant/gdap-management/relationships/relationship/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/gdap-management/relationships/relationship/index.js b/src/pages/tenant/gdap-management/relationships/relationship/index.js index 9c9bfee83400..17231abdb42f 100644 --- a/src/pages/tenant/gdap-management/relationships/relationship/index.js +++ b/src/pages/tenant/gdap-management/relationships/relationship/index.js @@ -177,7 +177,7 @@ const Page = () => { This relationship does not have all the CIPP recommended roles. See the{" "} From e4417b2fe69ffe6740307fc3adcc47ab737e02bb Mon Sep 17 00:00:00 2001 From: "Brad M.K." Date: Tue, 3 Mar 2026 22:29:44 +0000 Subject: [PATCH 154/177] Add default passphrase field to PWPush extension Insert a password field (PWPush.DefaultPassphrase) to allow setting a default passphrase required to view pushed passwords. The field is conditionally disabled when PWPush.Enabled is false. --- src/data/Extensions.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 6d92649f60dc..00977617300d 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -780,6 +780,18 @@ "action": "disable" } }, + { + "type": "password", + "name": "PWPush.DefaultPassphrase", + "label": "Default Passphrase", + "placeholder": "Enter a default passphrase required to view pushed passwords. (optional)", + "condition": { + "field": "PWPush.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, { "type": "switch", "name": "PWPush.CFEnabled", From 9d7e9d5d8bd22af67cec5c317ba8b69f87819108 Mon Sep 17 00:00:00 2001 From: "Brad M.K." Date: Tue, 3 Mar 2026 23:11:21 +0000 Subject: [PATCH 155/177] Update PWPush retrieval step label to clarify passphrase recommendation Modify the RetrievalStep field label to indicate that clicking to retrieve password is recommended specifically when a passphrase is not set. --- src/data/Extensions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 00977617300d..c5d752f9bdbf 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -761,7 +761,7 @@ { "type": "switch", "name": "PWPush.RetrievalStep", - "label": "Click to retrieve password (recommended)", + "label": "Click to retrieve password (recommended if passphrase is not set)", "condition": { "field": "PWPush.Enabled", "compareType": "is", From 70486bfcc3df84755d928aa71f99e04f0f553d62 Mon Sep 17 00:00:00 2001 From: "Brad M.K." Date: Tue, 3 Mar 2026 23:17:19 +0000 Subject: [PATCH 156/177] Reorder PWPush extension fields and update CF-ZTNA label Move DefaultPassphrase field before RetrievalStep and DeletableByViewer fields in PWPush extension configuration. Simplify CFEnabled label from "Connect to PWPush through CloudFlare Tunnel with the Service Account credentials" to "Behind a CF-ZTNA Tunnel". --- src/data/Extensions.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index c5d752f9bdbf..52df55dd4726 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -759,9 +759,10 @@ } }, { - "type": "switch", - "name": "PWPush.RetrievalStep", - "label": "Click to retrieve password (recommended if passphrase is not set)", + "type": "password", + "name": "PWPush.DefaultPassphrase", + "label": "Default Passphrase", + "placeholder": "Enter a default passphrase required to view pushed passwords. (optional)", "condition": { "field": "PWPush.Enabled", "compareType": "is", @@ -771,8 +772,8 @@ }, { "type": "switch", - "name": "PWPush.DeletableByViewer", - "label": "Allow deletion of passwords", + "name": "PWPush.RetrievalStep", + "label": "Click to retrieve password (recommended if passphrase is not set)", "condition": { "field": "PWPush.Enabled", "compareType": "is", @@ -781,10 +782,9 @@ } }, { - "type": "password", - "name": "PWPush.DefaultPassphrase", - "label": "Default Passphrase", - "placeholder": "Enter a default passphrase required to view pushed passwords. (optional)", + "type": "switch", + "name": "PWPush.DeletableByViewer", + "label": "Allow deletion of passwords", "condition": { "field": "PWPush.Enabled", "compareType": "is", @@ -795,7 +795,7 @@ { "type": "switch", "name": "PWPush.CFEnabled", - "label": "Connect to PWPush through CloudFlare Tunnel with the Service Account credentials.", + "label": "Behind a CF-ZTNA Tunnel", "condition": { "field": "CFZTNA.Enabled", "compareType": "is", From fd766d35573ddcb1bfb700da632cb7e21040354f Mon Sep 17 00:00:00 2001 From: "Brad M.K." Date: Wed, 4 Mar 2026 03:11:04 +0000 Subject: [PATCH 157/177] Add bookmark management to sidebar and top navigation with drag-and-drop reordering Introduce a new SideNavBookmarks component that displays bookmarks in both desktop sidebar and mobile navigation. Add drag-and-drop functionality to reorder bookmarks, replacing the previous up/down arrow controls. Include a bookmarkMode setting to control bookmark display location (sidebar, topnav, or both). Update CippSettingsSideBar to persist the bookmarkMode preference. --- .../CippComponents/CippSettingsSideBar.jsx | 3 + src/layouts/mobile-nav.js | 14 +- src/layouts/side-nav-bookmarks.js | 259 ++++++++++++++++++ src/layouts/side-nav.js | 14 +- src/layouts/top-nav.js | 252 +++++++++-------- src/pages/cipp/preferences.js | 18 ++ 6 files changed, 430 insertions(+), 130 deletions(-) create mode 100644 src/layouts/side-nav-bookmarks.js diff --git a/src/components/CippComponents/CippSettingsSideBar.jsx b/src/components/CippComponents/CippSettingsSideBar.jsx index 693c55673d77..90839464161a 100644 --- a/src/components/CippComponents/CippSettingsSideBar.jsx +++ b/src/components/CippComponents/CippSettingsSideBar.jsx @@ -65,6 +65,9 @@ export const CippSettingsSideBar = (props) => { // Table Filter Preferences persistFilters: formValues.persistFilters, + // Bookmark Display Mode + bookmarkMode: formValues.bookmarkMode, + // Portal Links Configuration portalLinks: { M365_Portal: formValues.portalLinks?.M365_Portal, diff --git a/src/layouts/mobile-nav.js b/src/layouts/mobile-nav.js index 30612f3fd28a..728cb05bb083 100644 --- a/src/layouts/mobile-nav.js +++ b/src/layouts/mobile-nav.js @@ -1,12 +1,14 @@ import NextLink from "next/link"; import { usePathname } from "next/navigation"; import PropTypes from "prop-types"; -import { Box, Drawer, Stack } from "@mui/material"; +import { Box, Divider, Drawer, Stack } from "@mui/material"; import { Logo } from "../components/logo"; import { Scrollbar } from "../components/scrollbar"; import { paths } from "../paths"; import { MobileNavItem } from "./mobile-nav-item"; +import { SideNavBookmarks } from "./side-nav-bookmarks"; import { CippTenantSelector } from "../components/CippComponents/CippTenantSelector"; +import { useSettings } from "../hooks/use-settings"; const MOBILE_NAV_WIDTH = "80%"; @@ -77,6 +79,8 @@ const reduceChildRoutes = ({ acc, depth, item, pathname }) => { export const MobileNav = (props) => { const { open, onClose, items } = props; const pathname = usePathname(); + const settings = useSettings(); + const bookmarkMode = settings.bookmarkMode?.value || "both"; return ( { p: 0, }} > + {/* Bookmarks section above Dashboard */} + {(bookmarkMode === "sidebar" || bookmarkMode === "both") && ( + <> + + + + )} + {/* Render all menu items */} {renderItems({ depth: 0, items, diff --git a/src/layouts/side-nav-bookmarks.js b/src/layouts/side-nav-bookmarks.js new file mode 100644 index 000000000000..451e0e354035 --- /dev/null +++ b/src/layouts/side-nav-bookmarks.js @@ -0,0 +1,259 @@ +import { useCallback, useState, useEffect, useRef } from "react"; +import NextLink from "next/link"; +import { + Box, + ButtonBase, + Collapse, + IconButton, + Stack, + SvgIcon, + Typography, +} from "@mui/material"; +import BookmarkIcon from "@mui/icons-material/Bookmark"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import CloseIcon from "@mui/icons-material/Close"; +import ChevronRightIcon from "@heroicons/react/24/outline/ChevronRightIcon"; +import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon"; +import { useSettings } from "../hooks/use-settings"; + +export const SideNavBookmarks = ({ collapse = false }) => { + const settings = useSettings(); + const [open, setOpen] = useState(settings.bookmarksOpen ?? false); + const [dragIndex, setDragIndex] = useState(null); + const [dragOverIndex, setDragOverIndex] = useState(null); + + const handleToggle = useCallback(() => { + setOpen((prev) => { + const next = !prev; + settings.handleUpdate({ bookmarksOpen: next }); + return next; + }); + }, [settings]); + + const removeBookmark = useCallback( + (index) => { + const updatedBookmarks = [...(settings.bookmarks || [])]; + updatedBookmarks.splice(index, 1); + settings.handleUpdate({ bookmarks: updatedBookmarks }); + }, + [settings] + ); + + const handleDragStart = useCallback((index) => { + setDragIndex(index); + }, []); + + const handleDragOver = useCallback((e, index) => { + e.preventDefault(); + setDragOverIndex(index); + }, []); + + const handleDrop = useCallback( + (e, dropIndex) => { + e.preventDefault(); + if (dragIndex === null || dragIndex === dropIndex) { + setDragIndex(null); + setDragOverIndex(null); + return; + } + const items = [...(settings.bookmarks || [])]; + const [reordered] = items.splice(dragIndex, 1); + items.splice(dropIndex, 0, reordered); + settings.handleUpdate({ bookmarks: items }); + setDragIndex(null); + setDragOverIndex(null); + }, + [dragIndex, settings] + ); + + const handleDragEnd = useCallback(() => { + setDragIndex(null); + setDragOverIndex(null); + }, []); + + const displayBookmarks = settings.bookmarks || []; + + return ( +
  • + + theme.typography.fontFamily, + fontSize: 14, + fontWeight: 500, + justifyContent: "flex-start", + px: "6px", + py: "12px", + textAlign: "left", + whiteSpace: "nowrap", + width: "100%", + }} + > + + + + + + + Bookmarks + + + {open ? : } + + + + + + {displayBookmarks.length === 0 ? ( +
  • + + No bookmarks added yet + +
  • + ) : ( + displayBookmarks.map((bookmark, idx) => ( +
  • handleDragStart(idx)} + onDragOver={(e) => handleDragOver(e, idx)} + onDrop={(e) => handleDrop(e, idx)} + onDragEnd={handleDragEnd} + > + + + + + theme.typography.fontFamily, + fontSize: 13, + fontWeight: 500, + justifyContent: "flex-start", + py: "6px", + textAlign: "left", + whiteSpace: "nowrap", + flexGrow: 1, + overflow: "hidden", + }} + > + + {bookmark.label} + + + + { + e.preventDefault(); + removeBookmark(idx); + }} + sx={{ p: "2px" }} + > + + + + +
  • + )) + )} + + + + ); +}; diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index fc2d19b396df..9605ecf75ce1 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -1,11 +1,13 @@ import { useState } from "react"; import { usePathname } from "next/navigation"; import PropTypes from "prop-types"; -import { Box, Drawer, Stack } from "@mui/material"; +import { Box, Divider, Drawer, Stack } from "@mui/material"; import { Scrollbar } from "../components/scrollbar"; import { SideNavItem } from "./side-nav-item"; +import { SideNavBookmarks } from "./side-nav-bookmarks"; import { ApiGetCall } from "../api/ApiCall.jsx"; import { CippSponsor } from "../components/CippComponents/CippSponsor"; +import { useSettings } from "../hooks/use-settings"; const SIDE_NAV_WIDTH = 270; const SIDE_NAV_COLLAPSED_WIDTH = 73; // icon size + padding + border right @@ -105,6 +107,8 @@ export const SideNav = (props) => { const [hovered, setHovered] = useState(false); const collapse = !(pinned || hovered); const { data: profile } = ApiGetCall({ url: "/api/me", queryKey: "authmecipp" }); + const settings = useSettings(); + const bookmarkMode = settings.bookmarkMode?.value || "both"; // Preprocess items to mark which should be open const processedItems = markOpenItems(items, pathname); @@ -159,6 +163,14 @@ export const SideNav = (props) => { p: 0, }} > + {/* Bookmarks section above Dashboard */} + {(bookmarkMode === "sidebar" || bookmarkMode === "both") && ( + <> + + + + )} + {/* Render all menu items */} {renderItems({ collapse, depth: 0, diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index ec7f06c7f3dd..a1ad87c3e868 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -5,10 +5,8 @@ import Bars3Icon from "@heroicons/react/24/outline/Bars3Icon"; import MoonIcon from "@heroicons/react/24/outline/MoonIcon"; import SunIcon from "@heroicons/react/24/outline/SunIcon"; import BookmarkIcon from "@mui/icons-material/Bookmark"; -import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; -import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; -import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import CloseIcon from "@mui/icons-material/Close"; import { Box, Divider, @@ -31,7 +29,6 @@ import { NotificationsPopover } from "./notifications-popover"; import { useDialog } from "../hooks/use-dialog"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { CippCentralSearch } from "../components/CippComponents/CippCentralSearch"; -import { applySort } from "../utils/apply-sort"; const TOP_NAV_HEIGHT = 64; @@ -40,6 +37,7 @@ export const TopNav = (props) => { const { onNavOpen } = props; const settings = useSettings(); const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); + const bookmarkMode = settings.bookmarkMode?.value || "both"; const handleThemeSwitch = useCallback(() => { const themeName = settings.currentTheme?.value === "light" ? "dark" : "light"; settings.handleUpdate({ @@ -49,7 +47,8 @@ export const TopNav = (props) => { }, [settings]); const [anchorEl, setAnchorEl] = useState(null); - const [sortOrder, setSortOrder] = useState("asc"); + const [dragIndex, setDragIndex] = useState(null); + const [dragOverIndex, setDragOverIndex] = useState(null); const handleBookmarkClick = (event) => { setAnchorEl(event.currentTarget); @@ -59,43 +58,45 @@ export const TopNav = (props) => { setAnchorEl(null); }; - const handleSortToggle = () => { - const newSortOrder = sortOrder === "asc" ? "desc" : "asc"; - setSortOrder(newSortOrder); - - // Save the new sort order and re-order bookmarks - const sortedBookmarks = applySort(settings.bookmarks || [], "label", newSortOrder); - settings.handleUpdate({ - bookmarks: sortedBookmarks, - sortOrder: newSortOrder, - }); + const handleDragStart = (index) => { + setDragIndex(index); }; - // Move a bookmark up in the list - const moveBookmarkUp = (index) => { - if (index <= 0) return; - - const updatedBookmarks = [...(settings.bookmarks || [])]; - const temp = updatedBookmarks[index]; - updatedBookmarks[index] = updatedBookmarks[index - 1]; - updatedBookmarks[index - 1] = temp; - - settings.handleUpdate({ bookmarks: updatedBookmarks }); + const handleDragOver = (e, index) => { + e.preventDefault(); + setDragOverIndex(index); }; - // Move a bookmark down in the list - const moveBookmarkDown = (index) => { - const bookmarks = settings.bookmarks || []; - if (index >= bookmarks.length - 1) return; + const handleDrop = (e, dropIndex) => { + e.preventDefault(); + if (dragIndex === null || dragIndex === dropIndex) { + setDragIndex(null); + setDragOverIndex(null); + return; + } + const items = [...(settings.bookmarks || [])]; + const [reordered] = items.splice(dragIndex, 1); + items.splice(dropIndex, 0, reordered); + settings.handleUpdate({ bookmarks: items }); + setDragIndex(null); + setDragOverIndex(null); + }; - const updatedBookmarks = [...bookmarks]; - const temp = updatedBookmarks[index]; - updatedBookmarks[index] = updatedBookmarks[index + 1]; - updatedBookmarks[index + 1] = temp; + const handleDragEnd = () => { + setDragIndex(null); + setDragOverIndex(null); + }; + const removeBookmark = (index) => { + const updatedBookmarks = [...(settings.bookmarks || [])]; + updatedBookmarks.splice(index, 1); settings.handleUpdate({ bookmarks: updatedBookmarks }); }; + const displayBookmarks = settings.bookmarks || []; + const popoverOpen = Boolean(anchorEl); + const popoverId = popoverOpen ? "bookmark-popover" : undefined; + useEffect(() => { const handleKeyDown = (event) => { if ((event.metaKey || event.ctrlKey) && event.key === "k") { @@ -109,22 +110,10 @@ export const TopNav = (props) => { }; }, []); - useEffect(() => { - if (settings.sortOrder) { - setSortOrder(settings.sortOrder); - } - }, [settings.sortOrder]); - const openSearch = () => { searchDialog.handleOpen(); }; - // Use the sorted bookmarks if sorting is applied, otherwise use the bookmarks in their current order - const displayBookmarks = settings.bookmarks || []; - - const open = Boolean(anchorEl); - const id = open ? "bookmark-popover" : undefined; - return ( { - - - - - - - - - - - {sortOrder === "asc" ? : } - - - Sort Alphabetically - - {displayBookmarks.length === 0 ? ( - - No bookmarks added yet
    } - /> - - ) : ( - displayBookmarks.map((bookmark, idx) => ( - - handleBookmarkClose()} - sx={{ - textDecoration: "none", - color: "inherit", - flexGrow: 1, - marginRight: 2, - }} - > - {bookmark.label} - - - { - e.preventDefault(); - moveBookmarkUp(idx); + {(bookmarkMode === "popover" || bookmarkMode === "both") && ( + <> + + + + + + + + {displayBookmarks.length === 0 ? ( + + No bookmarks added yet} + /> + + ) : ( + displayBookmarks.map((bookmark, idx) => ( + handleDragStart(idx)} + onDragOver={(e) => handleDragOver(e, idx)} + onDrop={(e) => handleDrop(e, idx)} + onDragEnd={handleDragEnd} + sx={{ + color: "inherit", + display: "flex", + justifyContent: "space-between", + cursor: "grab", + ...(dragIndex === idx && { + opacity: 0.4, + }), + ...(dragOverIndex === idx && dragIndex !== idx && { + borderTop: "2px solid", + borderColor: "primary.main", + }), }} - disabled={idx === 0} > - - - { - e.preventDefault(); - moveBookmarkDown(idx); - }} - disabled={idx === displayBookmarks.length - 1} - > - - - - - )) - )} - - + + + + handleBookmarkClose()} + sx={{ + textDecoration: "none", + color: "inherit", + flexGrow: 1, + marginRight: 2, + }} + > + {bookmark.label} + + { + e.preventDefault(); + removeBookmark(idx); + }} + > + + + + )) + )} + + + + )} { /> ), }, + { + label: "Bookmark Display Mode", + value: ( + + ), + }, ]} /> From c48361f3f7adbe6c7d2caa60f60fdffed13898b2 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:38:05 +0100 Subject: [PATCH 158/177] fix: update CA Test Results table columns and fetching state - Changed column name from 'reasons' to 'analysisReasons'. - Added isFetching prop to indicate loading state for the data table. --- .../identity/administration/users/user/conditional-access.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index ea58b93207dc..d6bd88803c0b 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -246,9 +246,9 @@ const Page = () => { From 10f74a905efb60a6b98d22f078568a1c2a745d3f Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:57:13 +0100 Subject: [PATCH 159/177] feat: add authentication flow selection and reorder parameters - Introduced a new component for selecting the authentication flow. - Reordered the optional parameters for better user experience. --- .../users/user/conditional-access.jsx | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index d6bd88803c0b..87c981111f19 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -154,28 +154,6 @@ const Page = () => { {/* Optional Parameters */} Optional Parameters: - - {/* Test from this country */} - ({ - value: Code, - label: Name, - }))} - formControl={formControl} - /> - - {/* Test from this IP */} - - {/* Device Platform */} { formControl={formControl} /> + {/* Authentication Flow */} + + + {/* Test from this IP */} + + + {/* Test from this country */} + ({ + value: Code, + label: Name, + }))} + formControl={formControl} + /> + {/* Sign-in risk level */} Date: Wed, 4 Mar 2026 20:11:43 +0100 Subject: [PATCH 160/177] fix: update autoComplete fields to disable creatable option - Set creatable to false for various autoComplete fields to restrict user input to predefined options. - Ensured multiple selection is disabled for all relevant fields. --- .../users/user/conditional-access.jsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index 87c981111f19..8449148b8562 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -133,6 +133,7 @@ const Page = () => { label="Select the application to test" name="includeApplications" multiple={false} + creatable={false} api={{ tenantFilter: tenant, url: "/api/ListGraphRequest", @@ -149,6 +150,7 @@ const Page = () => { $top: 999, }, }} + validators={{ required: "Application is required" }} formControl={formControl} /> @@ -159,6 +161,8 @@ const Page = () => { type="autoComplete" label="Select the device platform to test" name="devicePlatform" + multiple={false} + creatable={false} options={[ { value: "Windows", label: "Windows" }, { value: "iOS", label: "iOS" }, @@ -174,6 +178,8 @@ const Page = () => { type="autoComplete" label="Select the client application type to test" name="clientAppType" + multiple={false} + creatable={false} options={[ { value: "all", label: "All" }, { value: "Browser", label: "Browser" }, @@ -193,6 +199,8 @@ const Page = () => { type="autoComplete" label="Select the authentication flow" name="authenticationFlow" + multiple={false} + creatable={false} options={[ { value: "none", label: "None" }, { value: "deviceCodeFlow", label: "Device code flow" }, @@ -215,6 +223,8 @@ const Page = () => { type="autoComplete" label="Test from this country" name="country" + multiple={false} + creatable={false} options={countryList.map(({ Code, Name }) => ({ value: Code, label: Name, @@ -227,6 +237,8 @@ const Page = () => { type="autoComplete" label="Select the sign-in risk level of the user signing in" name="SignInRiskLevel" + multiple={false} + creatable={false} options={[ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, @@ -241,6 +253,8 @@ const Page = () => { type="autoComplete" label="Select the user risk level of the user signing in" name="userRiskLevel" + multiple={false} + creatable={false} options={[ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, From c0bdac841ec1a21de4924935ecf7aa430878dc87 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:48:02 +0100 Subject: [PATCH 161/177] fix: restore Shift+Home text selection in autocomplete inputs MUI's Autocomplete intercepted the Home/End keys for list navigation, preventing native browser shortcuts like Shift+Home from working in the input field. Expose handleHomeEndKeys as a prop defaulting to false so callsites can opt back in where list-jump behavior is preferred. --- src/components/CippComponents/CippAutocomplete.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index d09551f1f316..bea469290be3 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -82,6 +82,7 @@ export const CippAutoComplete = (props) => { groupBy, renderGroup, customAction, + handleHomeEndKeys = false, ...other } = props; @@ -311,6 +312,7 @@ export const CippAutoComplete = (props) => { setOpen(true)} onClose={(event, reason) => { @@ -422,7 +424,7 @@ export const CippAutoComplete = (props) => { if (input) { input.focus(); } - + // Restore the scroll position if (listboxRef.current && scrollPositionRef.current > 0) { listboxRef.current.scrollTop = scrollPositionRef.current; From 4877f71d380c03930c9958c973d8b88ebc042b20 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:27:43 +0100 Subject: [PATCH 162/177] feat: add Ctrl+Alt+K shortcut to focus tenant selector Exposes a focus() handle on CippAutoComplete via forwardRef + useImperativeHandle, threads it through CippTenantSelector, and registers a Ctrl+Alt+K keydown handler in TopNav that focuses and selects-all the tenant input for quick keyboard-driven tenant switching. --- .../CippComponents/CippAutocomplete.jsx | 15 +++++++++++--- .../CippComponents/CippTenantSelector.jsx | 9 ++++++--- src/layouts/top-nav.js | 20 +++++++++++-------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index bea469290be3..a27bbe535e8a 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -10,7 +10,7 @@ import { Typography, } from "@mui/material"; import Link from "next/link"; -import { useEffect, useState, useMemo, useCallback, useRef } from "react"; +import { useEffect, useState, useMemo, useCallback, useRef, useImperativeHandle } from "react"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; import { ApiGetCallWithPagination } from "../../api/ApiCall"; @@ -57,7 +57,7 @@ const MemoTextField = React.memo(function MemoTextField({ ); }); -export const CippAutoComplete = (props) => { +export const CippAutoComplete = React.forwardRef((props, ref) => { const { size, api, @@ -90,6 +90,14 @@ export const CippAutoComplete = (props) => { const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "" }); const hasPreselectedRef = useRef(false); const autocompleteRef = useRef(null); // Ref for focusing input after selection + + useImperativeHandle(ref, () => ({ + focus() { + const input = autocompleteRef.current?.querySelector("input"); + input?.focus(); + input?.select(); + }, + }), []); const listboxRef = useRef(null); // Ref for the listbox to preserve scroll position const scrollPositionRef = useRef(0); // Store scroll position const filter = createFilterOptions({ @@ -682,4 +690,5 @@ export const CippAutoComplete = (props) => { )} ); -}; +}); +CippAutoComplete.displayName = "CippAutoComplete"; diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index ab9e50ea8fcf..f215f9879006 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -19,14 +19,14 @@ import { ServerIcon, UsersIcon, } from "@heroicons/react/24/outline"; -import { useEffect, useState, useMemo, useCallback, useRef } from "react"; +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"; -export const CippTenantSelector = (props) => { +export const CippTenantSelector = React.forwardRef((props, ref) => { const { width, allTenants = false, multiple = false, refreshButton, tenantButton } = props; //get the current tenant from SearchParams called 'tenantFilter' const router = useRouter(); @@ -325,6 +325,7 @@ export const CippTenantSelector = (props) => { )} { /> ); -}; +}); + +CippTenantSelector.displayName = "CippTenantSelector"; CippTenantSelector.propTypes = { allTenants: PropTypes.bool, diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index ec7f06c7f3dd..392f6b7d148c 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import NextLink from "next/link"; import PropTypes from "prop-types"; import Bars3Icon from "@heroicons/react/24/outline/Bars3Icon"; @@ -37,6 +37,7 @@ const TOP_NAV_HEIGHT = 64; export const TopNav = (props) => { const searchDialog = useDialog(); + const tenantSelectorRef = useRef(null); const { onNavOpen } = props; const settings = useSettings(); const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); @@ -96,9 +97,16 @@ export const TopNav = (props) => { settings.handleUpdate({ bookmarks: updatedBookmarks }); }; + const openSearch = useCallback(() => { + searchDialog.handleOpen(); + }, [searchDialog.handleOpen]); + useEffect(() => { const handleKeyDown = (event) => { - if ((event.metaKey || event.ctrlKey) && event.key === "k") { + if ((event.metaKey || event.ctrlKey) && event.altKey && event.key === "k") { + event.preventDefault(); + tenantSelectorRef.current?.focus(); + } else if ((event.metaKey || event.ctrlKey) && event.key === "k") { event.preventDefault(); openSearch(); } @@ -107,7 +115,7 @@ export const TopNav = (props) => { return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, []); + }, [openSearch]); useEffect(() => { if (settings.sortOrder) { @@ -115,10 +123,6 @@ export const TopNav = (props) => { } }, [settings.sortOrder]); - const openSearch = () => { - searchDialog.handleOpen(); - }; - // Use the sorted bookmarks if sorting is applied, otherwise use the bookmarks in their current order const displayBookmarks = settings.bookmarks || []; @@ -169,7 +173,7 @@ export const TopNav = (props) => { > - {!mdDown && } + {!mdDown && } {mdDown && ( From 18f8d25c03a6e014cbd95b7e12454df5dc8ce79c Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:36:03 +0100 Subject: [PATCH 163/177] feat: add assignment filter options to application assignments --- src/pages/endpoint/applications/list/index.js | 91 ++++++++++++++++--- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 0405962e188b..49e5bc4b2816 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -20,6 +20,11 @@ const assignmentModeOptions = [ { label: "Append to existing assignments", value: "append" }, ]; +const assignmentFilterTypeOptions = [ + { label: "Include - Apply to devices matching filter", value: "include" }, + { label: "Exclude - Apply to devices NOT matching filter", value: "exclude" }, +]; + const getAppAssignmentSettingsType = (odataType) => { if (!odataType || typeof odataType !== "string") { return undefined; @@ -33,15 +38,35 @@ const Page = () => { const syncDialog = useDialog(); const tenant = useSettings().currentTenant; + const getAssignmentFilterFields = () => [ + { + type: "autoComplete", + name: "assignmentFilter", + label: "Assignment Filter (Optional)", + multiple: false, + creatable: false, + api: { + url: "/api/ListAssignmentFilters", + queryKey: `ListAssignmentFilters-${tenant}`, + labelField: (filter) => filter.displayName, + valueField: "displayName", + }, + }, + { + type: "radio", + name: "assignmentFilterType", + label: "Assignment Filter Mode", + options: assignmentFilterTypeOptions, + defaultValue: "include", + helperText: "Choose whether to include or exclude devices matching the filter.", + }, + ]; + const actions = [ { label: "Assign to All Users", type: "POST", url: "/api/ExecAssignApp", - data: { - AssignTo: "!AllUsers", - ID: "id", - }, fields: [ { type: "radio", @@ -62,7 +87,22 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", }, + ...getAssignmentFilterFields(), ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + AssignTo: "AllUsers", + Intent: formData?.Intent || "Required", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users?', icon: , color: "info", @@ -71,10 +111,6 @@ const Page = () => { label: "Assign to All Devices", type: "POST", url: "/api/ExecAssignApp", - data: { - AssignTo: "!AllDevices", - ID: "id", - }, fields: [ { type: "radio", @@ -95,7 +131,22 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", }, + ...getAssignmentFilterFields(), ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + AssignTo: "AllDevices", + Intent: formData?.Intent || "Required", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', icon: , color: "info", @@ -104,10 +155,6 @@ const Page = () => { label: "Assign Globally (All Users / All Devices)", type: "POST", url: "/api/ExecAssignApp", - data: { - AssignTo: "!AllDevicesAndUsers", - ID: "id", - }, fields: [ { type: "radio", @@ -128,7 +175,22 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", }, + ...getAssignmentFilterFields(), ], + customDataformatter: (row, action, formData) => { + const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant; + return { + tenantFilter: tenantFilterValue, + ID: row?.id, + AssignTo: "AllDevicesAndUsers", + Intent: formData?.Intent || "Required", + assignmentMode: formData?.assignmentMode || "replace", + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, + }; + }, confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', icon: , color: "info", @@ -188,6 +250,7 @@ const Page = () => { helperText: "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups/intents.", }, + ...getAssignmentFilterFields(), ], customDataformatter: (row, action, formData) => { const selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : []; @@ -200,6 +263,10 @@ const Page = () => { Intent: formData?.assignmentIntent || "Required", AssignmentMode: formData?.assignmentMode || "replace", AppType: getAppAssignmentSettingsType(row?.["@odata.type"]), + AssignmentFilterName: formData?.assignmentFilter?.value || null, + AssignmentFilterType: formData?.assignmentFilter?.value + ? formData?.assignmentFilterType || "include" + : null, }; }, }, From 03dc06d7223da4299775aa7f146d5a3c44c19890 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:42:42 +0100 Subject: [PATCH 164/177] feat: add button to deploy group template on the groups page --- src/pages/identity/administration/groups/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index 1a05309ee7d2..3320ac353204 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -5,13 +5,13 @@ import Link from "next/link"; import { TrashIcon, EyeIcon } from "@heroicons/react/24/outline"; import { Visibility, - VisibilityOff, GroupAdd, Edit, LockOpen, Lock, GroupSharp, CloudSync, + RocketLaunch, } from "@mui/icons-material"; import { Stack } from "@mui/system"; import { useState } from "react"; @@ -313,6 +313,13 @@ const Page = () => { + } apiUrl="/api/ListGroups" From de8c5fc772dc7aba087c5121eb5f9b7d29e07d78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:44:55 +0000 Subject: [PATCH 165/177] Bump react from 19.2.3 to 19.2.4 Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) from 19.2.3 to 19.2.4. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react) --- updated-dependencies: - dependency-name: react dependency-version: 19.2.4 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 11957bf4dc59..a0310e77ef05 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "numeral": "2.0.6", "prop-types": "15.8.1", "punycode": "^2.3.1", - "react": "19.2.3", + "react": "19.2.4", "react-apexcharts": "1.7.0", "react-beautiful-dnd": "13.1.1", "react-copy-to-clipboard": "^5.1.0", diff --git a/yarn.lock b/yarn.lock index 33f7f0fb0628..300433a19ad0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6908,10 +6908,10 @@ react-window@^2.2.5: resolved "https://registry.yarnpkg.com/react-window/-/react-window-2.2.5.tgz#425a29609980083aafd5a48a1711a2af9319c1d2" integrity sha512-6viWvPSZvVuMIe9hrl4IIZoVfO/npiqOb03m4Z9w+VihmVzBbiudUrtUqDpsWdKvd/Ai31TCR25CBcFFAUm28w== -react@19.2.3: - version "19.2.3" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8" - integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== +react@19.2.4: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.4.tgz#438e57baa19b77cb23aab516cf635cd0579ee09a" + integrity sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ== readable-stream@^2.0.2: version "2.3.8" From 0aeb4451e88ac6de2aef9366f46b8c74672d2be8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:45:44 +0000 Subject: [PATCH 166/177] Bump LanceMcCarthy/Action-AzureBlobUpload from 3.7.0 to 3.8.0 Bumps [LanceMcCarthy/Action-AzureBlobUpload](https://github.com/lancemccarthy/action-azureblobupload) from 3.7.0 to 3.8.0. - [Release notes](https://github.com/lancemccarthy/action-azureblobupload/releases) - [Commits](https://github.com/lancemccarthy/action-azureblobupload/compare/v3.7.0...v3.8.0) --- updated-dependencies: - dependency-name: LanceMcCarthy/Action-AzureBlobUpload dependency-version: 3.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/cipp_dev_build.yml | 2 +- .github/workflows/cipp_frontend_build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml index 432d9363cade..ee28a418e72c 100644 --- a/.github/workflows/cipp_dev_build.yml +++ b/.github/workflows/cipp_dev_build.yml @@ -47,7 +47,7 @@ jobs: # Upload to Azure Blob Storage - name: Azure Blob Upload - uses: LanceMcCarthy/Action-AzureBlobUpload@v3.7.0 + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.8.0 with: connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} container_name: cipp diff --git a/.github/workflows/cipp_frontend_build.yml b/.github/workflows/cipp_frontend_build.yml index 5db059b438a8..d7c549442d89 100644 --- a/.github/workflows/cipp_frontend_build.yml +++ b/.github/workflows/cipp_frontend_build.yml @@ -47,7 +47,7 @@ jobs: # Upload to Azure Blob Storage - name: Azure Blob Upload - uses: LanceMcCarthy/Action-AzureBlobUpload@v3.7.0 + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.8.0 with: connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} container_name: cipp From 8cb96ab4488d0d0f1b049cdbea7d6ac0a4ebe104 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:45:49 +0000 Subject: [PATCH 167/177] Bump actions/setup-node from 6.2.0 to 6.3.0 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.2.0 to 6.3.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v6.2.0...v6.3.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/Node_Project_Check.yml | 2 +- .github/workflows/cipp_dev_build.yml | 2 +- .github/workflows/cipp_frontend_build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Node_Project_Check.yml b/.github/workflows/Node_Project_Check.yml index a1581e036833..1116a307ceb7 100644 --- a/.github/workflows/Node_Project_Check.yml +++ b/.github/workflows/Node_Project_Check.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version: ${{ matrix.node-version }} - name: Install and Build Test diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml index 432d9363cade..44e0b9b591a8 100644 --- a/.github/workflows/cipp_dev_build.yml +++ b/.github/workflows/cipp_dev_build.yml @@ -26,7 +26,7 @@ jobs: echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT - name: Set up Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version: ${{ steps.get_node_version.outputs.node_version }} diff --git a/.github/workflows/cipp_frontend_build.yml b/.github/workflows/cipp_frontend_build.yml index 5db059b438a8..af84a0196602 100644 --- a/.github/workflows/cipp_frontend_build.yml +++ b/.github/workflows/cipp_frontend_build.yml @@ -26,7 +26,7 @@ jobs: echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT - name: Set up Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version: ${{ steps.get_node_version.outputs.node_version }} From 1b9bb8fc0344da9f68cf534cf4b1b0488d5d5c4d Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:51:10 +0100 Subject: [PATCH 168/177] feat: update button link to new "add a tenant" wizard --- src/pages/tenant/gdap-management/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/gdap-management/index.js b/src/pages/tenant/gdap-management/index.js index 915caff047a0..9aafd4a40531 100644 --- a/src/pages/tenant/gdap-management/index.js +++ b/src/pages/tenant/gdap-management/index.js @@ -167,7 +167,7 @@ const Page = () => {