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 }}"
diff --git a/.github/workflows/Check_for_Version_Update.yml b/.github/workflows/Check_for_Version_Update.yml
index 001dfdb4ffa5..de9156a94582 100644
--- a/.github/workflows/Check_for_Version_Update.yml
+++ b/.github/workflows/Check_for_Version_Update.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-slim
steps:
- name: Check for Changed Files
- uses: brettcannon/check-for-changed-files@v1.1.0
+ uses: brettcannon/check-for-changed-files@v1.2.1
with:
file-pattern: public/version.json
failure-message: "You have not updated version.json. This is a required file to update at each PR. Please sync your latest changes and update the version number."
diff --git a/.github/workflows/Close_Stale_Issues.yml b/.github/workflows/Close_Stale_Issues.yml
index f061a5a5a7ff..b1878078ac90 100644
--- a/.github/workflows/Close_Stale_Issues.yml
+++ b/.github/workflows/Close_Stale_Issues.yml
@@ -8,7 +8,7 @@ jobs:
if: github.repository_owner == 'KelvinTegelaar'
runs-on: ubuntu-slim
steps:
- - uses: actions/stale@v4
+ - uses: actions/stale@v10
with:
stale-issue-message: "This issue is stale because it has been open 10 days with no activity. We will close this issue soon. If you want this feature implemented you can contribute it. See: https://docs.cipp.app/dev-documentation/contributing-to-the-code . Please notify the team if you are working on this yourself."
close-issue-message: "This issue was closed because it has been stalled for 14 days with no activity."
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/Label_Issues.yml b/.github/workflows/Label_Issues.yml
index ebdc3992980f..cc0032ee08e8 100644
--- a/.github/workflows/Label_Issues.yml
+++ b/.github/workflows/Label_Issues.yml
@@ -12,7 +12,7 @@ jobs:
issues: write
steps:
- name: Label Issues
- uses: andymckay/labeler@5c59dabdfd4dd5bd9c6e6d255b01b9d764af4414
+ uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90
with:
add-labels: "not-assigned"
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -23,7 +23,7 @@ jobs:
issues: write
steps:
- name: Label Issues
- uses: andymckay/labeler@5c59dabdfd4dd5bd9c6e6d255b01b9d764af4414
+ uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90
with:
add-labels: "enhancement, not-assigned"
repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/Node_Project_Check.yml b/.github/workflows/Node_Project_Check.yml
index 0a777ff925da..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
+ uses: actions/setup-node@v6.3.0
with:
node-version: ${{ matrix.node-version }}
- name: Install and Build Test
diff --git a/.github/workflows/auto_comments.yml b/.github/workflows/auto_comments.yml
index 6cd003a36ae8..9a7664c9007c 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: |
@@ -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: |
diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml
index dad0dbebe307..f19a6d403a92 100644
--- a/.github/workflows/cipp_dev_build.yml
+++ b/.github/workflows/cipp_dev_build.yml
@@ -15,7 +15,7 @@ jobs:
steps:
# Checkout the repository
- name: Checkout Code
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6
# Set up Node.js
- name: Get Node version
@@ -26,7 +26,7 @@ jobs:
echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT
- name: Set up Node.js
- uses: actions/setup-node@v4.2.0
+ uses: actions/setup-node@v6.3.0
with:
node-version: ${{ steps.get_node_version.outputs.node_version }}
@@ -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.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 76a7dbb2fbbf..bf86646e19cd 100644
--- a/.github/workflows/cipp_frontend_build.yml
+++ b/.github/workflows/cipp_frontend_build.yml
@@ -15,7 +15,7 @@ jobs:
steps:
# Checkout the repository
- name: Checkout Code
- uses: actions/checkout@v4.2.2
+ uses: actions/checkout@v6
# Set up Node.js
- name: Get Node version
@@ -26,7 +26,7 @@ jobs:
echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT
- name: Set up Node.js
- uses: actions/setup-node@v4.2.0
+ uses: actions/setup-node@v6.3.0
with:
node-version: ${{ steps.get_node_version.outputs.node_version }}
@@ -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.8.0
with:
connection_string: ${{ secrets.AZURE_CONNECTION_STRING }}
container_name: cipp
diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml
index d8af6078d94f..027b23b85658 100644
--- a/.github/workflows/dev_deploy.yml
+++ b/.github/workflows/dev_deploy.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
submodules: true
- name: Build And Deploy
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:
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: |
diff --git a/Tools/Start-CippDevEmulators.ps1 b/Tools/Start-CippDevEmulators.ps1
index 74de265cc59a..e0d0146f3ab6 100644
--- a/Tools/Start-CippDevEmulators.ps1
+++ b/Tools/Start-CippDevEmulators.ps1
@@ -1,18 +1,26 @@
+Write-Host 'Starting CIPP Dev Emulators' -ForegroundColor Cyan
+
+# Verify Windows Terminal is available
Get-Command wt -ErrorAction Stop | Out-Null
+
+# Stop any existing node processes
Get-Process node -ErrorAction SilentlyContinue | Stop-Process -ErrorAction SilentlyContinue
+
+# Get paths
$Path = (Get-Item $PSScriptRoot).Parent.Parent.FullName
-Write-Host "CIPP Dev Emulators starting in $Path" -ForegroundColor Green
-pwsh -file (Join-Path $PSScriptRoot 'Start-CippDevInstallation.ps1')
+# 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'
-Write-Host 'Starting CIPP Dev Emulators'
+Write-Host 'Starting emulators...' -ForegroundColor Cyan
-if (Test-Path (Join-Path $Path 'CIPP-API-Processor')) {
- $Process = Read-Host -Prompt 'Start Process Function (y/N)?'
-}
+# Build commands with error handling
+$azuriteCommand = 'try { azurite } catch { Write-Error $_.Exception.Message } finally { Read-Host "Press Enter to exit" }'
+$apiCommand = 'try { func start } catch { Write-Error $_.Exception.Message } finally { Read-Host "Press Enter to exit" }'
+$frontendCommand = 'try { npm run dev } catch { Write-Error $_.Exception.Message } finally { Read-Host "Press Enter to exit" }'
+$swaCommand = 'try { npm run start-swa } catch { Write-Error $_.Exception.Message } finally { Read-Host "Press Enter to exit" }'
-if ($Process -eq 'y') {
- wt --title CIPP`; new-tab --title 'Azurite' -d $Path pwsh -c azurite`; new-tab --title 'FunctionApp' -d $Path\CIPP-API pwsh -c func start`; new-tab --title 'CIPP Frontend' -d $Path\CIPP pwsh -c npm run dev`; new-tab --title 'SWA' -d $Path\CIPP pwsh -c npm run start-swa`; new-tab --title 'CIPP-API-Processor' -d $Path\CIPP-API-Processor pwsh -c func start --port 7072
-} else {
- wt --title CIPP`; new-tab --title 'Azurite' -d $Path pwsh -c azurite`; new-tab --title 'FunctionApp' -d $Path\CIPP-API pwsh -c func start`; new-tab --title 'CIPP Frontend' -d $Path\CIPP pwsh -c npm run dev`; new-tab --title 'SWA' -d $Path\CIPP pwsh -c npm run start-swa
-}
+# Start Windows Terminal with all tabs
+wt --title CIPP`; new-tab --title 'Azurite' -d $Path pwsh -c $azuriteCommand`; new-tab --title 'FunctionApp' -d $ApiPath pwsh -c $apiCommand`; new-tab --title 'CIPP Frontend' -d $FrontendPath pwsh -c $frontendCommand`; new-tab --title 'SWA' -d $FrontendPath pwsh -c $swaCommand
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
diff --git a/package.json b/package.json
index 58019e25bb41..a0310e77ef05 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "10.0.9",
+ "version": "10.1.2",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
@@ -34,11 +34,11 @@
"@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.2",
"@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",
@@ -48,36 +48,36 @@
"@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",
- "@uiw/react-json-view": "^2.0.0-alpha.30",
+ "@tiptap/starter-kit": "^3.20.0",
+ "@uiw/react-json-view": "^2.0.0-alpha.41",
"@vvo/tzdb": "^6.198.0",
"apexcharts": "5.3.5",
"axios": "^1.7.2",
"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",
+ "i18next": "25.8.13",
"javascript-time-ago": "^2.6.2",
- "jspdf": "^4.1.0",
- "jspdf-autotable": "^5.0.2",
+ "jspdf": "^4.2.0",
+ "jspdf-autotable": "^5.0.7",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"leaflet.markercluster": "^1.5.3",
"lodash.isequal": "4.5.0",
"material-react-table": "^3.0.1",
"monaco-editor": "^0.55.1",
- "mui-tiptap": "^1.14.0",
- "next": "^16.1.2",
+ "mui-tiptap": "^1.29.0",
+ "next": "^16.1.6",
"nprogress": "0.2.0",
"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",
@@ -85,10 +85,10 @@
"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",
+ "react-i18next": "16.2.4",
"react-leaflet": "5.0.0",
"react-leaflet-markercluster": "^5.0.0-rc.0",
"react-markdown": "10.1.0",
@@ -98,16 +98,16 @@
"react-redux": "9.2.0",
"react-syntax-highlighter": "^16.1.0",
"react-time-ago": "^7.3.3",
- "react-virtuoso": "^4.12.8",
+ "react-virtuoso": "^4.18.1",
"react-window": "^2.2.5",
- "recharts": "^3.6.0",
+ "recharts": "^3.7.0",
"redux": "5.0.1",
"redux-devtools-extension": "2.13.9",
"redux-persist": "^6.0.0",
"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",
@@ -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/public/version.json b/public/version.json
index ad15f02cceba..22ac50c109c0 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "10.0.9"
+ "version": "10.1.2"
}
\ No newline at end of file
diff --git a/src/components/BECRemediationReportButton.js b/src/components/BECRemediationReportButton.js
new file mode 100644
index 000000000000..c7a98231f7d3
--- /dev/null
+++ b/src/components/BECRemediationReportButton.js
@@ -0,0 +1,1513 @@
+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 (
+ <>
+
+ }
+ onClick={handleOpenDialog}
+ disabled={!hasData}
+ color="primary"
+ >
+ Generate PDF Report
+
+
+
+
+ >
+ );
+};
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/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/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx
new file mode 100644
index 000000000000..0f0d6e88ad8b
--- /dev/null
+++ b/src/components/CippCards/CippUniversalSearchV2.jsx
@@ -0,0 +1,467 @@
+import React, { useState, useRef, useEffect } from "react";
+import {
+ TextField,
+ Box,
+ Typography,
+ Skeleton,
+ MenuItem,
+ ListItemText,
+ Paper,
+ CircularProgress,
+ InputAdornment,
+ Portal,
+ Button,
+} from "@mui/material";
+import { Search as SearchIcon } from "@mui/icons-material";
+import { ApiGetCall } from "../../api/ApiCall";
+import { useRouter } from "next/router";
+import { BulkActionsMenu } from "../bulk-actions-menu";
+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 universalSearch = ApiGetCall({
+ url: `/api/ExecUniversalSearchV2`,
+ data: {
+ searchTerms: searchValue,
+ limit: maxResults,
+ type: searchType,
+ },
+ queryKey: `searchV2-${searchType}-${searchValue}`,
+ 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);
+ 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) {
+ handleSearch();
+ }
+ };
+
+ const handleSearch = () => {
+ if (searchValue.length > 0) {
+ updateDropdownPosition();
+ activeSearch.refetch();
+ setShowDropdown(true);
+ }
+ };
+
+ const handleResultClick = (match) => {
+ const itemData = match.Data || {};
+ const tenantDomain = match.Tenant || "";
+ if (searchType === "Users") {
+ router.push(
+ `/identity/administration/users/user?tenantFilter=${tenantDomain}&userId=${itemData.id}`,
+ );
+ } else if (searchType === "Groups") {
+ router.push(
+ `/identity/administration/groups/group?groupId=${itemData.id}&tenantFilter=${tenantDomain}`,
+ );
+ }
+ setShowDropdown(false);
+ };
+
+ 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);
+ };
+
+ const typeMenuActions = [
+ {
+ label: "Users",
+ icon: "UsersIcon",
+ onClick: () => handleTypeChange("Users"),
+ },
+ {
+ label: "Groups",
+ 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
+ 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 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 = () => {
+ if (searchType === "Users") {
+ 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";
+ };
+
+ return (
+ <>
+
+
+ {searchType === "BitLocker" && (
+
+ )}
+ {
+ textFieldRef.current = node;
+ if (typeof ref === "function") {
+ ref(node);
+ } else if (ref) {
+ ref.current = node;
+ }
+ }}
+ fullWidth
+ type="text"
+ label={getLabel()}
+ onKeyDown={handleKeyDown}
+ onChange={handleChange}
+ value={searchValue}
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ endAdornment: activeSearch.isFetching ? (
+
+
+
+ ) : null,
+ sx: {
+ "& .MuiInputAdornment-root": {
+ marginTop: "0 !important",
+ alignSelf: "center",
+ },
+ },
+ }}
+ />
+ }
+ sx={{ flexShrink: 0 }}
+ >
+ Search
+
+
+
+ {shouldShowDropdown && (
+
+
+ {activeSearch.isFetching ? (
+
+
+
+
+ ) : hasResults ? (
+ searchType === "BitLocker" ? (
+
+ ) : (
+
+ )
+ ) : (
+
+
+ No results found.
+
+
+ )}
+
+
+ )}
+
+ setBitlockerDrawerVisible(false)}
+ size="xl"
+ contentPadding={0}
+ >
+
+
+ >
+ );
+ },
+);
+
+CippUniversalSearchV2.displayName = "CippUniversalSearchV2";
+
+const Results = ({ items = [], searchValue, onResultClick, searchType = "Users" }) => {
+ 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
+ ),
+ );
+ };
+
+ return (
+ <>
+ {items.map((match, index) => {
+ const itemData = match.Data || {};
+ const tenantDomain = match.Tenant || "";
+
+ return (
+
+ );
+ })}
+ >
+ );
+};
+
+const BitlockerResults = ({ items = [], onResultClick }) => {
+ return (
+ <>
+ {items.map((result, index) => (
+
+ ))}
+ >
+ );
+};
diff --git a/src/components/CippCards/CippUserInfoCard.jsx b/src/components/CippCards/CippUserInfoCard.jsx
index 43e4d6d26cb2..6814a6e32790 100644
--- a/src/components/CippCards/CippUserInfoCard.jsx
+++ b/src/components/CippCards/CippUserInfoCard.jsx
@@ -18,6 +18,7 @@ import { getCippFormatting } from "../../utils/get-cipp-formatting";
import { Stack, Grid, Box } from "@mui/system";
import { useState, useRef, useCallback } from "react";
import { ApiPostCall } from "../../api/ApiCall";
+import { useLicenseBackfill } from "../../hooks/use-license-backfill";
export const CippUserInfoCard = (props) => {
const { user, tenant, isFetching = false, ...other } = props;
@@ -25,6 +26,9 @@ export const CippUserInfoCard = (props) => {
const [uploadError, setUploadError] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
const fileInputRef = useRef(null);
+
+ // Hook to trigger re-render when license backfill completes
+ const { updateTrigger } = useLicenseBackfill();
// API mutations
const setPhotoMutation = ApiPostCall({ urlFromData: true });
@@ -280,6 +284,7 @@ export const CippUserInfoCard = (props) => {
diff --git a/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx b/src/components/CippComponents/CippAddTenantAllowBlockListDrawer.jsx
index 23fbe252ae55..182f897966aa 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";
@@ -67,7 +67,7 @@ export const CippAddTenantAllowBlockListDrawer = ({
formControl.setValue(
"listMethod",
{ label: "Block", value: "Block" },
- { shouldValidate: true }
+ { shouldValidate: true },
);
}
@@ -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;
@@ -273,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"
+ : ""
}
/>
diff --git a/src/components/CippComponents/CippAddUserDrawer.jsx b/src/components/CippComponents/CippAddUserDrawer.jsx
index 6e8e333b317f..297fa91dd58f 100644
--- a/src/components/CippComponents/CippAddUserDrawer.jsx
+++ b/src/components/CippComponents/CippAddUserDrawer.jsx
@@ -18,7 +18,7 @@ export const CippAddUserDrawer = ({
const userSettingsDefaults = useSettings();
const formControl = useForm({
- mode: "onBlur",
+ mode: "onChange",
defaultValues: {
tenantFilter: userSettingsDefaults.currentTenant,
usageLocation: userSettingsDefaults.usageLocation,
@@ -52,22 +52,36 @@ 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]);
- const handleSubmit = () => {
- formControl.trigger();
- if (!isValid) {
+ const handleSubmit = async () => {
+ const isFormValid = await formControl.trigger();
+ if (!isFormValid) {
return;
}
const values = formControl.getValues();
@@ -84,17 +98,40 @@ 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);
+ };
+
+ const handleOpenDrawer = () => {
+ const resetValues = {
+ tenantFilter: userSettingsDefaults.currentTenant,
+ usageLocation: userSettingsDefaults.usageLocation,
+ };
+
+ const currentTemplate = formControl.getValues("userTemplate");
+ if (currentTemplate?.addedFields?.defaultForTenant) {
+ resetValues.userTemplate = currentTemplate;
+ }
+
+ formControl.reset(resetValues);
+ setDrawerVisible(true);
};
return (
<>
setDrawerVisible(true)}
+ onClick={handleOpenDrawer}
startIcon={}
>
{buttonText}
@@ -117,8 +154,8 @@ export const CippAddUserDrawer = ({
{createUser.isPending
? "Creating User..."
: createUser.isSuccess
- ? "Create Another User"
- : "Create User"}
+ ? "Create Another User"
+ : "Create User"}
@@ -446,8 +450,8 @@ export const CippApplicationDeployDrawer = ({
}
multiple={false}
formControl={formControl}
- disabled={winGetSearchResults.isLoading}
- isFetching={winGetSearchResults.isLoading}
+ disabled={winGetSearchResults.isPending}
+ isFetching={winGetSearchResults.isPending}
/>
@@ -541,6 +545,7 @@ export const CippApplicationDeployDrawer = ({
onClick={() => {
searchApp(formControl.getValues("searchQuery"), "choco");
}}
+ disabled={ChocosearchResults.isPending}
>
Search
@@ -561,7 +566,7 @@ export const CippApplicationDeployDrawer = ({
}
multiple={false}
formControl={formControl}
- isFetching={ChocosearchResults.isLoading}
+ isFetching={ChocosearchResults.isPending}
/>
@@ -811,6 +816,144 @@ export const CippApplicationDeployDrawer = ({
+ {/* Win32 Script App Section */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Assign To Options */}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx
index ea5d9811bda8..a27bbe535e8a 100644
--- a/src/components/CippComponents/CippAutocomplete.jsx
+++ b/src/components/CippComponents/CippAutocomplete.jsx
@@ -9,7 +9,8 @@ import {
Box,
Typography,
} from "@mui/material";
-import { useEffect, useState, useMemo, useCallback, useRef } from "react";
+import Link from "next/link";
+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";
@@ -56,7 +57,7 @@ const MemoTextField = React.memo(function MemoTextField({
);
});
-export const CippAutoComplete = (props) => {
+export const CippAutoComplete = React.forwardRef((props, ref) => {
const {
size,
api,
@@ -80,6 +81,8 @@ export const CippAutoComplete = (props) => {
preselectedValue,
groupBy,
renderGroup,
+ customAction,
+ handleHomeEndKeys = false,
...other
} = props;
@@ -87,6 +90,16 @@ 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({
stringify: (option) => JSON.stringify(option),
});
@@ -183,10 +196,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 +208,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 +312,7 @@ export const CippAutoComplete = (props) => {
const foundOption = memoizedOptions.find((option) => option.value === value);
return foundOption || { label: value, value: value };
},
- [memoizedOptions]
+ [memoizedOptions],
);
return (
@@ -307,6 +320,7 @@ export const CippAutoComplete = (props) => {
setOpen(true)}
onClose={(event, reason) => {
@@ -336,7 +350,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,16 +369,21 @@ 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) => {
+ // Store scroll position before processing the change
+ if (multiple && listboxRef.current) {
+ scrollPositionRef.current = listboxRef.current.scrollTop;
+ }
+
if (Array.isArray(newValue)) {
newValue = newValue.map((item) => {
// If user typed a new item or missing label
@@ -381,7 +400,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) {
@@ -405,7 +424,7 @@ export const CippAutoComplete = (props) => {
onChange(newValue, newValue?.addedFields);
}
- // In multiple mode, refocus the input after selection to allow continuous adding
+ // In multiple mode, refocus the input and restore scroll position
if (multiple && newValue && autocompleteRef.current) {
// Use setTimeout to ensure the selection is processed first
setTimeout(() => {
@@ -413,6 +432,11 @@ export const CippAutoComplete = (props) => {
if (input) {
input.focus();
}
+
+ // Restore the scroll position
+ if (listboxRef.current && scrollPositionRef.current > 0) {
+ listboxRef.current.scrollTop = scrollPositionRef.current;
+ }
}, 0);
}
}}
@@ -433,7 +457,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,98 +485,178 @@ 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();
+ customAction.onClick(value || internalValue);
+ }
+ : (e) => e.stopPropagation()
+ }
+ sx={{
+ opacity: 0,
+ transition: "all 0.2s",
+ p: "4px",
+ mr: "-4px",
+ mt: -1,
+ cursor: "pointer",
+ color: "inherit",
+ textDecoration: "none",
+ "&: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}
+ slotProps={{
+ listbox: {
+ ref: listboxRef,
+ onScroll: (e) => {
+ if (listboxRef.current) {
+ scrollPositionRef.current = e.target.scrollTop;
+ }
+ },
+ },
+ }}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
@@ -586,4 +690,5 @@ export const CippAutoComplete = (props) => {
)}
>
);
-};
+});
+CippAutoComplete.displayName = "CippAutoComplete";
diff --git a/src/components/CippComponents/CippBitlockerKeySearch.jsx b/src/components/CippComponents/CippBitlockerKeySearch.jsx
new file mode 100644
index 000000000000..90c643e1dd74
--- /dev/null
+++ b/src/components/CippComponents/CippBitlockerKeySearch.jsx
@@ -0,0 +1,288 @@
+import { useEffect, useRef, useState } from "react";
+import {
+ Button,
+ Box,
+ Typography,
+ Skeleton,
+ Grid,
+ Paper,
+ Divider,
+ Chip,
+ Alert,
+ CircularProgress,
+} from "@mui/material";
+import { VpnKey, Computer, CheckCircle, Cancel, Info, Key } from "@mui/icons-material";
+import { ApiGetCall, ApiPostCall } from "../../api/ApiCall";
+import { CippCopyToClipBoard } from "./CippCopyToClipboard";
+
+const getVolumeTypeLabel = (volumeType) => {
+ const types = {
+ 0: "Operating System Volume",
+ 1: "Fixed Data Volume",
+ 2: "Removable Data Volume",
+ 3: "Unknown",
+ };
+ return types[volumeType] || `Type ${volumeType}`;
+};
+
+export const CippBitlockerKeySearch = ({
+ initialSearchTerm = "",
+ initialSearchType = "keyId",
+ autoSearch = false,
+}) => {
+ const searchTerm = initialSearchTerm;
+ const searchType = initialSearchType || "keyId";
+ const hasAutoSearched = useRef(false);
+
+ // 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 },
+ queryKey: `bitlocker-${searchType}-${searchTerm}`,
+ waiting: false,
+ });
+ const { data, isSuccess, isFetching, refetch } = getBitlockerKeys;
+ const isLoading = isFetching;
+
+ useEffect(() => {
+ hasAutoSearched.current = false;
+ }, [initialSearchTerm, initialSearchType]);
+
+ useEffect(() => {
+ if (autoSearch && searchTerm && !hasAutoSearched.current) {
+ refetch();
+ hasAutoSearched.current = true;
+ }
+ }, [autoSearch, refetch, searchTerm]);
+
+ const results = data?.Results || [];
+
+ const content = (
+
+ {isLoading && (
+
+
+
+ )}
+
+ {isSuccess && (
+ <>
+
+ {results.map((result, index) => (
+
+
+ {/* BitLocker Key Information */}
+
+
+
+ BitLocker Key Information
+
+
+
+
+
+ Key ID
+
+
+
+ {result.keyId || "N/A"}
+
+
+
+
+
+
+ Volume Type
+
+
+
+
+
+
+ Created
+
+
+ {result.createdDateTime
+ ? new Date(result.createdDateTime).toLocaleString()
+ : "N/A"}
+
+
+
+
+
+ Tenant
+
+ {result.tenant || "N/A"}
+
+
+
+
+ Recovery Key
+
+
+ {recoveryKeys[result.keyId] ? (
+ <>
+
+ {recoveryKeys[result.keyId]}
+
+
+ >
+ ) : (
+ :
+ }
+ onClick={() =>
+ handleRetrieveKey(result.keyId, result.deviceId, result.tenant)
+ }
+ disabled={loadingKeys[result.keyId] || !result.keyId || !result.deviceId}
+ >
+ Retrieve Key
+
+ )}
+
+
+
+ {/* Device Information */}
+ {result.deviceFound && (
+ <>
+
+
+
+
+ Device Information
+
+
+
+
+
+ Device Name
+
+ {result.deviceName || "N/A"}
+
+
+
+
+ Device ID
+
+
+
+ {result.deviceId || "N/A"}
+
+
+
+
+
+
+ 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.
+
+
+ )}
+
+
+ ))}
+
+ >
+ )}
+
+ );
+ return content;
+};
+
+export default CippBitlockerKeySearch;
diff --git a/src/components/CippComponents/CippCodeBlock.jsx b/src/components/CippComponents/CippCodeBlock.jsx
index 507a26667bbd..2896c6cb8db8 100644
--- a/src/components/CippComponents/CippCodeBlock.jsx
+++ b/src/components/CippComponents/CippCodeBlock.jsx
@@ -31,6 +31,7 @@ export const CippCodeBlock = (props) => {
wrapLongLines = true,
type = "syntax",
editorHeight = "500px",
+ readOnly = false,
...other
} = props;
const [codeCopied, setCodeCopied] = useState(false);
@@ -48,13 +49,14 @@ export const CippCodeBlock = (props) => {
{type === "editor" && (
diff --git a/src/components/CippComponents/CippIntunePolicyActions.jsx b/src/components/CippComponents/CippIntunePolicyActions.jsx
new file mode 100644
index 000000000000..531245e94625
--- /dev/null
+++ b/src/components/CippComponents/CippIntunePolicyActions.jsx
@@ -0,0 +1,238 @@
+import { Book, LaptopChromebook } from "@mui/icons-material";
+import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline";
+
+const assignmentModeOptions = [
+ { label: "Replace existing assignments", value: "replace" },
+ { label: "Append to existing assignments", value: "append" },
+];
+
+const assignmentFilterTypeOptions = [
+ { label: "Include - Apply policy to devices matching filter", value: "include" },
+ { label: "Exclude - Apply policy to devices NOT matching filter", value: "exclude" },
+];
+
+/**
+ * Get assignment actions for Intune policies
+ * @param {string} tenant - The tenant filter
+ * @param {string} policyType - The policy type (URLName, deviceCompliancePolicies, etc.)
+ * @param {object} options - Additional options
+ * @param {string} options.platformType - Platform type for app protection policies (deviceAppManagement)
+ * @param {boolean} options.includeCreateTemplate - Whether to include create template action (default: true)
+ * @param {boolean} options.includeDelete - Whether to include delete action (default: true)
+ * @param {string} options.deleteUrlName - URLName for delete action (default: same as policyType)
+ * @param {object} options.templateData - Data for template creation
+ * @returns {Array} Array of action objects
+ */
+export const useCippIntunePolicyActions = (tenant, policyType, options = {}) => {
+ const {
+ platformType = null,
+ includeCreateTemplate = true,
+ includeDelete = true,
+ deleteUrlName = policyType,
+ templateData = null,
+ } = options;
+
+ const getAssignmentFields = () => [
+ {
+ type: "radio",
+ name: "assignmentMode",
+ label: "Assignment mode",
+ options: assignmentModeOptions,
+ defaultValue: "replace",
+ 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.",
+ },
+ ];
+
+ const getCustomDataFormatter = (assignTo) => (row, action, formData) => {
+ const rows = Array.isArray(row) ? row : [row];
+ return rows.map((item) => ({
+ tenantFilter: tenant === "AllTenants" && item?.Tenant ? item.Tenant : tenant,
+ ID: item?.id,
+ type: item?.URLName || policyType,
+ ...(platformType && { platformType }),
+ AssignTo: assignTo,
+ assignmentMode: formData?.assignmentMode || "replace",
+ AssignmentFilterName: formData?.assignmentFilter?.value || null,
+ AssignmentFilterType: formData?.assignmentFilter?.value
+ ? formData?.assignmentFilterType || "include"
+ : null,
+ }));
+ };
+
+ const getCustomDataFormatterForGroups = () => (row, action, formData) => {
+ const rows = Array.isArray(row) ? row : [row];
+ const selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : [];
+ return rows.map((item) => ({
+ tenantFilter: tenant === "AllTenants" && item?.Tenant ? item.Tenant : tenant,
+ ID: item?.id,
+ type: item?.URLName || policyType,
+ ...(platformType && { platformType }),
+ GroupIds: selectedGroups.map((group) => group.value).filter(Boolean),
+ GroupNames: selectedGroups.map((group) => group.label).filter(Boolean),
+ assignmentMode: formData?.assignmentMode || "replace",
+ AssignmentFilterName: formData?.assignmentFilter?.value || null,
+ AssignmentFilterType: formData?.assignmentFilter?.value
+ ? formData?.assignmentFilterType || "include"
+ : null,
+ }));
+ };
+
+ const actions = [];
+
+ // Create template action
+ if (includeCreateTemplate) {
+ actions.push({
+ label: "Create template based on policy",
+ type: "POST",
+ url: "/api/AddIntuneTemplate",
+ data: templateData || {
+ ID: "id",
+ URLName: policyType === "URLName" ? "URLName" : policyType,
+ },
+ confirmText: "Are you sure you want to create a template based on this policy?",
+ icon: ,
+ color: "info",
+ multiPost: false,
+ });
+ }
+
+ // Assign to All Users
+ actions.push({
+ label: "Assign to All Users",
+ type: "POST",
+ url: "/api/ExecAssignPolicy",
+ data: {
+ AssignTo: "allLicensedUsers",
+ ID: "id",
+ type: policyType === "URLName" ? "URLName" : policyType,
+ ...(platformType && { platformType: "!deviceAppManagement" }),
+ },
+ multiPost: false,
+ fields: getAssignmentFields(),
+ customDataformatter: getCustomDataFormatter("allLicensedUsers"),
+ confirmText: 'Are you sure you want to assign "[displayName]" to all users?',
+ icon: ,
+ color: "info",
+ });
+
+ // Assign to All Devices
+ actions.push({
+ label: "Assign to All Devices",
+ type: "POST",
+ url: "/api/ExecAssignPolicy",
+ data: {
+ AssignTo: "AllDevices",
+ ID: "id",
+ type: policyType === "URLName" ? "URLName" : policyType,
+ ...(platformType && { platformType: "!deviceAppManagement" }),
+ },
+ multiPost: false,
+ fields: getAssignmentFields(),
+ customDataformatter: getCustomDataFormatter("AllDevices"),
+ confirmText: 'Are you sure you want to assign "[displayName]" to all devices?',
+ icon: ,
+ color: "info",
+ });
+
+ // Assign Globally (All Users / All Devices)
+ actions.push({
+ label: "Assign Globally (All Users / All Devices)",
+ type: "POST",
+ url: "/api/ExecAssignPolicy",
+ data: {
+ AssignTo: "AllDevicesAndUsers",
+ ID: "id",
+ type: policyType === "URLName" ? "URLName" : policyType,
+ ...(platformType && { platformType: "!deviceAppManagement" }),
+ },
+ multiPost: false,
+ fields: getAssignmentFields(),
+ customDataformatter: getCustomDataFormatter("AllDevicesAndUsers"),
+ confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?',
+ icon: ,
+ color: "info",
+ });
+
+ // Assign to Custom Group
+ actions.push({
+ label: "Assign to Custom Group",
+ type: "POST",
+ url: "/api/ExecAssignPolicy",
+ icon: ,
+ color: "info",
+ confirmText: 'Select the target groups for "[displayName]".',
+ multiPost: false,
+ fields: [
+ {
+ type: "autoComplete",
+ name: "groupTargets",
+ label: "Group(s)",
+ multiple: true,
+ creatable: false,
+ allowResubmit: true,
+ validators: { required: "Please select at least one group" },
+ api: {
+ url: "/api/ListGraphRequest",
+ dataKey: "Results",
+ queryKey: `ListPolicyAssignmentGroups-${tenant}`,
+ labelField: (group) =>
+ group.id ? `${group.displayName} (${group.id})` : group.displayName,
+ valueField: "id",
+ addedField: {
+ description: "description",
+ },
+ data: {
+ Endpoint: "groups",
+ manualPagination: true,
+ $select: "id,displayName,description",
+ $orderby: "displayName",
+ $top: 999,
+ $count: true,
+ },
+ },
+ },
+ ...getAssignmentFields(),
+ ],
+ customDataformatter: getCustomDataFormatterForGroups(),
+ });
+
+ // Delete action
+ if (includeDelete) {
+ actions.push({
+ label: "Delete Policy",
+ type: "POST",
+ url: "/api/RemovePolicy",
+ data: {
+ ID: "id",
+ URLName: deleteUrlName === "URLName" ? "URLName" : deleteUrlName,
+ },
+ confirmText: "Are you sure you want to delete this policy?",
+ icon: ,
+ color: "danger",
+ });
+ }
+
+ return actions;
+};
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/components/CippComponents/CippMessageViewer.jsx b/src/components/CippComponents/CippMessageViewer.jsx
index e91530db384b..557f63daa7af 100644
--- a/src/components/CippComponents/CippMessageViewer.jsx
+++ b/src/components/CippComponents/CippMessageViewer.jsx
@@ -69,9 +69,7 @@ export const CippMessageViewer = ({ emailSource }) => {
const currentTheme = useSettings()?.currentTheme?.value;
const [darkMode, setDarkMode] = useState(currentTheme === "dark");
- const emailStyle = (
-
- );
+ const emailStyle = ;
const theme = createTheme({
palette: {
@@ -133,7 +131,7 @@ export const CippMessageViewer = ({ emailSource }) => {
fileBytes = new Uint8Array(
atob(attachment.data64)
.split("")
- .map((c) => c.charCodeAt(0))
+ .map((c) => c.charCodeAt(0)),
);
}
@@ -163,7 +161,12 @@ export const CippMessageViewer = ({ emailSource }) => {
} else if (contentType.includes("text")) {
const textContent = fileBytes;
setDialogContent(
-
+ ,
);
setDialogTitle(fileName);
setDialogOpen(true);
@@ -188,7 +191,9 @@ export const CippMessageViewer = ({ emailSource }) => {
}
const showEmailModal = (emailSource, title = "Email Source") => {
- setDialogContent();
+ setDialogContent(
+ ,
+ );
setDialogTitle(title);
setDialogOpen(true);
};
@@ -276,7 +281,7 @@ export const CippMessageViewer = ({ emailSource }) => {
return React.cloneElement(element, {
children: React.Children.map(
element.props.children,
- replaceCidWithBase64
+ replaceCidWithBase64,
),
});
}
@@ -354,10 +359,10 @@ export const CippMessageViewer = ({ emailSource }) => {
const color = noResults
? ""
: allPass
- ? "green"
- : somePass
- ? "orange"
- : "red";
+ ? "green"
+ : somePass
+ ? "orange"
+ : "red";
const icon = noResults ? (
) : allPass ? (
@@ -377,8 +382,8 @@ export const CippMessageViewer = ({ emailSource }) => {
allPass
? "All authentication checks successful"
: somePass
- ? "Some authentication checks failed"
- : "None of the authentication checks passed"
+ ? "Some authentication checks failed"
+ : "None of the authentication checks passed"
} - DMARC: ${dmarcPass ? "pass" : "fail"}, DKIM: ${
dkimPass ? "pass" : "fail"
}, SPF: ${spfPass ? "pass" : "fail"}, ARC: ${
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/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/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",
+ }}
+ />
+
+
+
+
+
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/components/CippComponents/CippSponsor.jsx b/src/components/CippComponents/CippSponsor.jsx
new file mode 100644
index 000000000000..6db3086f6e75
--- /dev/null
+++ b/src/components/CippComponents/CippSponsor.jsx
@@ -0,0 +1,97 @@
+import { useMemo } from "react";
+import { Box, Divider, Tooltip, Typography } from "@mui/material";
+import { useSettings } from "../../hooks/use-settings";
+import sponsorsData from "../../data/sponsors.json";
+
+// Filter sponsors by date (runs once on module load)
+const getActiveSponsors = () => {
+ const now = new Date();
+ return sponsorsData.filter((sponsor) => {
+ if (!sponsor.startDate && !sponsor.endDate) {
+ return true;
+ }
+ const startDate = sponsor.startDate ? new Date(sponsor.startDate) : null;
+ const endDate = sponsor.endDate ? new Date(sponsor.endDate) : null;
+ const afterStart = !startDate || now >= startDate;
+ const beforeEnd = !endDate || now <= endDate;
+ return afterStart && beforeEnd;
+ });
+};
+
+// Select random sponsor based on priority (runs once on module load)
+const selectRandomSponsor = (sponsors) => {
+ if (sponsors.length === 0) return null;
+
+ let totalPriority = 0;
+ for (let i = 0; i < sponsors.length; i++) {
+ totalPriority += sponsors[i].priority;
+ }
+ let random = Math.floor(Math.random() * totalPriority);
+ let runningTotal = 0;
+ for (let i = 0; i < sponsors.length; i++) {
+ runningTotal += sponsors[i].priority;
+ if (random < runningTotal) {
+ return sponsors[i];
+ }
+ }
+ return null;
+};
+
+const activeSponsors = getActiveSponsors();
+const selectedSponsor = selectRandomSponsor(activeSponsors);
+
+export const CippSponsor = () => {
+ const currentSettings = useSettings();
+ const theme = currentSettings?.currentTheme?.value;
+
+ // Get the appropriate image based on current theme
+ const randomimg = useMemo(() => {
+ if (!selectedSponsor) return null;
+ return {
+ link: selectedSponsor.link,
+ imagesrc: theme === "light" ? selectedSponsor.imagesrcLight : selectedSponsor.imagesrcDark,
+ altText: selectedSponsor.altText,
+ tooltip: selectedSponsor.tooltip,
+ };
+ }, [theme]);
+
+ // Don't render if no sponsors are available
+ if (!randomimg) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ This application is sponsored by
+
+
+
+
window.open(randomimg.link)}
+ />
+
+
+ >
+ );
+};
diff --git a/src/components/CippComponents/CippTenantLookup.jsx b/src/components/CippComponents/CippTenantLookup.jsx
new file mode 100644
index 000000000000..a1f8b3680cfc
--- /dev/null
+++ b/src/components/CippComponents/CippTenantLookup.jsx
@@ -0,0 +1,415 @@
+import React, { useState, useEffect } from "react";
+import {
+ Box,
+ Button,
+ Typography,
+ Skeleton,
+ Chip,
+ Grid,
+ Paper,
+ Divider,
+ useTheme,
+ TextField,
+ InputAdornment,
+} from "@mui/material";
+import {
+ Search,
+ Public,
+ Language,
+ LocationOn,
+ Cloud,
+} from "@mui/icons-material";
+import { useForm, useWatch } from "react-hook-form";
+import CippButtonCard from "../CippCards/CippButtonCard";
+import { ApiGetCall } from "../../api/ApiCall";
+import { CippCopyToClipBoard } from "./CippCopyToClipboard";
+
+// Region icon mapping
+const getRegionIcon = (region) => {
+ const regionUpper = region?.toUpperCase();
+ switch (regionUpper) {
+ case "EU":
+ return ;
+ case "US":
+ return ;
+ case "ASIA":
+ return ;
+ case "GCC":
+ case "GCC-HIGH":
+ return ;
+ case "DE":
+ return ;
+ case "CN":
+ return ;
+ default:
+ return ;
+ }
+};
+
+// Region color mapping
+const getRegionColor = (region) => {
+ const regionUpper = region?.toUpperCase();
+ switch (regionUpper) {
+ case "EU":
+ return "primary";
+ case "US":
+ return "success";
+ case "ASIA":
+ return "warning";
+ case "GCC":
+ case "GCC-HIGH":
+ return "info";
+ case "DE":
+ return "secondary";
+ case "CN":
+ return "error";
+ default:
+ return "default";
+ }
+};
+
+export const CippTenantLookup = () => {
+ 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,
+ });
+
+ const theme = useTheme();
+ const tenantData = getTenant.data;
+ const graphData = tenantData?.GraphRequest;
+ const openIdData = tenantData?.OpenIdConfig;
+ const brandingData = tenantData?.UserTenantBranding?.[0];
+ const [illustrationUrl, setIllustrationUrl] = useState(null);
+ const [tileLogoUrl, setTileLogoUrl] = useState(null);
+
+ // Fetch illustration as blob and convert to object URL
+ useEffect(() => {
+ let currentObjectUrl = null;
+
+ if (brandingData?.Illustration && typeof brandingData.Illustration === "string" && brandingData.Illustration.trim() !== "") {
+ const fetchIllustration = async () => {
+ try {
+ const response = await fetch(brandingData.Illustration);
+ if (response.ok && response.headers.get("content-type")?.startsWith("image/")) {
+ const blob = await response.blob();
+ if (blob.size > 0) {
+ currentObjectUrl = URL.createObjectURL(blob);
+ setIllustrationUrl(currentObjectUrl);
+ } else {
+ setIllustrationUrl(null);
+ }
+ } else {
+ setIllustrationUrl(null);
+ }
+ } catch (error) {
+ console.error("Failed to fetch illustration:", error);
+ setIllustrationUrl(null);
+ }
+ };
+ fetchIllustration();
+ } else {
+ setIllustrationUrl(null);
+ }
+
+ // Cleanup: revoke object URL when component unmounts or illustration changes
+ return () => {
+ if (currentObjectUrl) {
+ URL.revokeObjectURL(currentObjectUrl);
+ }
+ };
+ }, [brandingData?.Illustration]);
+
+ // Cleanup illustration URL on unmount
+ useEffect(() => {
+ return () => {
+ if (illustrationUrl) {
+ URL.revokeObjectURL(illustrationUrl);
+ }
+ };
+ }, [illustrationUrl]);
+
+ // Fetch tile logo as blob and convert to object URL (respects theme, falls back to available logo)
+ useEffect(() => {
+ let currentObjectUrl = null;
+ const isDarkMode = theme.palette.mode === "dark";
+
+ // Determine which logo to use: prefer theme-appropriate, but fall back to whichever is available
+ let logoUrl = null;
+ if (isDarkMode) {
+ logoUrl = brandingData?.TileDarkLogo || brandingData?.TileLogo;
+ } else {
+ logoUrl = brandingData?.TileLogo || brandingData?.TileDarkLogo;
+ }
+
+ if (logoUrl && typeof logoUrl === "string" && logoUrl.trim() !== "") {
+ const fetchLogo = async () => {
+ try {
+ const response = await fetch(logoUrl);
+ if (response.ok && response.headers.get("content-type")?.startsWith("image/")) {
+ const blob = await response.blob();
+ if (blob.size > 0) {
+ currentObjectUrl = URL.createObjectURL(blob);
+ setTileLogoUrl(currentObjectUrl);
+ } else {
+ setTileLogoUrl(null);
+ }
+ } else {
+ setTileLogoUrl(null);
+ }
+ } catch (error) {
+ console.error("Failed to fetch tile logo:", error);
+ setTileLogoUrl(null);
+ }
+ };
+ fetchLogo();
+ } else {
+ setTileLogoUrl(null);
+ }
+
+ // Cleanup: revoke object URL when component unmounts or logo changes
+ return () => {
+ if (currentObjectUrl) {
+ URL.revokeObjectURL(currentObjectUrl);
+ }
+ };
+ }, [brandingData?.TileLogo, brandingData?.TileDarkLogo, theme.palette.mode]);
+
+ // Cleanup tile logo URL on unmount
+ useEffect(() => {
+ return () => {
+ if (tileLogoUrl) {
+ URL.revokeObjectURL(tileLogoUrl);
+ }
+ };
+ }, [tileLogoUrl]);
+
+ return (
+
+
+ {/* Search Section */}
+
+ {
+ e.preventDefault();
+ if (domain && !getTenant.isFetching) {
+ getTenant.refetch();
+ }
+ }}
+ sx={{ width: "100%", maxWidth: "600px", display: "flex", gap: 1 }}
+ >
+ formControl.setValue("domain", e.target.value)}
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ sx: {
+ "& .MuiInputAdornment-root": {
+ marginTop: "0 !important",
+ alignSelf: "center",
+ },
+ },
+ }}
+ />
+
+
+
+
+ {/* 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/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/components/CippComponents/CippTransportRuleDrawer.jsx b/src/components/CippComponents/CippTransportRuleDrawer.jsx
index 494ae0fdce1a..54d69cd258f5 100644
--- a/src/components/CippComponents/CippTransportRuleDrawer.jsx
+++ b/src/components/CippComponents/CippTransportRuleDrawer.jsx
@@ -143,6 +143,7 @@ export const CippTransportRuleDrawer = ({
DeleteMessage: "Delete the message without notifying anyone",
Quarantine: "Quarantine the message",
RedirectMessageTo: "Redirect the message to...",
+ RouteMessageOutboundConnector: "Route the message using the connector named...",
BlindCopyTo: "Add recipients to the Bcc box...",
CopyTo: "Add recipients to the Cc box...",
ModerateMessageByUser: "Forward the message for approval to...",
@@ -206,7 +207,7 @@ export const CippTransportRuleDrawer = ({
// Build form data
const formData = {
Name: rule.Name || "",
- Priority: rule.Priority || "",
+ Priority: rule.Priority ?? "",
Comments: rule.Comments || "",
Enabled: boolHelper(rule.State),
Mode: rule.Mode ? { value: rule.Mode, label: rule.Mode } : { value: "Enforce", label: "Enforce" },
@@ -294,6 +295,8 @@ export const CippTransportRuleDrawer = ({
if (rule[field] !== null && rule[field] !== undefined && !formData[field]) {
if (field === "SetSCL" && rule[field] !== null) {
formData[field] = { value: rule[field].toString(), label: rule[field].toString() };
+ } else if (field === "RouteMessageOutboundConnector") {
+ formData[field] = { value: rule[field], label: rule[field] };
} else {
formData[field] = rule[field];
}
@@ -667,6 +670,7 @@ export const CippTransportRuleDrawer = ({
{ value: "DeleteMessage", label: "Delete the message without notifying anyone" },
{ value: "Quarantine", label: "Quarantine the message" },
{ value: "RedirectMessageTo", label: "Redirect the message to..." },
+ { value: "RouteMessageOutboundConnector", label: "Route the message using the connector named..." },
{ value: "BlindCopyTo", label: "Add recipients to the Bcc box..." },
{ value: "CopyTo", label: "Add recipients to the Cc box..." },
{ value: "ModerateMessageByUser", label: "Forward the message for approval to..." },
@@ -704,6 +708,7 @@ export const CippTransportRuleDrawer = ({
creatable={true}
api={{
url: "/api/ListGraphRequest",
+ queryKey: `Users-TransportRules-${currentTenant}`,
data: {
Endpoint: "users",
tenantFilter: currentTenant,
@@ -734,6 +739,7 @@ export const CippTransportRuleDrawer = ({
creatable={true}
api={{
url: "/api/ListGraphRequest",
+ queryKey: `Groups-TransportRules-${currentTenant}`,
data: {
Endpoint: "groups",
tenantFilter: currentTenant,
@@ -950,6 +956,7 @@ export const CippTransportRuleDrawer = ({
multiple={true}
api={{
url: "/api/ListGraphRequest",
+ queryKey: `Users-TransportRules-${currentTenant}`,
data: {
Endpoint: "users",
tenantFilter: currentTenant,
@@ -964,6 +971,28 @@ export const CippTransportRuleDrawer = ({
);
+ case "RouteMessageOutboundConnector":
+ return (
+
+ `${option.Name}`,
+ valueField: "Name",
+ dataFilter: (options) =>
+ options.filter((option) => option.rawData?.cippconnectortype === "outbound"),
+ }}
+ />
+
+ );
+
case "SetSCL":
return (
@@ -1260,6 +1289,7 @@ export const CippTransportRuleDrawer = ({
type="number"
label="Priority"
name="Priority"
+ required
formControl={formControl}
placeholder="0 (lowest priority)"
/>
@@ -1484,4 +1514,4 @@ export const CippTransportRuleDrawer = ({
>
);
-};
\ No newline at end of file
+};
diff --git a/src/components/CippComponents/MailboxRestoreDetails.jsx b/src/components/CippComponents/MailboxRestoreDetails.jsx
index 23feefa76f76..cd3eedff4091 100644
--- a/src/components/CippComponents/MailboxRestoreDetails.jsx
+++ b/src/components/CippComponents/MailboxRestoreDetails.jsx
@@ -91,6 +91,7 @@ const MailboxRestoreDetails = ({ data }) => {
open={dialogOpen}
onClose={() => setDialogOpen(false)}
code={restoreStatistics?.data?.[0]?.Report}
+ readOnly={true}
/>
diff --git a/src/components/CippComponents/TenantMetricsGrid.jsx b/src/components/CippComponents/TenantMetricsGrid.jsx
index b8b0cfacc272..323bd44a7f9f 100644
--- a/src/components/CippComponents/TenantMetricsGrid.jsx
+++ b/src/components/CippComponents/TenantMetricsGrid.jsx
@@ -1,4 +1,5 @@
import { Box, Grid, Tooltip, Avatar, Typography, Skeleton } from "@mui/material";
+import { useRouter } from "next/router";
import {
Person as UserIcon,
PersonOutline as GuestIcon,
@@ -15,53 +16,71 @@ const formatNumber = (num) => {
};
export const TenantMetricsGrid = ({ data, isLoading }) => {
+ const router = useRouter();
+
const metrics = [
{
label: "Users",
value: data?.UserCount || 0,
icon: UserIcon,
color: "primary",
+ path: "/identity/administration/users",
},
{
label: "Guests",
value: data?.GuestCount || 0,
icon: GuestIcon,
color: "info",
+ path: "/identity/administration/users",
},
{
label: "Groups",
value: data?.GroupCount || 0,
icon: GroupIcon,
color: "secondary",
+ path: "/identity/administration/groups",
},
{
label: "Service Principals",
value: data?.ApplicationCount || 0,
icon: AppsIcon,
color: "error",
+ path: "/tenant/administration/applications/enterprise-apps",
},
{
label: "Devices",
value: data?.DeviceCount || 0,
icon: DevicesIcon,
color: "warning",
+ path: "/identity/administration/devices",
},
{
label: "Managed",
value: data?.ManagedDeviceCount || 0,
icon: ManagedIcon,
color: "success",
+ path: "/identity/administration/devices",
},
];
+ const handleClick = (metric) => {
+ if (metric.path) {
+ router.push(metric.path);
+ }
+ };
+
return (
{metrics.map((metric) => {
const IconComponent = metric.icon;
return (
-
+
handleClick(metric)}
sx={{
display: "flex",
alignItems: "center",
@@ -70,6 +89,14 @@ export const TenantMetricsGrid = ({ data, isLoading }) => {
border: 1,
borderColor: "divider",
borderRadius: 1,
+ cursor: "pointer",
+ transition: "all 0.2s ease-in-out",
+ "&:hover": {
+ borderColor: `${metric.color}.main`,
+ backgroundColor: "action.hover",
+ transform: "translateY(-2px)",
+ boxShadow: "0 4px 8px rgba(0,0,0,0.1)",
+ },
}}
>
{
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 [];
@@ -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
@@ -139,21 +139,33 @@ const CippAddEditUser = (props) => {
const generatedUsername = generateUsername(
formatString,
watcher.givenName,
- watcher.surname
+ watcher.surname,
);
if (generatedUsername) {
- formControl.setValue("username", generatedUsername);
+ formControl.setValue("username", generatedUsername, { shouldDirty: true });
}
}
}
}
}, [watcher.givenName, watcher.surname, selectedTemplate]);
+ // Reset manual flags and selected template when form is reset (fields become empty)
+ useEffect(() => {
+ if (formType === "add" && !watcher.givenName && !watcher.surname && !watcher.userTemplate) {
+ setDisplayNameManuallySet(false);
+ setUsernameManuallySet(false);
+ // Only clear selected template if it's not the default template
+ if (selectedTemplate && !selectedTemplate.defaultForTenant) {
+ setSelectedTemplate(null);
+ }
+ }
+ }, [watcher.givenName, watcher.surname, watcher.userTemplate, formType, selectedTemplate]);
+
// Auto-select default template for tenant
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 +319,8 @@ const CippAddEditUser = (props) => {
onChange={(e) => {
setDisplayNameManuallySet(true);
}}
+ required={true}
+ validators={{ required: "Display Name is required" }}
/>
@@ -322,6 +336,8 @@ const CippAddEditUser = (props) => {
onChange={(e) => {
setUsernameManuallySet(true);
}}
+ required={true}
+ validators={{ required: "Username is required" }}
/>
@@ -573,6 +589,9 @@ const CippAddEditUser = (props) => {
name="setManager"
label="Set Manager"
valueField="userPrincipalName"
+ select={
+ "id,userPrincipalName,displayName,givenName,surname,mailNickname,jobTitle,department,streetAddress,city,state,postalCode,companyName,mobilePhone,businessPhones,usageLocation,office"
+ }
multiple={false}
/>
@@ -583,6 +602,9 @@ const CippAddEditUser = (props) => {
name="setSponsor"
label="Set Sponsor"
valueField="userPrincipalName"
+ select={
+ "id,userPrincipalName,displayName,givenName,surname,mailNickname,jobTitle,department,streetAddress,city,state,postalCode,companyName,mobilePhone,businessPhones,usageLocation,office"
+ }
multiple={false}
/>
@@ -592,6 +614,9 @@ const CippAddEditUser = (props) => {
formControl={formControl}
name="copyFrom"
label="Copy groups from user"
+ select={
+ "id,userPrincipalName,displayName,givenName,surname,mailNickname,jobTitle,department,streetAddress,city,state,postalCode,companyName,mobilePhone,businessPhones,usageLocation,office"
+ }
multiple={false}
/>
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) => {
{
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default CippAddIntuneReusableSettingTemplateForm;
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/CippGDAPResults.jsx b/src/components/CippSettings/CippGDAPResults.jsx
index 89897fd278d0..46d505a4535d 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 (
<>
@@ -57,9 +57,9 @@ export const CippGDAPResults = (props) => {
resultProperty: "Memberships",
matchProperty: "displayName",
match: "^M365 GDAP.+",
- count: 12,
- successMessage: "User is a member of the 12 CIPP Recommended GDAP groups",
- failureMessage: "User is not a member of the 12 CIPP Recommended GDAP groups",
+ count: 15,
+ successMessage: "User is a member of the 15 CIPP Recommended GDAP groups",
+ failureMessage: "User is not a member of the 15 CIPP Recommended GDAP groups",
},
{
resultProperty: "GDAPIssues",
@@ -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/CippLogRetentionSettings.jsx b/src/components/CippSettings/CippLogRetentionSettings.jsx
new file mode 100644
index 000000000000..a45b0c45bea3
--- /dev/null
+++ b/src/components/CippSettings/CippLogRetentionSettings.jsx
@@ -0,0 +1,108 @@
+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 = () => {
+ 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/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..bd8e37000c0e 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"]}
/>
)}
- {results?.Results?.AccessTokenDetails?.Scope.length > 0 && (
+ {results?.Results?.AccessTokenDetails?.Scope?.length > 0 && (
<>
{
+ data={results?.Results?.AccessTokenDetails?.Scope?.map((scope) => {
return {
Scope: scope,
};
@@ -228,13 +233,13 @@ export const CippPermissionResults = (props) => {
/>
>
)}
- {results?.Results?.ApplicationTokenDetails?.Roles.length > 0 && (
+ {results?.Results?.ApplicationTokenDetails?.Roles?.length > 0 && (
<>
{
+ data={results?.Results?.ApplicationTokenDetails?.Roles?.map((role) => {
return {
Role: role,
};
diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx
index 6873936d9cda..ffe959653259 100644
--- a/src/components/CippStandards/CippStandardDialog.jsx
+++ b/src/components/CippStandards/CippStandardDialog.jsx
@@ -646,6 +646,7 @@ const CippStandardDialog = ({
const [selectedRecommendedBy, setSelectedRecommendedBy] = useState([]);
const [selectedTagFrameworks, setSelectedTagFrameworks] = useState([]);
const [showOnlyNew, setShowOnlyNew] = useState(false); // Show only standards added in last 30 days
+ const [statusFilter, setStatusFilter] = useState("all"); // "all" | "enabled" | "disabled"
const [filtersExpanded, setFiltersExpanded] = useState(false); // Control filter section collapse/expand
// Auto-adjust sort order when sort type changes
@@ -823,13 +824,21 @@ const CippStandardDialog = ({
};
const matchesNewFilter = !showOnlyNew || isNewStandard(standard.addedDate);
+ // Status filter: enabled = already in selectedStandards, disabled = not yet added
+ const isEnabled = !!selectedStandards[standard.name];
+ const matchesStatusFilter =
+ statusFilter === "all" ||
+ (statusFilter === "enabled" && isEnabled) ||
+ (statusFilter === "disabled" && !isEnabled);
+
return (
matchesSearch &&
matchesCategory &&
matchesImpact &&
matchesRecommendedBy &&
matchesTagFramework &&
- matchesNewFilter
+ matchesNewFilter &&
+ matchesStatusFilter
);
});
},
@@ -840,6 +849,8 @@ const CippStandardDialog = ({
selectedRecommendedBy,
selectedTagFrameworks,
showOnlyNew,
+ statusFilter,
+ selectedStandards,
]
);
@@ -935,6 +946,7 @@ const CippStandardDialog = ({
setSelectedRecommendedBy([]);
setSelectedTagFrameworks([]);
setShowOnlyNew(false);
+ setStatusFilter("all");
setSortBy("addedDate");
setSortOrder("desc");
setViewMode("card"); // Reset to card view
@@ -949,6 +961,7 @@ const CippStandardDialog = ({
setSelectedRecommendedBy([]);
setSelectedTagFrameworks([]);
setShowOnlyNew(false);
+ setStatusFilter("all");
setViewMode("card"); // Reset to card view
handleSearchQueryChange(""); // Clear parent search state
handleCloseDialog();
@@ -1024,7 +1037,8 @@ const CippStandardDialog = ({
selectedImpacts.length +
selectedRecommendedBy.length +
selectedTagFrameworks.length +
- (showOnlyNew ? 1 : 0);
+ (showOnlyNew ? 1 : 0) +
+ (statusFilter !== "all" ? 1 : 0);
// Don't render dialog contents until it's actually open (improves performance)
return (
@@ -1271,6 +1285,21 @@ const CippStandardDialog = ({
sx={{ ml: 1 }}
/>
+ {/* Status Filter */}
+ {
+ if (newValue !== null) setStatusFilter(newValue);
+ }}
+ size="small"
+ sx={{ height: 45 }}
+ >
+ All
+ Enabled
+ Disabled
+
+
{/* Clear Button */}
{activeFiltersCount > 0 && (
)}
+ {statusFilter !== "all" && (
+ setStatusFilter("all")}
+ color="default"
+ variant="outlined"
+ />
+ )}
)}
diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx
index 8fde6343fe41..59d89462e0e9 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)) && (
<>
@@ -516,10 +516,10 @@ const CippStandardsSideBar = ({
title="Add Standard"
api={{
confirmText: isDriftMode
- ? "This template will automatically every 12 hours to detect drift. Are you sure you want to apply this Drift Template?"
+ ? "This template will run 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 12 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,
diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js
index 32c28c31e84a..7c532764e666 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"
}
>
{
// 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 = ({
-
-
-
-
- >
- );
-};
-
-Page.getLayout = (page) => {page};
-
-export default Page;
diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js
index eb2df5e015ae..3a7af2fa1223 100644
--- a/src/pages/email/administration/mailboxes/index.js
+++ b/src/pages/email/administration/mailboxes/index.js
@@ -3,9 +3,26 @@ import { CippTablePage } from "../../../../components/CippComponents/CippTablePa
import CippExchangeActions from "../../../../components/CippComponents/CippExchangeActions";
import { CippHVEUserDrawer } from "../../../../components/CippComponents/CippHVEUserDrawer.jsx";
import { CippSharedMailboxDrawer } from "../../../../components/CippComponents/CippSharedMailboxDrawer.jsx";
+import { Sync, Info } from "@mui/icons-material";
+import { Button, SvgIcon, IconButton, Tooltip } from "@mui/material";
+import { useSettings } from "../../../../hooks/use-settings";
+import { Stack } from "@mui/system";
+import { useDialog } from "../../../../hooks/use-dialog";
+import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog";
+import { useState } from "react";
+import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker";
const Page = () => {
const pageTitle = "Mailboxes";
+ const currentTenant = useSettings().currentTenant;
+ const syncDialog = useDialog();
+ const [syncQueueId, setSyncQueueId] = useState(null);
+
+ const isAllTenants = currentTenant === "AllTenants";
+
+ const apiData = {
+ UseReportDB: true,
+ };
// Define off-canvas details
const offCanvas = {
@@ -37,33 +54,90 @@ const Page = () => {
];
// Simplified columns for the table
- const simpleColumns = [
- "displayName", // Display Name
- "recipientTypeDetails", // Recipient Type Details
- "UPN", // User Principal Name
- "primarySmtpAddress", // Primary Email Address
- "recipientType", // Recipient Type
- "AdditionalEmailAddresses", // Additional Email Addresses
- ];
+ const simpleColumns = isAllTenants
+ ? [
+ "Tenant", // Tenant
+ "displayName", // Display Name
+ "recipientTypeDetails", // Recipient Type Details
+ "UPN", // User Principal Name
+ "primarySmtpAddress", // Primary Email Address
+ "recipientType", // Recipient Type
+ "AdditionalEmailAddresses", // Additional Email Addresses
+ "CacheTimestamp", // Cache Timestamp
+ ]
+ : [
+ "displayName", // Display Name
+ "recipientTypeDetails", // Recipient Type Details
+ "UPN", // User Principal Name
+ "primarySmtpAddress", // Primary Email Address
+ "recipientType", // Recipient Type
+ "AdditionalEmailAddresses", // Additional Email Addresses
+ "CacheTimestamp", // Cache Timestamp
+ ];
return (
-
-
-
- >
- }
- />
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+ {
+ if (response?.Metadata?.QueueId) {
+ setSyncQueueId(response.Metadata.QueueId);
+ }
+ },
+ }}
+ />
+ >
);
};
-Page.getLayout = (page) => {page};
+Page.getLayout = (page) => {page};
export default Page;
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]);
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.") }}
/>
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}
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..6caa775a2616
--- /dev/null
+++ b/src/pages/endpoint/MEM/devices/device/index.jsx
@@ -0,0 +1,1014 @@
+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,
+ Fingerprint,
+ Group,
+} 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, 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, EyeIcon } from "@heroicons/react/24/outline";
+
+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,
+ });
+
+ // 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: requests,
+ 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 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 || [];
+ const deviceMemberOf = deviceMemberOfData?.body?.value || [];
+
+ // 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...";
+
+ const subtitle = deviceRequest.isSuccess
+ ? [
+ {
+ icon: ,
+ text: ,
+ },
+ {
+ icon: ,
+ text: ,
+ },
+ {
+ icon: ,
+ text: (
+ <>
+ Last Sync:
+ >
+ ),
+ },
+ {
+ 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,
+ actions: [
+ {
+ icon: ,
+ label: "View User",
+ link: `/identity/administration/users/user?userId=[id]&tenantFilter=${userSettingsDefaults.currentTenant}`,
+ },
+ ],
+ },
+ },
+ ];
+ } 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: [],
+ },
+ ];
+ }
+
+ // 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 (
+
+ {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"}
+
+
+ )}
+ {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)`}
+
+
+ )}
+
+ }
+ />
+
+
+
+
+
+ Compliance Policies
+ 0}
+ />
+ Configuration Policies
+ 0}
+ />
+ Detected Applications
+
+ Associated Users
+
+ Memberships
+
+
+
+
+
+ )}
+
+ );
+};
+
+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 f2f905a968ce..e8e34e9338d5 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,8 +25,16 @@ import {
const Page = () => {
const pageTitle = "Devices";
const tenantFilter = useSettings().currentTenant;
+ 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]`,
@@ -373,31 +384,52 @@ const Page = () => {
actions: actions,
};
+ const simpleColumns = [
+ "deviceName",
+ "userPrincipalName",
+ "complianceState",
+ "manufacturer",
+ "model",
+ "operatingSystem",
+ "osVersion",
+ "enrolledDateTime",
+ "managedDeviceOwnerType",
+ "deviceEnrollmentType",
+ "joinType",
+ ];
+
return (
-
+ <>
+
+ }>
+ Sync DEP
+
+
+ }
+ />
+
+ >
);
};
diff --git a/src/pages/endpoint/MEM/list-appprotection-policies/index.js b/src/pages/endpoint/MEM/list-appprotection-policies/index.js
index 85349e8668ed..0c8fc70161fc 100644
--- a/src/pages/endpoint/MEM/list-appprotection-policies/index.js
+++ b/src/pages/endpoint/MEM/list-appprotection-policies/index.js
@@ -1,212 +1,23 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { Book, LaptopChromebook } from "@mui/icons-material";
-import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import { PermissionButton } from "../../../../utils/permissions.js";
import { CippPolicyDeployDrawer } from "../../../../components/CippComponents/CippPolicyDeployDrawer.jsx";
import { useSettings } from "../../../../hooks/use-settings.js";
-
-const assignmentModeOptions = [
- { label: "Replace existing assignments", value: "replace" },
- { label: "Append to existing assignments", value: "append" },
-];
-
-const assignmentFilterTypeOptions = [
- { label: "Include - Apply policy to devices matching filter", value: "include" },
- { label: "Exclude - Apply policy to devices NOT matching filter", value: "exclude" },
-];
+import { useCippIntunePolicyActions } from "../../../../components/CippComponents/CippIntunePolicyActions.jsx";
const Page = () => {
const pageTitle = "App Protection & Configuration Policies";
const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"];
const tenant = useSettings().currentTenant;
- const actions = [
- {
- label: "Create template based on policy",
- type: "POST",
- url: "/api/AddIntuneTemplate",
- data: {
- ID: "id",
- URLName: "URLName",
- },
- confirmText: "Are you sure you want to create a template based on this policy?",
- icon: ,
- color: "info",
- },
- {
- label: "Assign to All Users",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "allLicensedUsers",
- ID: "id",
- type: "URLName",
- platformType: "!deviceAppManagement",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all users?',
- icon: ,
- color: "info",
- },
- {
- label: "Assign to All Devices",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "AllDevices",
- ID: "id",
- type: "URLName",
- platformType: "!deviceAppManagement",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all devices?',
- icon: ,
- color: "info",
+ const actions = useCippIntunePolicyActions(tenant, "URLName", {
+ templateData: {
+ ID: "id",
+ URLName: "managedAppPolicies",
},
- {
- label: "Assign Globally (All Users / All Devices)",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "AllDevicesAndUsers",
- ID: "id",
- type: "URLName",
- platformType: "!deviceAppManagement",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?',
- icon: ,
- color: "info",
- },
- {
- label: "Assign to Custom Group",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- icon: ,
- color: "info",
- confirmText: 'Select the target groups for "[displayName]".',
- fields: [
- {
- type: "autoComplete",
- name: "groupTargets",
- label: "Group(s)",
- multiple: true,
- creatable: false,
- allowResubmit: true,
- validators: { required: "Please select at least one group" },
- api: {
- url: "/api/ListGraphRequest",
- dataKey: "Results",
- queryKey: `ListPolicyAssignmentGroups-${tenant}`,
- labelField: (group) =>
- group.id ? `${group.displayName} (${group.id})` : group.displayName,
- valueField: "id",
- addedField: {
- description: "description",
- },
- data: {
- Endpoint: "groups",
- manualPagination: true,
- $select: "id,displayName,description",
- $orderby: "displayName",
- $top: 999,
- $count: true,
- },
- },
- },
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- 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 selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : [];
- const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant;
- return {
- tenantFilter: tenantFilterValue,
- ID: row?.id,
- type: row?.URLName,
- platformType: "deviceAppManagement",
- GroupIds: selectedGroups.map((group) => group.value).filter(Boolean),
- GroupNames: selectedGroups.map((group) => group.label).filter(Boolean),
- assignmentMode: formData?.assignmentMode || "replace",
- AssignmentFilterName: formData?.assignmentFilter?.value || null,
- AssignmentFilterType: formData?.assignmentFilter?.value
- ? formData?.assignmentFilterType || "include"
- : null,
- };
- },
- },
- {
- label: "Delete Policy",
- type: "POST",
- url: "/api/RemovePolicy",
- data: {
- ID: "id",
- URLName: "URLName",
- },
- confirmText: "Are you sure you want to delete this policy?",
- icon: ,
- color: "danger",
- },
- ];
+ platformType: "deviceAppManagement",
+ deleteUrlName: "URLName",
+ });
const offCanvas = {
extendedInfoFields: [
diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js
index dcb37a6eadc7..b3394023c492 100644
--- a/src/pages/endpoint/MEM/list-compliance-policies/index.js
+++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js
@@ -1,208 +1,22 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { Book, LaptopChromebook } from "@mui/icons-material";
-import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import { PermissionButton } from "../../../../utils/permissions.js";
import { CippPolicyDeployDrawer } from "../../../../components/CippComponents/CippPolicyDeployDrawer.jsx";
import { useSettings } from "../../../../hooks/use-settings.js";
-
-const assignmentModeOptions = [
- { label: "Replace existing assignments", value: "replace" },
- { label: "Append to existing assignments", value: "append" },
-];
-
-const assignmentFilterTypeOptions = [
- { label: "Include - Apply policy to devices matching filter", value: "include" },
- { label: "Exclude - Apply policy to devices NOT matching filter", value: "exclude" },
-];
+import { useCippIntunePolicyActions } from "../../../../components/CippComponents/CippIntunePolicyActions.jsx";
const Page = () => {
const pageTitle = "Intune Compliance Policies";
const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"];
const tenant = useSettings().currentTenant;
- const actions = [
- {
- label: "Create template based on policy",
- type: "POST",
- url: "/api/AddIntuneTemplate",
- data: {
- ID: "id",
- ODataType: "@odata.type",
- },
- confirmText: "Are you sure you want to create a template based on this policy?",
- icon: ,
- color: "info",
- },
- {
- label: "Assign to All Users",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "allLicensedUsers",
- ID: "id",
- type: "deviceCompliancePolicies",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all users?',
- icon: ,
- color: "info",
- },
- {
- label: "Assign to All Devices",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "AllDevices",
- ID: "id",
- type: "deviceCompliancePolicies",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all devices?',
- icon: ,
- color: "info",
+ const actions = useCippIntunePolicyActions(tenant, "deviceCompliancePolicies", {
+ templateData: {
+ ID: "id",
+ ODataType: "@odata.type",
},
- {
- label: "Assign Globally (All Users / All Devices)",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "AllDevicesAndUsers",
- ID: "id",
- type: "deviceCompliancePolicies",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?',
- icon: ,
- color: "info",
- },
- {
- label: "Assign to Custom Group",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- icon: ,
- color: "info",
- confirmText: 'Select the target groups for "[displayName]".',
- fields: [
- {
- type: "autoComplete",
- name: "groupTargets",
- label: "Group(s)",
- multiple: true,
- creatable: false,
- allowResubmit: true,
- validators: { required: "Please select at least one group" },
- api: {
- url: "/api/ListGraphRequest",
- dataKey: "Results",
- queryKey: `ListPolicyAssignmentGroups-${tenant}`,
- labelField: (group) =>
- group.id ? `${group.displayName} (${group.id})` : group.displayName,
- valueField: "id",
- addedField: {
- description: "description",
- },
- data: {
- Endpoint: "groups",
- manualPagination: true,
- $select: "id,displayName,description",
- $orderby: "displayName",
- $top: 999,
- $count: true,
- },
- },
- },
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- 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 selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : [];
- const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant;
- return {
- tenantFilter: tenantFilterValue,
- ID: row?.id,
- type: "deviceCompliancePolicies",
- GroupIds: selectedGroups.map((group) => group.value).filter(Boolean),
- GroupNames: selectedGroups.map((group) => group.label).filter(Boolean),
- assignmentMode: formData?.assignmentMode || "replace",
- AssignmentFilterName: formData?.assignmentFilter?.value || null,
- AssignmentFilterType: formData?.assignmentFilter?.value
- ? formData?.assignmentFilterType || "include"
- : null,
- };
- },
- },
- {
- label: "Delete Policy",
- type: "POST",
- url: "/api/RemovePolicy",
- data: {
- ID: "id",
- URLName: "deviceCompliancePolicies",
- },
- confirmText: "Are you sure you want to delete this policy?",
- icon: ,
- color: "danger",
- },
- ];
+ deleteUrlName: "deviceCompliancePolicies",
+ });
const offCanvas = {
extendedInfoFields: [
diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js
index ec6fa08d0d08..1b4d2d2c97bc 100644
--- a/src/pages/endpoint/MEM/list-policies/index.js
+++ b/src/pages/endpoint/MEM/list-policies/index.js
@@ -1,208 +1,22 @@
import { Layout as DashboardLayout } from "../../../../layouts/index.js";
import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx";
-import { Book, LaptopChromebook } from "@mui/icons-material";
-import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import { PermissionButton } from "../../../../utils/permissions.js";
import { CippPolicyDeployDrawer } from "../../../../components/CippComponents/CippPolicyDeployDrawer.jsx";
import { useSettings } from "../../../../hooks/use-settings.js";
-
-const assignmentModeOptions = [
- { label: "Replace existing assignments", value: "replace" },
- { label: "Append to existing assignments", value: "append" },
-];
-
-const assignmentFilterTypeOptions = [
- { label: "Include - Apply policy to devices matching filter", value: "include" },
- { label: "Exclude - Apply policy to devices NOT matching filter", value: "exclude" },
-];
+import { useCippIntunePolicyActions } from "../../../../components/CippComponents/CippIntunePolicyActions.jsx";
const Page = () => {
const pageTitle = "Configuration Policies";
const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"];
const tenant = useSettings().currentTenant;
- const actions = [
- {
- label: "Create template based on policy",
- type: "POST",
- url: "/api/AddIntuneTemplate",
- data: {
- ID: "id",
- URLName: "URLName",
- },
- confirmText: "Are you sure you want to create a template based on this policy?",
- icon: ,
- color: "info",
- },
- {
- label: "Assign to All Users",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "allLicensedUsers",
- ID: "id",
- type: "URLName",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all users?',
- icon: ,
- color: "info",
- },
- {
- label: "Assign to All Devices",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "AllDevices",
- ID: "id",
- type: "URLName",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all devices?',
- icon: ,
- color: "info",
+ const actions = useCippIntunePolicyActions(tenant, "URLName", {
+ templateData: {
+ ID: "id",
+ URLName: "URLName",
},
- {
- label: "Assign Globally (All Users / All Devices)",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- data: {
- AssignTo: "AllDevicesAndUsers",
- ID: "id",
- type: "URLName",
- },
- fields: [
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- helperText:
- "Replace will overwrite existing assignments. Append keeps current assignments and adds/overwrites only for the selected groups.",
- },
- ],
- confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?',
- icon: ,
- color: "info",
- },
- {
- label: "Assign to Custom Group",
- type: "POST",
- url: "/api/ExecAssignPolicy",
- icon: ,
- color: "info",
- confirmText: 'Select the target groups for "[displayName]".',
- fields: [
- {
- type: "autoComplete",
- name: "groupTargets",
- label: "Group(s)",
- multiple: true,
- creatable: false,
- allowResubmit: true,
- validators: { required: "Please select at least one group" },
- api: {
- url: "/api/ListGraphRequest",
- dataKey: "Results",
- queryKey: `ListPolicyAssignmentGroups-${tenant}`,
- labelField: (group) =>
- group.id ? `${group.displayName} (${group.id})` : group.displayName,
- valueField: "id",
- addedField: {
- description: "description",
- },
- data: {
- Endpoint: "groups",
- manualPagination: true,
- $select: "id,displayName,description",
- $orderby: "displayName",
- $top: 999,
- $count: true,
- },
- },
- },
- {
- type: "radio",
- name: "assignmentMode",
- label: "Assignment mode",
- options: assignmentModeOptions,
- defaultValue: "replace",
- 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 selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : [];
- const tenantFilterValue = tenant === "AllTenants" && row?.Tenant ? row.Tenant : tenant;
- return {
- tenantFilter: tenantFilterValue,
- ID: row?.id,
- type: row?.URLName,
- GroupIds: selectedGroups.map((group) => group.value).filter(Boolean),
- GroupNames: selectedGroups.map((group) => group.label).filter(Boolean),
- assignmentMode: formData?.assignmentMode || "replace",
- AssignmentFilterName: formData?.assignmentFilter?.value || null,
- AssignmentFilterType: formData?.assignmentFilter?.value
- ? formData?.assignmentFilterType || "include"
- : null,
- };
- },
- },
- {
- label: "Delete Policy",
- type: "POST",
- url: "/api/RemovePolicy",
- data: {
- ID: "id",
- URLName: "URLName",
- },
- confirmText: "Are you sure you want to delete this policy?",
- icon: ,
- color: "danger",
- },
- ];
+ deleteUrlName: "URLName",
+ });
const offCanvas = {
extendedInfoFields: [
diff --git a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx
index c2ef4ec5a281..481499eb557d 100644
--- a/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx
+++ b/src/pages/endpoint/MEM/reusable-settings-templates/add.jsx
@@ -1,20 +1,271 @@
-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 "/src/layouts/index.js";
-import CippAddIntuneReusableSettingTemplateForm from "../../../../components/CippFormPages/CippAddIntuneReusableSettingTemplateForm";
+import { Layout as DashboardLayout } from "../../../../layouts/index.js";
+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 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: 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 buildInitialGroupEntry = () =>
+ buildGroupEntryFromBase(baseSettingDefinitionId, { idValue: generateGuid() });
+
+ 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 = () => {
+ return buildGroupEntryFromDefinitions(groupChildDefinitions);
+ };
+
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 d1ca5e5f444b..82baa233ba27 100644
--- a/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx
+++ b/src/pages/endpoint/MEM/reusable-settings-templates/edit.jsx
@@ -1,7 +1,19 @@
-import { Alert, Box, Button, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography, Divider } from "@mui/material";
+import {
+ Alert,
+ Box,
+ Button,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+ Typography,
+ Divider,
+} from "@mui/material";
import { useForm, useFieldArray } from "react-hook-form";
import { useRouter } from "next/router";
-import { Layout as DashboardLayout } from "/src/layouts/index.js";
+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";
@@ -15,10 +27,61 @@ const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
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);
+ const s4 = () =>
+ Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
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];
+};
+
const EditReusableSettingsTemplate = () => {
const router = useRouter();
const { id: rawId } = router.query;
@@ -54,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]);
@@ -78,19 +141,23 @@ const EditReusableSettingsTemplate = () => {
}, [parsedRaw]);
const groupCollection = useMemo(() => {
- return (
+ const source =
parsedRaw?.settingInstance?.groupSettingCollectionValue ||
templateData?.settingInstance?.groupSettingCollectionValue ||
- []
- );
+ [];
+ return normalizeCollection(source);
}, [parsedRaw, templateData]);
const groupChildDefinitions = useMemo(() => {
const first = groupCollection?.[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,
+ 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,
};
}, [groupCollection]);
@@ -105,8 +172,14 @@ const EditReusableSettingsTemplate = () => {
useEffect(() => {
if (normalizedTemplate) {
- formControl.setValue("displayName", normalizedTemplate.displayName || normalizedTemplate.name);
- formControl.setValue("description", normalizedTemplate.description || normalizedTemplate.Description);
+ formControl.setValue(
+ "displayName",
+ normalizedTemplate.displayName || normalizedTemplate.name,
+ );
+ formControl.setValue(
+ "description",
+ normalizedTemplate.description || normalizedTemplate.Description,
+ );
}
}, [normalizedTemplate, formControl]);
@@ -185,7 +258,10 @@ const EditReusableSettingsTemplate = () => {
processedValues.parsedRAWJson.description = processedValues.description;
}
- if (processedValues.groupSettingCollectionValue && processedValues.parsedRAWJson.settingInstance) {
+ if (
+ processedValues.groupSettingCollectionValue &&
+ processedValues.parsedRAWJson.settingInstance
+ ) {
processedValues.parsedRAWJson.settingInstance.groupSettingCollectionValue =
processedValues.groupSettingCollectionValue;
}
@@ -208,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 (
@@ -250,7 +296,7 @@ const EditReusableSettingsTemplate = () => {
normalizedTemplate?.displayName ||
normalizedTemplate?.name ||
normalizedTemplate?.Displayname ||
- "Edit Reusable Settings Template"
+ "Reusable Settings Template"
}
formControl={formControl}
queryKey={[`ReusableSettingTemplate-${normalizedId}`, "ListIntuneReusableSettingTemplates"]}
@@ -356,7 +402,11 @@ const EditReusableSettingsTemplate = () => {
-
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..ac1285e02f42 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();
@@ -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,
@@ -72,7 +76,9 @@ const EditReusableSetting = () => {
return (
{
return (
}
+ cardButton={
+
+ }
apiUrl="/api/ListIntuneReusableSettings"
queryKey={`ListIntuneReusableSettings-${currentTenant}`}
actions={actions}
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,
};
},
},
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: (
+
+ View in Entra
+
+ ),
+ },
+ ]
+ : [];
+
+ // 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..3320ac353204 100644
--- a/src/pages/identity/administration/groups/index.js
+++ b/src/pages/identity/administration/groups/index.js
@@ -2,16 +2,16 @@ 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,
GroupAdd,
Edit,
LockOpen,
Lock,
GroupSharp,
CloudSync,
+ RocketLaunch,
} from "@mui/icons-material";
import { Stack } from "@mui/system";
import { useState } from "react";
@@ -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",
@@ -306,6 +313,13 @@ const Page = () => {
}>
Add Group
+ }
+ >
+ Deploy Group Template
+
}
apiUrl="/api/ListGroups"
diff --git a/src/pages/identity/administration/jit-admin/index.js b/src/pages/identity/administration/jit-admin/index.js
index b92faa05decb..fe623ec569db 100644
--- a/src/pages/identity/administration/jit-admin/index.js
+++ b/src/pages/identity/administration/jit-admin/index.js
@@ -39,7 +39,7 @@ const Page = () => {
>
}
- title="JIT Admin Table"
+ title="JIT Admins"
apiUrl="/api/ListJITAdmin"
apiDataKey="Results"
simpleColumns={simpleColumns}
diff --git a/src/pages/identity/administration/users/add.jsx b/src/pages/identity/administration/users/add.jsx
index eabb890fa965..28606efc90ba 100644
--- a/src/pages/identity/administration/users/add.jsx
+++ b/src/pages/identity/administration/users/add.jsx
@@ -37,6 +37,12 @@ const Page = () => {
}
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]);
diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx
index 6d305994513d..a5b44d5a7893 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();
@@ -102,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.";
@@ -125,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.";
@@ -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 && (
- {
- const blob = new Blob([JSON.stringify(becPollingCall.data, null, 2)], {
- type: "application/json",
- });
- const url = URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = `BEC_Report_${userRequest.data[0].userPrincipalName}.json`;
- link.click();
- URL.revokeObjectURL(url);
- }}
- variant="contained"
- startIcon={
-
-
-
- }
- >
- Download Report
-
+
+
+ {
+ const blob = new Blob([JSON.stringify(becPollingCall.data, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `BEC_Report_${userRequest.data[0].userPrincipalName}.json`;
+ link.click();
+ URL.revokeObjectURL(url);
+ }}
+ variant="outlined"
+ startIcon={
+
+
+
+ }
+ >
+ Download JSON
+
+
)}
diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx
index ea58b93207dc..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,38 +150,19 @@ const Page = () => {
$top: 999,
},
}}
+ validators={{ required: "Application is required" }}
formControl={formControl}
/>
{/* Optional Parameters */}
Optional Parameters:
-
- {/* Test from this country */}
- ({
- value: Code,
- label: Name,
- }))}
- formControl={formControl}
- />
-
- {/* Test from this IP */}
-
-
{/* Device Platform */}
{
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" },
@@ -210,11 +194,51 @@ const Page = () => {
formControl={formControl}
/>
+ {/* Authentication Flow */}
+
+
+ {/* Test from this IP */}
+
+
+ {/* Test from this country */}
+ ({
+ value: Code,
+ label: Name,
+ }))}
+ formControl={formControl}
+ />
+
{/* Sign-in risk level */}
{
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" },
@@ -246,9 +272,9 @@ const Page = () => {
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))
);
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
+
diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js
index ee7f73445035..59f101d6f8f8 100644
--- a/src/pages/onboardingv2.js
+++ b/src/pages/onboardingv2.js
@@ -8,6 +8,10 @@ import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDep
import { CippBaselinesStep } from "../components/CippWizard/CippBaselinesStep.jsx";
import { CippNotificationsStep } from "../components/CippWizard/CippNotificationsStep.jsx";
import { CippAlertsStep } from "../components/CippWizard/CippAlertsStep.jsx";
+import { CippAddTenantTypeSelection } from "../components/CippWizard/CippAddTenantTypeSelection.jsx";
+import { CippDirectTenantDeploy } from "../components/CippWizard/CippDirectTenantDeploy.jsx";
+import { CippGDAPTenantSetup } from "../components/CippWizard/CippGDAPTenantSetup.jsx";
+import { CippGDAPTenantOnboarding } from "../components/CippWizard/CippGDAPTenantOnboarding.jsx";
import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline";
const Page = () => {
@@ -67,9 +71,32 @@ const Page = () => {
description: "Tenants",
component: CippTenantModeDeploy,
showStepWhen: (values) =>
- values?.selectedOption === "CreateApp" ||
- values?.selectedOption === "FirstSetup" ||
- values?.selectedOption === "AddTenant",
+ values?.selectedOption === "CreateApp" || values?.selectedOption === "FirstSetup",
+ },
+ {
+ description: "Tenant Type",
+ component: CippAddTenantTypeSelection,
+ showStepWhen: (values) => values?.selectedOption === "AddTenant",
+ },
+ {
+ description: "Direct Tenant",
+ component: CippDirectTenantDeploy,
+ showStepWhen: (values) =>
+ values?.selectedOption === "AddTenant" && values?.tenantType === "Direct",
+ },
+ {
+ description: "GDAP Setup",
+ component: CippGDAPTenantSetup,
+ showStepWhen: (values) =>
+ values?.selectedOption === "AddTenant" && values?.tenantType === "GDAP",
+ },
+ {
+ description: "GDAP Onboarding",
+ component: CippGDAPTenantOnboarding,
+ showStepWhen: (values) =>
+ values?.selectedOption === "AddTenant" &&
+ values?.tenantType === "GDAP" &&
+ values?.GDAPInviteAccepted === true,
},
{
description: "Baselines",
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;
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 = () => {
}
variant="contained"
>
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{" "}
diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js
index fbd55c304e6c..61bce5fda961 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,35 @@ 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;
@@ -1697,8 +1779,8 @@ 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.
)}
@@ -1988,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"}
+
+
-
- ))}
+ );
+ })}
) : (
{
textTransform: "uppercase",
letterSpacing: 0.5,
display: "block",
- mb: 1,
+ mb: 2,
}}
>
Current Configuration
@@ -2338,13 +2482,7 @@ const Page = () => {
standard.currentTenantValue.CurrentValue !== null ? (
{
{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"}
+
+
-
- ))}
+ );
+ })}
) : (
-
+
{
textTransform: "uppercase",
letterSpacing: 0.5,
display: "block",
- mb: 1,
+ mb: 2,
}}
>
Current Configuration
diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js
index cda8f505d56f..15154dc5e258 100644
--- a/src/pages/tenant/manage/configuration-backup.js
+++ b/src/pages/tenant/manage/configuration-backup.js
@@ -10,9 +10,12 @@ import {
AlertTitle,
Card,
CardContent,
+ IconButton,
Stack,
Skeleton,
Chip,
+ CircularProgress,
+ Drawer,
} from "@mui/material";
import { Grid } from "@mui/system";
import {
@@ -25,15 +28,20 @@ import {
CheckCircle,
Cancel,
Delete,
+ Sync,
+ CloudDownload,
+ Visibility,
+ Close,
} 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";
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";
@@ -50,6 +58,16 @@ const Page = () => {
// Prioritize URL query parameter, then fall back to settings
const currentTenant = router.query.tenantFilter || settings.currentTenant;
+ const downloadAction = ApiPostCall({
+ 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",
@@ -84,6 +102,76 @@ 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);
+ },
+ },
+ );
+ };
+
+ 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" &&
@@ -104,7 +192,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 +342,15 @@ const Page = () => {
title="Backup Schedule Details"
propertyItems={configPropertyItems}
isFetching={existingBackupConfig.isFetching}
+ actionButton={
+
+
+
+ }
/>
@@ -308,19 +405,28 @@ const Page = () => {
Backup History
- {settings.currentTenant === "AllTenants" && (
-
-
-
- )}
+
+ {settings.currentTenant === "AllTenants" && (
+
+
+
+ )}
+
+
+
+
@@ -358,7 +464,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 +484,24 @@ const Page = () => {
)}
+ }
+ onClick={() => handleOpenBackupPreview(backup)}
+ >
+ Preview
+
+ }
+ onClick={() => handleDownloadBackup(backup)}
+ >
+ Download
+
{
/>
-
- {backup.tags.map((tag, idx) => (
-
- ))}
-
@@ -413,6 +524,51 @@ const Page = () => {
+ {/* Backup Preview Drawer */}
+
+
+
+
+ Backup Preview
+ {selectedBackup && (
+
+ {(() => {
+ const match = selectedBackup.name.match(
+ /.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/,
+ );
+ return match ? `${match[1]} @ ${match[2]}:${match[3]}` : selectedBackup.name;
+ })()}
+
+ )}
+
+
+
+
+
+ {isLoadingBackup ? (
+
+
+
+ ) : backupContent ? (
+
+ ) : (
+
+ Failed to Load Backup
+ Unable to load backup contents. Please try again.
+
+ )}
+
+
+
{/* Remove Backup Schedule Dialog */}
{
const driftApi = ApiGetCall({
url: "/api/listTenantDrift",
data: {
- TenantFilter: tenantFilter,
+ tenantFilter: tenantFilter,
},
queryKey: `TenantDrift-${tenantFilter}`,
});
@@ -99,7 +100,7 @@ const ManageDriftPage = () => {
url: "/api/ListStandardsCompare",
data: {
TemplateId: templateId,
- TenantFilter: tenantFilter,
+ tenantFilter: tenantFilter,
CompareToStandard: true,
},
queryKey: `StandardsCompare-${templateId}-${tenantFilter}`,
@@ -1109,7 +1110,7 @@ const ManageDriftPage = () => {
receivedValue: deviation.receivedValue,
},
],
- TenantFilter: tenantFilter,
+ tenantFilter: tenantFilter,
},
action: {
text: actionText,
@@ -1162,7 +1163,7 @@ const ManageDriftPage = () => {
receivedValue: deviation.receivedValue,
},
],
- TenantFilter: tenantFilter,
+ tenantFilter: tenantFilter,
},
action: {
text: actionText,
@@ -1239,7 +1240,7 @@ const ManageDriftPage = () => {
setActionData({
data: {
deviations: deviations,
- TenantFilter: tenantFilter,
+ tenantFilter: tenantFilter,
receivedValues: deviations.map((d) => d.receivedValue),
},
action: {
@@ -1259,7 +1260,7 @@ const ManageDriftPage = () => {
setActionData({
data: {
RemoveDriftCustomization: true,
- TenantFilter: tenantFilter,
+ tenantFilter: tenantFilter,
},
action: {
text: "remove all drift customizations",
@@ -1315,6 +1316,11 @@ const ManageDriftPage = () => {
}
}, [templateId]);
+ // Effect to clear selected items when tenant changes
+ useEffect(() => {
+ setSelectedItems([]);
+ }, [tenantFilter]);
+
// Add action buttons to each deviation item
const deviationItemsWithActions = actualDeviationItems.map((item) => {
return {
@@ -1720,6 +1726,15 @@ const ManageDriftPage = () => {
);
}}
placeholder="Select a drift template..."
+ disableClearable={true}
+ customAction={{
+ icon: ,
+ link: selectedTemplateOption?.value
+ ? `/tenant/standards/templates/template?id=${selectedTemplateOption.value}&type=drift`
+ : undefined,
+ tooltip: "Edit Template",
+ position: "inside",
+ }}
/>
{
? "for this tenant"
: "this deviation"
}?`,
+ onSuccess: () => {
+ // Clear selected items after successful action
+ setSelectedItems([]);
+ },
}}
row={actionData.data}
relatedQueryKeys={[`TenantDrift-${tenantFilter}`]}
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 = () => {
Reset All to Off
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()}
-
- }
- >
- Recover Selected Policies ({selectedPolicies.length})
-
-
- setSelectedPolicies(selectedRows)}
- />
-
-
-
- )}
-
-
-
- );
-};
-
-RecoverPoliciesPage.getLayout = (page) => {page};
-
-export default RecoverPoliciesPage;
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"}
{
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 (
-
+
}
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 (
{
}}
>
-
-
-
-
-
-
-
-
- getTenant.refetch()}
- variant="contained"
- startIcon={}
- >
- Check
-
-
-
-
-
-
- {/* 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}
-
+
);
diff --git a/src/pages/tools/community-repos/index.js b/src/pages/tools/community-repos/index.js
index 06706162ea9f..4213f74127d9 100644
--- a/src/pages/tools/community-repos/index.js
+++ b/src/pages/tools/community-repos/index.js
@@ -140,7 +140,7 @@ const Page = () => {
const watchIncludeForks = searchForm.watch("includeforks");
const handleSearch = () => {
- const searchTerms = watchSearchTerm.map((t) => t.value) ?? [];
+ const searchTerms = watchSearchTerm?.map((t) => t.value) ?? [];
searchMutation.mutate({
url: "/api/ExecGitHubAction",
data: {
diff --git a/src/utils/cipp-license-backfill-manager.js b/src/utils/cipp-license-backfill-manager.js
new file mode 100644
index 000000000000..8df50b94d83b
--- /dev/null
+++ b/src/utils/cipp-license-backfill-manager.js
@@ -0,0 +1,160 @@
+/**
+ * Global license backfill manager
+ * Tracks missing licenses and triggers batch API calls to fetch them
+ */
+
+import { getMissingFromCache, addLicensesToCache } from "./cipp-license-cache";
+
+class LicenseBackfillManager {
+ constructor() {
+ this.pendingSkuIds = new Set();
+ this.isBackfilling = false;
+ this.backfillTimeout = null;
+ this.callbacks = new Set();
+ this.BATCH_DELAY = 500; // Wait 500ms to batch multiple requests
+ }
+
+ /**
+ * Add a callback to be notified when backfill completes
+ */
+ addCallback(callback) {
+ this.callbacks.add(callback);
+ return () => this.callbacks.delete(callback);
+ }
+
+ /**
+ * Notify all callbacks
+ */
+ notifyCallbacks() {
+ this.callbacks.forEach((callback) => {
+ try {
+ callback();
+ } catch (error) {
+ console.error("Error in backfill callback:", error);
+ }
+ });
+ }
+
+ /**
+ * Add missing skuIds to the queue
+ */
+ addMissingSkuIds(skuIds) {
+ if (!Array.isArray(skuIds)) return;
+
+ let added = false;
+ skuIds.forEach((skuId) => {
+ if (skuId && !this.pendingSkuIds.has(skuId)) {
+ this.pendingSkuIds.add(skuId);
+ added = true;
+ }
+ });
+
+ if (added && !this.isBackfilling) {
+ this.scheduleBatchBackfill();
+ }
+ }
+
+ /**
+ * Schedule a batch backfill with debouncing
+ */
+ scheduleBatchBackfill() {
+ // Clear existing timeout to debounce
+ if (this.backfillTimeout) {
+ clearTimeout(this.backfillTimeout);
+ }
+
+ // Schedule new backfill
+ this.backfillTimeout = setTimeout(() => {
+ this.executeBatchBackfill();
+ }, this.BATCH_DELAY);
+ }
+
+ /**
+ * Execute the batch backfill
+ */
+ async executeBatchBackfill() {
+ if (this.isBackfilling || this.pendingSkuIds.size === 0) {
+ return;
+ }
+
+ // Get all pending skuIds
+ const skuIdsToFetch = Array.from(this.pendingSkuIds);
+ this.pendingSkuIds.clear();
+ this.isBackfilling = true;
+
+ try {
+ // Import axios dynamically to avoid circular dependencies
+ const axios = (await import("axios")).default;
+ const { buildVersionedHeaders } = await import("./cippVersion");
+
+ console.log(`[License Backfill] Fetching ${skuIdsToFetch.length} licenses...`);
+
+ const response = await axios.post(
+ "/api/ExecLicenseSearch",
+ { skuIds: skuIdsToFetch },
+ { headers: await buildVersionedHeaders() }
+ );
+
+ if (response.data && Array.isArray(response.data)) {
+ console.log(`[License Backfill] Received ${response.data.length} licenses`);
+ addLicensesToCache(response.data);
+
+ // Notify all callbacks that backfill completed
+ this.notifyCallbacks();
+ }
+ } catch (error) {
+ console.error("[License Backfill] Error fetching licenses:", error);
+
+ // Re-add failed skuIds back to pending if we want to retry
+ // Commenting this out to avoid infinite retry loops
+ // skuIdsToFetch.forEach(skuId => this.pendingSkuIds.add(skuId));
+ } finally {
+ this.isBackfilling = false;
+
+ // If more skuIds were added during backfill, schedule another batch
+ if (this.pendingSkuIds.size > 0) {
+ this.scheduleBatchBackfill();
+ }
+ }
+ }
+
+ /**
+ * Check skuIds and add missing ones to backfill queue
+ */
+ checkAndQueueMissing(skuIds) {
+ const missing = getMissingFromCache(skuIds);
+ if (missing.length > 0) {
+ this.addMissingSkuIds(missing);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get current backfill status
+ */
+ getStatus() {
+ return {
+ isBackfilling: this.isBackfilling,
+ pendingCount: this.pendingSkuIds.size,
+ };
+ }
+
+ /**
+ * Clear all pending requests (useful for cleanup/testing)
+ */
+ clear() {
+ if (this.backfillTimeout) {
+ clearTimeout(this.backfillTimeout);
+ this.backfillTimeout = null;
+ }
+ this.pendingSkuIds.clear();
+ this.isBackfilling = false;
+ this.callbacks.clear();
+ }
+}
+
+// Global singleton instance
+const licenseBackfillManager = new LicenseBackfillManager();
+
+export default licenseBackfillManager;
diff --git a/src/utils/cipp-license-cache.js b/src/utils/cipp-license-cache.js
new file mode 100644
index 000000000000..089a09a7d4ef
--- /dev/null
+++ b/src/utils/cipp-license-cache.js
@@ -0,0 +1,109 @@
+/**
+ * License cache manager for dynamically loaded licenses
+ * Uses localStorage to permanently cache licenses fetched from the API
+ * Cache only grows (appends missing licenses) and never expires
+ */
+
+const CACHE_KEY = "cipp_dynamic_licenses";
+const CACHE_VERSION = "1.0";
+
+/**
+ * Get the license cache from localStorage
+ * @returns {Object} Cache object with version, timestamp, and licenses map
+ */
+const getCache = () => {
+ try {
+ const cached = localStorage.getItem(CACHE_KEY);
+ if (!cached) {
+ return { version: CACHE_VERSION, timestamp: Date.now(), licenses: {} };
+ }
+
+ const parsed = JSON.parse(cached);
+
+ // Check cache version - clear if outdated
+ if (parsed.version !== CACHE_VERSION) {
+ localStorage.removeItem(CACHE_KEY);
+ return { version: CACHE_VERSION, timestamp: Date.now(), licenses: {} };
+ }
+
+ return parsed;
+ } catch (error) {
+ console.error("Error reading license cache:", error);
+ return { version: CACHE_VERSION, timestamp: Date.now(), licenses: {} };
+ }
+};
+
+/**
+ * Save the license cache to localStorage
+ * @param {Object} cache - Cache object to save
+ */
+const saveCache = (cache) => {
+ try {
+ localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
+ } catch (error) {
+ console.error("Error saving license cache:", error);
+ }
+};
+
+/**
+ * Get a license from the cache by skuId
+ * @param {string} skuId - The license skuId (GUID)
+ * @returns {string|null} The display name if found, null otherwise
+ */
+export const getCachedLicense = (skuId) => {
+ if (!skuId) return null;
+
+ const cache = getCache();
+ return cache.licenses[skuId.toLowerCase()] || null;
+};
+
+/**
+ * Add licenses to the cache
+ * @param {Array} licenses - Array of license objects with skuId and displayName
+ */
+export const addLicensesToCache = (licenses) => {
+ if (!Array.isArray(licenses) || licenses.length === 0) return;
+
+ const cache = getCache();
+
+ licenses.forEach((license) => {
+ if (license.skuId && license.displayName) {
+ cache.licenses[license.skuId.toLowerCase()] = license.displayName;
+ }
+ });
+
+ cache.timestamp = Date.now();
+ saveCache(cache);
+};
+
+/**
+ * Check if licenses exist in cache
+ * @param {Array} skuIds - Array of skuIds to check
+ * @returns {Array} Array of skuIds that are NOT in cache
+ */
+export const getMissingFromCache = (skuIds) => {
+ if (!Array.isArray(skuIds) || skuIds.length === 0) return [];
+
+ const cache = getCache();
+ return skuIds.filter((skuId) => !cache.licenses[skuId.toLowerCase()]);
+};
+
+/**
+ * Clear the entire license cache
+ */
+export const clearLicenseCache = () => {
+ try {
+ localStorage.removeItem(CACHE_KEY);
+ } catch (error) {
+ console.error("Error clearing license cache:", error);
+ }
+};
+
+/**
+ * Get all cached licenses
+ * @returns {Object} Map of skuId -> displayName
+ */
+export const getAllCachedLicenses = () => {
+ const cache = getCache();
+ return cache.licenses;
+};
diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js
index 405139ae6e93..538116993b53 100644
--- a/src/utils/get-cipp-formatting.js
+++ b/src/utils/get-cipp-formatting.js
@@ -106,11 +106,19 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr
if (Array.isArray(data)) {
return isText ? data.join(", ") : renderChipList(data);
} else {
- return isText ? (
- data
- ) : (
-
- );
+ if (isText) return data.label ?? data;
+ const label = data.label ?? data;
+ const severityColor = {
+ info: "info",
+ warn: "warning",
+ warning: "warning",
+ error: "error",
+ critical: "error",
+ alert: "warning",
+ debug: "default",
+ };
+ const color = severityColor[String(label).toLowerCase()] ?? "info";
+ return ;
}
}
@@ -185,7 +193,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
diff --git a/src/utils/get-cipp-license-translation.js b/src/utils/get-cipp-license-translation.js
index 4a85312eb95b..0397585d927a 100644
--- a/src/utils/get-cipp-license-translation.js
+++ b/src/utils/get-cipp-license-translation.js
@@ -1,10 +1,13 @@
import M365LicensesDefault from "../data/M365Licenses.json";
import M365LicensesAdditional from "../data/M365Licenses-additional.json";
+import { getCachedLicense } from "./cipp-license-cache";
+import licenseBackfillManager from "./cipp-license-backfill-manager";
export const getCippLicenseTranslation = (licenseArray) => {
//combine M365LicensesDefault and M365LicensesAdditional to one array
const M365Licenses = [...M365LicensesDefault, ...M365LicensesAdditional];
let licenses = [];
+ let missingSkuIds = [];
if (Array.isArray(licenseArray) && typeof licenseArray[0] === "string") {
return licenseArray;
@@ -20,22 +23,47 @@ export const getCippLicenseTranslation = (licenseArray) => {
licenseArray?.forEach((licenseAssignment) => {
let found = false;
+
+ // First, check static JSON files
for (let x = 0; x < M365Licenses.length; x++) {
if (licenseAssignment.skuId === M365Licenses[x].GUID) {
licenses.push(
M365Licenses[x].Product_Display_Name
? M365Licenses[x].Product_Display_Name
- : licenseAssignment.skuPartNumber
+ : licenseAssignment.skuPartNumber,
);
found = true;
break;
}
}
+
+ // Second, check dynamic cache
+ if (!found && licenseAssignment.skuId) {
+ const cachedName = getCachedLicense(licenseAssignment.skuId);
+ if (cachedName) {
+ licenses.push(cachedName);
+ found = true;
+ }
+ }
+
+ // Finally, fall back to skuPartNumber, then skuId, then "Unknown License"
if (!found) {
- licenses.push(licenseAssignment.skuPartNumber);
+ const fallbackName =
+ licenseAssignment.skuPartNumber || licenseAssignment.skuId || "Unknown License";
+ licenses.push(fallbackName);
+
+ // Queue this skuId for backfill if we have it
+ if (licenseAssignment.skuId) {
+ missingSkuIds.push(licenseAssignment.skuId);
+ }
}
});
+ // Trigger backfill for missing licenses
+ if (missingSkuIds.length > 0) {
+ licenseBackfillManager.addMissingSkuIds(missingSkuIds);
+ }
+
if (!licenses || licenses.length === 0) {
return ["No Licenses Assigned"];
}
diff --git a/yarn.lock b/yarn.lock
index ca973629395e..300433a19ad0 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"
@@ -842,10 +931,10 @@
"@babel/plugin-transform-modules-commonjs" "^7.27.1"
"@babel/plugin-transform-typescript" "^7.28.5"
-"@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.27.6", "@babel/runtime@^7.28.3", "@babel/runtime@^7.28.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
- version "7.28.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
- integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
+"@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.27.6", "@babel/runtime@^7.28.3", "@babel/runtime@^7.28.4", "@babel/runtime@^7.28.6", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b"
+ integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
"@babel/template@^7.27.1", "@babel/template@^7.27.2":
version "7.27.2"
@@ -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==
@@ -1458,23 +1584,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.2":
+ version "8.27.2"
+ resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-8.27.2.tgz#5ada1fb3adffff3e0fd0fee7702fba7f770dca68"
+ integrity sha512-06LFkHFRXJ2O9DMXtWAA3kY0jpbL7XH8iqa8L5cBlN+8bRx/UVLKlZYlhGv06C88jF9kuZWY1bUgrv/EoY/2Ww==
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"
@@ -1495,57 +1621,57 @@
"@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"
- 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"
-"@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"
@@ -1678,43 +1804,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 +1863,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"
@@ -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"
@@ -2134,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"
@@ -2156,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-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@^3.19.0":
+"@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.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.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.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.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.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-list/-/extension-list-3.19.0.tgz#737dcb56ba9838a4431c1afb035bd622fab46d21"
- integrity sha512-N6nKbFB2VwMsPlCw67RlAtYSK48TAsAUgjnD+vd3ieSlIufdQnLXDFUP6hFKx9mwoUVUgZGz02RA6bkxOdYyTw==
-
-"@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-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-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-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==
+ 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"
@@ -2309,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"
@@ -2587,106 +2708,106 @@
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.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"
@@ -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"
@@ -4395,10 +4526,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"
@@ -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"
@@ -4750,19 +4898,19 @@ 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"
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==
@@ -5178,17 +5326,17 @@ 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"
- resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-4.1.0.tgz#4fb476251c8751c996175cfaac02d30fdf8c7b7a"
- integrity sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==
+jspdf@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-4.2.0.tgz#f5b42a8e1592c3da1531d005adc87ccc19272965"
+ integrity sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==
dependencies:
- "@babel/runtime" "^7.28.4"
+ "@babel/runtime" "^7.28.6"
fast-png "^6.2.0"
fflate "^0.8.1"
optionalDependencies:
@@ -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==
@@ -5922,10 +6070,10 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-mui-tiptap@^1.14.0:
- version "1.28.1"
- resolved "https://registry.yarnpkg.com/mui-tiptap/-/mui-tiptap-1.28.1.tgz#d494eed6fa78897791815e1ec86b8abc17af1e70"
- integrity sha512-tKSToZBti+qMkHHPYU33ws4bnQ7ssIKUgpCRfVRAkEU5hC7jSFRdEjlSyDiympQaXgSe0XBxdD+XxF25WXm9uA==
+mui-tiptap@^1.29.0:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/mui-tiptap/-/mui-tiptap-1.29.0.tgz#5316a5aad8f9c6d14d317daf17e1d5b3e5a59477"
+ integrity sha512-2fupRo0RI2o+xX59qoSGpNertfMoDGLhxpzZjXP/loXlrbzuIVw+AGbUg4zeHbFJrbXH2RGIiPbI4ysPbvO8mg==
dependencies:
clsx "^2.1.1"
encodeurl "^2.0.0"
@@ -5955,26 +6103,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:
@@ -6586,10 +6734,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"
@@ -6606,13 +6754,14 @@ react-html-parser@^2.0.2:
dependencies:
htmlparser2 "^3.9.0"
-react-i18next@15.7.3:
- version "15.7.3"
- resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.7.3.tgz#2eba235247dff0cbf9f0338e2ab85e10e127aa54"
- integrity sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==
+react-i18next@16.2.4:
+ version "16.2.4"
+ resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-16.2.4.tgz#0a6f3eb982b702b8810323c97c09e7452448e03b"
+ integrity sha512-pvbcPQ+YuQQoRkKBA4VCU9aO8dOgP/vdKEizIYXcAk3+AmI8yQKSJaCzxQQu4Kgg2zWZm3ax9KqHv8ItUlRY0A==
dependencies:
"@babel/runtime" "^7.27.6"
html-parse-stringify "^3.0.1"
+ use-sync-external-store "^1.6.0"
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
@@ -6749,20 +6898,20 @@ react-virtualized-auto-sizer@^1.0.26:
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz#e9470ef6a778dc4f1d5fd76305fa2d8b610c357a"
integrity sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==
-react-virtuoso@^4.12.8:
- version "4.17.0"
- resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.17.0.tgz#e81f2da99792cfd9317e910b243d847ebeb09248"
- integrity sha512-od3pi2v13v31uzn5zPXC2u3ouISFCVhjFVFch2VvS2Cx7pWA2F1aJa3XhNTN2F07M3lhfnMnsmGeH+7wZICr7w==
+react-virtuoso@^4.18.1:
+ version "4.18.1"
+ resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.18.1.tgz#3eb7078f2739a31b96c723374019e587deeb6ebc"
+ integrity sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==
react-window@^2.2.5:
version "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"
@@ -6796,10 +6945,10 @@ readable-stream@~1.0.17, readable-stream@~1.0.27-1:
isarray "0.0.1"
string_decoder "~0.10.x"
-recharts@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.6.0.tgz#403f0606581153601857e46733277d1411633df3"
- integrity sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==
+recharts@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-3.7.0.tgz#e3c72656ba18841085293e83bfc9a4f78b20abdd"
+ integrity sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==
dependencies:
"@reduxjs/toolkit" "1.x.x || 2.x.x"
clsx "^2.1.1"
@@ -7109,7 +7258,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==
@@ -7253,10 +7402,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"
@@ -7569,10 +7718,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 +7795,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 +8159,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"