diff --git a/.github/ISSUE_TEMPLATE/CLA.yml b/.github/ISSUE_TEMPLATE/CLA.yml deleted file mode 100644 index c2180bb38d05..000000000000 --- a/.github/ISSUE_TEMPLATE/CLA.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: CLA - Contributor License Agreement -description: Sign the CLA for CIPP and CIPP-API -labels: [CLA] - -body: -- type: markdown - attributes: - value: > - CONTRIBUTOR LICENSE AGREEMENT ("Agreement") - - Version 1.0 - - 1. Definitions - - "Contribution" means any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Kelvin Tegelaar for inclusion in, or documentation of, any of the products owned or managed by Kelvin Tegelaar (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Kelvin Tegelaar or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Kelvin Tegelaar for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - - "You" (or "Your") means the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Kelvin Tegelaar. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single contributor. This Agreement applies both to future Contributions and Contributions made prior to the date of this Agreement. - - 2. Grant of Copyright License - - Subject to the terms and conditions of this Agreement, You hereby grant to Kelvin Tegelaar and to recipients of software distributed by Kelvin Tegelaar a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works for the purpose of dual licensing the application. - - 3. Grant of Patent License - - You grant Kelvin Tegelaar, and those who receive the Contribution directly or indirectly from Kelvin Tegelaar, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under Your patent claims that are necessarily infringed by the Contribution or the combination of the Contribution with the Project to which it was submitted, to make, have made, use, offer to sell, sell, import, and otherwise dispose of the Contribution alone or with the Project. - - 4. Other Rights Reserved - - Each party reserves all rights not expressly granted in this Agreement. No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are granted by implication, exhaustion, estoppel, or otherwise. - - You are not expected to provide support for your Contributions, except to the extent you desire to provide - support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in - writing, you provide your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF - ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES - OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A - PARTICULAR PURPOSE. - - 5. Representations - - You represent that you are legally entitled to grant the above licenses. If your employer(s) has rights to intellectual property that you create, you represent that you have received permission to make Contributions on behalf of that employer, or that your employer has waived such rights for your Contributions to Kelvin Tegelaar. - - You represent that each of Your Contributions is Your original creation. You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. - - 6. Project Sale - - In the event that the Project is sold or otherwise transferred in ownership in its entirety to a third party, a sum equivalent to fifteen percent (15%) of the total sale price or value of the consideration received shall be set aside. This sum shall be divided amongst all Contributors who have entered into this Agreement, with each Contributor receiving a portion proportional to the relative quantity and significance of their Contributions to the Project, as determined by Kelvin Tegelaar. The method and timeframe of the distribution shall be at the discretion of Kelvin Tegelaar and shall be communicated - -- type: textarea - attributes: - label: Description - description: > - Type "I Agree" in the text area. - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 1e0e6afd8faa..db613f4551c2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,42 +1,92 @@ -name: 🐞 Bug report -description: Report errors or unexpected behaviors for CIPP and CIPP-API -labels: [unconfirmed-by-user , bug] +name: "🐞 Bug report" +description: "Report errors or unexpected behaviors for CIPP and CIPP-API" +title: "[Bug]: " +labels: + - "unconfirmed-by-user" + - "bug" body: -- type: markdown - attributes: - value: > - Thanks for reporting. - - - Make sure you are able to reproduce this issue on the latest released version of CIPP & CIPP-API. - - - Please search the existing issues to see if there has been a similar issue filed - - - This is not the location for support. Issues that request support or are not a bug will be closed. -- type: textarea - attributes: - label: Description - description: > - Please describe the issue and expected result. You can include a screenshot by pasting it. Issues with a description that is too short or does not explain each step in detail will be closed. - - Example: - - 1.) go to Settings - 2.) Click on a tenant in access check - 3.) Click on the green pixel in the bottom right corner - 4.) A bug appears. - validations: - required: true -- type: textarea - attributes: - label: Environment data - description: > - Please let us know your environment information. This must follow this format or the ticket will be closed: - Sponsored / Non-sponsored instance - Front end version number: - Back end version number: - Tried Tenant Cache Clear: true/false - Tried Token Cache Clear: true/false - render: PowerShell - validations: - required: true + - type: markdown + attributes: + value: | + **Thank you for taking the time to report a potential bug for CIPP and CIPP-API!** + + Please follow the instructions below and provide as much detail as possible to help us understand and reproduce the issue. + + - type: checkboxes + id: confirmations + attributes: + label: "Required confirmations before submitting" + description: "Please check all boxes that apply." + options: + - label: "**I can reproduce this issue on the latest released versions** of both CIPP and CIPP-API." + required: true + - label: "**I have searched existing issues** (both open and closed) to avoid duplicates." + required: true + - label: "I am **not** requesting general support; this is an actual bug report." + required: true + + # 3) Description / Steps to reproduce + - type: textarea + id: description + attributes: + label: "Issue Description" + description: | + **Describe the issue clearly and provide step-by-step instructions to reproduce it.** + Screenshots can be attached by pasting them here. + + Example steps to reproduce: + 1. Go to **Settings** + 2. Click on a tenant in **Access Check** + 3. Click on the green pixel in the bottom-right corner + 4. Observe the unexpected behavior + validations: + required: true + + # 4) Environment type (Sponsored vs. Non-sponsored) + - type: dropdown + id: environment_type + attributes: + label: "Environment Type" + description: "Select whether you are using currently a paying user of the product, or if you are utilizing the free version" + options: + - "Sponsored (paying) user" + - "Non-sponsored user" + validations: + required: true + + # 5) Front End / Back End versions + - type: input + id: front_end_version + attributes: + label: "Front End Version" + description: "Please specify the front end version number (e.g., v1.2.3)." + validations: + required: true + + - type: input + id: back_end_version + attributes: + label: "Back End Version" + description: "Please specify the back end version number (e.g., v1.2.3)." + validations: + required: true + + # 7) Additional logs or trace (optional) + - type: textarea + id: logs + attributes: + label: "Relevant Logs / Stack Trace" + description: | + If available, please share any relevant logs or stack trace data. + Remove or redact any sensitive info before posting. + render: plaintext + validations: + required: false + + # 8) Closing note + - type: markdown + attributes: + value: | + Thank you for your submission! A maintainer will review your report. + Please watch the issue for follow-up questions or status updates. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..9fa384883c3b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: false +contact_links: + - name: Security Reports + url: https://github.com/KelvinTegelaar/CIPP/security/advisories + about: Please report security vulnerabilities here. + - name: Community Discord + url: https://discord.gg/cyberdrain + about: Join our discord community here. + diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index de839daf7a30..4978afebd026 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,32 +1,84 @@ -name: ✨ Feature request -description: Suggest a new feature or improvement -title: '[Feature Request]: ' -labels: [enhancement, no-priority] +name: "✨ Feature request" +description: "Suggest a new feature or improvement" +title: "[Feature Request]: " +labels: + - "enhancement" + - "no-priority" body: -- type: markdown - attributes: - value: > - Thanks for suggesting a feature! - - - Please search the existing feature request to see if there has been a similar issue filed. - - - If a feature has been filed before, but not followed up by a contributor, you can develop the feature yourself by checking the development documentation [here](https://docs.cipp.app/dev-documentation/cipp-dev-guide/setting-up-for-local-development). - - - Repeat feature requests are allowed if the previous request has been closed for more than 30 days - - - drive-by feature requests without effort will be closed. - - - A feature request has 14 days to be fullfilled before automatically being closed. if you want to work on the feature yourself use the phrase "I'd like to work on this please!" - - - Feature requests that are detrimental to security will also be closed without notice. -- type: textarea - attributes: - label: Description of the new feature - must be an in-depth explanation of the feature you want, reasoning why, and the added benefits for MSPs as a whole. - validations: - required: true -- type: textarea - attributes: - label: PowerShell commands you would normally use to achieve above request - validations: - required: false + # Introductory Markdown + - type: markdown + attributes: + value: | + **Thank you for suggesting a new feature or improvement for CIPP** + + Before creating a request, please: + + 1. Check that you have an active sponsorship, only users that are sponsoring CIPP at the $99,- sponsorship level can create feature requests. + 1. Search existing **open and closed** feature requests to avoid duplicates. + 2. Note that **repeat feature requests** are permitted if a previous request was closed more than 30 days ago. + 3. Consider implementing the feature yourself by reviewing the [development documentation](https://docs.cipp.app/dev-documentation/cipp-dev-guide/setting-up-for-local-development). + 4. Feature requests that lack sufficient detail or feasibility may be closed at any time. + 5. **This request will auto-close in 14 days** if no meaningful progress or collaboration occurs. + 6. If you would like to work on this feature, comment `"I'd like to work on this please!"` + 7. Any request that is detrimental to security or the product’s stability will be closed without notice. + + # Checkboxes for Confirmations + - type: checkboxes + id: confirmations + attributes: + label: "Please confirm:" + description: "Check all boxes that apply." + options: + - label: "**I have searched existing feature requests** (open and closed) and found no duplicates." + required: true + - label: "**me or my organization is currently an active sponsor of the product at the $99,- level." + required: true + + - type: textarea + id: problem-statement + attributes: + label: "Problem Statement" + description: | + **What problem does this feature solve or what gap does it fill?** + Provide a concise explanation. For example: + - "When I need to enable MFA for all users, it takes a lot of time to do it per user. I'd like to enable this for all users in button press" + - "To update a user property for exchange I need to go through 5 portals and 2 systems to be able to make a change." + validations: + required: true + + # Field 2: Benefits for MSPs + - type: textarea + id: msp-benefits + attributes: + label: "Benefits for MSPs" + description: | + **How would this feature help MSPs in their day-to-day tasks or overall operations?** + - Does it reduce manual work? + - Does it improve security or scalability? + - Does it offer clarity or automation to commonly repeated tasks? + validations: + required: true + + # Field 3: Value or Importance + - type: textarea + id: feature-value + attributes: + label: "Value or Importance" + description: | + **Why is this feature particularly valuable or important to add?** + - If it's critical, explain why. + - If it's optional or nice-to-have, describe how it still adds notable value. + validations: + required: true + + # Optional field for PowerShell commands + - type: textarea + id: powershell-commands + attributes: + label: "PowerShell Commands (Optional)" + description: | + If you currently achieve this functionality or a similar workaround using PowerShell, please share your scripts or snippets here. + This information helps contributors understand the existing workflow and aids in development. + validations: + required: false diff --git a/.github/workflows/Check_for_Version_Update.yml b/.github/workflows/Check_for_Version_Update.yml index ec6f3e68769b..f8a7db0343f2 100644 --- a/.github/workflows/Check_for_Version_Update.yml +++ b/.github/workflows/Check_for_Version_Update.yml @@ -7,17 +7,17 @@ on: jobs: build: if: github.repository_owner == 'KelvinTegelaar' - name: 'Check for Version Update' + name: "Check for Version Update" runs-on: ubuntu-latest steps: - name: Check for Changed Files uses: brettcannon/check-for-changed-files@v1.1.0 with: - file-pattern: version_latest.txt - failure-message: 'You have not updated version_latest.txt. This is a required file to update at each PR. Please sync your latest changes and update the version number.' + 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." - name: Prevent changes to workflow files uses: DovnarAlexander/github-action-file-detection@v0.3.0 with: - wildcard: '.github/workflows/*.yml' + wildcard: ".github/workflows/*.yml" exit_code_found: 1 exit_code_not_found: 0 diff --git a/.github/workflows/Close_Stale_Issues_and_PRs.yml b/.github/workflows/Close_Stale_Issues_and_PRs.yml index 41dc1be235d7..88607a42675d 100644 --- a/.github/workflows/Close_Stale_Issues_and_PRs.yml +++ b/.github/workflows/Close_Stale_Issues_and_PRs.yml @@ -13,6 +13,6 @@ jobs: 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.' stale-issue-label: 'no-activity' - exempt-issue-labels: 'planned' + exempt-issue-labels: 'planned,bug,roadmap' days-before-stale: 9 - days-before-close: 14 + days-before-close: 5 diff --git a/.github/workflows/Comment_on_Issues.yml b/.github/workflows/Comment_on_Issues.yml index 38e9a5405a28..8d83f6237d82 100644 --- a/.github/workflows/Comment_on_Issues.yml +++ b/.github/workflows/Comment_on_Issues.yml @@ -16,15 +16,7 @@ jobs: with: issue-number: ${{ github.event.issue.number }} body: | - Thank you for creating a bug. Please make sure your bug is indeed a unique case by checking current and past issues, and reading the complete documentation at https://docs.cipp.app/ - If your bug is a known documentation issue, it will be closed without notice by a contributor. To confirm that this is not a bug found in the documentation, please copy and paste the following comment: "I confirm that I have checked the documentation thoroughly and believe this to be an actual bug." - - Without confirming, your report will be closed in 24 hours. If you'd like this bug to be assigned to you, please comment "I would like to work on this please!". - add-comment_fr: - if: github.repository_owner == 'KelvinTegelaar' && github.event.label.name == 'enhancement' - runs-on: ubuntu-latest - permissions: - issues: write - steps: - - name: Add Comment - uses: peter-evans/create-or-update@v3 + Thank you for reporting a potential bug. If you would like to work on this bug, please comment: + > I would like to work on this please! + + Thank you for helping us maintain the project! diff --git a/.github/workflows/Label_Issues.yml b/.github/workflows/Label_Issues.yml index a0ec41e47894..38865a62fae8 100644 --- a/.github/workflows/Label_Issues.yml +++ b/.github/workflows/Label_Issues.yml @@ -14,7 +14,7 @@ jobs: - name: Label Issues uses: andymckay/labeler@5c59dabdfd4dd5bd9c6e6d255b01b9d764af4414 with: - add-labels: 'unconfirmed-by-user' + add-labels: 'not-assigned' repo-token: ${{ secrets.GITHUB_TOKEN }} label_issues_frs: if: github.repository_owner == 'KelvinTegelaar' && contains(github.event.issue.title, 'Feature') @@ -25,5 +25,5 @@ jobs: - name: Label Issues uses: andymckay/labeler@5c59dabdfd4dd5bd9c6e6d255b01b9d764af4414 with: - add-labels: 'enhancement, no-priority' + add-labels: 'enhancement, not-assigned' repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/auto_comments.yml b/.github/workflows/auto_comments.yml new file mode 100644 index 000000000000..7d7b11b45474 --- /dev/null +++ b/.github/workflows/auto_comments.yml @@ -0,0 +1,83 @@ +name: "Handle Comment Commands" + +on: + issue_comment: + types: + - created + +jobs: + handle_comment: + runs-on: ubuntu-latest + # We need permissions to modify issue comments. + # 'issues: write' is required for deleting comments. + permissions: + issues: write + + steps: + # 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 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id + }); + + # 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 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Hello, + + Thank you for your interest in improving CIPP! + To keep our development process focused and manageable, **feature requests are limited to paying users**. This policy helps us prioritize improvements that directly benefit those actively supporting CIPP and ensures we can sustain our development and support. + + When a sponsor makes a feature request, their support covers training, development, documentation, and security checks. Allowing non-sponsor requests could lead to a backlog that slows down updates and stretches resources thin, ultimately affecting the quality and sustainability of CIPP. + + While we’ve closed this request, we appreciate your input. You’re always welcome to participate in ongoing discussions or contribute to open issues. If you are a developer, feel free to open a PR that includes your feature request or comment "**I’d like to work on this!**" to assign the issue to yourself. + + **Did you get this notification in error?** Reply with a screenshot of your sponsorship payment and we’ll reopen the issue. + + _Thank you for understanding,_ + **The CIPP Team** + + # 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 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Hello, + + Thank you for reaching out! This report has been classified as a **support request** rather than a bug or feature request. To keep our development process focused, support requests are limited to paying users. This policy allows us to prioritize resources for those actively supporting CIPP, helping us maintain high-quality development and support. + + Sponsors can contact our helpdesk directly via email for assistance with any issues or questions. For non-sponsor support, please refer to our documentation and community discussions—many questions have been answered there. + + **Did you get this notification in error?** Reply with a screenshot of your sponsorship payment, and we’ll gladly reopen the request. + + _Thank you for your understanding,_ + **The CIPP Team** + + # 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 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Hello, + + Thank you for your submission! It appears this **bug report or feature request is incomplete**. We need a clear description, steps to reproduce (for bugs), or a comprehensive overview of the requested feature. + + Please submit a new request with all the necessary details. Without sufficient information, it’s difficult for contributors to triage or implement solutions. + + _Thank you!_ + **The CIPP Team** diff --git a/.github/workflows/cipp_dev_build.yml b/.github/workflows/cipp_dev_build.yml new file mode 100644 index 000000000000..dad0dbebe307 --- /dev/null +++ b/.github/workflows/cipp_dev_build.yml @@ -0,0 +1,56 @@ +name: CIPP Frontend Dev Build + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + if: github.event.repository.fork == false + name: Build and Upload CIPP Frontend + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout Code + uses: actions/checkout@v4.2.2 + + # Set up Node.js + - name: Get Node version + id: get_node_version + run: | + node_raw_version=$(node -p "require('./package.json').engines.node") + node_sanitized_version=$(echo $node_raw_version | sed -E 's/[^0-9.]+//g') + echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT + + - name: Set up Node.js + uses: actions/setup-node@v4.2.0 + with: + node-version: ${{ steps.get_node_version.outputs.node_version }} + + # Install dependencies + - name: Install Dependencies + run: yarn install + + # Build the project + - name: Build Project + run: npm run build + + # Create ZIP File in a New Source Directory + - name: Prepare and Zip Build Files + run: | + mkdir -p build + cp staticwebapp.config.json out/ + zip -r build/dev.zip out + + # Upload to Azure Blob Storage + - name: Azure Blob Upload + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.3.1 + with: + connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} + container_name: cipp + source_folder: build/ + destination_folder: / + delete_if_exists: true diff --git a/.github/workflows/cipp_frontend_build.yml b/.github/workflows/cipp_frontend_build.yml new file mode 100644 index 000000000000..76a7dbb2fbbf --- /dev/null +++ b/.github/workflows/cipp_frontend_build.yml @@ -0,0 +1,56 @@ +name: CIPP Frontend Build + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + if: github.event.repository.fork == false + name: Build and Upload CIPP Frontend + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout Code + uses: actions/checkout@v4.2.2 + + # Set up Node.js + - name: Get Node version + id: get_node_version + run: | + node_raw_version=$(node -p "require('./package.json').engines.node") + node_sanitized_version=$(echo $node_raw_version | sed -E 's/[^0-9.]+//g') + echo "node_version=$node_sanitized_version" >> $GITHUB_OUTPUT + + - name: Set up Node.js + uses: actions/setup-node@v4.2.0 + with: + node-version: ${{ steps.get_node_version.outputs.node_version }} + + # Install dependencies + - name: Install Dependencies + run: yarn install + + # Build the project + - name: Build Project + run: npm run build + + # Create ZIP File in a New Source Directory + - name: Prepare and Zip Build Files + run: | + mkdir -p build + cp staticwebapp.config.json out/ + zip -r build/latest.zip out + + # Upload to Azure Blob Storage + - name: Azure Blob Upload + uses: LanceMcCarthy/Action-AzureBlobUpload@v3.3.1 + with: + connection_string: ${{ secrets.AZURE_CONNECTION_STRING }} + container_name: cipp + source_folder: build/ + destination_folder: / + delete_if_exists: true diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml new file mode 100644 index 000000000000..d8af6078d94f --- /dev/null +++ b/.github/workflows/dev_deploy.yml @@ -0,0 +1,41 @@ +name: CIPP Development Frontend CI/CD + +on: + push: + branches: + - dev + +jobs: + build_and_deploy_job: + if: github.event.repository.fork == false && github.event_name == 'push' + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Build And Deploy + id: builddeploy + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AMBITIOUS_MOSS_0A047A40F }} # change this to your repository secret name + repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) + action: 'upload' + ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### + # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig + app_location: '/' # App source code path + api_location: '' # Api source code path - optional + output_location: '/out' # Built app content directory - optional + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: github.event.repository.fork == false && github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request Job + steps: + - name: Close Pull Request + id: closepullrequest + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AMBITIOUS_MOSS_0A047A40F }} # change this to your repository secret name + action: 'close' diff --git a/.gitignore b/.gitignore index 1f85272ad618..5ee28a7a617b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,8 @@ yarn-debug.log* yarn-error.log* # vscode debug logs -debug.log \ No newline at end of file +debug.log +app.log + +# AI rules +.*/rules diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..6eaf6dd1ea6f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker", + ] +} diff --git a/Tools/Start-CippDevEmulators.ps1 b/Tools/Start-CippDevEmulators.ps1 index 882579f1bdc4..74de265cc59a 100644 --- a/Tools/Start-CippDevEmulators.ps1 +++ b/Tools/Start-CippDevEmulators.ps1 @@ -1,12 +1,18 @@ -Write-Host 'Starting CIPP Dev Emulators' +Get-Command wt -ErrorAction Stop | Out-Null Get-Process node -ErrorAction SilentlyContinue | Stop-Process -ErrorAction SilentlyContinue $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') + +Write-Host 'Starting CIPP Dev Emulators' -$Process = Read-Host -Prompt 'Start Process Function (y/N)?' +if (Test-Path (Join-Path $Path 'CIPP-API-Processor')) { + $Process = Read-Host -Prompt 'Start Process Function (y/N)?' +} 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 + 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 + 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 } - diff --git a/Tools/Start-CippDevEmulatorsWithKitty.ps1 b/Tools/Start-CippDevEmulatorsWithKitty.ps1 index 39666bdcb05c..7013c054f375 100644 --- a/Tools/Start-CippDevEmulatorsWithKitty.ps1 +++ b/Tools/Start-CippDevEmulatorsWithKitty.ps1 @@ -15,7 +15,7 @@ if ($Process -eq 'y') { kitty @new-window --new-tab --tab-title `"Azurite`" --cwd $Path -- azurite ; kitty @new-window --new-tab --tab-title `"FunctionApp`" --cwd (Join-Path $Path `"CIPP-API`") -- func start; kitty @new-window --new-tab --tab-title `"CIPP Frontend`" --cwd (Join-Path $Path `"CIPP`") -- yarn run dev ; - kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- npm run start-swa; + kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- yarn run start-swa; kitty @new-window --new-tab --tab-title `"CIPP-API-Processor`" --cwd (Join-Path $Path `"CIPP-API-Processor`") -- func start --port 7072" } else { @@ -23,5 +23,5 @@ if ($Process -eq 'y') { kitty @new-window --new-tab --tab-title `"Azurite`" --cwd $Path -- azurite ; kitty @new-window --new-tab --tab-title `"FunctionApp`" --cwd (Join-Path $Path `"CIPP-API`") -- func start; kitty @new-window --new-tab --tab-title `"CIPP Frontend`" --cwd (Join-Path $Path `"CIPP`") -- yarn run dev ; - kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- npm run start-swa" + kitty @new-window --new-tab --tab-title `"SWA`" --cwd (Join-Path $Path `"CIPP`") -- yarn run start-swa" } diff --git a/Tools/Start-CippDevInstallation.ps1 b/Tools/Start-CippDevInstallation.ps1 index fa1de465b2eb..2d00a12c6a26 100644 --- a/Tools/Start-CippDevInstallation.ps1 +++ b/Tools/Start-CippDevInstallation.ps1 @@ -1,32 +1,32 @@ $Path = (Get-Item $PSScriptRoot).Parent.Parent.FullName -if (-not(Get-Command npm)) { - throw 'npm is required to install the CIPP development environment' +if (-not((Get-Command npm -ErrorAction SilentlyContinue) -or (Get-Command yarn -ErrorAction SilentlyContinue))) { + throw 'npm or yarn is required to install the CIPP development environment.' } -if (-not(Get-Command azurite)) { +if (-not(Get-Command yarn -ErrorAction SilentlyContinue)) { + Write-Host 'Installing Yarn' + npm install --global yarn +} + +if (-not(Get-Command azurite -ErrorAction SilentlyContinue)) { Write-Host 'Installing Azurite' - npm install --global 'azurite' + yarn global add 'azurite' } -if (-not(Get-Command swa)) { +if (-not(Get-Command swa -ErrorAction SilentlyContinue)) { Write-Host 'Installing @azure/static-web-apps-cli' - npm install --global '@azure/static-web-apps-cli' + yarn global add '@azure/static-web-apps-cli' } -if (-not(Get-Command func)) { +if (-not(Get-Command func -ErrorAction SilentlyContinue)) { Write-Host 'Installing Azure Functions Core Tools' - npm install --global 'azure-functions-core-tools@4' --unsafe-perms true -} - -if (-not(Get-Command yarn)) { - Write-Host 'Installing Yarn' - npm install --global yarn + yarn global add 'azure-functions-core-tools@4' } -if (-not(yarn list --global --pattern 'next' | Select-String -Pattern 'next')) { +if (-not(yarn global list | Select-String -Pattern 'next')) { Write-Host 'Installing Next.js' - yarn install --global next --network-timeout 500000 + yarn global add 'next' } yarn install --cwd (Join-Path $Path "CIPP") --network-timeout 500000 diff --git a/cspell.json b/cspell.json new file mode 100644 index 000000000000..69e05ddee5f8 --- /dev/null +++ b/cspell.json @@ -0,0 +1,78 @@ +{ + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [], + "words": [ + "ADMS", + "AITM", + "Augmentt", + "Automapping", + "Autotask", + "Choco", + "cipp", + "CIPP", + "CIPP-API", + "Datto", + "Entra", + "ESET", + "GDAP", + "HIBP", + "Hudu", + "ImmyBot", + "Intune", + "LCID", + "OBEE", + "passwordless", + "Passwordless", + "pwpush", + "Reshare", + "Rewst", + "Sherweb", + "Syncro", + "TERRL", + "Yubikey", + "DMARC" + ], + "ignoreWords": [ + "Addins", + "Disablex", + "Displayname", + "CIPPAPI", + "PSTN", + "TNEF", + "Equivio", + "defaultvalues", + "Excludedfile", + "exo_individualsharing", + "exo_mailboxaudit", + "exo_mailtipsenabled", + "exo_outlookaddins", + "exo_storageproviderrestricted", + "donotchange", + "locationcipp", + "mdo_antiphishingpolicies", + "mdo_autoforwardingmode", + "mdo_blockmailforward", + "mdo_commonattachmentsfilter", + "mdo_highconfidencephishaction", + "mdo_highconfidencespamaction", + "mdo_phishthresholdlevel", + "mdo_phisspamacation", + "mdo_safeattachmentpolicy", + "mdo_safeattachments", + "mdo_safedocuments", + "mdo_safelinksforOfficeApps", + "mdo_safelinksforemail", + "mdo_spam_notifications_only_for_admins", + "mdo_zapmalware", + "mdo_zapphish", + "mdo_zapspam", + "microsoftonline", + "mip_search_auditlog", + "winmail", + "onmicrosoft.com", + "MOERA" + ], + "import": [] +} diff --git a/deployment/AzureDeploymentTemplate.json b/deployment/AzureDeploymentTemplate.json index 91ce52c678a7..54cacf9f1064 100644 --- a/deployment/AzureDeploymentTemplate.json +++ b/deployment/AzureDeploymentTemplate.json @@ -241,6 +241,19 @@ "functionAppResourceId": "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]", "functionAppRegion": "[resourceGroup().location]" } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, variables('funcAppName'), 'Contributor')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('funcAppName'))]" + ], + "properties": { + "roleDefinitionId": "[concat(subscription().id, '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('funcAppName')),'2019-08-01', 'full').identity.principalId]", + "scope": "[resourceGroup().id]" + } } ], "outputs": {} diff --git a/deployment/AzureDeploymentTemplate_regionoptions.json b/deployment/AzureDeploymentTemplate_regionoptions.json index 7e23726404c2..71e0cef97aca 100644 --- a/deployment/AzureDeploymentTemplate_regionoptions.json +++ b/deployment/AzureDeploymentTemplate_regionoptions.json @@ -241,6 +241,19 @@ "functionAppResourceId": "[resourceId('Microsoft.Web/sites/', variables('funcAppName'))]", "functionAppRegion": "[resourceGroup().location]" } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, variables('funcAppName'), 'Contributor')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('funcAppName'))]" + ], + "properties": { + "roleDefinitionId": "[concat(subscription().id, '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('funcAppName')),'2019-08-01', 'full').identity.principalId]", + "scope": "[resourceGroup().id]" + } } ], "outputs": {} diff --git a/generate-placeholders.js b/generate-placeholders.js index 9858fc7f7b7c..34e28fb31ccf 100644 --- a/generate-placeholders.js +++ b/generate-placeholders.js @@ -35,11 +35,11 @@ const pages = [ { title: "Backup Wizard", path: "/tenant/backup/backup-wizard" }, { title: "Restore Wizard", path: "/tenant/backup/restore-wizard" }, { title: "Tools", path: "/tenant/administration" }, - { title: "Graph Explorer", path: "/tenant/administration/graph-explorer" }, - { title: "Application Approval", path: "/tenant/administration/appapproval" }, + { title: "Graph Explorer", path: "/tenant/tools/graph-explorer" }, + { title: "Application Approval", path: "/tenant/tools/appapproval" }, { title: "IP Database", path: "/tenant/tools/geoiplookup" }, { title: "Tenant Lookup", path: "/tenant/administration/tenantlookup" }, - { title: "Individual Domain Check", path: "/tenant/standards/individual-domains" }, + { title: "Individual Domain Check", path: "/tenant/tools/individual-domains" }, { title: "BPA Report Builder", path: "/tenant/tools/bpa-report-builder" }, { title: "Standards", path: "/tenant/standards" }, { title: "Edit Standards", path: "/tenant/standards/list-applied-standards" }, @@ -70,6 +70,8 @@ const pages = [ { title: "Defender Deployment", path: "/security/defender/deployment" }, { title: "Vulnerabilities", path: "/security/defender/list-defender-tvm" }, { title: "Device Compliance", path: "/security/reports/list-device-compliance" }, + { title: "Safe Links", path: "/security/safelinks/safelinks" }, + { title: "Safe Links Templates", path: "/security/safelinks/safelinks-template" }, { title: "Applications", path: "/endpoint/applications/list" }, { title: "Application Queue", path: "/endpoint/applications/queue" }, { title: "Add Choco App", path: "/endpoint/applications/add-choco-app" }, @@ -82,7 +84,7 @@ const pages = [ { title: "Add Profile", path: "/endpoint/autopilot/add-profile" }, { title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" }, { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" }, - { title: "Devices", path: "/endpoint/reports/devices" }, + { title: "Devices", path: "/endpoint/MEM/devices" }, { title: "Configuration Policies", path: "/endpoint/MEM/list-policies" }, { title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" }, { title: "Protection Policies", path: "/endpoint/MEM/list-appprotection-policies" }, @@ -99,6 +101,7 @@ const pages = [ { title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" }, { title: "Mailbox Rules", path: "/email/administration/mailbox-rules" }, { title: "Contacts", path: "/email/administration/contacts" }, + { title: "Contact Templates", path: "/email/administration/contacts-template" }, { title: "Quarantine", path: "/email/administration/quarantine" }, { title: "Tenant Allow/Block Lists", path: "/email/administration/tenant-allow-block-lists" }, { title: "Mailbox Restore Wizard", path: "/email/tools/mailbox-restore-wizard" }, @@ -108,9 +111,9 @@ const pages = [ { title: "Transport rules", path: "/email/transport/list-rules" }, { title: "Deploy Transport rule", path: "/email/transport/deploy-rules" }, { title: "Transport Templates", path: "/email/transport/list-templates" }, - { title: "Connectors", path: "/email/connectors/list-connectors" }, + { title: "Connectors", path: "/email/transport/list-connectors" }, { title: "Deploy Connector Templates", path: "/email/connectors/deploy-connector" }, - { title: "Connector Templates", path: "/email/connectors/list-connector-templates" }, + { title: "Connector Templates", path: "/email/transport/list-connector-templates" }, { title: "Spamfilter", path: "/email/spamfilter/list-spamfilter" }, { title: "Apply Spamfilter Template", path: "/email/spamfilter/deploy" }, { title: "Templates", path: "/email/spamfilter/list-templates" }, @@ -121,7 +124,6 @@ const pages = [ { title: "Message Trace", path: "/email/reports/message-trace" }, { title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters" }, { title: "Malware Filters", path: "/email/reports/malware-filters" }, - { title: "Safe Links Filters", path: "/email/reports/safelinks-filters" }, { title: "Safe Attachments Filters", path: "/email/reports/safeattachments-filters" }, { title: "Shared Mailbox with Enabled Account", diff --git a/next.config.js b/next.config.js index 32f5eb3bea26..7a97a124b4bb 100644 --- a/next.config.js +++ b/next.config.js @@ -14,6 +14,8 @@ const config = { async redirects() { return []; }, + output: "export", + distDir: "./out", }; module.exports = config; diff --git a/package.json b/package.json index 6c2c57ba925d..437a7553b68a 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "cipp", - "version": "7.0.0", + "version": "8.0.3", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { "url": "https://github.com/KelvinTegelaar/CIPP/issues" }, + "license": "AGPL-3.0", "engines": { - "node": "^18.17.0" + "node": "^22.13.0" }, "repository": { "type": "git", @@ -15,7 +16,7 @@ }, "scripts": { "dev": "next -H 127.0.0.1", - "build": "next build && next export", + "build": "next build && rm -rf package.json yarn.lock", "start": "next start", "export": "next export", "lint": "next lint", @@ -23,22 +24,24 @@ "start-swa": "swa start --swa-config-location .vscode http://127.0.0.1:3000 --api-location http://127.0.0.1:7071 --verbose=silly" }, "dependencies": { - "@emotion/cache": "11.10.5", - "@emotion/react": "11.13.3", - "@emotion/server": "11.10.0", - "@emotion/styled": "11.13.0", - "@heroicons/react": "2.0.15", + "@emotion/cache": "11.14.0", + "@emotion/react": "11.14.0", + "@emotion/server": "11.11.0", + "@emotion/styled": "11.14.0", + "@heroicons/react": "2.2.0", "@monaco-editor/react": "^4.6.0", - "@mui/icons-material": "6.1.6", - "@mui/lab": "6.0.0-beta.14", - "@mui/material": "6.1.6", - "@mui/system": "6.1.6", - "@mui/x-date-pickers": "7.22.1", + "@mui/icons-material": "6.4.7", + "@mui/lab": "6.0.0-beta.30", + "@mui/material": "6.4.7", + "@mui/system": "6.4.7", + "@mui/x-date-pickers": "7.27.3", "@musement/iso-duration": "^1.0.0", - "@react-pdf/renderer": "3.1.2", - "@reduxjs/toolkit": "1.9.2", + "@react-pdf/renderer": "^4.3.0", + "@reduxjs/toolkit": "2.6.1", + "@tanstack/query-sync-storage-persister": "^5.76.0", "@tanstack/react-query": "^5.51.11", "@tanstack/react-query-devtools": "^5.51.11", + "@tanstack/react-query-persist-client": "^5.76.0", "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^2.9.1", "@tiptap/extension-heading": "^2.9.1", @@ -48,17 +51,17 @@ "@tiptap/react": "^2.9.1", "@tiptap/starter-kit": "^2.9.1", "@uiw/react-json-view": "^2.0.0-alpha.30", - "apexcharts": "3.36.3", + "apexcharts": "4.5.0", "axios": "^1.7.2", - "date-fns": "2.29.3", - "eml-parse-js": "^1.1.15", + "date-fns": "4.1.0", + "eml-parse-js": "^1.2.0-beta.0", "export-to-csv": "^1.3.0", - "formik": "2.2.9", + "formik": "2.4.6", "gray-matter": "4.0.3", - "i18next": "22.4.9", + "i18next": "24.2.3", "javascript-time-ago": "^2.5.11", - "jspdf": "^2.5.1", - "jspdf-autotable": "^3.8.2", + "jspdf": "^3.0.0", + "jspdf-autotable": "^5.0.2", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.2", "leaflet.markercluster": "^1.5.3", @@ -66,45 +69,47 @@ "material-react-table": "^3.0.1", "monaco-editor": "^0.52.0", "mui-tiptap": "^1.14.0", - "next": "^13.5.6", + "next": "^15.2.2", "nprogress": "0.2.0", "numeral": "2.0.6", "prop-types": "15.8.1", - "react": "18.2.0", - "react-apexcharts": "1.4.0", + "punycode": "^2.3.1", + "react": "19.0.0", + "react-apexcharts": "1.7.0", "react-beautiful-dnd": "13.1.1", "react-copy-to-clipboard": "^5.1.0", - "react-dom": "18.2.0", - "react-dropzone": "14.2.3", - "react-error-boundary": "^4.0.13", + "react-dom": "19.0.0", + "react-dropzone": "14.3.8", + "react-error-boundary": "^5.0.0", "react-grid-layout": "^1.5.0", "react-hook-form": "^7.53.0", - "react-hot-toast": "2.4.0", + "react-hot-toast": "2.5.2", "react-html-parser": "^2.0.2", - "react-i18next": "12.1.4", - "react-leaflet": "4.2.1", - "react-leaflet-markercluster": "^4.2.1", - "react-markdown": "8.0.5", + "react-i18next": "15.4.1", + "react-leaflet": "5.0.0", + "react-leaflet-markercluster": "^5.0.0-rc.0", + "react-markdown": "10.1.0", "react-media-hook": "^0.5.0", "react-papaparse": "^4.4.0", - "react-quill": "^0.0.2", - "react-redux": "8.0.5", + "react-quill": "^2.0.0", + "react-redux": "9.2.0", "react-syntax-highlighter": "^15.6.1", "react-time-ago": "^7.3.3", + "react-virtuoso": "^4.12.8", "react-window": "^1.8.10", - "redux": "4.2.1", + "redux": "5.0.1", "redux-devtools-extension": "2.13.9", "redux-persist": "^6.0.0", - "redux-thunk": "2.4.2", - "simplebar": "6.2.0", - "simplebar-react": "3.2.0", + "redux-thunk": "3.1.0", + "simplebar": "6.3.0", + "simplebar-react": "3.3.0", "stylis-plugin-rtl": "2.1.1", - "typescript": "4.9.4", - "yup": "0.32.11" + "typescript": "5.8.2", + "yup": "1.6.1" }, "devDependencies": { - "@svgr/webpack": "6.5.1", - "eslint": "8.32.0", - "eslint-config-next": "13.1.6" + "@svgr/webpack": "8.1.0", + "eslint": "9.22.0", + "eslint-config-next": "15.2.2" } } diff --git a/public/assets/illustrations/undraw-into-the-night-nd84.svg b/public/assets/illustrations/undraw-into-the-night-nd84.svg new file mode 100644 index 000000000000..a7d05162299b --- /dev/null +++ b/public/assets/illustrations/undraw-into-the-night-nd84.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/integrations/github.png b/public/assets/integrations/github.png new file mode 100644 index 000000000000..e03d8dd8bcf0 Binary files /dev/null and b/public/assets/integrations/github.png differ diff --git a/public/assets/integrations/github_dark.png b/public/assets/integrations/github_dark.png new file mode 100644 index 000000000000..c61ab9d0582e Binary files /dev/null and b/public/assets/integrations/github_dark.png differ diff --git a/public/discord-mark-blue.svg b/public/discord-mark-blue.svg new file mode 100644 index 000000000000..4cadbc7f7ed3 --- /dev/null +++ b/public/discord-mark-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/languageList.json b/public/languageList.json index 3bd0ec3d8e82..769bf0b60f6b 100644 --- a/public/languageList.json +++ b/public/languageList.json @@ -3,240 +3,595 @@ "language": "Arabic", "Geographic area": "Saudi Arabia", "tag": "ar-SA", + "languageTag": "Arabic (ar-SA)", "LCID": "1025" }, + { + "language": "Arabic", + "Geographic area": "Algeria", + "tag": "ar-DZ", + "languageTag": "Arabic (ar-DZ)", + "LCID": "5121" + }, + { + "language": "Arabic", + "Geographic area": "Egypt", + "tag": "ar-EG", + "languageTag": "Arabic (ar-EG)", + "LCID": "3073" + }, + { + "language": "Arabic", + "Geographic area": "Bahrain", + "tag": "ar-BH", + "languageTag": "Arabic (ar-BH)", + "LCID": "15361" + }, + { + "language": "Arabic", + "Geographic area": "Iraq", + "tag": "ar-IQ", + "languageTag": "Arabic (ar-IQ)", + "LCID": "2049" + }, + { + "language": "Arabic", + "Geographic area": "Jordan", + "tag": "ar-JO", + "languageTag": "Arabic (ar-JO)", + "LCID": "11265" + }, + { + "language": "Arabic", + "Geographic area": "Kuwait", + "tag": "ar-KW", + "languageTag": "Arabic (ar-KW)", + "LCID": "13313" + }, + { + "language": "Arabic", + "Geographic area": "Lebanon", + "tag": "ar-LB", + "languageTag": "Arabic (ar-LB)", + "LCID": "12289" + }, + { + "language": "Arabic", + "Geographic area": "Libya", + "tag": "ar-LY", + "languageTag": "Arabic (ar-LY)", + "LCID": "4097" + }, + { + "language": "Arabic", + "Geographic area": "Morocco", + "tag": "ar-MA", + "languageTag": "Arabic (ar-MA)", + "LCID": "6145" + }, + { + "language": "Arabic", + "Geographic area": "Oman", + "tag": "ar-OM", + "languageTag": "Arabic (ar-OM)", + "LCID": "8193" + }, + { + "language": "Arabic", + "Geographic area": "Qatar", + "tag": "ar-QA", + "languageTag": "Arabic (ar-QA)", + "LCID": "16385" + }, + { + "language": "Arabic", + "Geographic area": "Syria", + "tag": "ar-SY", + "languageTag": "Arabic (ar-SY)", + "LCID": "10241" + }, + { + "language": "Arabic", + "Geographic area": "Tunisia", + "tag": "ar-TN", + "languageTag": "Arabic (ar-TN)", + "LCID": "7169" + }, + { + "language": "Arabic", + "Geographic area": "UAE", + "tag": "ar-AE", + "languageTag": "Arabic (ar-AE)", + "LCID": "14337" + }, + { + "language": "Arabic", + "Geographic area": "Yemen", + "tag": "ar-YE", + "languageTag": "Arabic (ar-YE)", + "LCID": "9217" + }, { "language": "Bulgarian", "Geographic area": "Bulgaria", "tag": "bg-BG", + "languageTag": "Bulgarian (bg-BG)", "LCID": "1026" }, { "language": "Chinese (Simplified)", "Geographic area": "People's Republic of China", "tag": "zh-CN", + "languageTag": "Chinese (Simplified) (zh-CN)", "LCID": "2052" }, { "language": "Chinese", "Geographic area": "Taiwan", "tag": "zh-TW", + "languageTag": "Chinese (zh-TW)", "LCID": "1028" }, + { + "language": "Chinese", + "Geographic area": "Hong Kong SAR", + "tag": "zh-HK", + "languageTag": "Chinese (zh-HK)", + "LCID": "3076" + }, { "language": "Croatian", "Geographic area": "Croatia", "tag": "hr-HR", + "languageTag": "Croatian (hr-HR)", "LCID": "1050" }, { "language": "Czech", "Geographic area": "Czech Republic", "tag": "cs-CZ", + "languageTag": "Czech (cs-CZ)", "LCID": "1029" }, { "language": "Danish", "Geographic area": "Denmark", "tag": "da-DK", + "languageTag": "Danish (da-DK)", "LCID": "1030" }, { "language": "Dutch", "Geographic area": "Netherlands", "tag": "nl-NL", + "languageTag": "Dutch (nl-NL)", "LCID": "1043" }, { "language": "English", "Geographic area": "United States", "tag": "en-US", + "languageTag": "English (en-US)", "LCID": "1033" }, + { + "language": "English", + "Geographic area": "Australia", + "tag": "en-AU", + "languageTag": "English (en-AU)", + "LCID": "3081" + }, + { + "language": "English", + "Geographic area": "United Kingdom", + "tag": "en-GB", + "languageTag": "English (en-GB)", + "LCID": "2057" + }, + { + "language": "English", + "Geographic area": "New Zealand", + "tag": "en-NZ", + "languageTag": "English (en-NZ)", + "LCID": "5129" + }, + { + "language": "English", + "Geographic area": "Canada", + "tag": "en-CA", + "languageTag": "English (en-CA)", + "LCID": "4105" + }, + { + "language": "English", + "Geographic area": "South Africa", + "tag": "en-ZA", + "languageTag": "English (en-ZA)", + "LCID": "7177" + }, + { + "language": "English", + "Geographic area": "Singapore", + "tag": "en-SG", + "languageTag": "English (en-SG)", + "LCID": "4100" + }, { "language": "Estonian", "Geographic area": "Estonia", "tag": "et-EE", + "languageTag": "Estonian (et-EE)", "LCID": "1061" }, { "language": "Finnish", "Geographic area": "Finland", "tag": "fi-FI", + "languageTag": "Finnish (fi-FI)", "LCID": "1035" }, { "language": "French", "Geographic area": "France", "tag": "fr-FR", + "languageTag": "French (fr-FR)", "LCID": "1036" }, + { + "language": "French", + "Geographic area": "Canada", + "tag": "fr-CA", + "languageTag": "French (fr-CA)", + "LCID": "3084" + }, + { + "language": "French", + "Geographic area": "Switzerland", + "tag": "fr-CH", + "languageTag": "French (fr-CH)", + "LCID": "4108" + }, { "language": "German", "Geographic area": "Germany", "tag": "de-DE", + "languageTag": "German (de-DE)", "LCID": "1031" }, + { + "language": "German", + "Geographic area": "Switzerland", + "tag": "de-CH", + "languageTag": "German (de-CH)", + "LCID": "2055" + }, { "language": "Greek", "Geographic area": "Greece", "tag": "el-GR", + "languageTag": "Greek (el-GR)", "LCID": "1032" }, { "language": "Hebrew", "Geographic area": "Israel", "tag": "he-IL", + "languageTag": "Hebrew (he-IL)", "LCID": "1037" }, { "language": "Hindi", "Geographic area": "India", "tag": "hi-IN", + "languageTag": "Hindi (hi-IN)", "LCID": "1081" }, { "language": "Hungarian", "Geographic area": "Hungary", "tag": "hu-HU", + "languageTag": "Hungarian (hu-HU)", "LCID": "1038" }, { "language": "Indonesian", "Geographic area": "Indonesia", "tag": "id-ID", + "languageTag": "Indonesian (id-ID)", "LCID": "1057" }, { "language": "Italian", "Geographic area": "Italy", "tag": "it-IT", + "languageTag": "Italian (it-IT)", "LCID": "1040" }, { "language": "Japanese", "Geographic area": "Japan", "tag": "ja-JP", + "languageTag": "Japanese (ja-JP)", "LCID": "1041" }, { "language": "Kazakh", "Geographic area": "Kazakhstan", "tag": "kk-KZ", + "languageTag": "Kazakh (kk-KZ)", "LCID": "1087" }, { "language": "Korean", "Geographic area": "Korea", "tag": "ko-KR", + "languageTag": "Korean (ko-KR)", "LCID": "1042" }, { "language": "Latvian", "Geographic area": "Latvia", "tag": "lv-LV", + "languageTag": "Latvian (lv-LV)", "LCID": "1062" }, { "language": "Lithuanian", "Geographic area": "Lithuania", "tag": "lt-LT", + "languageTag": "Lithuanian (lt-LT)", "LCID": "1063" }, { "language": "Malay", "Geographic area": "Malaysia", "tag": "ms-MY", + "languageTag": "Malay (ms-MY)", "LCID": "1086" }, { "language": "Norwegian (Bokmål)", "Geographic area": "Norway", "tag": "nb-NO", + "languageTag": "Norwegian (Bokmål) (nb-NO)", "LCID": "1044" }, + { + "language": "Persian", + "Geographic area": "Iran", + "tag": "fa-IR", + "languageTag": "Persian (fa-IR)", + "LCID": "1065" + }, { "language": "Polish", "Geographic area": "Poland", "tag": "pl-PL", + "languageTag": "Polish (pl-PL)", "LCID": "1045" }, { "language": "Portuguese", "Geographic area": "Brazil", "tag": "pt-BR", + "languageTag": "Portuguese (pt-BR)", "LCID": "1046" }, { "language": "Portuguese", "Geographic area": "Portugal", "tag": "pt-PT", + "languageTag": "Portuguese (pt-PT)", "LCID": "2070" }, { "language": "Romanian", "Geographic area": "Romania", "tag": "ro-RO", + "languageTag": "Romanian (ro-RO)", "LCID": "1048" }, { "language": "Russian", "Geographic area": "Russia", "tag": "ru-RU", + "languageTag": "Russian (ru-RU)", "LCID": "1049" }, { "language": "Serbian (Latin)", "Geographic area": "Serbia", "tag": "sr-latn-RS", + "languageTag": "Serbian (Latin) (sr-latn-RS)", "LCID": "2074" }, { "language": "Slovak", "Geographic area": "Slovakia", "tag": "sk-SK", + "languageTag": "Slovak (sk-SK)", "LCID": "1051" }, { "language": "Slovenian", "Geographic area": "Slovenia", "tag": "sl-SI", + "languageTag": "Slovenian (sl-SI)", "LCID": "1060" }, { "language": "Spanish", "Geographic area": "Spain", "tag": "es-ES", + "languageTag": "Spanish (es-ES)", "LCID": "3082" }, + { + "language": "Spanish", + "Geographic area": "Argentina", + "tag": "es-AR", + "languageTag": "Spanish (es-AR)", + "LCID": "11274" + }, + { + "language": "Spanish", + "Geographic area": "Bolivia", + "tag": "es-BO", + "languageTag": "Spanish (es-BO)", + "LCID": "16394" + }, + { + "language": "Spanish", + "Geographic area": "Chile", + "tag": "es-CL", + "languageTag": "Spanish (es-CL)", + "LCID": "13322" + }, + { + "language": "Spanish", + "Geographic area": "Colombia", + "tag": "es-CO", + "languageTag": "Spanish (es-CO)", + "LCID": "9226" + }, + { + "language": "Spanish", + "Geographic area": "Costa Rica", + "tag": "es-CR", + "languageTag": "Spanish (es-CR)", + "LCID": "5130" + }, + { + "language": "Spanish", + "Geographic area": "Dominican Republic", + "tag": "es-DO", + "languageTag": "Spanish (es-DO)", + "LCID": "7178" + }, + { + "language": "Spanish", + "Geographic area": "Ecuador", + "tag": "es-EC", + "languageTag": "Spanish (es-EC)", + "LCID": "12298" + }, + { + "language": "Spanish", + "Geographic area": "El Salvador", + "tag": "es-SV", + "languageTag": "Spanish (es-SV)", + "LCID": "17418" + }, + { + "language": "Spanish", + "Geographic area": "Guatemala", + "tag": "es-GT", + "languageTag": "Spanish (es-GT)", + "LCID": "4106" + }, + { + "language": "Spanish", + "Geographic area": "Honduras", + "tag": "es-HN", + "languageTag": "Spanish (es-HN)", + "LCID": "18442" + }, + { + "language": "Spanish", + "Geographic area": "Mexico", + "tag": "es-MX", + "languageTag": "Spanish (es-MX)", + "LCID": "2058" + }, + { + "language": "Spanish", + "Geographic area": "Nicaragua", + "tag": "es-NI", + "languageTag": "Spanish (es-NI)", + "LCID": "19466" + }, + { + "language": "Spanish", + "Geographic area": "Panama", + "tag": "es-PA", + "languageTag": "Spanish (es-PA)", + "LCID": "6154" + }, + { + "language": "Spanish", + "Geographic area": "Paraguay", + "tag": "es-PY", + "languageTag": "Spanish (es-PY)", + "LCID": "15370" + }, + { + "language": "Spanish", + "Geographic area": "Peru", + "tag": "es-PE", + "languageTag": "Spanish (es-PE)", + "LCID": "10250" + }, + { + "language": "Spanish", + "Geographic area": "Uruguay", + "tag": "es-UY", + "languageTag": "Spanish (es-UY)", + "LCID": "14346" + }, + { + "language": "Spanish", + "Geographic area": "Venezuela", + "tag": "es-VE", + "languageTag": "Spanish (es-VE)", + "LCID": "8202" + }, { "language": "Swedish", "Geographic area": "Sweden", "tag": "sv-SE", + "languageTag": "Swedish (sv-SE)", "LCID": "1053" }, { "language": "Thai", "Geographic area": "Thailand", "tag": "th-TH", + "languageTag": "Thai (th-TH)", "LCID": "1054" }, { "language": "Turkish", "Geographic area": "Turkey", "tag": "tr-TR", + "languageTag": "Turkish (tr-TR)", "LCID": "1055" }, { "language": "Ukrainian", - "Geographic area": "Ukrainian", + "Geographic area": "Ukraine", "tag": "uk-UA", + "languageTag": "Ukrainian (uk-UA)", "LCID": "1058" }, + { + "language": "Urdu", + "Geographic area": "Pakistan", + "tag": "ur-PK", + "languageTag": "Urdu (ur-PK)", + "LCID": "1056" + }, { "language": "Vietnamese", "Geographic area": "Vietnam", "tag": "vi-VN", + "languageTag": "Vietnamese (vi-VN)", "LCID": "1066" + }, + { + "language": "Welsh", + "Geographic area": "Wales", + "tag": "cy-GB", + "languageTag": "Welsh (cy-GB)", + "LCID": "1106" } -] +] \ No newline at end of file diff --git a/public/reportImages/board.jpg b/public/reportImages/board.jpg new file mode 100644 index 000000000000..05c3e51ddc34 Binary files /dev/null and b/public/reportImages/board.jpg differ diff --git a/public/reportImages/city.jpg b/public/reportImages/city.jpg new file mode 100644 index 000000000000..155439cf30f2 Binary files /dev/null and b/public/reportImages/city.jpg differ diff --git a/public/reportImages/glasses.jpg b/public/reportImages/glasses.jpg new file mode 100644 index 000000000000..b41d200ea650 Binary files /dev/null and b/public/reportImages/glasses.jpg differ diff --git a/public/reportImages/laptop.jpg b/public/reportImages/laptop.jpg new file mode 100644 index 000000000000..31a14fc4383e Binary files /dev/null and b/public/reportImages/laptop.jpg differ diff --git a/public/reportImages/soc.jpg b/public/reportImages/soc.jpg new file mode 100644 index 000000000000..f8da4eba5139 Binary files /dev/null and b/public/reportImages/soc.jpg differ diff --git a/public/reportImages/working.jpg b/public/reportImages/working.jpg new file mode 100644 index 000000000000..c979c21b1ea0 Binary files /dev/null and b/public/reportImages/working.jpg differ diff --git a/public/sponsors/domotz-dark.png b/public/sponsors/domotz-dark.png new file mode 100644 index 000000000000..ac26f6f0f6ad Binary files /dev/null and b/public/sponsors/domotz-dark.png differ diff --git a/public/sponsors/domotz-light.png b/public/sponsors/domotz-light.png new file mode 100644 index 000000000000..dab40b067807 Binary files /dev/null and b/public/sponsors/domotz-light.png differ diff --git a/public/version.json b/public/version.json index fa68337146a1..c46c1c3a30a4 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "7.1.1" + "version": "8.3.0" } diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index c9c62c1f8048..2081c91b2a11 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -1,9 +1,13 @@ -import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import axios, { isAxiosError } from "axios"; import { useDispatch } from "react-redux"; import { showToast } from "../store/toasts"; import { getCippError } from "../utils/get-cipp-error"; -import { useRouter } from "next/router"; export function ApiGetCall(props) { const { @@ -16,6 +20,11 @@ export function ApiGetCall(props) { bulkRequest = false, toast = false, onResult, + staleTime = 300000, + refetchOnWindowFocus = false, + refetchOnMount = true, + refetchOnReconnect = true, + keepPreviousData = false, } = props; const queryClient = useQueryClient(); const dispatch = useDispatch(); @@ -27,6 +36,12 @@ export function ApiGetCall(props) { returnRetry = false; } if (isAxiosError(error) && HTTP_STATUS_TO_NOT_RETRY.includes(error.response?.status ?? 0)) { + if ( + error.response?.status === 302 && + error.response?.headers.get("location").includes("/.auth/login/aad") + ) { + queryClient.invalidateQueries({ queryKey: ["authmecipp"] }); + } returnRetry = false; } if (returnRetry === false && toast) { @@ -93,8 +108,11 @@ export function ApiGetCall(props) { return response.data; } }, - staleTime: 600000, // 10 minutes - refetchOnWindowFocus: false, + staleTime: staleTime, + refetchOnWindowFocus: refetchOnWindowFocus, + refetchOnMount: refetchOnMount, + refetchOnReconnect: refetchOnReconnect, + keepPreviousData: keepPreviousData, retry: retryFn, }); return queryInfo; @@ -190,12 +208,16 @@ export function ApiGetCallWithPagination({ return response.data; }, getNextPageParam: (lastPage) => { - if (data?.noPagination || data?.manualPagination === false) { + if ( + data?.noPagination || + data?.manualPagination === false || + data?.tenantFilter === "AllTenants" + ) { return undefined; } return lastPage?.Metadata?.nextLink ? { nextLink: lastPage.Metadata.nextLink } : undefined; }, - staleTime: 600000, // 10 minutes + staleTime: 300000, refetchOnWindowFocus: false, retry: retryFn, }); diff --git a/src/components/CSVReader.jsx b/src/components/CSVReader.jsx index d34d9ee79007..be3f6f67f02a 100644 --- a/src/components/CSVReader.jsx +++ b/src/components/CSVReader.jsx @@ -1,86 +1,67 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useCSVReader, lightenDarkenColor, formatFileSize } from "react-papaparse"; +import { Box, Typography, useTheme } from "@mui/material"; +import { CloudUpload } from "@mui/icons-material"; -const GREY = "#CCC"; -const GREY_LIGHT = "rgba(255, 255, 255, 0.4)"; +/* + * These colors define our remove button states. The light version is + * calculated rather than hardcoded - a small touch that ensures + * consistent color relationships no matter what base color we use. + * + * Sometimes it's these little details that make a component feel polished. + */ const DEFAULT_REMOVE_HOVER_COLOR = "#A01919"; const REMOVE_HOVER_COLOR_LIGHT = lightenDarkenColor(DEFAULT_REMOVE_HOVER_COLOR, 40); -const GREY_DIM = "#686868"; -const styles = { - zone: { - alignItems: "center", - border: `2px dashed`, - borderRadius: 20, - display: "flex", - flexDirection: "column", - height: "100%", - justifyContent: "center", - padding: 20, - }, - file: { - background: "linear-gradient(to bottom, #aaa, #aaa)", - borderRadius: 20, - display: "flex", - height: 60, - width: 120, - position: "relative", - zIndex: 10, - flexDirection: "column", - justifyContent: "center", - }, - info: { - alignItems: "center", - display: "flex", - flexDirection: "column", - paddingLeft: 10, - paddingRight: 10, - }, - size: { - borderRadius: 3, - marginBottom: "0.5em", - justifyContent: "center", - display: "flex", - }, - name: { - borderRadius: 3, - fontSize: 12, - marginBottom: "0.5em", - }, - progressBar: { - bottom: 14, - position: "absolute", - width: "100%", - paddingLeft: 10, - paddingRight: 10, - }, - zoneHover: { - borderColor: GREY_DIM, - }, - default: { - borderColor: GREY, - }, - remove: { - height: 23, - position: "absolute", - right: 6, - top: 6, - width: 23, - }, -}; - -export default function CSVReader(props) { +/* + * This component has evolved from a simple file input to a polished + * upload zone that maintains state between wizard steps. It's a good + * example of how components grow with requirements while trying to + * keep their core purpose clear. + * + * The journey to this version taught us about: + * - Proper event handling with third-party libraries + * - State persistence in multi-step forms + * - The value of simple solutions (sessionStorage vs complex state) + */ +export default function CSVReader({ config, onDrop, onRemove }) { const { CSVReader } = useCSVReader(); const [zoneHover, setZoneHover] = useState(false); const [removeHoverColor, setRemoveHoverColor] = useState(DEFAULT_REMOVE_HOVER_COLOR); + const [storedFile, setStoredFile] = useState(null); + const theme = useTheme(); + + /* + * On mount, we check sessionStorage for file details. This lets us + * restore the preview when users navigate back to this step. + * + * It's a simple solution that works well - sometimes the best + * approaches don't need complex state management. The fact that + * it "just works" is a feature, not a bug. + */ + useEffect(() => { + const fileName = sessionStorage.getItem('csvFileName'); + const fileSize = sessionStorage.getItem('csvFileSize'); + if (fileName && fileSize) { + console.log('Restoring file preview:', fileName); + setStoredFile({ + name: fileName, + size: parseInt(fileSize, 10) + }); + } + }, []); return ( { - //call the ondrop function from the props, passing the results. - props.onDrop(results.data); + config={config} + onUploadAccepted={(results, file) => { + console.log('File accepted:', file.name); + onDrop(results.data); setZoneHover(false); + setStoredFile(file); + // Store file details for persistence between steps + sessionStorage.setItem('csvFileName', file.name); + sessionStorage.setItem('csvFileSize', file.size.toString()); }} onDragOver={(event) => { event.preventDefault(); @@ -92,46 +73,91 @@ export default function CSVReader(props) { }} > {({ getRootProps, acceptedFile, ProgressBar, getRemoveFileProps, Remove }) => ( - <> -
- {acceptedFile ? ( - <> -
-
- {formatFileSize(acceptedFile.size)} - {acceptedFile.name} -
-
- -
-
{ - event.preventDefault(); - setRemoveHoverColor(REMOVE_HOVER_COLOR_LIGHT); - }} - onMouseOut={(event) => { - event.preventDefault(); - setRemoveHoverColor(DEFAULT_REMOVE_HOVER_COLOR); - }} - > - -
-
- - ) : ( - "Drop CSV file here or click to upload" - )} -
- + + {(acceptedFile || storedFile) ? ( + + + + + + {acceptedFile?.name || storedFile?.name} + + + {formatFileSize(acceptedFile?.size || storedFile?.size)} + + + + + {acceptedFile && } + + {/* + * The remove button's event handling taught us about working with + * third-party libraries. Instead of fighting the library's patterns, + * we adapted to work with them. A good reminder that sometimes + * the best solution is to follow the path of least resistance. + */} + { + console.log('Removing file'); + setStoredFile(null); + sessionStorage.removeItem('csvFileName'); + sessionStorage.removeItem('csvFileSize'); + // Notify parent that file was removed + onRemove?.(); + } + })} + > + + + + ) : ( + + + + Drop CSV file here + + + or click to browse + + + )} + )}
); -} +} \ No newline at end of file diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index aa7fc42a8bca..7c96e641df6e 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -39,8 +39,8 @@ export const CippBannerListCard = (props) => { - + @@ -72,17 +72,31 @@ export const CippBannerListCard = (props) => {
  • handleExpand(item.id) : undefined} > {/* Left Side: cardLabelBox */} - + {typeof item.cardLabelBox === "object" ? ( @@ -102,8 +116,16 @@ export const CippBannerListCard = (props) => { {/* Main Text and Subtext */} - - + + {item.text} @@ -113,7 +135,7 @@ export const CippBannerListCard = (props) => { {/* Right Side: Status and Expand Icon */} - + {item?.statusText && ( { {item.statusText} )} + {item?.cardLabelBoxActions && ( + e.stopPropagation()}>{item.cardLabelBoxActions} + )} {isCollapsible && ( - handleExpand(item.id)}> + { + e.stopPropagation(); + handleExpand(item.id); + }} + > { {isCollapsible && ( - + {item?.propertyItems?.length > 0 && ( )} @@ -187,9 +217,9 @@ CippBannerListCard.propTypes = { actionButton: PropTypes.element, propertyItems: PropTypes.array, table: PropTypes.object, - actionButton: PropTypes.element, isFetching: PropTypes.bool, children: PropTypes.node, + cardLabelBoxActions: PropTypes.element, }) ).isRequired, isCollapsible: PropTypes.bool, diff --git a/src/components/CippCards/CippButtonCard.jsx b/src/components/CippCards/CippButtonCard.jsx index d02a21b7a697..73f1009e1ec7 100644 --- a/src/components/CippCards/CippButtonCard.jsx +++ b/src/components/CippCards/CippButtonCard.jsx @@ -23,6 +23,7 @@ export default function CippButtonCard({ variant, component = "card", accordionExpanded = false, + onAccordionChange, }) { const [cardExpanded, setCardExpanded] = useState(accordionExpanded); useEffect(() => { @@ -31,6 +32,12 @@ export default function CippButtonCard({ } }, [accordionExpanded]); + useEffect(() => { + if (onAccordionChange) { + onAccordionChange(cardExpanded); + } + }, [cardExpanded]); + return ( {component === "card" && ( diff --git a/src/components/CippCards/CippChartCard.jsx b/src/components/CippCards/CippChartCard.jsx index 2bd455d87761..b05652ef9ec8 100644 --- a/src/components/CippCards/CippChartCard.jsx +++ b/src/components/CippCards/CippChartCard.jsx @@ -92,6 +92,7 @@ export const CippChartCard = ({ chartType = "donut", title, actions, + onClick, }) => { const [range, setRange] = useState("Last 7 days"); const [barSeries, setBarSeries] = useState([]); @@ -109,7 +110,18 @@ export const CippChartCard = ({ }, [chartType, chartSeries.length, labels]); return ( - + theme.shadows[8], + transform: "translateY(-2px)", + } : {}, + }} + > ( @@ -151,7 +152,7 @@ function DomainResultCard({ title, data, isFetching, info, type }) { ? { children: ( - + {info} @@ -186,7 +187,6 @@ function DomainResultCard({ title, data, isFetching, info, type }) { Record: - - + {info} setVisible(false)} {...offCanvasData} /> @@ -395,7 +395,8 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) }, [propDomain, setValue]); const onSubmit = (values) => { - setDomain(values.domain); + const punycodedDomain = punycode.toASCII(values.domain); + setDomain(punycodedDomain); setSelector(values.dkimSelector); setSpfRecord(values.spfRecord); setSubdomains(values.subdomains); @@ -476,13 +477,13 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) waiting: !!domain && enableHttps, }); - // Adjust grid item size based on fullwidth prop + // Adjust Grid size based on fullwidth prop const gridItemSize = fullwidth ? 12 : 4; return (
    - + - + - + - + - + - + - + - + - + - + - + - + {enableHttps && ( - + { - const { exchangeData, isFetching = false, ...other } = props; + const { exchangeData, isLoading = false, isFetching = false, handleRefresh, ...other } = props; // Define the protocols array const protocols = [ @@ -19,19 +30,78 @@ export const CippExchangeInfoCard = (props) => { { name: "ActiveSync", enabled: exchangeData?.MailboxActiveSyncEnabled }, ]; + // Define mailbox hold types array + const holds = [ + { name: "Compliance Tag Hold", enabled: exchangeData?.ComplianceTagHold }, + { name: "Retention Hold", enabled: exchangeData?.RetentionHold }, + { name: "Litigation Hold", enabled: exchangeData?.LitigationHold }, + { name: "In-Place Hold", enabled: exchangeData?.InPlaceHold }, + { name: "eDiscovery Hold", enabled: exchangeData?.EDiscoveryHold }, + { name: "Purview Retention Hold", enabled: exchangeData?.PurviewRetentionHold }, + { name: "Excluded from Org-Wide Hold", enabled: exchangeData?.ExcludedFromOrgWideHold }, + ]; + return ( - + + Exchange Information + {isFetching ? ( + + ) : ( + + + + )} + + } + /> + {exchangeData?.BlockedForSpam ? ( + + This mailbox is currently blocked for spam. + + ) : null} + isLoading ? ( + ) : ( - exchangeData?.RecipientTypeDetails || "N/A" + + + + Mailbox Type: + + + {exchangeData?.RecipientTypeDetails || "N/A"} + + + + + Hidden from GAL: + + + {getCippFormatting( + exchangeData?.HiddenFromAddressLists, + "HiddenFromAddressLists" + )} + + + + + Blocked For Spam: + + + {getCippFormatting(exchangeData?.BlockedForSpam, "BlockedForSpam")} + + + ) } /> @@ -39,12 +109,15 @@ export const CippExchangeInfoCard = (props) => { divider label="Mailbox Usage" value={ - isFetching ? ( + isLoading ? ( ) : exchangeData?.TotalItemSize != null ? ( { /> - ) : ( - getCippFormatting(exchangeData?.HiddenFromAddressLists, "HiddenFromAddressLists") - ) - } - /> - - ) : ( - getCippFormatting(exchangeData?.ForwardAndDeliver, "ForwardAndDeliver") - ) - } - /> - - ) : ( - exchangeData?.ForwardingAddress || "N/A" - ) - } - /> - - ) : ( - getCippFormatting(exchangeData?.ArchiveMailBox, "ArchiveMailBox") - ) - } - /> - - ) : ( - getCippFormatting(exchangeData?.AutoExpandingArchive, "AutoExpandingArchive") - ) - } - /> - - ) : exchangeData?.TotalArchiveItemSize != null ? ( - `${exchangeData.TotalArchiveItemSize} GB` + isLoading ? ( + ) : ( - "N/A" + (() => { + const forwardingAddress = exchangeData?.ForwardingAddress; + const forwardAndDeliver = exchangeData?.ForwardAndDeliver; + + // Determine forwarding type and clean address + let forwardingType = "None"; + let cleanAddress = ""; + + if (forwardingAddress) { + if (forwardingAddress.startsWith("smtp:")) { + forwardingType = "External"; + cleanAddress = forwardingAddress.replace("smtp:", ""); + } else { + forwardingType = "Internal"; + cleanAddress = forwardingAddress; + } + } + + return ( + + + + Forwarding Status: + + + {forwardingType === "None" + ? getCippFormatting(false, "ForwardingStatus") + : `${forwardingType} Forwarding`} + + + {forwardingType !== "None" && ( + <> + + + Keep Copy in Mailbox: + + + {getCippFormatting(forwardAndDeliver, "ForwardAndDeliver")} + + + + + Forwarding Address: + + {cleanAddress} + + + )} + + ); + })() ) } /> + + {/* Archive section - always show status */} - ) : exchangeData?.TotalArchiveItemCount != null ? ( - exchangeData.TotalArchiveItemCount + isLoading ? ( + ) : ( - "N/A" + + + + Archive Mailbox Enabled: + + + {getCippFormatting(exchangeData?.ArchiveMailBox, "ArchiveMailBox")} + + + {exchangeData?.ArchiveMailBox && ( + <> + + + Auto Expanding Archive: + + + {getCippFormatting( + exchangeData?.AutoExpandingArchive, + "AutoExpandingArchive" + )} + + + + + Total Archive Item Size: + + + {exchangeData?.TotalArchiveItemSize != null + ? `${exchangeData.TotalArchiveItemSize} GB` + : "N/A"} + + + + + Total Archive Item Count: + + + {exchangeData?.TotalArchiveItemCount != null + ? exchangeData.TotalArchiveItemCount + : "N/A"} + + + + )} + ) } /> + + isLoading ? ( + ) : ( - getCippFormatting(exchangeData?.LitigationHold, "LitigationHold") +
    + {holds.map((hold) => ( + : } + color={hold.enabled ? "success" : "default"} + variant="outlined" + size="small" + sx={{ mr: 1, mb: 1 }} + /> + ))} +
    ) } /> - {/* Combine protocols into a single PropertyListItem */} ) : (
    @@ -170,17 +294,6 @@ export const CippExchangeInfoCard = (props) => { ) } /> - - ) : ( - getCippFormatting(exchangeData?.BlockedForSpam, "BlockedForSpam") - ) - } - /> ); @@ -188,5 +301,7 @@ export const CippExchangeInfoCard = (props) => { CippExchangeInfoCard.propTypes = { exchangeData: PropTypes.object, + isLoading: PropTypes.bool, isFetching: PropTypes.bool, + handleRefresh: PropTypes.func, }; diff --git a/src/components/CippCards/CippInfoBar.jsx b/src/components/CippCards/CippInfoBar.jsx index c7ce557fdfbe..0bfb8c5e62e1 100644 --- a/src/components/CippCards/CippInfoBar.jsx +++ b/src/components/CippCards/CippInfoBar.jsx @@ -1,58 +1,118 @@ -import { Box, Card, Stack, SvgIcon, Typography, Skeleton } from "@mui/material"; -import Grid from "@mui/material/Grid"; +import React, { useState } from "react"; +import { Box, Card, Stack, SvgIcon, Typography, Skeleton, Tooltip } from "@mui/material"; +import { Grid } from "@mui/system"; +import { CippOffCanvas } from "../CippComponents/CippOffCanvas"; +import { CippPropertyListCard } from "./CippPropertyListCard"; -export const CippInfoBar = ({ data, isFetching }) => ( - - - {data.map((item) => ( - ({ - xs: `1px solid ${theme.palette.divider}`, - md: "none", - }), - borderRight: (theme) => ({ - md: `1px solid ${theme.palette.divider}`, - }), - "&:nth-of-type(3)": { - borderBottom: (theme) => ({ - xs: `1px solid ${theme.palette.divider}`, - sm: "none", - }), - }, - "&:nth-of-type(4)": { - borderBottom: "none", - borderRight: "none", - }, - }} - > - - {item?.icon && ( - - {item.icon} - - )} - { - if (!item?.icon) { - return { pl: 2 }; - } +export const CippInfoBar = ({ data, isFetching }) => { + const [visibleIndex, setVisibleIndex] = useState(null); + + return ( + + + {data.map((item, index) => ( + <> + setVisibleIndex(index) : undefined} + sx={{ + cursor: item.offcanvas ? "pointer" : "default", + borderBottom: (theme) => ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + "&:nth-of-type(3)": { + borderBottom: (theme) => ({ + xs: `1px solid ${theme.palette.divider}`, + sm: "none", + }), + }, + "&:nth-of-type(4)": { + borderBottom: "none", + borderRight: "none", + }, }} > - - {item.name} - - - {isFetching ? : item.data} - - - - - ))} - - -); + + {item?.icon && ( + + {item.icon} + + )} + {item?.toolTip ? ( + + { + if (!item?.icon) { + return { pl: 2 }; + } + }} + > + + {item.name} + + + {isFetching ? : item.data} + + + + ) : ( + { + if (!item?.icon) { + return { pl: 2 }; + } + }} + > + + {item.name} + + + {isFetching ? : item.data} + + + )} + + + {item.offcanvas && ( + <> + setVisibleIndex(null)} + > + + + + {item?.offcanvas?.propertyItems?.length > 0 && ( + + )} + + + + + + )} + + ))} + + + ); +}; diff --git a/src/components/CippCards/CippPageCard.jsx b/src/components/CippCards/CippPageCard.jsx index 9c1e1bce81b7..004f34965127 100644 --- a/src/components/CippCards/CippPageCard.jsx +++ b/src/components/CippCards/CippPageCard.jsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { Box, Container, Stack, Button, SvgIcon, Typography, Card } from "@mui/material"; import ArrowLeftIcon from "@mui/icons-material/ArrowLeft"; -import Head from "next/head"; +import { CippHead } from "../CippComponents/CippHead"; const CippPageCard = (props) => { const { title, @@ -10,6 +10,7 @@ const CippPageCard = (props) => { cardSize = "xl", hideTitleText = false, hideBackButton = false, + noTenantInHead = false, infoBar, } = props; const router = useRouter(); @@ -20,9 +21,7 @@ const CippPageCard = (props) => { return ( <> - - {title} - + { ) : ( // Two-column layout - { )} - {secondHalf.map((item, index) => ( + {isFetching ? ( } /> - ))} + ) : ( + secondHalf.map((item, index) => ( + + )) + )} - + ) )} @@ -142,22 +152,18 @@ export const CippPropertyListCard = (props) => { key={`${item.label}-${index}-ActionList-OffCanvas`} icon={{item.icon}} label={item.label} - onClick={ - item.link - ? () => window.open(item.link, "_blank") - : () => { - setActionData({ - data: data, - action: item, - ready: true, - }); - if (item?.noConfirm) { - item.customFunction(item, data, {}); - } else { - createDialog.handleOpen(); - } - } - } + onClick={() => { + setActionData({ + data: data, + action: item, + ready: true, + }); + if (item?.noConfirm) { + item.customFunction(item, data, {}); + } else { + createDialog.handleOpen(); + } + }} disabled={handleActionDisabled(data, item)} /> ))} diff --git a/src/components/CippCards/CippRemediationCard.jsx b/src/components/CippCards/CippRemediationCard.jsx index abfaac8df111..d864719f80e1 100644 --- a/src/components/CippCards/CippRemediationCard.jsx +++ b/src/components/CippCards/CippRemediationCard.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { Button, Typography, List, ListItem, SvgIcon } from "@mui/material"; import CippButtonCard from "./CippButtonCard"; // Adjust the import path as needed import { CippApiDialog } from "../CippComponents/CippApiDialog"; @@ -59,6 +58,7 @@ export default function CippRemediationCard(props) { Block user sign-in Reset user password Disconnect all current sessions + Remove all MFA methods for the user Disable all inbox rules for the user { + switch (category) { + case "Global Standards": + return ; + case "Entra (AAD) Standards": + return ; + case "Exchange Standards": + return ; + case "Defender Standards": + return ; + case "Intune Standards": + return ; + default: + return ; + } +}; + +const getActionIcon = (action) => { + switch (action?.toLowerCase()) { + case "report": + return ; + case "alert": + case "warn": + return ; + case "remediate": + return ; + default: + return ; + } +}; + +const getImpactColor = (impact) => { + switch (impact?.toLowerCase()) { + case "low impact": + return "info"; + case "medium impact": + return "warning"; + case "high impact": + return "error"; + default: + return "default"; + } +}; + +export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenant }) => { + const [expanded, setExpanded] = useState(false); + if (!standardsData) return null; + + // Get applicable templates for the current tenant + const applicableTemplates = standardsData.filter((template) => { + const tenantFilterArr = Array.isArray(template?.tenantFilter) ? template.tenantFilter : []; + const excludedTenantsArr = Array.isArray(template?.excludedTenants) + ? template.excludedTenants + : []; + + const tenantInFilter = + tenantFilterArr.length > 0 && tenantFilterArr.some((tf) => tf.value === currentTenant); + + const allTenantsTemplate = + tenantFilterArr.some((tf) => tf.value === "AllTenants") && + (excludedTenantsArr.length === 0 || + !excludedTenantsArr.some((et) => et.value === currentTenant)); + + return tenantInFilter || allTenantsTemplate; + }); + + // Combine standards from all applicable templates + const combinedStandards = {}; + for (const template of applicableTemplates) { + for (const [standardKey, standardValue] of Object.entries(template.standards)) { + combinedStandards[standardKey] = standardValue; + } + } + + // Group standards by category + const standardsByCategory = {}; + Object.entries(combinedStandards).forEach(([standardKey, standardConfig]) => { + const standardInfo = standards.find((s) => s.name === `standards.${standardKey}`); + if (standardInfo) { + const category = standardInfo.cat; + if (!standardsByCategory[category]) { + standardsByCategory[category] = []; + } + standardsByCategory[category].push({ + key: standardKey, + config: standardConfig, + info: standardInfo, + }); + } + }); + + const handleAccordionChange = (panel) => (event, isExpanded) => { + setExpanded(isExpanded ? panel : false); + }; + + return ( + + + Standards Configuration + theme.palette.grey[500], + }} + > + + + + + + + + Showing standards configuration for tenant: {currentTenant} + + + Total templates applied: {applicableTemplates.length} | Total + standards: {Object.keys(combinedStandards).length} + + + + {Object.entries(standardsByCategory).map(([category, categoryStandards], idx) => ( + `1px solid ${theme.palette.divider}`, + "&:before": { display: "none" }, + }} + > + } + aria-controls={`${category}-content`} + id={`${category}-header`} + sx={{ + minHeight: 48, + "& .MuiAccordionSummary-content": { alignItems: "center", m: 0 }, + }} + > + + {getCategoryIcon(category)} + + {category} + + + + + + + {categoryStandards.map(({ key, config, info }) => ( + + + + + + + {info.label} + + + {info.helpText} + + + + + {info.tag && info.tag.length > 0 && ( + + )} + + + + Actions: + + + {config.action && Array.isArray(config.action) ? ( + config.action.map((action, index) => ( + + )) + ) : ( + + No actions configured + + )} + + + + {info.addedComponent && info.addedComponent.length > 0 && ( + + + Fields: + + + {info.addedComponent.map((component, index) => { + const componentValue = _.get(config, component.name); + const displayValue = + componentValue?.label || componentValue || "N/A"; + return ( + + + {component.label || component.name}: + + + + ); + })} + + + )} + + + + + ))} + + + + ))} + + {Object.keys(standardsByCategory).length === 0 && ( + + + No standards configured for this tenant + + + Standards templates may not be applied to this tenant or no standards are currently + active. + + + )} + + + + + + + ); +}; diff --git a/src/components/CippCards/CippUniversalSearch.jsx b/src/components/CippCards/CippUniversalSearch.jsx index 9d9c90d63c5f..589ac2c117bf 100644 --- a/src/components/CippCards/CippUniversalSearch.jsx +++ b/src/components/CippCards/CippUniversalSearch.jsx @@ -3,7 +3,6 @@ import { TextField, Box, Typography, - Grid, Card, CardContent, CardHeader, @@ -11,6 +10,7 @@ import { Button, Link, } from "@mui/material"; +import { Grid } from "@mui/system"; import { ApiGetCall } from "../../api/ApiCall"; export const CippUniversalSearch = React.forwardRef( @@ -34,48 +34,88 @@ export const CippUniversalSearch = React.forwardRef( }; return ( - - - - - + + - {search.isFetching && ( - - - - )} - {search.isSuccess && search?.data?.length > 0 ? ( - - ) : ( - search.isSuccess && "No results found." - )} + {search.isFetching && ( + + - - + )} + {search.isSuccess && search?.data?.length > 0 ? ( + + ) : ( + search.isSuccess && "No results found." + )} + ); } ); CippUniversalSearch.displayName = "CippUniversalSearch"; -const Results = ({ items = [], searchValue }) => ( - - {items.slice(0, 9).map((item, key) => ( - - +const Results = ({ items = [], searchValue }) => { + const [currentPage, setCurrentPage] = useState(1); + const resultsPerPage = 9; // Number of results per page + const totalResults = items.length; // Total number of results + const totalPages = Math.ceil(totalResults / resultsPerPage); // Total pages + + // Calculate the results to display for the current page + const startIndex = (currentPage - 1) * resultsPerPage; + const endIndex = startIndex + resultsPerPage; + const displayedResults = items.slice(startIndex, endIndex); + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1); + } + }; + + const handlePreviousPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + return ( + <> + + {totalResults} results (Page {currentPage} of {totalPages}) + + + {displayedResults.map((item, key) => ( + + + + ))} - ))} - -); + + + + + + ); +}; const ResultsRow = ({ match, searchValue }) => { const highlightMatch = (text) => { @@ -118,7 +158,7 @@ const ResultsRow = ({ match, searchValue }) => { href={`identity/administration/users/user?tenantFilter=${ currentTenantInfo.data?.find((tenant) => tenant.customerId === match._tenantId) ?.defaultDomainName || match._tenantId - }&userId=${match.userPrincipalName}`} + }&userId=${match.id}`} variant="outlined" color="primary" size="small" diff --git a/src/components/CippCards/CippUserInfoCard.jsx b/src/components/CippCards/CippUserInfoCard.jsx index 8b9048f46fec..19585794ac4b 100644 --- a/src/components/CippCards/CippUserInfoCard.jsx +++ b/src/components/CippCards/CippUserInfoCard.jsx @@ -1,146 +1,271 @@ import PropTypes from "prop-types"; -import { Avatar, Card, CardHeader, Divider, Skeleton, Stack } from "@mui/material"; +import { Avatar, Card, CardHeader, Divider, Skeleton, Typography, Alert } from "@mui/material"; +import { AccountCircle } from "@mui/icons-material"; import { PropertyList } from "/src/components/property-list"; import { PropertyListItem } from "/src/components/property-list-item"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { Stack, Grid } from "@mui/system"; export const CippUserInfoCard = (props) => { const { user, tenant, isFetching = false, ...other } = props; + // Helper function to check if a section has any data + const hasWorkInfo = user?.jobTitle || user?.department || user?.manager?.displayName || user?.companyName; + const hasAddressInfo = + user?.streetAddress || user?.postalCode || user?.city || user?.country || user?.officeLocation; + const hasContactInfo = + user?.mobilePhone || (user?.businessPhones && user?.businessPhones.length > 0); + + // Handle image URL - only set if user and tenant exist, otherwise let Avatar fall back to children + const imageUrl = + user?.id && tenant ? `/api/ListUserPhoto?TenantFilter=${tenant}&UserId=${user.id}` : undefined; + return ( - - {isFetching ? ( - - ) : ( - - {user?.displayName?.[0] + user?.surname?.[0] || ""} - - )} - - - ) : ( - getCippFormatting(user?.accountEnabled, "accountEnabled") - ) - } - /> - - ) : ( - getCippFormatting(user?.onPremisesSyncEnabled, "onPremisesSyncEnabled") - ) - } - /> + ) : ( - getCippFormatting(user?.displayName, "displayName") + + {/* Avatar section */} + + + + + + + + + {/* Status information section */} + + + + + Account Enabled: + + + {getCippFormatting(user?.accountEnabled, "accountEnabled")} + + + + + + Synced from AD: + + + {getCippFormatting(user?.onPremisesSyncEnabled, "onPremisesSyncEnabled")} + + + + + ) } /> + + {/* Basic Identity Information */} + ) : ( - getCippFormatting(user?.userPrincipalName, "userPrincipalName") + + + + Display Name: + + + {getCippFormatting(user?.displayName, "displayName") || "N/A"} + + + + + Email Address: + + + {getCippFormatting(user?.proxyAddresses, "proxyAddresses") || "N/A"} + + + + + User Principal Name: + + + {getCippFormatting(user?.userPrincipalName, "userPrincipalName") || "N/A"} + + + ) } /> + + {/* Licenses */} + ) : !user?.assignedLicenses || user?.assignedLicenses.length === 0 ? ( + + No licenses assigned to this user + ) : ( getCippFormatting(user?.assignedLicenses, "assignedLicenses") ) } /> + + {/* Work Information Section */} + + ) : !hasWorkInfo ? ( + + No work information available + ) : ( - getCippFormatting(user?.proxyAddresses, "proxyAddresses") + + {user?.jobTitle && ( + + + Job Title: + + {user.jobTitle} + + )} + {user?.companyName && ( + + + Company Name: + + {user.companyName} + + )} + {user?.department && ( + + + Department: + + {user.department} + + )} + {user?.manager?.displayName && ( + + + Manager: + + {user.manager.displayName} + + )} + ) } /> - : user?.jobTitle || "N/A"} - /> - : user?.department || "N/A"} - /> - : user?.streetAddress || "N/A" - } - /> - : user?.postalCode || "N/A"} - /> + + {/* Contact Information Section */} : user?.officeLocation || "N/A" + isFetching ? ( + + ) : !hasContactInfo ? ( + + No contact information available + + ) : ( + + {user?.mobilePhone && ( + + + Mobile Phone: + + {user.mobilePhone} + + )} + {user?.businessPhones && user.businessPhones.length > 0 && ( + + + Business Phones: + + {user.businessPhones.join(", ")} + + )} + + ) } /> + + {/* Address Information Section */} : user?.mobilePhone || "N/A"} - /> - + + ) : !hasAddressInfo ? ( + + No address information available + ) : ( - user?.businessPhones?.join(", ") || "N/A" + + {user?.streetAddress && ( + + + Street Address: + + {user.streetAddress} + + )} + {user?.city && ( + + + City: + + {user.city} + + )} + {user?.postalCode && ( + + + Postal Code: + + {user.postalCode} + + )} + {user?.country && ( + + + Country: + + {user.country} + + )} + {user?.officeLocation && ( + + + Office Location: + + {user.officeLocation} + + )} + ) } /> @@ -151,5 +276,6 @@ export const CippUserInfoCard = (props) => { CippUserInfoCard.propTypes = { user: PropTypes.object, + tenant: PropTypes.string, isFetching: PropTypes.bool, }; diff --git a/src/components/CippComponents/AppApprovalTemplateForm.jsx b/src/components/CippComponents/AppApprovalTemplateForm.jsx new file mode 100644 index 000000000000..171a88c47938 --- /dev/null +++ b/src/components/CippComponents/AppApprovalTemplateForm.jsx @@ -0,0 +1,578 @@ +import { useState, useEffect, use } from "react"; +import { Alert, Skeleton, Stack, Typography, Button, Box } from "@mui/material"; +import { CippFormComponent } from "./CippFormComponent"; +import { CippFormCondition } from "./CippFormCondition"; +import { CippApiResults } from "./CippApiResults"; +import { Grid } from "@mui/system"; +import CippPermissionPreview from "./CippPermissionPreview"; +import { useWatch } from "react-hook-form"; + +const AppApprovalTemplateForm = ({ + formControl, + templateData, + templateLoading, + isEditing, + isCopy, + updatePermissions, + onSubmit, + refetchKey, +}) => { + const [selectedPermissionSet, setSelectedPermissionSet] = useState(null); + const [permissionsLoaded, setPermissionsLoaded] = useState(false); + + // Watch for app type selection changes + const selectedAppType = useWatch({ + control: formControl?.control, + name: "appType", + defaultValue: "EnterpriseApp", + }); + const selectedGalleryTemplate = useWatch({ + control: formControl?.control, + name: "galleryTemplateId", + }); + + // Watch for application manifest changes + const selectedApplicationManifest = useWatch({ + control: formControl?.control, + name: "applicationManifest", + }); + + // Watch for app selection changes to update template name + const selectedApp = useWatch({ + control: formControl?.control, + name: "appId", + }); + + // When templateData changes, update the form + useEffect(() => { + if (!formControl) return; // Early return if formControl is not available + + if (!isEditing && !isCopy) { + formControl.setValue("templateName", "New App Deployment Template"); + formControl.setValue("appType", "EnterpriseApp"); + setPermissionsLoaded(false); + } else if (templateData && isCopy) { + // When copying, we want to load the template data but not the ID + if (templateData[0]) { + const copyName = `Copy of ${templateData[0].TemplateName}`; + formControl.setValue("templateName", copyName); + + // Set app type based on whether it's a gallery template, defaulting to EnterpriseApp for backward compatibility + const appType = + templateData[0].AppType || + (templateData[0].GalleryTemplateId + ? "GalleryTemplate" + : templateData[0].ApplicationManifest + ? "ApplicationManifest" + : "EnterpriseApp"); + formControl.setValue("appType", appType); + + if (appType === "GalleryTemplate") { + formControl.setValue("galleryTemplateId", { + label: templateData[0].AppName || "Unknown", + value: templateData[0].GalleryTemplateId, + addedFields: { + displayName: templateData[0].AppName, + applicationId: templateData[0].AppId, + // Include saved gallery information for proper display + ...(templateData[0].GalleryInformation || {}), + }, + }); + } else if (appType === "ApplicationManifest") { + // For Application Manifest, load the manifest JSON + if (templateData[0].ApplicationManifest) { + formControl.setValue( + "applicationManifest", + JSON.stringify(templateData[0].ApplicationManifest, null, 2) + ); + } + } else { + formControl.setValue("appId", { + label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, + value: templateData[0].AppId, + addedFields: { + displayName: templateData[0].AppName, + }, + }); + } + + // Set permission set and trigger loading of permissions (only for Enterprise Apps) + if (appType === "EnterpriseApp") { + const permissionSetValue = { + label: templateData[0].PermissionSetName || "Custom Permissions", + value: templateData[0].PermissionSetId, + addedFields: { + Permissions: templateData[0].Permissions || {}, + }, + }; + + formControl.setValue("permissionSetId", permissionSetValue); + setSelectedPermissionSet(permissionSetValue); + setPermissionsLoaded(true); + } else { + // For Gallery Templates, no permission set needed + setSelectedPermissionSet(null); + setPermissionsLoaded(false); + } + } + } else if (templateData) { + // For editing, load all template data + if (templateData[0]) { + formControl.setValue("templateName", templateData[0].TemplateName); + + // Set app type based on whether it's a gallery template, defaulting to EnterpriseApp for backward compatibility + const appType = + templateData[0].AppType || + (templateData[0].GalleryTemplateId + ? "GalleryTemplate" + : templateData[0].ApplicationManifest + ? "ApplicationManifest" + : "EnterpriseApp"); + formControl.setValue("appType", appType); + + if (appType === "GalleryTemplate") { + formControl.setValue("galleryTemplateId", { + label: templateData[0].AppName || "Unknown", + value: templateData[0].GalleryTemplateId, + addedFields: { + displayName: templateData[0].AppName, + applicationId: templateData[0].AppId, + // Include saved gallery information for proper display + ...(templateData[0].GalleryInformation || {}), + }, + }); + } else if (appType === "ApplicationManifest") { + // For Application Manifest, load the manifest JSON + if (templateData[0].ApplicationManifest) { + formControl.setValue( + "applicationManifest", + JSON.stringify(templateData[0].ApplicationManifest, null, 2) + ); + } + } else { + formControl.setValue("appId", { + label: `${templateData[0].AppName || "Unknown"} (${templateData[0].AppId})`, + value: templateData[0].AppId, + addedFields: { + displayName: templateData[0].AppName, + }, + }); + } + + // Set permission set and trigger loading of permissions (only for Enterprise Apps) + if (appType === "EnterpriseApp") { + const permissionSetValue = { + label: templateData[0].PermissionSetName || "Custom Permissions", + value: templateData[0].PermissionSetId, + addedFields: { + Permissions: templateData[0].Permissions || {}, + }, + }; + + formControl.setValue("permissionSetId", permissionSetValue); + setSelectedPermissionSet(permissionSetValue); + setPermissionsLoaded(true); + } else { + // For Gallery Templates and Application Manifests, no permission set needed + setSelectedPermissionSet(null); + setPermissionsLoaded(false); + } + } + } + }, [templateData, isCopy, isEditing, formControl]); + + useEffect(() => { + if (!formControl) return; // Early return if formControl is not available + + // Update template name when app is selected if we're in add mode and name hasn't been manually changed + if (!isEditing && !isCopy) { + const currentName = formControl.getValues("templateName"); + // Only update if it's still the default or empty + if (currentName === "New App Deployment Template" || !currentName) { + let appName = null; + + if (selectedAppType === "GalleryTemplate" && selectedGalleryTemplate) { + appName = + selectedGalleryTemplate.addedFields?.displayName || selectedGalleryTemplate.label; + } else if (selectedAppType === "EnterpriseApp" && selectedApp) { + // Extract app name from the label (format is usually "AppName (AppId)") + appName = selectedApp.label.split(" (")[0]; + } + + if (appName) { + formControl.setValue("templateName", `${appName} Template`); + } + } + } + }, [selectedApp, selectedGalleryTemplate, selectedAppType, isEditing, isCopy, formControl]); + + // Watch for permission set selection changes + const selectedPermissionSetValue = useWatch({ + control: formControl?.control, + name: "permissionSetId", + }); + + useEffect(() => { + if (selectedPermissionSetValue?.value) { + setSelectedPermissionSet(selectedPermissionSetValue); + setPermissionsLoaded(true); + } else { + setSelectedPermissionSet(null); + setPermissionsLoaded(false); + } + }, [selectedPermissionSetValue]); + + // Handle initial data loading for editing and copying + useEffect(() => { + // When editing or copying, ensure permission data is properly loaded + if (isEditing || isCopy) { + if (templateData?.[0]?.Permissions) { + // Ensure permissions are immediately available for the preview + setPermissionsLoaded(true); + } + } + }, [isEditing, isCopy, templateData]); + + // Handle form submission + const handleSubmit = (data) => { + let appDisplayName, appId, galleryTemplateId, applicationManifest; + + if (data.appType === "GalleryTemplate") { + appDisplayName = + data.galleryTemplateId?.addedFields?.displayName || data.galleryTemplateId?.label; + appId = data.galleryTemplateId?.addedFields?.applicationId; + galleryTemplateId = data.galleryTemplateId?.value; + } else if (data.appType === "ApplicationManifest") { + try { + applicationManifest = JSON.parse(data.applicationManifest); + + // Validate signInAudience - only allow null/undefined or "AzureADMyOrg" + if ( + applicationManifest.signInAudience && + applicationManifest.signInAudience !== "AzureADMyOrg" + ) { + return; // Don't submit if validation fails + } + + // Extract app name from manifest + appDisplayName = + applicationManifest.displayName || + applicationManifest.appDisplayName || + "Custom Application"; + // Application ID will be generated during deployment for manifests + appId = null; + } catch (error) { + console.error("Failed to parse application manifest:", error); + return; // Don't submit if manifest is invalid + } + } else { + appDisplayName = + data.appId?.addedFields?.displayName || + (data.appId?.label ? data.appId.label.split(" (")[0] : undefined); + appId = data.appId?.value; + } + + const payload = { + TemplateName: data.templateName, + AppType: data.appType, + AppId: appId, + AppName: appDisplayName, + }; + + // Only include permission set data for Enterprise Apps + if (data.appType === "EnterpriseApp") { + payload.PermissionSetId = data.permissionSetId?.value; + payload.PermissionSetName = data.permissionSetId?.label; + payload.Permissions = data.permissionSetId?.addedFields?.Permissions; + } + // For Gallery Templates, permissions will be auto-handled from the template's app registration + if (data.appType === "GalleryTemplate") { + payload.Permissions = null; // No permissions needed for Gallery Templates + payload.GalleryTemplateId = galleryTemplateId; + payload.GalleryInformation = selectedGalleryTemplate?.addedFields || {}; + } + + // For Application Manifests, store the manifest data + if (data.appType === "ApplicationManifest") { + payload.Permissions = null; // Permissions defined in manifest + payload.ApplicationManifest = applicationManifest; + } + + if (isEditing && !isCopy && templateData?.[0]?.TemplateId) { + payload.TemplateId = templateData[0].TemplateId; + } + + // Store values before submission to set them back afterward + const currentValues = { + templateName: data.templateName, + appType: data.appType, + appId: data.appId, + galleryTemplateId: data.galleryTemplateId, + permissionSetId: data.permissionSetId, + applicationManifest: data.applicationManifest, + }; + + onSubmit(payload); + + // After submission, set the values back to what they were but mark as clean + // This will only apply to add page, as edit will get refreshed data + if (!isEditing) { + setTimeout(() => { + formControl.setValue("templateName", currentValues.templateName, { shouldDirty: false }); + formControl.setValue("appType", currentValues.appType, { shouldDirty: false }); + formControl.setValue("appId", currentValues.appId, { shouldDirty: false }); + formControl.setValue("galleryTemplateId", currentValues.galleryTemplateId, { + shouldDirty: false, + }); + formControl.setValue("permissionSetId", currentValues.permissionSetId, { + shouldDirty: false, + }); + formControl.setValue("applicationManifest", currentValues.applicationManifest, { + shouldDirty: false, + }); + }, 100); + } + }; + + return ( + + + + App Approval Template Details + {templateLoading && } + {(!templateLoading || !isEditing) && ( + <> + + App approval templates allow you to define an application with its permissions that + can be deployed to multiple tenants. Choose from three template types: +
    +
    + • Enterprise Application: Deploy existing multi-tenant apps from + your tenant. Requires "Multiple organizations" or "Personal Microsoft accounts" in + App Registration settings. +
    + • Gallery Template: Deploy pre-configured applications from + Microsoft's Enterprise Application Gallery with standard permissions. +
    + • Application Manifest: Deploy custom applications using JSON + manifests. For security, only single-tenant apps (AzureADMyOrg) are supported. +
    + + + + `${item.displayName} (${item.appId})`, + valueField: "appId", + addedField: { + displayName: "displayName", + signInAudience: "signInAudience", + }, + dataFilter: (data) => { + return data.filter( + (item) => + item.addedFields?.signInAudience === "AzureADMultipleOrgs" || + item.addedFields?.signInAudience === "AzureADandPersonalMicrosoftAccount" + ); + }, + showRefresh: true, + }} + multiple={false} + creatable={false} + required={true} + validators={{ required: "Application is required" }} + /> + + + item.displayName, + valueField: "id", + addedField: { + displayName: "displayName", + applicationId: "applicationId", + description: "description", + categories: "categories", + publisher: "publisher", + logoUrl: "logoUrl", + homePageUrl: "homePageUrl", + supportedSingleSignOnModes: "supportedSingleSignOnModes", + supportedProvisioningTypes: "supportedProvisioningTypes", + }, + showRefresh: true, + }} + multiple={false} + creatable={false} + required={true} + sortOptions={true} + validators={{ required: "Gallery template is required" }} + /> + + + { + try { + const manifest = JSON.parse(value); + + // Check for minimum required property + if (!manifest.displayName) { + return "Application manifest must include a 'displayName' property"; + } + + // Validate signInAudience if present + if (manifest.signInAudience && manifest.signInAudience !== "AzureADMyOrg") { + return "signInAudience must be null, undefined, or 'AzureADMyOrg' for security reasons"; + } + + return true; + } catch (e) { + return "Invalid JSON format"; + } + }, + }} + /> + + + + item.TemplateName, + valueField: "TemplateId", + addedField: { + Permissions: "Permissions", + }, + showRefresh: true, + }} + multiple={false} + creatable={false} + required={true} + validators={{ required: "Permission Set is required" }} + /> + + + + + + + + + + )} +
    +
    + + { + try { + return JSON.parse(selectedApplicationManifest); + } catch (e) { + return null; // Return null if JSON is invalid + } + })() + : null + } + /> + +
    + ); +}; + +export default AppApprovalTemplateForm; diff --git a/src/components/CippComponents/BPASyncDialog.jsx b/src/components/CippComponents/BPASyncDialog.jsx new file mode 100644 index 000000000000..43a16450416a --- /dev/null +++ b/src/components/CippComponents/BPASyncDialog.jsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogTitle, + Button, + DialogActions, +} from "@mui/material"; +import { Sync } from "@mui/icons-material"; +import { useForm, FormProvider } from "react-hook-form"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "./CippApiResults"; + +export const BPASyncDialog = ({ createDialog }) => { + const methods = useForm({ + defaultValues: { + tenantFilter: { + value: "AllTenants", + label: "*All Tenants", + }, + }, + }); + + // Use methods for form handling and control + const { handleSubmit, control } = methods; + + const [isSyncing, setIsSyncing] = useState(false); + const bpaSyncResults = ApiPostCall({ + urlFromData: true, + }); + + const handleForm = (values) => { + bpaSyncResults.mutate({ + url: "/api/ExecBPA", + queryKey: `bpa-sync-${values.tenantFilter}`, + data: values.tenantFilter ? { TenantFilter: values.tenantFilter } : {}, + }); + }; + + // Reset syncing state when dialog is closed + const handleClose = () => { + setIsSyncing(false); + createDialog.handleClose(); + }; + + return ( + + + + Force BPA Sync + +
    +

    + This will force a Best Practice Analyzer (BPA) sync. Select a tenant (or all + tenants) below. +

    + +
    + +
    + + + + + +
    +
    + ); +}; diff --git a/src/components/CippComponents/BreachSearchDialog.jsx b/src/components/CippComponents/BreachSearchDialog.jsx new file mode 100644 index 000000000000..e089908ffb43 --- /dev/null +++ b/src/components/CippComponents/BreachSearchDialog.jsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { Dialog, DialogContent, DialogTitle, Button, DialogActions } from "@mui/material"; +import { Search } from "@mui/icons-material"; +import { useForm, FormProvider } from "react-hook-form"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "./CippApiResults"; +import { useSettings } from "../../hooks/use-settings"; + +export const BreachSearchDialog = ({ createDialog }) => { + const tenantFilter = useSettings()?.currentTenant; + const methods = useForm({ + defaultValues: {}, + }); + + // Use methods for form handling and control + const { handleSubmit } = methods; + + const [isRunning, setIsRunning] = useState(false); + const breachSearchResults = ApiPostCall({ + urlFromData: true, + }); + + const handleForm = () => { + setIsRunning(true); + breachSearchResults.mutate({ + url: "/api/ExecBreachSearch", + queryKey: `breach-search-${tenantFilter}`, + data: { tenantFilter: tenantFilter }, + }); + }; + + // Reset running state when dialog is closed + const handleClose = () => { + setIsRunning(false); + createDialog.handleClose(); + }; + + return ( + + +
    + Run Breach Search + +
    +

    + This will run a breach search to check for potentially compromised passwords and information + for the current tenant: {tenantFilter?.displayName || tenantFilter} +

    +
    + +
    + + + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/components/CippComponents/CIPPDeviceCodeButton.js b/src/components/CippComponents/CIPPDeviceCodeButton.js new file mode 100644 index 000000000000..16711f8d1332 --- /dev/null +++ b/src/components/CippComponents/CIPPDeviceCodeButton.js @@ -0,0 +1,252 @@ +import { useState, useEffect } from "react"; +import { + Alert, + Button, + Typography, + CircularProgress, + Box, +} from "@mui/material"; +import { ApiGetCall } from "../../api/ApiCall"; + +/** + * CIPPDeviceCodeButton - A button component for Microsoft 365 OAuth authentication using device code flow + * + * @param {Object} props - Component props + * @param {Function} props.onAuthSuccess - Callback function called when authentication is successful with token data + * @param {Function} props.onAuthError - Callback function called when authentication fails with error data + * @param {string} props.buttonText - Text to display on the button (default: "Login with Device Code") + * @param {boolean} props.showResults - Whether to show authentication results in the component (default: true) + * @returns {JSX.Element} The CIPPDeviceCodeButton component + */ +export const CIPPDeviceCodeButton = ({ + onAuthSuccess, + onAuthError, + buttonText = "Login with Device Code", + showResults = true, +}) => { + const [authInProgress, setAuthInProgress] = useState(false); + const [authError, setAuthError] = useState(null); + const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + const [pollInterval, setPollInterval] = useState(null); + const [tokens, setTokens] = useState({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + // Get application ID information from API + const appIdInfo = ApiGetCall({ + url: `/api/ExecListAppId`, + queryKey: `ExecListAppId`, + waiting: true, + }); + + // Handle closing the error + const handleCloseError = () => { + setAuthError(null); + }; + + // Clear polling interval when component unmounts + useEffect(() => { + return () => { + if (pollInterval) { + clearInterval(pollInterval); + } + }; + }, [pollInterval]); + + // Start device code authentication + const startDeviceCodeAuth = async () => { + try { + setAuthInProgress(true); + setAuthError(null); + setDeviceCodeInfo(null); + setCurrentStep(1); + + // Call the API to start device code flow + const response = await fetch(`/api/ExecSAMSetup?CreateSAM=true`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (response.ok && data.code) { + // Store device code info + setDeviceCodeInfo({ + user_code: data.code, + verification_uri: data.url, + expires_in: 900, // Default to 15 minutes if not provided + }); + + // Start polling for token + const interval = setInterval(checkAuthStatus, 5000); + setPollInterval(interval); + } else { + // Error getting device code + setAuthError({ + errorCode: "device_code_error", + errorMessage: data.message || "Failed to get device code", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError(error); + } + } catch (error) { + console.error("Error starting device code authentication:", error); + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred during device code authentication", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError(error); + } + }; + + // Check authentication status + const checkAuthStatus = async () => { + try { + // Call the API to check auth status + const response = await fetch(`/api/ExecSAMSetup?CheckSetupProcess=true&step=1`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + + if (response.ok) { + if (data.step === 2) { + // Authentication successful + clearInterval(pollInterval); + setPollInterval(null); + + // Process token data + const tokenData = { + accessToken: "Successfully authenticated", + refreshToken: "Token stored on server", + accessTokenExpiresOn: new Date(Date.now() + 3600 * 1000), // 1 hour from now + refreshTokenExpiresOn: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now + username: "authenticated user", + tenantId: data.tenantId || "unknown", + onmicrosoftDomain: null, + }; + + // Store tokens in component state + setTokens(tokenData); + setDeviceCodeInfo(null); + setCurrentStep(2); + + // Call the onAuthSuccess callback if provided + if (onAuthSuccess) onAuthSuccess(tokenData); + + // Update UI state + setAuthInProgress(false); + } + } else { + // Error checking auth status + clearInterval(pollInterval); + setPollInterval(null); + + setAuthError({ + errorCode: "auth_status_error", + errorMessage: data.message || "Failed to check authentication status", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + if (onAuthError) onAuthError({ + errorCode: "auth_status_error", + errorMessage: data.message || "Failed to check authentication status", + timestamp: new Date().toISOString(), + }); + } + } catch (error) { + console.error("Error checking auth status:", error); + // Don't stop polling on transient errors + } + }; + + return ( +
    + + + {!appIdInfo.isLoading && + !appIdInfo?.data?.applicationId && ( + + The Application ID is not valid. Please check your configuration. + + ) + } + + {showResults && ( + + {deviceCodeInfo && authInProgress ? ( + + Device Code Authentication + + To sign in, use a web browser to open the page {deviceCodeInfo.verification_uri} and enter the code {deviceCodeInfo.user_code} to authenticate. + + + Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes + + + ) : tokens.accessToken ? ( + + Authentication Successful + + You've successfully refreshed your token using device code flow. + + {tokens.tenantId && ( + + Tenant ID: {tokens.tenantId} + + )} + + ) : authError ? ( + + Authentication Error: {authError.errorCode} + {authError.errorMessage} + + Time: {authError.timestamp} + + + + + + ) : null} + + )} +
    + ); +}; + +export default CIPPDeviceCodeButton; \ No newline at end of file diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx new file mode 100644 index 000000000000..88e517a2139d --- /dev/null +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -0,0 +1,683 @@ +import { useState, useEffect } from "react"; +import { Alert, Button, Typography, CircularProgress, Box } from "@mui/material"; +import { ApiGetCall } from "../../api/ApiCall"; +import { CippCopyToClipBoard } from "./CippCopyToClipboard"; + +export const CIPPM365OAuthButton = ({ + onAuthSuccess, + onAuthError, + buttonText = "Login with Microsoft", + showResults = true, + showSuccessAlert = true, + scope = "https://graph.microsoft.com/.default offline_access profile openid", + useDeviceCode = false, + applicationId = null, + autoStartDeviceLogon = false, + validateServiceAccount = true, +}) => { + const [authInProgress, setAuthInProgress] = useState(false); + const [authError, setAuthError] = useState(null); + const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); + const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false); + const [isServiceAccount, setIsServiceAccount] = useState(true); + const [tokens, setTokens] = useState({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + const appIdInfo = ApiGetCall({ + url: `/api/ExecListAppId`, + waiting: true, + }); + + useEffect(() => { + appIdInfo.refetch(); + }, []); + + const handleCloseError = () => { + setAuthError(null); + }; + + const checkIsServiceAccount = (username) => { + if (!username || !validateServiceAccount) return true; // If no username or validation disabled, don't show warning + + const lowerUsername = username.toLowerCase(); + return lowerUsername.includes("service") || lowerUsername.includes("cipp"); + }; + + // Function to retrieve device code + const retrieveDeviceCode = async () => { + setCodeRetrievalInProgress(true); + setAuthError(null); + + // Refetch appId to ensure we have the latest + await appIdInfo.refetch(); + + try { + // Get the application ID to use + const appId = + applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + + // Request device code from our API endpoint + const deviceCodeResponse = await fetch( + `/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent( + scope + )}` + ); + const deviceCodeData = await deviceCodeResponse.json(); + + if (deviceCodeResponse.ok && deviceCodeData.user_code) { + // Store device code info + setDeviceCodeInfo(deviceCodeData); + } else { + // Error getting device code + setAuthError({ + errorCode: deviceCodeData.error || "device_code_error", + errorMessage: deviceCodeData.error_description || "Failed to get device code", + timestamp: new Date().toISOString(), + }); + } + } catch (error) { + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred retrieving device code", + timestamp: new Date().toISOString(), + }); + } finally { + setCodeRetrievalInProgress(false); + } + }; + + // Device code authentication function - opens popup and starts polling + const handleDeviceCodeAuthentication = async () => { + // Refetch appId to ensure we have the latest + await appIdInfo.refetch(); + + if (!deviceCodeInfo) { + // If we don't have a device code yet, retrieve it first + await retrieveDeviceCode(); + return; + } + + setAuthInProgress(true); + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + try { + // Get the application ID to use - refetch already happened at the start of this function + const appId = + applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + + // Open popup to device login page + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + "https://microsoft.com/devicelogin", + "deviceLoginPopup", + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Start polling for token + const pollInterval = deviceCodeInfo.interval || 5; + const expiresIn = deviceCodeInfo.expires_in || 900; + const startTime = Date.now(); + + const pollForToken = async () => { + // Check if we've exceeded the expiration time + if (Date.now() - startTime >= expiresIn * 1000) { + if (popup && !popup.closed) { + popup.close(); + } + setAuthError({ + errorCode: "timeout", + errorMessage: "Device code authentication timed out", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + return; + } + + try { + // Poll for token using our API endpoint + const tokenResponse = await fetch( + `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}` + ); + const tokenData = await tokenResponse.json(); + + if (tokenResponse.ok && tokenData.status === "success") { + // Successfully got token + if (popup && !popup.closed) { + popup.close(); + } + handleTokenResponse(tokenData); + } else if ( + tokenData.error === "authorization_pending" || + tokenData.status === "pending" + ) { + // User hasn't completed authentication yet, continue polling + setTimeout(pollForToken, pollInterval * 1000); + } else if (tokenData.error === "slow_down") { + // Server asking us to slow down polling + setTimeout(pollForToken, (pollInterval + 5) * 1000); + } else { + // Other error + if (popup && !popup.closed) { + popup.close(); + } + setAuthError({ + errorCode: tokenData.error || "token_error", + errorMessage: tokenData.error_description || "Failed to get token", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + } + } catch (error) { + setTimeout(pollForToken, pollInterval * 1000); + } + }; + + // Start polling + setTimeout(pollForToken, pollInterval * 1000); + } catch (error) { + setAuthError({ + errorCode: "device_code_error", + errorMessage: error.message || "An error occurred during device code authentication", + timestamp: new Date().toISOString(), + }); + setAuthInProgress(false); + } + }; + + // Process token response (common for both auth methods) + const handleTokenResponse = (tokenData) => { + // Extract token information + const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000); + // Refresh tokens typically last for 90 days, but this can vary + const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); + + // Extract information from ID token if available + let username = "unknown user"; + let tenantId = "unknown tenant"; + let onmicrosoftDomain = null; + + if (tokenData.id_token) { + try { + const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1])); + + username = + idTokenPayload.preferred_username || + idTokenPayload.email || + idTokenPayload.upn || + idTokenPayload.name || + "unknown user"; + + if (idTokenPayload.tid) { + tenantId = idTokenPayload.tid; + } + + if (username && username.includes("@") && username.includes(".onmicrosoft.com")) { + onmicrosoftDomain = username.split("@")[1]; + } else if (idTokenPayload.iss) { + const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//); + if (issuerMatch && issuerMatch[1]) { + } + } + setIsServiceAccount(checkIsServiceAccount(username)); + } catch (error) {} + } + + // Create token result object + const tokenResult = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + accessTokenExpiresOn: accessTokenExpiresOn, + refreshTokenExpiresOn: refreshTokenExpiresOn, + username: username, + tenantId: tenantId, + onmicrosoftDomain: onmicrosoftDomain, + }; + + setTokens(tokenResult); + setDeviceCodeInfo(null); + + if (onAuthSuccess) onAuthSuccess(tokenResult); + + // Update UI state + setAuthInProgress(false); + setIsServiceAccount(checkIsServiceAccount(username)); + }; + + // MSAL-like authentication function + const handleMsalAuthentication = async () => { + // Clear previous authentication state when starting a new authentication + setAuthInProgress(true); + setAuthError(null); + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + + // Refetch app ID info to ensure we have the latest + await appIdInfo.refetch(); + + // Get the application ID to use - now we're sure to have the latest after the await + const appId = applicationId || appIdInfo?.data?.applicationId; + + // Generate MSAL-like authentication parameters + const msalConfig = { + auth: { + clientId: appId, + authority: `https://login.microsoftonline.com/common`, + redirectUri: `${window.location.origin}/authredirect`, + }, + }; + + // Define the request object similar to MSAL + const loginRequest = { + scopes: [scope], + }; + + // Generate PKCE code verifier and challenge + const generateCodeVerifier = () => { + const array = new Uint8Array(32); + window.crypto.getRandomValues(array); + return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join(""); + }; + + const codeVerifier = generateCodeVerifier(); + const codeChallenge = codeVerifier; + const state = Math.random().toString(36).substring(2, 15); + const authUrl = + `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + + `client_id=${appId}` + + `&response_type=code` + + `&redirect_uri=${encodeURIComponent(window.location.origin)}/authredirect` + + `&scope=${encodeURIComponent(scope)}` + + `&code_challenge=${codeChallenge}` + + `&code_challenge_method=plain` + + `&state=${state}` + + `&prompt=select_account`; + + // Open popup for authentication + const width = 500; + const height = 600; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + authUrl, + "msalAuthPopup", + `width=${width},height=${height},left=${left},top=${top}` + ); + + // Function to actually exchange the authorization code for tokens + const handleAuthorizationCode = async (code, receivedState) => { + // Verify the state parameter matches what we sent (security check) + if (receivedState !== state) { + const errorMessage = "State mismatch in auth response - possible CSRF attack"; + const error = { + errorCode: "state_mismatch", + errorMessage: errorMessage, + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + setAuthInProgress(false); + return; + } + try { + // Prepare the token request + const tokenRequest = { + grant_type: "authorization_code", + client_id: appId, + code: code, + redirect_uri: `${window.location.origin}/authredirect`, + code_verifier: codeVerifier, + }; + + // Make the token request through our API proxy to avoid origin header issues + const tokenResponse = await fetch(`/api/ExecTokenExchange`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + tokenRequest, + tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", + tenantId: appId, // Pass the tenant ID to retrieve the correct client secret + }), + }); + + // Parse the token response + const tokenData = await tokenResponse.json(); + + // Check if the response contains an error + if (tokenData.error) { + const error = { + errorCode: tokenData.error || "token_error", + errorMessage: + tokenData.error_description || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + setAuthInProgress(false); + return; + } + + if (tokenResponse.ok) { + // If we have a refresh token, store it + if (tokenData.refresh_token) { + try { + // Extract tid from access_token jwt base64 + const accessTokenParts = tokenData.access_token.split("."); + const accessTokenPayload = JSON.parse(atob(accessTokenParts[1] || "")); + tokenData.tid = accessTokenPayload.tid; + const refreshResponse = await fetch(`/api/ExecUpdateRefreshToken`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + tenantId: tokenData.tid, + refreshtoken: tokenData.refresh_token, + tenantMode: tokenData.tenantMode, + allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement, + }), + }); + + if (!refreshResponse.ok) { + console.warn("Failed to store refresh token, but continuing with authentication"); + } + } catch (error) { + console.error("Failed to store refresh token:", error); + } + } + + handleTokenResponse(tokenData); + } else { + // Handle token error - display in error box instead of throwing + const error = { + errorCode: tokenData.error || "token_error", + errorMessage: + tokenData.error_description || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + } + } catch (error) { + const errorObj = { + errorCode: "token_exchange_error", + errorMessage: error.message || "Failed to exchange authorization code for tokens", + timestamp: new Date().toISOString(), + }; + setAuthError(errorObj); + if (onAuthError) onAuthError(errorObj); + } finally { + // Close the popup window if it's still open + if (popup && !popup.closed) { + popup.close(); + } + + // Update UI state + setAuthInProgress(false); + } + }; + + // Monitor for the redirect with the authorization code + // This is what MSAL does internally + const checkPopupLocation = setInterval(() => { + if (!popup || popup.closed) { + clearInterval(checkPopupLocation); + + // If authentication is still in progress when popup closes, it's an error + if (authInProgress) { + const errorMessage = "Authentication was cancelled. Please try again."; + const error = { + errorCode: "user_cancelled", + errorMessage: errorMessage, + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + + // Ensure we're not showing any previous success state + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }); + } + + setAuthInProgress(false); + return; + } + + try { + // Try to access the popup location to check for the authorization code + const currentUrl = popup.location.href; + + // Check if the URL contains a code parameter (authorization code) + if (currentUrl.includes("code=") && currentUrl.includes("state=")) { + clearInterval(checkPopupLocation); + // Parse the URL to extract the code and state + const urlParams = new URLSearchParams(popup.location.search); + const code = urlParams.get("code"); + const receivedState = urlParams.get("state"); + + // Process the authorization code + handleAuthorizationCode(code, receivedState); + } + + // Check for error in the URL + if (currentUrl.includes("error=")) { + clearInterval(checkPopupLocation); + // Parse the URL to extract the error details + const urlParams = new URLSearchParams(popup.location.search); + const errorCode = urlParams.get("error"); + const errorDescription = urlParams.get("error_description"); + + // Set the error state + const error = { + errorCode: errorCode, + errorMessage: errorDescription || "Unknown authentication error", + timestamp: new Date().toISOString(), + }; + setAuthError(error); + if (onAuthError) onAuthError(error); + + // Close the popup + popup.close(); + setAuthInProgress(false); + } + } catch (error) { + // This will throw an error when the popup is on a different domain + // due to cross-origin restrictions, which is normal during auth flow + // Just continue monitoring + } + }, 500); + + // Also monitor for popup closing as a fallback + }; + + // Auto-start device code retrieval if requested + useEffect(() => { + if ( + useDeviceCode && + autoStartDeviceLogon && + !codeRetrievalInProgress && + !deviceCodeInfo && + !tokens.accessToken && + appIdInfo?.data + ) { + retrieveDeviceCode(); + } + }, [ + useDeviceCode, + autoStartDeviceLogon, + codeRetrievalInProgress, + deviceCodeInfo, + tokens.accessToken, + appIdInfo?.data, + ]); + + return ( +
    + {!applicationId && + !appIdInfo.isLoading && + appIdInfo?.data && // Only check if data is available + !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( + appIdInfo?.data?.applicationId + ) && ( + + The Application ID is not valid. Please check your configuration. + + )} + + {showResults && ( + + {deviceCodeInfo ? ( + + Application Creation + + {authInProgress ? ( + <> + When asked to log onto an account, please use a{" "} + CIPP Service Account. Enter this code to authenticate:{" "} + + ) : ( + <> + Click the button below to authenticate. When asked to log onto an account, + please use a CIPP Service Account. You will need to enter this + code:{" "} + + )} + + + + {authInProgress ? ( + <> + If the popup was blocked or you closed it, you can also go to{" "} + microsoft.com/devicelogin manually and enter the code shown + above. + + ) : ( + <> + When you click the button below, a popup will open to{" "} + microsoft.com/devicelogin where you'll enter this code. + + )} + + + Code expires in {Math.round(deviceCodeInfo.expires_in / 60)} minutes + + + ) : tokens.accessToken ? ( + <> + {showSuccessAlert ? ( + + Authentication Successful + + You've successfully refreshed your token. The account you're using for + authentication is: {tokens.username} + + + Tenant ID: {tokens.tenantId} + {tokens.onmicrosoftDomain && ( + <> + {" "} + | Domain: {tokens.onmicrosoftDomain} + + )} + + + Refresh token expires: {tokens.refreshTokenExpiresOn?.toLocaleString()} + + + ) : null} + + {!isServiceAccount && ( + + Service Account Required + + CIPP requires a service account for authentication. The account you're using ( + {tokens.username}) does not appear to be a service account. + + + Please redo authentication using an account with "service" or "cipp" in the + username. + + + )} + + ) : authError ? ( + + + Authentication Error: {authError.errorCode} + + {authError.errorMessage} + + Time: {authError.timestamp} + + + + + + ) : null} + + )} + +
    + ); +}; diff --git a/src/components/CippComponents/CertificateCredentialRemovalForm.jsx b/src/components/CippComponents/CertificateCredentialRemovalForm.jsx new file mode 100644 index 000000000000..9b499297ed59 --- /dev/null +++ b/src/components/CippComponents/CertificateCredentialRemovalForm.jsx @@ -0,0 +1,23 @@ +import { CippFormComponent } from "./CippFormComponent.jsx"; + +export const CertificateCredentialRemovalForm = ({ formHook, row }) => { + return ( + ({ + label: `${cred.displayName || "Unnamed"} (Expiration: ${new Date( + cred.endDateTime + ).toLocaleDateString()})`, + value: cred.keyId, + })) || [] + } + /> + ); +}; diff --git a/src/components/CippComponents/CippAddEditTenantGroups.jsx b/src/components/CippComponents/CippAddEditTenantGroups.jsx new file mode 100644 index 000000000000..35696ef1fe52 --- /dev/null +++ b/src/components/CippComponents/CippAddEditTenantGroups.jsx @@ -0,0 +1,54 @@ +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { Stack, Typography } from "@mui/material"; +import CippFormSection from "/src/components/CippFormPages/CippFormSection"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; + +const CippAddEditTenantGroups = ({ formControl, initialValues, title, backButtonTitle }) => { + return ( + { + return { + ...values, + Action: "AddEdit", + }; + }} + initialValues={initialValues} + > + Properties + + + + + + + ); +}; + +export default CippAddEditTenantGroups; diff --git a/src/components/CippComponents/CippAliasDialog.jsx b/src/components/CippComponents/CippAliasDialog.jsx new file mode 100644 index 000000000000..1046a5bb1cac --- /dev/null +++ b/src/components/CippComponents/CippAliasDialog.jsx @@ -0,0 +1,128 @@ +import { useState, useEffect } from "react"; +import { Typography, Box, Button, TextField, Chip, Stack } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { useWatch } from "react-hook-form"; + +const CippAliasDialog = ({ formHook }) => { + const [newAlias, setNewAlias] = useState(""); + + // Initialize the form field if it doesn't exist + useEffect(() => { + // Set default empty array if AddedAliases doesn't exist in the form + if (!formHook.getValues("AddedAliases")) { + formHook.setValue("AddedAliases", []); + } + }, [formHook]); + + // Use useWatch to subscribe to form field changes + const aliasList = useWatch({ + control: formHook.control, + name: "AddedAliases", + defaultValue: [], + }); + + const isPending = formHook.formState.isSubmitting; + + const handleAddAlias = () => { + if (newAlias.trim()) { + const currentAliases = formHook.getValues("AddedAliases") || []; + const newList = [...currentAliases, newAlias.trim()]; + formHook.setValue("AddedAliases", newList, { shouldValidate: true }); + setNewAlias(""); + } + }; + + const handleDeleteAlias = (aliasToDelete) => { + const currentAliases = formHook.getValues("AddedAliases") || []; + const updatedList = currentAliases.filter((alias) => alias !== aliasToDelete); + formHook.setValue("AddedAliases", updatedList, { shouldValidate: true }); + }; + + const handleKeyPress = (event) => { + if (event.key === "Enter") { + event.preventDefault(); + handleAddAlias(); + } + }; + + return ( + <> + + + Add proxy addresses (aliases) for this user. Enter each alias and click Add or press + Enter. + + + setNewAlias(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Enter an alias" + variant="outlined" + disabled={isPending} + size="small" + sx={{ + "& .MuiOutlinedInput-root": { + fontFamily: "monospace", + "& .MuiOutlinedInput-input": { + px: 2, + }, + }, + }} + /> + + + + {aliasList.length === 0 ? ( + + No aliases added yet + + ) : ( + aliasList.map((alias) => ( + handleDeleteAlias(alias)} + color="primary" + variant="outlined" + /> + )) + )} + + + + ); +}; + +export default CippAliasDialog; diff --git a/src/components/CippComponents/CippApiDialog.jsx b/src/components/CippComponents/CippApiDialog.jsx index 834cb3e958dc..6d441e7412ce 100644 --- a/src/components/CippComponents/CippApiDialog.jsx +++ b/src/components/CippComponents/CippApiDialog.jsx @@ -1,10 +1,18 @@ -import { useRouter } from "next/router"; // Import Next.js router -import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Grid } from "@mui/material"; +import { useRouter } from "next/router"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + useMediaQuery, +} from "@mui/material"; import { Stack } from "@mui/system"; import { CippApiResults } from "./CippApiResults"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; +import React, { useEffect, useState } from "react"; +import { useForm, useFormState } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; import CippFormComponent from "./CippFormComponent"; @@ -17,171 +25,161 @@ export const CippApiDialog = (props) => { row = {}, relatedQueryKeys, dialogAfterEffect, + allowResubmit = false, + children, + defaultvalues, ...other } = props; const router = useRouter(); const [addedFieldData, setAddedFieldData] = useState({}); const [partialResults, setPartialResults] = useState([]); + const [isFormSubmitted, setIsFormSubmitted] = useState(false); + const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); + + if (mdDown) { + other.fullScreen = true; + } + + const formHook = useForm({ + defaultValues: defaultvalues || {}, + mode: "onChange", // Enable real-time validation + }); + + // Get form state for validation + const { isValid } = useFormState({ control: formHook.control }); + + useEffect(() => { + if (createDialog.open) { + setIsFormSubmitted(false); + formHook.reset(defaultvalues || {}); + } + }, [createDialog.open, defaultvalues]); + const [getRequestInfo, setGetRequestInfo] = useState({ url: "", waiting: false, queryKey: "", - relatedQueryKeys: relatedQueryKeys - ? relatedQueryKeys - : api.relatedQueryKeys - ? api.relatedQueryKeys - : title, + relatedQueryKeys: relatedQueryKeys ?? api.relatedQueryKeys ?? title, bulkRequest: api.multiPost === false, - onResult: (result) => { - setPartialResults((prevResults) => [...prevResults, result]); - }, + onResult: (result) => setPartialResults((prev) => [...prev, result]), }); const actionPostRequest = ApiPostCall({ urlFromData: true, - relatedQueryKeys: relatedQueryKeys - ? relatedQueryKeys - : api.relatedQueryKeys - ? api.relatedQueryKeys - : title, + relatedQueryKeys: relatedQueryKeys ?? api.relatedQueryKeys ?? title, bulkRequest: api.multiPost === false, onResult: (result) => { - setPartialResults((prevResults) => [...prevResults, result]); - if (api?.onSuccess) { - api.onSuccess(result); - } + setPartialResults((prev) => [...prev, result]); + api?.onSuccess?.(result); }, }); + const actionGetRequest = ApiGetCall({ ...getRequestInfo, - relatedQueryKeys: relatedQueryKeys - ? relatedQueryKeys - : api.relatedQueryKeys - ? api.relatedQueryKeys - : title, + relatedQueryKeys: relatedQueryKeys ?? api.relatedQueryKeys ?? title, bulkRequest: api.multiPost === false, onResult: (result) => { - setPartialResults((prevResults) => [...prevResults, result]); - if (api?.onSuccess) { - api.onSuccess(result); - } + setPartialResults((prev) => [...prev, result]); + api?.onSuccess?.(result); }, }); const processActionData = (dataObject, row, replacementBehaviour) => { - if (typeof api?.dataFunction === "function") { - return api.dataFunction(row); - } - var newData = {}; + if (typeof api?.dataFunction === "function") return api.dataFunction(row, dataObject); + let newData = {}; if (api?.postEntireRow) { - newData = row; - } else { - Object.keys(dataObject).forEach((key) => { - const value = dataObject[key]; - - if (typeof value === "string" && value.startsWith("!")) { - newData[key] = value.slice(1); - } else if (typeof value === "string") { - if (row[value] !== undefined) { - newData[key] = row[value]; - } else { - newData[key] = value; - } - } else if (typeof value === "object" && value !== null) { - const processedValue = processActionData(value, row, replacementBehaviour); - if (replacementBehaviour !== "removeNulls" || Object.keys(processedValue).length > 0) { - newData[key] = processedValue; - } - } else if (replacementBehaviour !== "removeNulls") { - newData[key] = value; - } else if (row[value] !== undefined) { - newData[key] = row[value]; - } - }); + return row; } + + if (!dataObject) { + return dataObject; + } + + Object.keys(dataObject).forEach((key) => { + const value = dataObject[key]; + + if (typeof value === "string" && value.startsWith("!")) { + newData[key] = value.slice(1); + } else if (typeof value === "string") { + newData[key] = row[value] ?? value; + } else if (typeof value === "boolean") { + newData[key] = value; + } else if (typeof value === "object" && value !== null) { + const processedValue = processActionData(value, row, replacementBehaviour); + if (replacementBehaviour !== "removeNulls" || Object.keys(processedValue).length > 0) { + newData[key] = processedValue; + } + } else if (replacementBehaviour !== "removeNulls") { + newData[key] = value; + } + }); + return newData; }; + const tenantFilter = useSettings().currentTenant; const handleActionClick = (row, action, formData) => { - if (action.multiPost === undefined) { - action.multiPost = false; - } - if (api.customFunction) { - action.customFunction(row, action, formData); - createDialog.handleClose(); - return; - } + setIsFormSubmitted(true); + let finalData = {}; + if (typeof api?.customDataformatter === "function") { + finalData = api.customDataformatter(row, action, formData); + } else { + if (action.multiPost === undefined) action.multiPost = false; - const commonData = { - tenantFilter: tenantFilter, - ...formData, - ...addedFieldData, - }; - const processedActionData = processActionData(action.data, row, action.replacementBehaviour); - - if (Array.isArray(row) && action.multiPost === false) { - const arrayOfObjects = row.map((singleRow) => { - const itemData = { ...commonData }; - Object.keys(processedActionData).forEach((key) => { - const rowValue = singleRow[processedActionData[key]]; - itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; - }); - return itemData; - }); - if (action.type === "POST") { - actionPostRequest.mutate({ - url: action.url, - bulkRequest: true, - data: arrayOfObjects, - }); - } else if (action.type === "GET") { - setGetRequestInfo({ - url: action.url, - waiting: true, - queryKey: Date.now(), - bulkRequest: true, - data: arrayOfObjects, - }); + if (api.customFunction) { + action.customFunction(row, action, formData); + createDialog.handleClose(); + return; } - return; - } + const commonData = { + tenantFilter, + ...formData, + ...addedFieldData, + }; + const processedActionData = processActionData(action.data, row, action.replacementBehaviour); - if (Array.isArray(row) && action.multiPost === true) { - const singleArrayData = row.map((singleRow) => { - const itemData = { ...commonData }; - Object.keys(processedActionData).forEach((key) => { - const rowValue = singleRow[processedActionData[key]]; - itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; - }); - return itemData; - }); + if (!processedActionData || Object.keys(processedActionData).length === 0) { + console.warn("No data to process for action:", action); + } else { + // MULTI ROW CASES + if (Array.isArray(row)) { + const arrayData = row.map((singleRow) => { + const itemData = { ...commonData }; + Object.keys(processedActionData).forEach((key) => { + const rowValue = singleRow[processedActionData[key]]; + itemData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; + }); + return itemData; + }); + + const payload = { + url: action.url, + bulkRequest: !action.multiPost, + data: arrayData, + }; + + if (action.type === "POST") { + actionPostRequest.mutate(payload); + } else if (action.type === "GET") { + setGetRequestInfo({ + ...payload, + waiting: true, + queryKey: Date.now(), + }); + } - if (action.type === "POST") { - actionPostRequest.mutate({ - url: action.url, - bulkRequest: false, - data: singleArrayData, - }); - } else if (action.type === "GET") { - setGetRequestInfo({ - url: action.url, - waiting: true, - queryKey: Date.now(), - bulkRequest: false, - data: singleArrayData, - }); + return; + } } - return; + // ✅ FIXED: DIRECT MERGE INSTEAD OF CORRUPT TRANSFORMATION + finalData = { + ...commonData, + ...processedActionData, + }; } - const finalData = { ...commonData }; - Object.keys(processedActionData).forEach((key) => { - const rowValue = row[processedActionData[key]]; - finalData[key] = rowValue !== undefined ? rowValue : processedActionData[key]; - }); - if (action.type === "POST") { actionPostRequest.mutate({ url: action.url, @@ -198,84 +196,179 @@ export const CippApiDialog = (props) => { }); } }; - //add a useEffect, when dialogAfterEffect exists, and the post or get request is successful, run the dialogAfterEffect function + useEffect(() => { if (dialogAfterEffect && (actionPostRequest.isSuccess || actionGetRequest.isSuccess)) { - dialogAfterEffect(actionPostRequest.data.data || actionGetRequest.data); + dialogAfterEffect(actionPostRequest.data?.data || actionGetRequest.data); } }, [actionPostRequest.isSuccess, actionGetRequest.isSuccess]); - const formHook = useForm(); + const onSubmit = (data) => handleActionClick(row, api, data); const selectedType = api.type === "POST" ? actionPostRequest : actionGetRequest; - // Handling link navigation - if (api.link) { - const getNestedValue = (obj, path) => { - return path - .split(".") - .reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj); - }; + useEffect(() => { + if (api?.setDefaultValues && createDialog.open) { + fields.forEach((field) => { + const val = row[field.name]; + if ( + (typeof val === "string" && field.type === "textField") || + (typeof val === "boolean" && field.type === "switch") + ) { + formHook.setValue(field.name, val); + } else if (Array.isArray(val) && field.type === "autoComplete") { + const values = val + .map((el) => + el?.label && el?.value + ? el + : typeof el === "string" || typeof el === "number" + ? { label: el, value: el } + : null + ) + .filter(Boolean); + formHook.setValue(field.name, values); + } else if (field.type === "autoComplete" && val) { + formHook.setValue( + field.name, + typeof val === "string" + ? { label: val, value: val } + : val.label && val.value + ? val + : undefined + ); + } + }); + } + }, [createDialog.open, api?.setDefaultValues]); - const linkWithRowData = api.link.replace(/\[([^\]]+)\]/g, (_, key) => { - return getNestedValue(row, key) || `[${key}]`; - }); + const getNestedValue = (obj, path) => + path + .split(".") + .reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj); - if (linkWithRowData.startsWith("/")) { - router.push(linkWithRowData, undefined, { shallow: true }); - } else { - window.open(linkWithRowData, api.target || "_blank"); + const [linkClicked, setLinkClicked] = useState(false); + useEffect(() => setLinkClicked(false), [api.link]); + + useEffect(() => { + if (api.link && !linkClicked && row && Object.keys(row).length > 0) { + const timeoutId = setTimeout(() => { + const linkWithData = api.link.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row, key) || `[${key}]` + ); + setLinkClicked(true); + if (linkWithData.startsWith("/") && !api?.external) + router.push(linkWithData, undefined, { shallow: true }); + else window.open(linkWithData, api.target || "_blank"); + }, 0); + + return () => clearTimeout(timeoutId); } + }, [api.link, linkClicked, row, router]); - return null; - } useEffect(() => { - if (api.noConfirm) { - formHook.handleSubmit(onSubmit)(); // Submits the form on mount - createDialog.handleClose(); // Closes the dialog after submitting + if (api.noConfirm && !api.link) { + formHook.handleSubmit(onSubmit)(); + createDialog.handleClose(); } - }, [api.noConfirm]); // Run effect only when api.noConfirm changes + }, [api.noConfirm, api.link]); const handleClose = () => { createDialog.handleClose(); setPartialResults([]); }; + let confirmText; + if (typeof api?.confirmText === "string") { + if (!Array.isArray(row)) { + confirmText = api.confirmText.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row, key) || `[${key}]` + ); + } else if (row.length > 1) { + confirmText = api.confirmText.replace(/\[([^\]]+)\]/g, "the selected rows"); + } else if (row.length === 1) { + confirmText = api.confirmText.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row[0], key) || `[${key}]` + ); + } + } else { + const replaceTextInElement = (element) => { + if (!element) return element; + if (typeof element === "string") { + if (Array.isArray(row)) { + return row.length > 1 + ? element.replace(/\[([^\]]+)\]/g, "the selected rows") + : element.replace( + /\[([^\]]+)\]/g, + (_, key) => getNestedValue(row[0], key) || `[${key}]` + ); + } + return element.replace(/\[([^\]]+)\]/g, (_, key) => getNestedValue(row, key) || `[${key}]`); + } + if (React.isValidElement(element)) { + const newChildren = React.Children.map(element.props.children, replaceTextInElement); + return React.cloneElement(element, {}, newChildren); + } + return element; + }; + confirmText = replaceTextInElement(api?.confirmText); + } + return ( - -
    - {title} - - {api.confirmText} - - - - {fields && - fields.map((fieldProps, index) => { - return ( - - - - ); - })} - - - - - - - - - -
    -
    + <> + {!api?.link && ( + +
    + {title} + + {confirmText} + + + + {children ? ( + typeof children === "function" ? ( + children({ + formHook, + row, + }) + ) : ( + children + ) + ) : ( + <> + {fields?.map((fieldProps, i) => ( + + + + ))} + + )} + + + + + + + + + +
    +
    + )} + ); }; diff --git a/src/components/CippComponents/CippApiResults.jsx b/src/components/CippComponents/CippApiResults.jsx index b1fee51f09de..388b27984ced 100644 --- a/src/components/CippComponents/CippApiResults.jsx +++ b/src/components/CippComponents/CippApiResults.jsx @@ -1,16 +1,33 @@ -import { Close, ContentCopy } from "@mui/icons-material"; -import { Alert, CircularProgress, Collapse, IconButton, Typography } from "@mui/material"; -import { useEffect, useState, useMemo } from "react"; +import { Close, Download, Help, ExpandMore, ExpandLess } from "@mui/icons-material"; +import { + Alert, + CircularProgress, + Collapse, + IconButton, + Stack, + Typography, + Box, + SvgIcon, + Tooltip, + Button, + keyframes, +} from "@mui/material"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { getCippError } from "../../utils/get-cipp-error"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; -import { Grid } from "@mui/system"; +import { CippDocsLookup } from "./CippDocsLookup"; +import { CippCodeBlock } from "./CippCodeBlock"; +import React from "react"; +import { CippTableDialog } from "./CippTableDialog"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { useDialog } from "../../hooks/use-dialog"; const extractAllResults = (data) => { const results = []; const getSeverity = (text) => { if (typeof text !== "string") return "success"; - return /error|failed|exception|not found/i.test(text) ? "error" : "success"; + return /error|failed|exception|not found|invalid_grant/i.test(text) ? "error" : "success"; }; const processResultItem = (item) => { @@ -24,15 +41,18 @@ const extractAllResults = (data) => { if (item && typeof item === "object") { const text = item.resultText || ""; - const copyField = item.copyField || text; + const copyField = item.copyField || ""; const severity = typeof item.state === "string" ? item.state : getSeverity(item) ? "error" : "success"; + const details = item.details || null; if (text) { return { text, copyField, severity, + details, + ...item, }; } } @@ -52,40 +72,47 @@ const extractAllResults = (data) => { return; } - const ignoreKeys = ["metadata", "Metadata"]; + if (obj?.resultText) { + const processed = processResultItem(obj); + if (processed) { + results.push(processed); + } + } else { + const ignoreKeys = ["metadata", "Metadata", "severity"]; - if (typeof obj === "object") { - Object.keys(obj).forEach((key) => { - const value = obj[key]; - if (ignoreKeys.includes(key)) return; - if (["Results", "Result", "results", "result"].includes(key)) { - if (Array.isArray(value)) { - value.forEach((valItem) => { - const processed = processResultItem(valItem); + if (typeof obj === "object") { + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (ignoreKeys.includes(key)) return; + if (["Results", "Result", "results", "result"].includes(key)) { + if (Array.isArray(value)) { + value.forEach((valItem) => { + const processed = processResultItem(valItem); + if (processed) { + results.push(processed); + } else { + extractFrom(valItem); + } + }); + } else if (typeof value === "object") { + const processed = processResultItem(value); if (processed) { results.push(processed); } else { - extractFrom(valItem); + extractFrom(value); } - }); - } else if (typeof value === "object") { - const processed = processResultItem(value); - if (processed) { - results.push(processed); - } else { - extractFrom(value); + } else if (typeof value === "string") { + results.push({ + text: value, + copyField: value, + severity: getSeverity(value), + }); } - } else if (typeof value === "string") { - results.push({ - text: value, - copyField: value, - severity: getSeverity(value), - }); + } else { + extractFrom(value); } - } else { - extractFrom(value); - } - }); + }); + } } }; @@ -99,6 +126,9 @@ export const CippApiResults = (props) => { const [errorVisible, setErrorVisible] = useState(false); const [fetchingVisible, setFetchingVisible] = useState(false); const [finalResults, setFinalResults] = useState([]); + const [showDetails, setShowDetails] = useState({}); + const tableDialog = useDialog(); + const pageTitle = `${document.title} - Results`; const correctResultObj = useMemo(() => { if (!apiObject.isSuccess) return; @@ -129,17 +159,17 @@ export const CippApiResults = (props) => { const allResults = useMemo(() => { const apiResults = extractAllResults(correctResultObj); return apiResults; - }, [apiObject]); + }, [correctResultObj]); useEffect(() => { setErrorVisible(!!apiObject.isError); + if (apiObject.isFetching || (apiObject.isIdle === false && apiObject.isPending === true)) { + setFetchingVisible(true); + } else { + setFetchingVisible(false); + } if (!errorsOnly) { - if (apiObject.isFetching || (apiObject.isIdle === false && apiObject.isPending === true)) { - setFetchingVisible(true); - } else { - setFetchingVisible(false); - } if (allResults.length > 0) { setFinalResults( allResults.map((res, index) => ({ @@ -148,6 +178,7 @@ export const CippApiResults = (props) => { copyField: res.copyField, severity: res.severity, visible: true, + ...res, })) ); } else { @@ -163,16 +194,41 @@ export const CippApiResults = (props) => { errorsOnly, ]); - const handleCloseResult = (id) => { + const handleCloseResult = useCallback((id) => { setFinalResults((prev) => prev.map((r) => (r.id === id ? { ...r, visible: false } : r))); - }; + }, []); + + const toggleDetails = useCallback((id) => { + setShowDetails((prev) => ({ ...prev, [id]: !prev[id] })); + }, []); + + const handleDownloadCsv = useCallback(() => { + if (!finalResults?.length) return; + + const baseName = document.title.toLowerCase().replace(/[^a-z0-9]/g, "-"); + const fileName = `${baseName}-results.csv`; + + const headers = Object.keys(finalResults[0]); + const rows = finalResults.map((item) => + headers.map((header) => `"${item[header] || ""}"`).join(",") + ); + const csvContent = [headers.join(","), ...rows].join("\n"); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, [finalResults, apiObject]); const hasVisibleResults = finalResults.some((r) => r.visible); return ( - <> + {/* Loading alert */} {!errorsOnly && ( - + { )} - {/* Error alert */} - + {apiObject.isError && ( { {/* Individual result alerts */} {apiObject.isSuccess && !errorsOnly && hasVisibleResults && ( - + <> {finalResults.map((resultObj) => ( - - + + - + {resultObj.severity === "error" && ( + + )} + + + {resultObj.details && ( + + toggleDetails(resultObj.id)} + aria-label={showDetails[resultObj.id] ? "Hide Details" : "Show Details"} + > + {showDetails[resultObj.id] ? ( + + ) : ( + + )} + + + )} + { } > - {resultObj.text} + + {resultObj.text} + {resultObj.details && ( + + + + + + )} + - + ))} - + + )} + {(apiObject.isSuccess || apiObject.isError) && + finalResults?.length > 0 && + hasVisibleResults ? ( + + + tableDialog.handleOpen()}> + + + + + + + + + + + + ) : null} + {tableDialog.open && ( + )} - + ); }; diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx index c872e5e8389a..46adfc153008 100644 --- a/src/components/CippComponents/CippAppPermissionBuilder.jsx +++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx @@ -6,7 +6,6 @@ import { AccordionSummary, AccordionDetails, Tooltip, - Grid, Alert, Skeleton, IconButton, @@ -17,17 +16,19 @@ import { Tabs, Tab, } from "@mui/material"; - +import { Grid } from "@mui/system"; import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import { CippDataTable } from "../CippTable/CippDataTable"; import { PlusIcon, ShieldCheckIcon, WrenchIcon } from "@heroicons/react/24/outline"; import CippFormComponent from "./CippFormComponent"; import { + Apps, Delete, Download, Error, ExpandMore, Save, + Sync, TaskAlt, Undo, Upload, @@ -69,15 +70,27 @@ const CippAppPermissionBuilder = ({ setExpanded(newExpanded ? panel : false); }; + const deprecatedServicePrincipals = [ + "00000002-0000-0000-c000-000000000000", // Windows Azure Active Directory + "a0c73c16-a7e3-4564-9a95-2bdf47383716", // Microsoft Exchange Online Remote PowerShell + "1b730954-1685-4b74-9bfd-dac224a7b894", // Azure Active Directory PowerShell + ]; + const currentSelectedSp = useWatch({ control: formControl.control, name: "servicePrincipal" }); + + // Check if selected service principal is in the deprecated list + const isDeprecatedSp = + currentSelectedSp && deprecatedServicePrincipals.includes(currentSelectedSp.value); + const { data: servicePrincipals = [], isSuccess: spSuccess, isFetching: spFetching, isLoading: spLoading, + refetch: refetchServicePrincipals, } = ApiGetCall({ url: "/api/ExecServicePrincipals", - queryKey: "execServicePrincipals", + queryKey: "execServicePrincipalList", waiting: true, }); @@ -351,39 +364,44 @@ const CippAppPermissionBuilder = ({ initialAppIds = []; } - if (selectedApp.length == 0 && initialAppIds.length == 0) { + if (selectedApp.length === 0 && initialAppIds.length === 0) { var microsoftGraph = servicePrincipals?.Results?.find( (sp) => sp?.appId === "00000003-0000-0000-c000-000000000000" ); - setSelectedApp([microsoftGraph]); - setNewPermissions({ - Permissions: { - "00000003-0000-0000-c000-000000000000": { - applicationPermissions: [], - delegatedPermissions: [], + if (microsoftGraph) { + setSelectedApp([microsoftGraph]); // Ensure this does not trigger a loop + setNewPermissions({ + Permissions: { + "00000003-0000-0000-c000-000000000000": { + applicationPermissions: [], + delegatedPermissions: [], + }, }, - }, - }); - } else if (currentPermissions !== initialPermissions) { - setSelectedApp([]); + }); + setExpanded("00000003-0000-0000-c000-000000000000"); // Automatically expand Microsoft Graph + } + } else if (!_.isEqual(currentPermissions, initialPermissions)) { + setSelectedApp([]); // Avoid redundant updates setNewPermissions(currentPermissions); setInitialPermissions(currentPermissions); setPermissionsImported(false); - } else if (initialAppIds.length > 0 && permissionsImported == false) { + } else if (initialAppIds.length > 0 && !permissionsImported) { const newApps = servicePrincipals?.Results?.filter((sp) => initialAppIds.includes(sp.appId) )?.sort((a, b) => a.displayName.localeCompare(b.displayName)); - setSelectedApp((prevApps) => { - if (JSON.stringify(prevApps) !== JSON.stringify(newApps)) { - return newApps; - } - return prevApps; - }); + if (!_.isEqual(selectedApp, newApps)) { + setSelectedApp(newApps); // Prevent unnecessary updates + } setNewPermissions(currentPermissions); setInitialPermissions(currentPermissions); setPermissionsImported(true); + + // Automatically expand if only one service principal exists + if (newApps.length === 1) { + setExpanded(newApps[0].appId); + } } } }, [ @@ -400,18 +418,7 @@ const CippAppPermissionBuilder = ({ var delegatedPermissions = newPermissions?.Permissions[appId]?.delegatedPermissions; var counts = `${appRoles?.length ?? 0}/${delegatedPermissions?.length ?? 0}`; - return ( - - - - - {counts} - - ); + return counts; }; const ApiPermissionRow = ({ servicePrincipal = null, spPermissions, formControl }) => { @@ -431,6 +438,8 @@ const CippAppPermissionBuilder = ({ waiting: true, }); + //console.log(spInfo); + const currentAppPermission = useWatch({ control: formControl.control, name: `Permissions.${servicePrincipal.appId}.applicationPermissions`, @@ -442,7 +451,7 @@ const CippAppPermissionBuilder = ({ useEffect(() => { if (spInfoSuccess && !spInitialized) { - if (appTable.length === 0) { + if (appTable !== undefined && appTable?.length === 0) { setAppTable( spPermissions?.applicationPermissions ?.sort((a, b) => a.value.localeCompare(b.value)) @@ -454,7 +463,7 @@ const CippAppPermissionBuilder = ({ })) ); } - if (delegatedTable.length === 0) { + if (delegatedTable !== undefined && delegatedTable.length === 0) { setDelegatedTable( spPermissions?.delegatedPermissions ?.sort((a, b) => a.value.localeCompare(b.value)) @@ -469,10 +478,10 @@ const CippAppPermissionBuilder = ({ } setSpInitialized(true); } - }, [spInitialized, spInfoSuccess, appTable?.length, delegatedTable?.length]); + }, [spInitialized, spInfoSuccess, appTable, delegatedTable]); useEffect(() => { - if (spInfoSuccess) { + if (spInfoSuccess && appTable !== undefined && delegatedTable !== undefined) { var appRoles = appTable?.map((perm) => perm.id).sort(); var delegatedPermissions = delegatedTable?.map((perm) => perm.id).sort(); var originalAppRoles = spPermissions?.applicationPermissions.map((perm) => perm.id).sort(); @@ -528,8 +537,8 @@ const CippAppPermissionBuilder = ({ const handleSavePermissions = () => { savePermissionChanges( servicePrincipal.appId, - appTable.map((perm) => ({ id: perm.id, value: perm.value })), - delegatedTable.map((perm) => ({ id: perm.id, value: perm.value })) + appTable?.map((perm) => ({ id: perm.id, value: perm.value })) ?? [], + delegatedTable?.map((perm) => ({ id: perm.id, value: perm.value })) ?? [] ); }; @@ -546,114 +555,50 @@ const CippAppPermissionBuilder = ({ return ( <> - {servicePrincipal && spInfoSuccess && ( - <> - - Manage the permissions for the {servicePrincipal.displayName}. - + + Manage the permissions for the {servicePrincipal.displayName}. + - - - - - - - - - {servicePrincipal?.appRoles?.length > 0 ? ( - <> - - - - !appTable.find((perm) => perm.id === role.id)) - .map((role) => ({ - label: role.value, - value: role.id, - }))} - formControl={formControl} - multiple={false} - /> - - - -
    - handleAddRow("applicationPermissions", currentAppPermission) - } - > - -
    -
    -
    -
    - , - noConfirm: true, - customFunction: (row) => handleRemoveRow("applicationPermissions", row), - }, - ]} - /> -
    - - ) : ( - } sx={{ mb: 3 }}> - No Application Permissions found. - - )} -
    - + + + + + + + + + {servicePrincipal?.appRoles?.length > 0 ? ( + <> - {spInfo?.Results?.publishedPermissionScopes?.length === 0 && ( - }> - No Published Delegated Permissions found. - - )} - + !delegatedTable.find((perm) => perm.id === scope.id)) - .map((scope) => ({ - label: scope.value, - value: scope.id, + options={(spInfo?.Results?.appRoles || []) + .filter((role) => !appTable?.find((perm) => perm.id === role.id)) + .map((role) => ({ + label: role.value, + value: role.id, }))} formControl={formControl} multiple={false} /> - +
    - handleAddRow("delegatedPermissions", currentDelegatedPermission) + handleAddRow("applicationPermissions", currentAppPermission) } > - +
    +
    +
    +
    - -
    - - )} + , + noConfirm: true, + customFunction: (row) => handleRemoveRow("delegatedPermissions", row), + }, + ]} + isFetching={spInfoFetching} + /> + +
    + + +
    ); }; return ( <> - {spLoading && } + {spLoading && } {spSuccess && ( <> - + - - {servicePrincipals?.Metadata?.Success && ( - { - return { label: `${sp.displayName} (${sp.appId})`, value: sp.appId }; - })} - formControl={formControl} - multiple={false} - /> - )} + + + {servicePrincipals?.Metadata?.Success && ( + + { + return { label: `${sp.displayName} (${sp.appId})`, value: sp.appId }; + })} + formControl={formControl} + multiple={false} + /> + + )} + refetchServicePrincipals()} + disabled={servicePrincipals.isFetching} + > + + + - + - +
    { - setSelectedApp([ - ...selectedApp, - servicePrincipals?.Results?.find( - (sp) => sp.appId === currentSelectedSp.value - ), - ]); - formControl.setValue("servicePrincipal", null); + // Only add if not deprecated + if (!isDeprecatedSp) { + setSelectedApp([ + ...selectedApp, + servicePrincipals?.Results?.find( + (sp) => sp.appId === currentSelectedSp.value + ), + ]); + formControl.setValue("servicePrincipal", null); + } }} > - - - )} diff --git a/src/components/CippComponents/CippAuditLogDetails.jsx b/src/components/CippComponents/CippAuditLogDetails.jsx new file mode 100644 index 000000000000..b5a3077a0da6 --- /dev/null +++ b/src/components/CippComponents/CippAuditLogDetails.jsx @@ -0,0 +1,359 @@ +import { useEffect } from "react"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; +import { getCippFormatting } from "/src/utils/get-cipp-formatting"; +import CippGeoLocation from "/src/components/CippComponents/CippGeoLocation"; +import { Tooltip, CircularProgress, Stack } from "@mui/material"; +import { useGuidResolver } from "/src/hooks/use-guid-resolver"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; + +const CippAuditLogDetails = ({ row }) => { + const { + guidMapping, + upnMapping, + isLoadingGuids, + resolveGuids, + isGuid, + replaceGuidsAndUpnsInString, + } = useGuidResolver(); + + // Use effect for initial scan to resolve GUIDs and special UPNs + useEffect(() => { + if (row) { + // Scan the main row data + resolveGuids(row); + + // Scan audit data if present + if (row.auditData) { + resolveGuids(row.auditData); + } + } + }, [row?.id, resolveGuids]); // Dependencies for when to resolve GUIDs + + // Function to replace GUIDs and special UPNs in strings with resolved names + const replaceGuidsInString = (str) => { + if (typeof str !== "string") return str; + + // Use the hook's helper function to replace both GUIDs and special UPNs + const { result, hasResolvedNames } = replaceGuidsAndUpnsInString(str); + + // If we have resolved names, return a tooltip showing original and resolved + if (hasResolvedNames) { + return ( + + {result} + + ); + } + + // Check for GUIDs and special UPNs to see if we should show loading state + const guidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi; + const partnerUpnRegex = /user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)/gi; + + let hasGuids = guidRegex.test(str); + + // Reset regex state and check for partner UPNs + partnerUpnRegex.lastIndex = 0; + let hasUpns = false; + let match; + + // Need to extract and check if the GUIDs from UPNs are in the pending state + while ((match = partnerUpnRegex.exec(str)) !== null) { + const hexId = match[1]; + if (hexId && hexId.length === 32) { + hasUpns = true; + break; // At least one UPN pattern found + } + } + + // If we have unresolved GUIDs or UPNs and are currently loading + if ((hasGuids || hasUpns) && isLoadingGuids) { + return ( +
    + + {str} +
    + ); + } + + return str; + }; + + // Convert data to property items format for CippPropertyListCard + const convertToPropertyItems = (data, excludeAuditData = false) => { + if (!data) return []; + + return Object.entries(data) + .map(([key, value]) => { + // Skip certain blacklisted fields + const blacklist = ["selectedOption", "GUID", "ID", "id", "noSubmitButton"]; + if (blacklist.includes(key)) return null; + + // Exclude auditData from main log items if specified + if (excludeAuditData && key === "auditData") return null; + + let displayValue; + // Handle different value types + if (typeof value === "string" && isGuid(value)) { + // Handle pure GUID strings + displayValue = renderGuidValue(value); + } else if ( + typeof value === "string" && + value.match(/^user_[0-9a-f]{32}@[^@]+\.onmicrosoft\.com$/i) + ) { + // Handle special partner UPN format as direct values + displayValue = renderGuidValue(value); + } else if ( + key.toLowerCase().includes("clientip") && + value && + value !== null && + isValidIpAddress(value) + ) { + // Handle IP addresses (with optional ports) using CippGeoLocation + // Check for various IP field names: clientIp, ClientIP, IP, etc. + const cleanIp = extractIpForGeolocation(value); + displayValue = ( +
    + +
    + ); + } else if (typeof value === "string") { + // Handle strings that might contain embedded GUIDs + // First apply GUID replacement to get the processed string + const guidProcessedValue = replaceGuidsInString(value); + + // If GUID replacement returned a React element (with tooltips), use it directly + if (typeof guidProcessedValue === "object" && guidProcessedValue?.type) { + displayValue = guidProcessedValue; + } else { + // Otherwise, apply getCippFormatting to the GUID-processed string + // This preserves key-based formatting while including GUID replacements + displayValue = getCippFormatting(guidProcessedValue, key); + } + } else if (typeof value === "object" && value !== null) { + // Handle nested objects and arrays - expand GUIDs within them + displayValue = renderNestedValue(value); + } else { + // Handle regular values + displayValue = getCippFormatting(value, key); + } + + return { + label: getCippTranslation(key), + value: displayValue, + }; + }) + .filter(Boolean); + }; + + // Render GUID values with proper resolution states + const renderGuidValue = (guidValue) => { + // Handle standard GUIDs directly + if (guidMapping[guidValue]) { + return ( + + {guidMapping[guidValue]} + + ); + } + + // Special handling for partner UPN format (user_@partnertenant.onmicrosoft.com) + const partnerUpnRegex = /^user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)$/i; + const upnMatch = typeof guidValue === "string" ? guidValue.match(partnerUpnRegex) : null; + + if (upnMatch) { + const hexId = upnMatch[1]; + if (hexId && hexId.length === 32) { + const guid = [ + hexId.slice(0, 8), + hexId.slice(8, 12), + hexId.slice(12, 16), + hexId.slice(16, 20), + hexId.slice(20, 32), + ].join("-"); + + // For partner UPN format, use the actual UPN if available, otherwise fall back to display name + if (upnMapping && upnMapping[guid]) { + return ( + + {upnMapping[guid]} + + ); + } else if (guidMapping[guid]) { + return ( + + {guidMapping[guid]} + + ); + } + } + } + + // Loading state + if (isLoadingGuids) { + return ( +
    + + {guidValue} +
    + ); + } + + // Fallback for unresolved values + return ( + + {guidValue} + + ); + }; + + // Recursively render nested objects and arrays with GUID expansion + const renderNestedValue = (value) => { + if (Array.isArray(value)) { + // Handle arrays + return renderArrayValue(value); + } else if (typeof value === "object" && value !== null) { + // Handle objects + return renderObjectValue(value); + } + return getCippFormatting(value, "nested"); + }; + + // Render array values with GUID expansion + const renderArrayValue = (arrayValue) => { + if (arrayValue.length === 0) return "[]"; + + // If it's a simple array, show it formatted + if (arrayValue.length <= 5 && arrayValue.every((item) => typeof item !== "object")) { + return ( +
    + {arrayValue.map((item, index) => ( +
    + {typeof item === "string" && isGuid(item) + ? renderGuidValue(item) + : typeof item === "string" + ? replaceGuidsInString(item) + : getCippFormatting(item, `item-${index}`)} +
    + ))} +
    + ); + } + + // For complex arrays, use the formatted version which might include table buttons + return getCippFormatting(arrayValue, "array"); + }; + + // Render object values with GUID expansion + const renderObjectValue = (objectValue) => { + const entries = Object.entries(objectValue); + + // If it's a simple object with few properties, show them inline + if (entries.length <= 3 && entries.every(([, val]) => typeof val !== "object")) { + return ( +
    + {entries.map(([objKey, objVal]) => ( +
    + {getCippTranslation(objKey)}:{" "} + {typeof objVal === "string" && isGuid(objVal) + ? renderGuidValue(objVal) + : typeof objVal === "string" + ? replaceGuidsInString(objVal) + : getCippFormatting(objVal, objKey)} +
    + ))} +
    + ); + } + + // For complex objects, use the formatted version which might include table buttons + return getCippFormatting(objectValue, "object"); + }; + + // Helper function to validate IP addresses (with optional ports) + const isValidIpAddress = (ip) => { + if (typeof ip !== "string") return false; + + // Extract IP part if there's a port (split by last colon for IPv6 compatibility) + let ipPart = ip; + let portPart = null; + + // Check for IPv4:port format + const ipv4PortMatch = ip.match(/^(.+):(\d+)$/); + if (ipv4PortMatch) { + ipPart = ipv4PortMatch[1]; + portPart = ipv4PortMatch[2]; + } + + // IPv4 regex + const ipv4Regex = + /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + + // IPv6 regex (simplified) - note: IPv6 with ports use [::]:port format, handled separately + const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/; + + // Check for IPv6 with port [::]:port format + const ipv6PortMatch = ip.match(/^\[(.+)\]:(\d+)$/); + if (ipv6PortMatch) { + ipPart = ipv6PortMatch[1]; + portPart = ipv6PortMatch[2]; + } + + // Validate port number if present + if (portPart !== null) { + const port = parseInt(portPart, 10); + if (port < 1 || port > 65535) return false; + } + + return ipv4Regex.test(ipPart) || ipv6Regex.test(ipPart); + }; + + // Extract clean IP address from IP:port combinations for geolocation + const extractIpForGeolocation = (ipWithPort) => { + if (typeof ipWithPort !== "string") return ipWithPort; + + // IPv4:port format + const ipv4PortMatch = ipWithPort.match(/^(.+):(\d+)$/); + if (ipv4PortMatch) { + return ipv4PortMatch[1]; + } + + // IPv6 with port [::]:port format + const ipv6PortMatch = ipWithPort.match(/^\[(.+)\]:(\d+)$/); + if (ipv6PortMatch) { + return ipv6PortMatch[1]; + } + + // Return as-is if no port detected + return ipWithPort; + }; + + const mainLogItems = convertToPropertyItems(row, true); // Exclude auditData from main items + const auditDataItems = row?.auditData ? convertToPropertyItems(row.auditData) : []; + + return ( + + + + {auditDataItems.length > 0 && ( + + )} + + ); +}; + +export default CippAuditLogDetails; diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 8ecfd6aee8cd..106b3f0f646d 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -1,9 +1,53 @@ import { ArrowDropDown } from "@mui/icons-material"; -import { Autocomplete, CircularProgress, createFilterOptions, TextField } from "@mui/material"; -import { useEffect, useState } from "react"; +import { + Autocomplete, + CircularProgress, + createFilterOptions, + TextField, + IconButton, +} from "@mui/material"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; import { ApiGetCallWithPagination } from "../../api/ApiCall"; +import { Sync } from "@mui/icons-material"; +import { Stack } from "@mui/system"; +import React from "react"; + +const MemoTextField = React.memo(function MemoTextField({ + params, + label, + placeholder, + ...otherProps +}) { + const { InputProps, ...otherParams } = params; + + return ( + + ); +}); export const CippAutoComplete = (props) => { const { @@ -24,6 +68,11 @@ export const CippAutoComplete = (props) => { required = false, isFetching = false, sx, + removeOptions = [], + sortOptions = false, + preselectedValue, + groupBy, + renderGroup, ...other } = props; @@ -54,7 +103,7 @@ export const CippAutoComplete = (props) => { setGetRequestInfo({ url: api.url, data: { - ...(!api.excludeTenantFilter ? { TenantFilter: currentTenant } : null), + ...(!api.excludeTenantFilter ? { tenantFilter: currentTenant } : null), ...api.data, }, waiting: true, @@ -111,7 +160,11 @@ export const CippAutoComplete = (props) => { label: typeof api?.labelField === "function" ? api.labelField(option) - : option[api?.labelField], + : option[api?.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) @@ -119,23 +172,81 @@ export const CippAutoComplete = (props) => { addedFields, }; }); - setUsedOptions(convertedOptions); + + if (api?.dataFilter) { + setUsedOptions(api.dataFilter(convertedOptions)); + } else { + setUsedOptions(convertedOptions); + } } } if (actionGetRequest.isError) { setUsedOptions([{ label: getCippError(actionGetRequest.error), value: "error" }]); } - }, [api, actionGetRequest.data, actionGetRequest.isSuccess, actionGetRequest.isError]); + }, [ + api, + actionGetRequest.data, + actionGetRequest.isSuccess, + actionGetRequest.isError, + preselectedValue, + defaultValue, + value, + multiple, + onChange, + ]); + + const memoizedOptions = useMemo(() => { + let finalOptions = api ? usedOptions : options; + if (removeOptions && removeOptions.length) { + finalOptions = finalOptions.filter((o) => !removeOptions.includes(o.value)); + } + if (sortOptions) { + finalOptions.sort((a, b) => a.label?.localeCompare(b.label)); + } + return finalOptions; + }, [api, usedOptions, options, removeOptions, sortOptions]); + + // Dedicated effect for handling preselected value + useEffect(() => { + if (preselectedValue && !defaultValue && !value && memoizedOptions.length > 0) { + const preselectedOption = memoizedOptions.find((option) => option.value === preselectedValue); + + if (preselectedOption) { + const newValue = multiple ? [preselectedOption] : preselectedOption; + if (onChange) { + onChange(newValue, newValue?.addedFields); + } + } + } + }, [preselectedValue, defaultValue, value, memoizedOptions, multiple, onChange]); - const rand = Math.random().toString(36).substring(5); + // Create a stable key that only changes when necessary inputs change + const stableKey = useMemo(() => { + // Only regenerate the key when these values change + const keyParts = [ + JSON.stringify(defaultValue), + JSON.stringify(preselectedValue), + api?.url, + currentTenant, + ]; + return keyParts.join("-"); + }, [defaultValue, preselectedValue, api?.url, currentTenant]); + + const lookupOptionByValue = useCallback( + (value) => { + const foundOption = memoizedOptions.find((option) => option.value === value); + return foundOption || { label: value, value: value }; + }, + [memoizedOptions] + ); return ( ) : ( @@ -147,6 +258,7 @@ export const CippAutoComplete = (props) => { disableClearable={disableClearable} multiple={multiple} fullWidth + placeholder={placeholder} filterOptions={(options, params) => { const filtered = filter(options, params); const isExisting = @@ -154,21 +266,29 @@ export const CippAutoComplete = (props) => { options.some( (option) => params.inputValue === option.value || params.inputValue === option.label ); - if (params.inputValue !== "" && creatable && !isExisting) { - filtered.push({ + const newOption = { label: `Add option: "${params.inputValue}"`, value: params.inputValue, manual: true, - }); + }; + if (!filtered.some((option) => option.value === newOption.value)) { + filtered.push(newOption); + } } return filtered; }} size="small" defaultValue={ - typeof defaultValue === "string" - ? { label: defaultValue, value: defaultValue } + Array.isArray(defaultValue) + ? defaultValue.map((item) => + typeof item === "string" ? lookupOptionByValue(item) : item + ) + : typeof defaultValue === "object" && multiple + ? [defaultValue] + : typeof defaultValue === "string" + ? lookupOptionByValue(defaultValue) : defaultValue } name={name} @@ -182,11 +302,14 @@ export const CippAutoComplete = (props) => { value: item?.label ? item.value : item, }; if (onCreateOption) { - onCreateOption(item, item?.addedFields); + item = onCreateOption(item, item?.addedFields); } } return item; }); + newValue = newValue.filter( + (item) => item.value && item.value !== "" && item.value !== "error" && item.value !== -1 + ); } else { if (newValue?.manual || !newValue?.label) { newValue = { @@ -194,32 +317,60 @@ export const CippAutoComplete = (props) => { value: newValue?.label ? newValue.value : newValue, }; if (onCreateOption) { - onCreateOption(newValue, newValue?.addedFields); + newValue = onCreateOption(newValue, newValue?.addedFields); } } + if (!newValue?.value || newValue.value === "error") { + newValue = null; + } } if (onChange) { onChange(newValue, newValue?.addedFields); } }} - options={api ? usedOptions : options} - getOptionLabel={(option) => - option - ? option.label === null - ? "" - : option.label || "Label not found - Are you missing a labelField?" - : "" - } + options={memoizedOptions} + getOptionLabel={useCallback( + (option) => { + if (!option) return ""; + // For static options (non-API), the option should already have a label + if (!api && option.label !== undefined) { + return option.label === null ? "" : String(option.label); + } + // For API options, use the existing logic + if (api) { + return option.label === null + ? "" + : option.label || "Label not found - Are you missing a labelField?"; + } + // Fallback for any edge cases + return option.label || option.value || ""; + }, + [api] + )} sx={sx} renderInput={(params) => ( - + + + {api?.url && api?.showRefresh && ( + { + actionGetRequest.refetch(); + }} + > + + + )} + )} + groupBy={groupBy} + renderGroup={renderGroup} {...other} /> ); diff --git a/src/components/CippComponents/CippAutocompleteGrouping.jsx b/src/components/CippComponents/CippAutocompleteGrouping.jsx new file mode 100644 index 000000000000..2f65687278dc --- /dev/null +++ b/src/components/CippComponents/CippAutocompleteGrouping.jsx @@ -0,0 +1,16 @@ +import { styled, lighten, darken } from "@mui/system"; + +export const GroupHeader = styled("div")(({ theme }) => ({ + position: "sticky", + top: "-8px", + padding: "4px 10px", + color: theme.palette.primary.main, + backgroundColor: lighten(theme.palette.primary.light, 0.85), + ...theme.applyStyles("dark", { + backgroundColor: darken(theme.palette.primary.main, 0.8), + }), +})); + +export const GroupItems = styled("ul")({ + padding: 0, +}); \ No newline at end of file diff --git a/src/components/CippComponents/CippCalendarPermissionsDialog.jsx b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx new file mode 100644 index 000000000000..d2d4c89d71fe --- /dev/null +++ b/src/components/CippComponents/CippCalendarPermissionsDialog.jsx @@ -0,0 +1,87 @@ +import { useEffect } from "react"; +import { Box, Stack, Tooltip } from "@mui/material"; +import CippFormComponent from "./CippFormComponent"; +import { useWatch } from "react-hook-form"; + +const CippCalendarPermissionsDialog = ({ formHook, combinedOptions, isUserGroupLoading }) => { + const permissionLevel = useWatch({ + control: formHook.control, + name: "Permissions", + }); + + const isEditor = permissionLevel?.value === "Editor"; + + useEffect(() => { + if (!isEditor) { + formHook.setValue("CanViewPrivateItems", false); + } + }, [isEditor, formHook]); + + return ( + + + (value ? true : "Select a user or group to assign permissions to"), + }} + placeholder="Select a user or group to assign permissions to" + /> + + + (value ? true : "Select the permission level for the calendar"), + }} + options={[ + { value: "Author", label: "Author" }, + { value: "Contributor", label: "Contributor" }, + { value: "Editor", label: "Editor" }, + { value: "Owner", label: "Owner" }, + { value: "NonEditingAuthor", label: "Non Editing Author" }, + { value: "PublishingAuthor", label: "Publishing Author" }, + { value: "PublishingEditor", label: "Publishing Editor" }, + { value: "Reviewer", label: "Reviewer" }, + { value: "LimitedDetails", label: "Limited Details" }, + { value: "AvailabilityOnly", label: "Availability Only" }, + ]} + multiple={false} + formControl={formHook} + /> + + + + + + + + + + ); +}; + +export default CippCalendarPermissionsDialog; diff --git a/src/components/CippComponents/CippCentralSearch.jsx b/src/components/CippComponents/CippCentralSearch.jsx index c710a8d2d49b..ee1c80e2b426 100644 --- a/src/components/CippComponents/CippCentralSearch.jsx +++ b/src/components/CippComponents/CippCentralSearch.jsx @@ -6,12 +6,13 @@ import { DialogContent, DialogTitle, TextField, - Grid, Card, CardContent, + CardActionArea, Typography, Box, } from "@mui/material"; +import { Grid } from "@mui/system"; import { useRouter } from "next/router"; import { nativeMenuItems } from "/src/layouts/config"; @@ -95,6 +96,12 @@ export const CippCentralSearch = ({ handleClose, open }) => { label="Search any menu item or page in CIPP" onChange={handleChange} onKeyDown={handleKeyDown} + onFocus={(event) => { + // Select all text on focus if there's content + if (event.target.value) { + event.target.select(); + } + }} value={searchValue} autoFocus /> @@ -104,18 +111,19 @@ export const CippCentralSearch = ({ handleClose, open }) => { filteredItems.length > 0 ? ( {filteredItems.map((item, index) => ( - - handleCardClick(item.path)} - > - - {highlightMatch(item.title)} - - Path: {highlightMatch(item.path)} - - + + + handleCardClick(item.path)} + aria-label={`Navigate to ${item.title}`} + > + + {highlightMatch(item.title)} + + Path: {highlightMatch(item.path)} + + + ))} diff --git a/src/components/CippComponents/CippCodeBlock.jsx b/src/components/CippComponents/CippCodeBlock.jsx index e23cc9c2557e..6dbf8f0e047f 100644 --- a/src/components/CippComponents/CippCodeBlock.jsx +++ b/src/components/CippComponents/CippCodeBlock.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import SyntaxHighlighter from "react-syntax-highlighter"; import { CippCopyToClipBoard } from "./CippCopyToClipboard"; @@ -25,7 +25,7 @@ const CodeContainer = styled("div")` export const CippCodeBlock = (props) => { const { code, - language = "javascript", + language = "json", showLineNumbers = false, startingLineNumber = 1, wrapLongLines = true, @@ -47,13 +47,14 @@ export const CippCodeBlock = (props) => {
    {type === "editor" && ( @@ -66,7 +67,6 @@ export const CippCodeBlock = (props) => { showLineNumbers={showLineNumbers} startingLineNumber={startingLineNumber} wrapLongLines={wrapLongLines} - > {code} diff --git a/src/components/CippComponents/CippComponentDialog.jsx b/src/components/CippComponents/CippComponentDialog.jsx index 9f8c618ad179..03d4d64dcdaa 100644 --- a/src/components/CippComponents/CippComponentDialog.jsx +++ b/src/components/CippComponents/CippComponentDialog.jsx @@ -7,7 +7,7 @@ export const CippComponentDialog = (props) => {
    {title} - {...children} + {children} + } + /> + + setOpenAddDialog(false), + }} + title="Add Variable" + fields={[ + { + type: "textField", + name: "RowKey", + label: "Variable Name", + placeholder: "Enter the name for the custom variable without %.", + required: true, + validators: { validate: validateVariableName }, + }, + { + type: "textField", + name: "Value", + label: "Value", + placeholder: "Enter the value for the custom variable.", + required: true, + }, + ]} + api={{ + type: "POST", + url: "/api/ExecCippReplacemap", + data: { Action: "AddEdit", tenantId: id }, + relatedQueryKeys: [`CustomVariables_${id}`], + }} + /> + + ); +}; + +export default CippCustomVariables; diff --git a/src/components/CippComponents/CippDevOptions.jsx b/src/components/CippComponents/CippDevOptions.jsx index dab86d6c454a..7bddbbbc126a 100644 --- a/src/components/CippComponents/CippDevOptions.jsx +++ b/src/components/CippComponents/CippDevOptions.jsx @@ -1,6 +1,6 @@ import { useSettings } from "../../hooks/use-settings"; import { Button, Card, CardHeader, Divider, CardContent, SvgIcon } from "@mui/material"; -import { CodeBracketIcon, CogIcon } from "@heroicons/react/24/outline"; +import { CodeBracketIcon } from "@heroicons/react/24/outline"; export const CippDevOptions = () => { const settings = useSettings(); diff --git a/src/components/CippComponents/CippDocsLookup.jsx b/src/components/CippComponents/CippDocsLookup.jsx new file mode 100644 index 000000000000..987809b24f2c --- /dev/null +++ b/src/components/CippComponents/CippDocsLookup.jsx @@ -0,0 +1,71 @@ +import { Search } from "@mui/icons-material"; +import { Chip, IconButton, SvgIcon, Tooltip } from "@mui/material"; +import { useState } from "react"; + +export const CippDocsLookup = (props) => { + const { text, type = "button", visible = true, ...other } = props; + const [showPassword, setShowPassword] = useState(false); + + const handleTogglePassword = () => { + setShowPassword((prev) => !prev); + }; + + const handleDocsLookup = () => { + const searchUrl = `https://docs.cipp.app/?q=Help+with:+${encodeURIComponent(text)}&ask=true`; + window.open(searchUrl, '_blank'); + }; + + if (!visible) return null; + + if (type === "button") { + return ( + + + + + + + + ); + } + + if (type === "chip") { + return ( + + + + ); + } + + if (type === "password") { + return ( + <> + + + {showPassword ? : } + + + + + + + ); + } + + return null; +}; \ No newline at end of file diff --git a/src/components/CippComponents/CippDropzone.jsx b/src/components/CippComponents/CippDropzone.jsx index 6e400ef31e2c..72a1e2a6b70c 100644 --- a/src/components/CippComponents/CippDropzone.jsx +++ b/src/components/CippComponents/CippDropzone.jsx @@ -1,4 +1,3 @@ -import React, { useCallback, useMemo, useState } from "react"; import PropTypes from "prop-types"; //import { CippContentCard } from 'src/components/layout' import { useDropzone } from "react-dropzone"; diff --git a/src/components/CippComponents/CippExchangeActions.jsx b/src/components/CippComponents/CippExchangeActions.jsx new file mode 100644 index 000000000000..f0015c0fd3f6 --- /dev/null +++ b/src/components/CippComponents/CippExchangeActions.jsx @@ -0,0 +1,511 @@ +import { TrashIcon, MagnifyingGlassIcon, PlayCircleIcon } from "@heroicons/react/24/outline"; +import { + Archive, + MailOutline, + Visibility, + PhonelinkLock, + Key, + PostAdd, + Gavel, + Language, + Outbox, + NotificationImportant, + DataUsage, + MailLock, + SettingsEthernet, + CalendarMonth, + PersonAdd, + Email, +} from "@mui/icons-material"; +import { useSettings } from "/src/hooks/use-settings.js"; +import { useMemo } from "react"; + +export const CippExchangeActions = () => { + const tenant = useSettings().currentTenant; + + // API configuration for all user selection fields + const userApiConfig = useMemo(() => ({ + url: "/api/ListGraphRequest", + dataKey: "Results", + labelField: (option) => `${option.displayName} (${option.userPrincipalName})`, + valueField: "userPrincipalName", + queryKey: `users-${tenant}`, + data: { + Endpoint: "users", + tenantFilter: tenant, + $select: "id,displayName,userPrincipalName,mail", + $top: 999, + }, + }), [tenant]); + + return [ + { + label: "Bulk Add Mailbox Permissions", + type: "POST", + url: "/api/ExecModifyMBPerms", + icon: , + data: { + userID: "UPN", + }, + confirmText: "Add the specified permissions to selected mailboxes?", + multiPost: false, + data: { + }, + fields: [ + { + type: "autoComplete", + name: "fullAccessUser", + label: "Add Full Access User", + multiple: true, + creatable: false, + api: userApiConfig, + }, + { + type: "switch", + name: "autoMap", + label: "Enable Automapping", + defaultValue: true, + labelLocation: "behind", + }, + { + type: "autoComplete", + name: "sendAsUser", + label: "Add Send As User", + multiple: true, + creatable: false, + api: userApiConfig, + }, + { + type: "autoComplete", + name: "sendOnBehalfUser", + label: "Add Send On Behalf User", + multiple: true, + creatable: false, + api: userApiConfig, + }, + ], + customDataformatter: (rows, action, formData) => { + + const mailboxArray = Array.isArray(rows) ? rows : [rows]; + + // Create bulk request array - one object per mailbox + const bulkRequestData = mailboxArray.map(mailbox => { + const permissions = []; + const autoMap = formData.autoMap === undefined ? true : formData.autoMap; + + // Add type: "user" to match format + const addTypeToUsers = (users) => { + return users.map(user => ({ + ...user, + type: "user" + })); + }; + + // Handle FullAccess - formData.fullAccessUser is an array since multiple: true + if (formData.fullAccessUser && formData.fullAccessUser.length > 0) { + permissions.push({ + UserID: addTypeToUsers(formData.fullAccessUser), + PermissionLevel: "FullAccess", + Modification: "Add", + AutoMap: autoMap, + }); + } + + // Handle SendAs - formData.sendAsUser is an array since multiple: true + if (formData.sendAsUser && formData.sendAsUser.length > 0) { + permissions.push({ + UserID: addTypeToUsers(formData.sendAsUser), + PermissionLevel: "SendAs", + Modification: "Add", + }); + } + + // Handle SendOnBehalf - formData.sendOnBehalfUser is an array since multiple: true + if (formData.sendOnBehalfUser && formData.sendOnBehalfUser.length > 0) { + permissions.push({ + UserID: addTypeToUsers(formData.sendOnBehalfUser), + PermissionLevel: "SendOnBehalf", + Modification: "Add", + }); + } + + return { + userID: mailbox.UPN, + permissions: permissions, + }; + }); + + return { + mailboxRequests: bulkRequestData, + tenantFilter: tenant + }; + }, + color: "primary", + }, + { + label: "Edit permissions", + link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", + color: "info", + icon: , + }, + { + label: "Research Compromised Account", + link: "/identity/administration/users/user/bec?userId=[ExternalDirectoryObjectId]", + color: "info", + icon: , + }, + { + label: "Send MFA Push", + type: "POST", + url: "/api/ExecSendPush", + data: { + UserEmail: "UPN", + }, + confirmText: "Are you sure you want to send an MFA request to [UPN]?", + icon: , + }, + { + label: "Convert Mailbox", + type: "POST", + icon: , + url: "/api/ExecConvertMailbox", + data: { ID: "UPN" }, + fields: [ + { + type: "radio", + name: "MailboxType", + label: "Mailbox Type", + options: [ + { label: "User Mailbox", value: "Regular" }, + { label: "Shared Mailbox", value: "Shared" }, + { label: "Room Mailbox", value: "Room" }, + { label: "Equipment Mailbox", value: "Equipment" }, + ], + validators: { required: "Please select a mailbox type" }, + }, + ], + confirmText: + "Pick the type of mailbox you want to convert [UPN] of mailbox type [recipientTypeDetails] to:", + multiPost: false, + }, + { + label: "Enable Online Archive", + type: "POST", + icon: , + url: "/api/ExecEnableArchive", + data: { ID: "Id", username: "UPN" }, + confirmText: "Are you sure you want to enable the online archive for [UPN]?", + multiPost: false, + condition: (row) => row.ArchiveGuid === "00000000-0000-0000-0000-000000000000", + }, + { + label: "Enable Auto-Expanding Archive", + type: "POST", + icon: , + url: "/api/ExecEnableAutoExpandingArchive", + data: { ID: "Id", username: "UPN" }, + confirmText: + "Are you sure you want to enable auto-expanding archive for [UPN]? The archive must already be enabled.", + multiPost: false, + condition: (row) => row.ArchiveGuid !== "00000000-0000-0000-0000-000000000000", + }, + { + label: "Set Global Address List visibility", + type: "POST", + url: "/api/ExecHideFromGAL", + icon: , + data: { + ID: "UPN", + }, + fields: [ + { + type: "radio", + name: "HidefromGAL", + label: "Global Address List visibility", + options: [ + { label: "Hidden", value: true }, + { label: "Shown", value: false }, + ], + validators: { required: "Please select a global address list state" }, + }, + ], + confirmText: + "Are you sure you want to set the global address list state for [UPN]? Changes can take up to 72 hours to take effect.", + }, + { + label: "Start Managed Folder Assistant", + type: "POST", + url: "/api/ExecStartManagedFolderAssistant", + icon: , + data: { + ID: "ExchangeGuid", + UserPrincipalName: "UPN", + }, + confirmText: "Are you sure you want to start the managed folder assistant for [UPN]?", + }, + { + label: "Delete Mailbox", + type: "POST", + icon: , + url: "/api/RemoveUser", + data: { ID: "UPN" }, + confirmText: "Are you sure you want to delete [UPN]?", + multiPost: false, + }, + { + label: "Set Copy Sent Items for Delegated Mailboxes", + type: "POST", + icon: , + url: "/api/ExecCopyForSent", + data: { ID: "UPN" }, + fields: [ + { + type: "radio", + name: "MessageCopyForSentAsEnabled", + label: "Copy Sent Items", + options: [ + { label: "Enabled", value: true }, + { label: "Disabled", value: false }, + ], + validators: { required: "Please select a copy sent items state" }, + }, + ], + confirmText: "Are you sure you want to set Copy Sent Items for [UPN]?", + }, + { + label: "Set Litigation Hold", + type: "POST", + url: "/api/ExecSetLitigationHold", + data: { UPN: "UPN", Identity: "Id" }, + confirmText: "What do you want to set the Litigation Hold to?", + icon: , + condition: (row) => row.LicensedForLitigationHold === true, + fields: [ + { + type: "switch", + name: "disable", + label: "Disable Litigation Hold", + }, + { + type: "number", + name: "days", + label: "Hold Duration (Days)", + placeholder: "Blank or 0 for indefinite", + }, + ], + }, + { + label: "Set Retention Hold", + type: "POST", + url: "/api/ExecSetRetentionHold", + data: { UPN: "UPN", Identity: "Id" }, + confirmText: "What do you want to set Retention Hold to?", + icon: , + fields: [ + { + type: "switch", + name: "disable", + label: "Disable Retention Hold", + }, + ], + }, + { + label: "Set Mailbox Locale", + type: "POST", + url: "/api/ExecSetMailboxLocale", + data: { user: "UPN", ProhibitSendQuota: true }, + confirmText: "Enter a locale, e.g. en-US", + icon: , + fields: [ + { + label: "Locale", + name: "locale", + type: "textField", + placeholder: "e.g. en-US", + validators: { required: "Please enter a locale" }, + }, + ], + }, + { + label: "Set Max Send/Receive Size", + type: "POST", + url: "/api/ExecSetMailboxEmailSize", + data: { UPN: "UPN", id: "ExternalDirectoryObjectId" }, + confirmText: "Enter a size in from 1 to 150. Leave blank to not change.", + icon: , + fields: [ + { + label: "Send Size(MB)", + name: "maxSendSize", + type: "number", + placeholder: "e.g. 35", + }, + { + label: "Receive Size(MB)", + name: "maxReceiveSize", + type: "number", + placeholder: "e.g. 36", + }, + ], + }, + { + label: "Set Send Quota", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { user: "UPN", ProhibitSendQuota: true }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + validators: { required: "Please enter a quota" }, + }, + ], + }, + { + label: "Set Send and Receive Quota", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { + user: "UPN", + ProhibitSendReceiveQuota: true, + }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + validators: { required: "Please enter a quota" }, + }, + ], + }, + { + label: "Set Quota Warning Level", + type: "POST", + url: "/api/ExecSetMailboxQuota", + data: { user: "UPN", IssueWarningQuota: true }, + confirmText: "Enter a quota. e.g. 1000MB, 10GB,1TB", + icon: , + fields: [ + { + label: "Quota", + name: "quota", + type: "textField", + placeholder: "e.g. 1000MB, 10GB,1TB", + validators: { required: "Please enter a quota" }, + }, + ], + }, + { + label: "Set Calendar Processing", + type: "POST", + url: "/api/ExecSetCalendarProcessing", + data: { UPN: "UPN" }, + confirmText: "Configure calendar processing settings for [UPN]", + icon: , + condition: (row) => + row.recipientTypeDetails === "RoomMailbox" || + row.recipientTypeDetails === "EquipmentMailbox", + fields: [ + { + label: "Automatically Process Meeting Requests", + name: "automaticallyProcess", + type: "switch", + }, + { + label: "Automatically Accept Meeting Requests", + name: "automaticallyAccept", + type: "switch", + }, + { + label: "Allow Conflicts", + name: "allowConflicts", + type: "switch", + }, + { + label: "Maximum Number of Conflicts", + name: "maxConflicts", + type: "number", + placeholder: "e.g. 2", + }, + { + label: "Allow Recurring Meetings", + name: "allowRecurringMeetings", + type: "switch", + }, + { + label: "Schedule Only During Work Hours", + name: "scheduleOnlyDuringWorkHours", + type: "switch", + }, + { + label: "Maximum Duration (Minutes)", + name: "maximumDurationInMinutes", + type: "number", + placeholder: "e.g. 240", + }, + { + label: "Minimum Duration (Minutes)", + name: "minimumDurationInMinutes", + type: "number", + placeholder: "e.g. 30", + }, + { + label: "Booking Window (Days)", + name: "bookingWindowInDays", + type: "number", + placeholder: "e.g. 30", + }, + { + label: "Add Organizer to Subject", + name: "addOrganizerToSubject", + type: "switch", + }, + { + label: "Delete Comments", + name: "deleteComments", + type: "switch", + }, + { + label: "Delete Subject", + name: "deleteSubject", + type: "switch", + }, + { + label: "Remove Private Property", + name: "removePrivateProperty", + type: "switch", + }, + { + label: "Remove Canceled Meetings", + name: "removeCanceledMeetings", + type: "switch", + }, + { + label: "Remove Old Meeting Messages", + name: "removeOldMeetingMessages", + type: "switch", + }, + { + label: "Process External Meeting Messages", + name: "processExternalMeetingMessages", + type: "switch", + }, + { + label: "Additional Response", + name: "additionalResponse", + type: "textField", + placeholder: "Additional text to add to responses", + }, + ], + }, + ]; +}; + +export default CippExchangeActions; diff --git a/src/components/CippComponents/CippForefrontHeaderDialog.jsx b/src/components/CippComponents/CippForefrontHeaderDialog.jsx new file mode 100644 index 000000000000..9280fecd0800 --- /dev/null +++ b/src/components/CippComponents/CippForefrontHeaderDialog.jsx @@ -0,0 +1,164 @@ +import React from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + IconButton, +} from "@mui/material"; +import { Close, ReceiptLong } from "@mui/icons-material"; +import { CippPropertyList } from "./CippPropertyList"; + +const forefrontHeaderMapping = { + ARC: { + label: "ARC Protocol", + values: { + AAR: "Records the content of the Authentication-results header from DMARC.", + AMS: "Includes cryptographic signatures of the message.", + AS: "Includes cryptographic signatures of the message headers", + }, + }, + CAT: { + label: "The category of protection policy", + values: { + BULK: "Bulk", + DIMP: "Domain Impersonation", + GIMP: "Mailbox intelligence based impersonation", + HPHSH: "High confidence phishing", + HPHISH: "High confidence phishing", + HSPM: "High confidence spam", + MALW: "Malware", + PHSH: "Phishing", + SPM: "Spam", + SPOOF: "Spoofing", + UIMP: "User Impersonation", + AMP: "Anti-malware", + SAP: "Safe attachments", + OSPM: "Outbound spam", + NONE: "Clean message", + }, + }, + CIP: { + label: "Connecting IP Address", + }, + CTRY: { + label: "The source country as determined by the connecting IP address", + }, + H: { + label: "The HELO or EHLO string of the connecting email server", + }, + IPV: { + label: "Ingress Peer Verification status", + values: { + CAL: "Source IP address was Configured in Allowed List (CAL)", + NLI: "The IP address was not found on any IP reputation list.", + }, + }, + EFV: { + label: "Egress Verification status", + values: { + CAL: "Source IP address was Configured in Allowed List (CAL)", + NLI: "The IP address was not found on any IP reputation list.", + }, + }, + DIR: { + label: "Direction of email verification", + values: { + INB: "Inbound email verification", + OUT: "Outbound email verification", + OUB: "Outbound email verification", + OTB: "Outbound email verification", + }, + }, + LANG: { + label: "The language in which the message was written", + }, + PTR: { + label: "Reverse DNS of the Connecting IP peer's address", + }, + SFTY: { + label: "The message was identified as phishing", + values: { + "9.19": "Domain impersonation. The sending domain is attempting to impersonate a protected domain", + "9.20": + "User impersonation. The sending user is attempting to impersonate a user in the recipient's organization", + }, + }, + SRV: { + label: "Bulk Email analysis results", + values: { + BULK: "The message was identified as bulk email by spam filtering and the bulk complaint level (BCL) threshold", + }, + }, + SFV: { + label: "Message Filtering", + values: { + BLK: "Filtering was skipped and the message was blocked because it was sent from an address in a user's Blocked Senders list.", + NSPM: "Spam filtering marked the message as non-spam and the message was sent to the intended recipients.", + SFE: "Filtering was skipped and the message was allowed because it was sent from an address in a user's Safe Senders list.", + SKA: "The message skipped spam filtering and was delivered to the Inbox because the sender was in the allowed senders list or allowed domains list in an anti-spam policy.", + SKB: "The message was marked as spam because it matched a sender in the blocked senders list or blocked domains list in an anti-spam policy.", + SKI: "Similar to SFV:SKN, the message skipped spam filtering for another reason (for example, an intra-organizational email within a tenant).", + SKN: "The message was marked as non-spam prior to being processed by spam filtering. For example, the message was marked as SCL -1 or Bypass spam filtering by a mail flow rule.", + SKQ: "The message was released from the quarantine and was sent to the intended recipients.", + SKS: "The message was marked as spam prior to being processed by spam filtering. For example, the message was marked as SCL 5 to 9 by a mail flow rule.", + SPM: "The message was marked as spam by spam filtering.", + }, + }, + SCL: { + label: "Spam Confidence Level", + values: { + "-1": "-1: The message skipped spam filtering. Deliver the message to recipient Inbox folders.", + "0": "0: Spam filtering determined the message wasn't spam. Deliver the message to recipient Inbox folders.", + "1": "1: Spam filtering determined the message wasn't spam. Deliver the message to recipient Inbox folders.", + "5": "5: Spam filtering marked the message as Spam. Deliver the message to recipient Junk Email folders.", + "6": "6: Spam filtering marked the message as Spam. Deliver the message to recipient Junk Email folders.", + "7": "7: Spam filtering marked the message as High confidence spam. Deliver the message to recipient Junk Email folders.", + "8": "8: Spam filtering marked the message as High confidence spam. Deliver the message to recipient Junk Email folders.", + "9": "9: Spam filtering marked the message as High confidence spam. Deliver the message to recipient Junk Email folders.", + }, + }, +}; + +const parseForefrontHeader = (header) => { + const fields = header.split(";"); + return fields.map((field) => { + const [key, value] = field.split(":"); + return { key: key.trim(), value: value?.trim() }; + }); +}; + +const CippForefrontHeaderDialog = ({ open, onClose, header }) => { + const parsedFields = parseForefrontHeader(header); + + const propertyItems = parsedFields + .filter((field) => field.key && field.value && field.key !== "SFS") + .map((field) => ({ + label: forefrontHeaderMapping[field.key]?.label || field.key, + value: forefrontHeaderMapping[field.key]?.values?.[field.value] || field.value || "N/A", + })); + + return ( + + + Anti-Spam Report + theme.palette.grey[500], + }} + > + + + + + + + + ); +}; + +export default CippForefrontHeaderDialog; diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index aa9329e66f85..7c6f7ccb6050 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -8,6 +8,9 @@ import { FormControl, FormLabel, RadioGroup, + Button, + Box, + Input, } from "@mui/material"; import { CippAutoComplete } from "./CippAutocomplete"; import { Controller, useFormState } from "react-hook-form"; @@ -24,12 +27,20 @@ import { } from "mui-tiptap"; import StarterKit from "@tiptap/starter-kit"; import { CippDataTable } from "../CippTable/CippDataTable"; +import React from "react"; +import { CloudUpload } from "@mui/icons-material"; // Helper function to convert bracket notation to dot notation +// Improved to correctly handle nested bracket notations const convertBracketsToDots = (name) => { + if (!name) return ""; return name.replace(/\[(\d+)\]/g, ".$1"); // Replace [0] with .0 }; +const MemoizedCippAutoComplete = React.memo((props) => { + return ; +}); + export const CippFormComponent = (props) => { const { validators, @@ -38,6 +49,8 @@ export const CippFormComponent = (props) => { name, // The name that may have bracket notation label, labelLocation = "behind", // Default location for switches + defaultValue, + helperText, ...other } = props; const { errors } = useFormState({ control: formControl.control }); @@ -116,11 +129,17 @@ export const CippFormComponent = (props) => { {...other} {...formControl.register(convertedName, { ...validators })} label={label} + defaultValue={defaultValue} />
    {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); case "password": @@ -131,14 +150,23 @@ export const CippFormComponent = (props) => { type="password" variant="filled" fullWidth + InputLabelProps={{ + shrink: true, + }} {...other} {...formControl.register(convertedName, { ...validators })} label={label} + defaultValue={defaultValue} /> {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); case "number": @@ -148,14 +176,23 @@ export const CippFormComponent = (props) => { {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); @@ -166,6 +203,7 @@ export const CippFormComponent = (props) => { renderSwitchWithLabel( { {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); @@ -204,19 +247,26 @@ export const CippFormComponent = (props) => { ( - - {props.options.map((option, idx) => ( - } - label={option.label} - /> - ))} - - )} + render={({ field }) => { + return ( + field.onChange(e.target.value)} + {...other} + > + {props.options.map((option, idx) => ( + } + label={option.label} + /> + ))} + + ); + }} /> @@ -234,14 +284,14 @@ export const CippFormComponent = (props) => { control={formControl.control} rules={validators} render={({ field }) => ( - field.onChange(value.value)} + onChange={(value) => field.onChange(value?.value)} /> )} /> @@ -261,7 +311,7 @@ export const CippFormComponent = (props) => { control={formControl.control} rules={validators} render={({ field }) => ( - { {get(errors, convertedName, {}).message} + {helperText && ( + + {helperText} + + )} ); - case "richText": + case "richText": { + const editorInstanceRef = React.useRef(null); + const hasSetInitialValue = React.useRef(false); + return ( <>
    @@ -286,27 +344,48 @@ export const CippFormComponent = (props) => { name={convertedName} control={formControl.control} rules={validators} - render={({ field }) => ( - <> - {label} - field.onChange(editor.getHTML())} // Update react-hook-form on change - label={label} - renderControls={() => ( - - - - - - - )} - /> - - )} + render={({ field }) => { + const { value, onChange, ref } = field; + + // Set content only once on first render + React.useEffect(() => { + if ( + editorInstanceRef.current && + !hasSetInitialValue.current && + typeof value === "string" + ) { + editorInstanceRef.current.commands.setContent(value || "", false); + hasSetInitialValue.current = true; + } + }, [value]); + + return ( + <> + {label} + { + editorInstanceRef.current = editor; + }} + onUpdate={({ editor }) => { + onChange(editor.getHTML()); + }} + label={label} + renderControls={() => ( + + + + + + + )} + /> + + ); + }} />
    @@ -314,7 +393,7 @@ export const CippFormComponent = (props) => { ); - + } case "CSVReader": const remapData = (data, nameToCSVMapping) => { if (nameToCSVMapping && data) { @@ -378,44 +457,138 @@ export const CippFormComponent = (props) => { control={formControl.control} rules={validators} render={({ field }) => ( - { - if (date) { - const unixTimestamp = Math.floor(date.getTime() / 1000); // Convert to Unix timestamp - field.onChange(unixTimestamp); // Pass the Unix timestamp to the form - } else { - field.onChange(null); // Handle the case where no date is selected - } - }} - ampm={false} - minutesStep={15} - inputFormat="yyyy/MM/dd HH:mm" // Display format - renderInput={(inputProps) => ( - + + { + if (date) { + const unixTimestamp = Math.floor(date.getTime() / 1000); // Convert to Unix timestamp + field.onChange(unixTimestamp); // Pass the Unix timestamp to the form + } else { + field.onChange(null); // Handle the case where no date is selected + } + }} + ampm={false} + minutesStep={15} + inputFormat="yyyy/MM/dd HH:mm" // Display format + renderInput={(inputProps) => ( + + )} {...other} - fullWidth - error={!!errors[convertedName]} - helperText={get(errors, convertedName, {})?.message} - variant="filled" /> - )} - {...other} - /> + + + + )} + /> + + + {get(errors, convertedName, {})?.message} + + + ); + + case "file": + return ( + <> +
    + ( + + + {label} + + document.getElementById(`file-input-${convertedName}`).click()} + > + + + {field.value ? field.value.name : "Click to upload file or drag and drop"} + + {field.value && ( + + Size: {(field.value.size / 1024).toFixed(2)} KB + + )} + + { + const file = e.target.files[0]; + field.onChange(file); + if (other.onChange) { + other.onChange(file); + } + }} + /> + )} />
    {get(errors, convertedName, {})?.message} + {helperText && ( + + {helperText} + + )} ); diff --git a/src/components/CippComponents/CippFormCondition.jsx b/src/components/CippComponents/CippFormCondition.jsx index 1763091fbe27..b3630acd7098 100644 --- a/src/components/CippComponents/CippFormCondition.jsx +++ b/src/components/CippComponents/CippFormCondition.jsx @@ -1,8 +1,19 @@ import { useWatch } from "react-hook-form"; import isEqual from "lodash/isEqual"; // lodash for deep comparison +import get from "lodash/get"; // Add lodash get for safer property access +import React, { useEffect } from "react"; // Added useEffect export const CippFormCondition = (props) => { - let { field, compareType = "is", compareValue, children, formControl } = props; + let { + field, + compareType = "is", + compareValue, + propertyName = "value", + action = "hide", + children, + formControl, + disabled = false, + } = props; if ( field === undefined || @@ -10,142 +21,217 @@ export const CippFormCondition = (props) => { children === undefined || formControl === undefined ) { + console.warn("CippFormCondition: Missing required props", { + field, + compareValue, + children, + formControl, + }); return null; } - let watcher = useWatch({ control: formControl.control, name: field }); - if (watcher?.value !== undefined) { - watcher = watcher.value; + // Convert bracket notation to dot notation for array fields if needed + const normalizedField = field.replace(/\[(\d+)\]/g, ".$1"); + + // Watch the form field value + const watcher = useWatch({ + control: formControl.control, + name: normalizedField, + }); + + // Safer property access with get for nested paths + let watchedValue = watcher; + let compareTargetValue = compareValue; + + if (propertyName && propertyName !== "value") { + watchedValue = get(watcher, propertyName); + // Only extract from compareValue if it's an object, otherwise use as-is + if (typeof compareValue === "object" && compareValue !== null) { + compareTargetValue = get(compareValue, propertyName); + } else { + compareTargetValue = compareValue; + } } - if (compareValue?.value !== undefined) { - compareValue = compareValue.value; - } - - switch (compareType) { - case "regex": - if (watcher?.match(new RegExp(compareValue))) { - return children; - } - return null; - case "is": - // Deep comparison for objects and arrays - if (isEqual(watcher, compareValue)) { - return children; + /*console.log("CippFormCondition: ", { + watcher, + watchedValue, + compareTargetValue, + compareType, + compareValue, + action, + field, + propertyName, + });*/ + + // Function to recursively extract field names from child components + const extractFieldNames = (children) => { + const fieldNames = []; + + React.Children.forEach(children, (child) => { + if (!React.isValidElement(child)) return; + + // Check if the child is a CippFormComponent with a name prop + if (child.props?.name && child.type?.name === "CippFormComponent") { + fieldNames.push(child.props.name); } - return null; - case "isNot": - // Deep comparison for objects and arrays (negation) - if (!isEqual(watcher, compareValue)) { - return children; + // Check if child has nested children + if (child.props?.children) { + fieldNames.push(...extractFieldNames(child.props.children)); } - return null; - - case "contains": - if (Array.isArray(watcher)) { - if (watcher.includes(compareValue)) { - return children; + }); + + return fieldNames; + }; + + // Function to check if the condition is met + const isConditionMet = () => { + switch (compareType) { + case "regex": + return watcher?.match?.(new RegExp(compareValue)); + case "is": + return isEqual(watchedValue, compareTargetValue); + case "isNot": + return !isEqual(watchedValue, compareTargetValue); + case "contains": + if (Array.isArray(watcher)) { + return watcher.some((item) => isEqual(item, compareValue)); + } else if (typeof watcher === "string") { + return watcher.includes(compareValue); + } else if (typeof watcher === "object" && watcher !== null) { + // Handle checking if object contains value or key + if (typeof compareValue === "string") { + // Check for "value" property containing the string + if (watcher.value && typeof watcher.value === "string") { + return watcher.value.includes(compareValue); + } + // Check for "label" property containing the string + if (watcher.label && typeof watcher.label === "string") { + return watcher.label.includes(compareValue); + } + // Check if object has the compareValue as a key + return compareValue in watcher; + } else { + return Object.values(watcher).some((val) => isEqual(val, compareValue)); + } } - } else if (typeof watcher === "string") { - if (watcher.includes(compareValue)) { - return children; + return false; + case "doesNotContain": + if (watcher === undefined || watcher === null) { + return true; + } else if (Array.isArray(watcher)) { + return !watcher.some((item) => isEqual(item, compareValue)); + } else if (typeof watcher === "string") { + return !watcher.includes(compareValue); + } else if (typeof watcher === "object") { + if (typeof compareValue === "string") { + return !(compareValue in watcher); + } else { + return !Object.values(watcher).some((val) => isEqual(val, compareValue)); + } } - } else if (typeof watcher === "object" && compareValue in watcher) { - // Check if object contains the key - return children; - } - return null; - - case "doesNotContain": - if (Array.isArray(watcher)) { - if (!watcher.includes(compareValue)) { - return children; + return true; + case "greaterThan": + return ( + typeof watcher === "number" && typeof compareValue === "number" && watcher > compareValue + ); + case "lessThan": + return ( + typeof watcher === "number" && typeof compareValue === "number" && watcher < compareValue + ); + case "arrayLength": + return ( + Array.isArray(watcher) && + typeof compareValue === "number" && + watcher.length >= compareValue + ); + case "hasValue": + return ( + (watcher !== undefined && watcher !== null && watcher !== "") || + (watcher?.value !== undefined && watcher?.value !== null && watcher?.value !== "") + ); + case "labelEq": + return Array.isArray(watcher) && watcher.some((item) => item?.label === compareValue); + case "labelContains": + return ( + Array.isArray(watcher) && + watcher.some( + (item) => typeof item?.label === "string" && item.label.includes(compareValue) + ) + ); + case "valueEq": + if (Array.isArray(watcher)) { + return watcher.some((item) => item?.value === compareValue); + } else if (typeof watcher === "object" && watcher !== null) { + return watcher?.value === compareValue; } - } else if (typeof watcher === "string") { - if (!watcher.includes(compareValue)) { - return children; + return false; + case "valueNotEq": + if (Array.isArray(watcher)) { + return watcher.some((item) => item?.value !== compareValue); + } else if (typeof watcher === "object" && watcher !== null) { + return watcher?.value !== compareValue; + } + return false; + case "valueContains": + return ( + Array.isArray(watcher) && + watcher.some( + (item) => typeof item?.value === "string" && item.value.includes(compareValue) + ) + ); + default: + return false; + } + }; + + // Reset field values when condition is not met and action is "hide" + useEffect(() => { + if (action === "hide" && !isConditionMet()) { + const fieldNames = extractFieldNames(children); + + // Reset each field + fieldNames.forEach((fieldName) => { + // Don't reset if the field doesn't exist in the form + if (formControl.getValues(fieldName) !== undefined) { + formControl.setValue(fieldName, null, { + shouldValidate: false, + shouldDirty: false, + }); + } + }); + } + }, [watcher, action]); + + const disableChildren = (children) => { + return React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + if (child.props?.children) { + return React.cloneElement(child, { + children: disableChildren(child.props.children), + disabled: true, + }); + } else { + return React.cloneElement(child, { disabled: true }); } - } else if (typeof watcher === "object" && !(compareValue in watcher)) { - // Check if object does not contain the key - return children; - } - return null; - - case "greaterThan": - if ( - typeof watcher === "number" && - typeof compareValue === "number" && - watcher > compareValue - ) { - return children; - } - return null; - - case "lessThan": - if ( - typeof watcher === "number" && - typeof compareValue === "number" && - watcher < compareValue - ) { - return children; - } - return null; - - case "arrayLength": - if ( - Array.isArray(watcher) && - typeof compareValue === "number" && - watcher.length >= compareValue - ) { - return children; } - return null; + return child; + }); + }; - case "hasValue": - if (watcher !== undefined && watcher !== null && watcher !== "") { - return children; - } - return null; - - /* - * NEW CASES - */ - case "labelEq": - // Checks if any object in array has .label exactly equal to compareValue - if (Array.isArray(watcher) && watcher.some((item) => item?.label === compareValue)) { - return children; - } - return null; - - case "labelContains": - // Checks if any object in array has a .label that contains compareValue - if ( - Array.isArray(watcher) && - watcher.some((item) => typeof item?.label === "string" && item.label.includes(compareValue)) - ) { - return children; - } - return null; + if (disabled) { + return disableChildren(children); + } - case "valueEq": - // Checks if any object in array has .value exactly equal to compareValue - if (Array.isArray(watcher) && watcher.some((item) => item?.value === compareValue)) { - return children; - } - return null; - - case "valueContains": - // Checks if any object in array has a .value that contains compareValue - if ( - Array.isArray(watcher) && - watcher.some((item) => typeof item?.value === "string" && item.value.includes(compareValue)) - ) { - return children; - } - return null; + // Return based on condition check + const conditionMet = isConditionMet(); - default: - return null; + if (conditionMet) { + return children; + } else if (action === "disable") { + return disableChildren(children); + } else { + return null; } }; diff --git a/src/components/CippComponents/CippFormContactSelector.jsx b/src/components/CippComponents/CippFormContactSelector.jsx index 094f36014eb7..ec79ea8424bd 100644 --- a/src/components/CippComponents/CippFormContactSelector.jsx +++ b/src/components/CippComponents/CippFormContactSelector.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { CippFormComponent } from "./CippFormComponent"; import { useWatch } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; @@ -13,6 +12,7 @@ export const CippFormContactSelector = ({ select, addedField, valueField, + dataFilter = null, ...other }) => { const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); @@ -28,10 +28,21 @@ export const CippFormContactSelector = ({ addedField: addedField, tenantFilter: currentTenant ? currentTenant.value : selectedTenant, url: "/api/ListContacts", - labelField: (option) => `${option.displayName} (${option.mail})`, - valueField: valueField ? valueField : "id", + labelField: (option) => + `${option.displayName || option.DisplayName} (${ + option.mail || option.WindowsEmailAddress + })`, + valueField: valueField ? valueField : "WindowsEmailAddress" || "mail", queryKey: `listcontacts-${currentTenant?.value ? currentTenant.value : selectedTenant}`, + dataFilter: (options) => { + if (dataFilter) { + return options.filter(dataFilter); + } + return options; + }, }} + creatable={false} + {...other} /> ); }; diff --git a/src/components/CippComponents/CippFormDomainSelector.jsx b/src/components/CippComponents/CippFormDomainSelector.jsx index f22a5f74e884..9bf5f65639d0 100644 --- a/src/components/CippComponents/CippFormDomainSelector.jsx +++ b/src/components/CippComponents/CippFormDomainSelector.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { CippFormComponent } from "./CippFormComponent"; import { useWatch } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; @@ -9,6 +8,7 @@ export const CippFormDomainSelector = ({ label, allTenants = false, type = "multiple", + multiple = false, ...other }) => { const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); @@ -19,7 +19,7 @@ export const CippFormDomainSelector = ({ label={label} type="autoComplete" formControl={formControl} - multiple={false} + multiple={multiple} api={{ autoSelectFirstItem: true, tenantFilter: currentTenant ? currentTenant.value : selectedTenant, @@ -35,6 +35,7 @@ export const CippFormDomainSelector = ({ $top: 99, }, }} + {...other} /> ); }; diff --git a/src/components/CippComponents/CippFormGroupSelector.jsx b/src/components/CippComponents/CippFormGroupSelector.jsx new file mode 100644 index 000000000000..ff8eb4ac6dc5 --- /dev/null +++ b/src/components/CippComponents/CippFormGroupSelector.jsx @@ -0,0 +1,47 @@ +import { CippFormComponent } from "./CippFormComponent"; +import { useWatch } from "react-hook-form"; +import { useSettings } from "../../hooks/use-settings"; + +export const CippFormGroupSelector = ({ + formControl, + name, + label, + allTenants = false, + multiple = false, + type = "multiple", + select, + addedField, + creatable = false, + ...other +}) => { + const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + const selectedTenant = useSettings().currentTenant; + return ( + option.displayName, + valueField: "id", + queryKey: `ListGroups-${currentTenant?.value ? currentTenant.value : selectedTenant}`, + data: { + Endpoint: "groups", + manualPagination: true, + $select: select ? select : "id,displayName,description", + $count: true, + $orderby: "displayName", + $top: 999, + }, + }} + creatable={creatable} + {...other} + /> + ); +}; diff --git a/src/components/CippComponents/CippFormInputArray.jsx b/src/components/CippComponents/CippFormInputArray.jsx index 1eabcbefcb0d..159b11b36119 100644 --- a/src/components/CippComponents/CippFormInputArray.jsx +++ b/src/components/CippComponents/CippFormInputArray.jsx @@ -1,4 +1,4 @@ -import { Button, TextField, IconButton, Typography, SvgIcon } from "@mui/material"; +import { TextField, IconButton, Typography, Box } from "@mui/material"; import { Controller, useFieldArray } from "react-hook-form"; import { Add, Remove } from "@mui/icons-material"; @@ -7,68 +7,111 @@ const convertBracketsToDots = (name) => { return name.replace(/\[(\d+)\]/g, ".$1"); // Replace [0] with .0 }; -export const CippFormInputArray = ({ formControl, name, label, validators, ...other }) => { +export const CippFormInputArray = ({ + formControl, + name, + label, + validators, + mode = "keyValue", // Default to keyValue for backward compatibility + placeholder, + keyPlaceholder = "Key", + valuePlaceholder = "Value", + ...other +}) => { // Convert the name from bracket notation to dot notation const convertedName = convertBracketsToDots(name); + // Determine initial value based on mode + const getInitialValue = () => { + if (mode === "simple") { + return ""; + } else { + return { Key: "", Value: "" }; + } + }; + // Use `useFieldArray` to manage dynamic field arrays const { fields, append, remove } = useFieldArray({ control: formControl.control, - name: convertedName, // Specify the converted name for useFieldArray + name: convertedName, }); + // Render simple mode (single input field) + const renderSimpleField = (field, index) => ( + + ( + + )} + /> + remove(index)} aria-label="remove item" size="small"> + + + + ); + + // Render key-value mode (two input fields) - original functionality + const renderKeyValueField = (field, index) => ( + + ( + + )} + /> + ( + + )} + /> + remove(index)} aria-label="remove item" size="small"> + + + + ); + return ( - <> -
    + + {label && {label}} - append({ Key: "", Value: "" })} variant="outlined"> + append(getInitialValue())} variant="outlined" size="small"> -
    + - {fields.map((field, index) => ( -
    - ( - - )} - /> - ( - - )} - /> - remove(index)} aria-label="remove item"> - - - - -
    - ))} - + {fields.map((field, index) => + mode === "simple" ? renderSimpleField(field, index) : renderKeyValueField(field, index) + )} + ); }; diff --git a/src/components/CippComponents/CippFormLicenseSelector.jsx b/src/components/CippComponents/CippFormLicenseSelector.jsx index e435c1b467e1..1bd640850857 100644 --- a/src/components/CippComponents/CippFormLicenseSelector.jsx +++ b/src/components/CippComponents/CippFormLicenseSelector.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { CippFormComponent } from "./CippFormComponent"; import { getCippLicenseTranslation } from "../../utils/get-cipp-license-translation"; import { useSettings } from "../../hooks/use-settings"; @@ -24,14 +23,11 @@ export const CippFormLicenseSelector = ({ api={{ addedField: addedField, tenantFilter: userSettingsDefaults.currentTenant ?? undefined, - url: "/api/ListGraphRequest", - dataKey: "Results", + url: "/api/ListLicenses", labelField: (option) => - `${getCippLicenseTranslation([option])} (${ - option.prepaidUnits.enabled - option.consumedUnits - } available)`, + `${getCippLicenseTranslation([option])} (${option?.availableUnits} available)`, valueField: "skuId", - queryKey: `ListLicenses-${userSettingsDefaults.currentTenant ?? undefined}`, + queryKey: `ListLicenses-${userSettingsDefaults?.currentTenant ?? undefined}`, data: { Endpoint: "subscribedSkus", $count: true, diff --git a/src/components/CippComponents/CippFormTenantSelector.jsx b/src/components/CippComponents/CippFormTenantSelector.jsx index 032d4d6ad96f..262f85e677ad 100644 --- a/src/components/CippComponents/CippFormTenantSelector.jsx +++ b/src/components/CippComponents/CippFormTenantSelector.jsx @@ -1,14 +1,21 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { CippFormComponent } from "./CippFormComponent"; +import { useSettings } from "../../hooks/use-settings"; +import { GroupHeader, GroupItems } from "../CippComponents/CippAutocompleteGrouping"; +import { ApiGetCall } from "/src/api/ApiCall"; export const CippFormTenantSelector = ({ formControl, + componentType = "autoComplete", allTenants = false, type = "multiple", name = "tenantFilter", valueField = "defaultDomainName", required = true, disableClearable = true, + preselectedEnabled = false, + removeOptions = [], + includeGroups = false, // New parameter ...other }) => { const validators = () => { @@ -19,28 +26,70 @@ export const CippFormTenantSelector = ({ } return {}; }; + const currentTenant = useSettings()?.currentTenant; + + // Fetch tenant list + const tenantList = ApiGetCall({ + url: allTenants ? "/api/ListTenants?AllTenantSelector=true" : "/api/ListTenants", + queryKey: allTenants ? "ListTenants-FormAllTenantSelector" : "ListTenants-FormnotAllTenants", + }); + + // Fetch tenant group list if includeGroups is true + const tenantGroupList = ApiGetCall({ + url: "/api/ListTenantGroups", + data: { AllTenantSelector: true }, + queryKey: "TenantGroupSelector", + waiting: includeGroups, + }); + + const [options, setOptions] = useState([]); + + useEffect(() => { + if (tenantList.isSuccess && (!includeGroups || tenantGroupList.isSuccess)) { + const tenantData = tenantList.data.map((tenant) => ({ + value: tenant[valueField], + label: `${tenant.displayName} (${tenant.defaultDomainName})`, + type: "Tenant", + addedFields: { + defaultDomainName: tenant.defaultDomainName, + displayName: tenant.displayName, + customerId: tenant.customerId, + }, + })); + + const groupData = includeGroups + ? tenantGroupList?.data?.Results?.map((group) => ({ + value: group.Id, + label: group.Name, + type: "Group", + })) + : []; + + setOptions([...tenantData, ...groupData]); + } + }, [tenantList.isSuccess, tenantGroupList.isSuccess, includeGroups]); return ( `${option.displayName} (${option.defaultDomainName})`, - valueField: valueField, - addedField: { - defaultDomainName: "defaultDomainName", - displayName: "displayName", - customerId: "customerId", - }, - }} + creatable={false} multiple={type === "single" ? false : true} disableClearable={disableClearable} validators={validators} + removeOptions={removeOptions} + options={options} + groupBy={(option) => option.type} + renderGroup={(params) => ( +
  • + {includeGroups && {params.group}} + {includeGroups ? {params.children} : params.children} +
  • + )} + isFetching={tenantList.isFetching || tenantGroupList.isFetching} {...other} /> ); diff --git a/src/components/CippComponents/CippFormUserSelector.jsx b/src/components/CippComponents/CippFormUserSelector.jsx index 51395ef830a8..7e5b11c0f551 100644 --- a/src/components/CippComponents/CippFormUserSelector.jsx +++ b/src/components/CippComponents/CippFormUserSelector.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { CippFormComponent } from "./CippFormComponent"; import { useWatch } from "react-hook-form"; import { useSettings } from "../../hooks/use-settings"; @@ -13,6 +12,7 @@ export const CippFormUserSelector = ({ select, addedField, valueField, + dataFilter = null, ...other }) => { const currentTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); @@ -31,7 +31,9 @@ export const CippFormUserSelector = ({ dataKey: "Results", labelField: (option) => `${option.displayName} (${option.userPrincipalName})`, valueField: valueField ? valueField : "id", - queryKey: `ListUsers-${currentTenant?.value ? currentTenant.value : selectedTenant}`, + queryKey: `ListUsers-${currentTenant?.value ? currentTenant.value : selectedTenant}-${ + select ? select : "default" + }`, data: { Endpoint: "users", manualPagination: true, @@ -40,7 +42,15 @@ export const CippFormUserSelector = ({ $orderby: "displayName", $top: 999, }, + dataFilter: (options) => { + if (dataFilter) { + return options.filter(dataFilter); + } + return options; + } }} + creatable={false} + {...other} /> ); }; diff --git a/src/components/CippComponents/CippForwardingSection.jsx b/src/components/CippComponents/CippForwardingSection.jsx new file mode 100644 index 000000000000..df7fcba9b177 --- /dev/null +++ b/src/components/CippComponents/CippForwardingSection.jsx @@ -0,0 +1,98 @@ +import { Stack, Button } from "@mui/material"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormCondition } from "./CippFormCondition"; +import { Grid } from "@mui/system"; +import { CippApiResults } from "./CippApiResults"; +import { getCippValidator } from "/src/utils/get-cipp-validator"; + +const CippForwardingSection = ({ formControl, usersList, contactsList, postRequest, handleSubmit }) => { + + const internalAddressOptions = [ + // Add users + ...(usersList?.data?.Results?.map((user) => ({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName}) - User`, + })) || []), + // Add contacts + ...(contactsList?.data?.Results?.map((contact) => ({ + value: contact.mail || contact.emailAddress, + label: `${contact.displayName} (${contact.mail || contact.emailAddress}) - Contact`, + })) || []) + ]; + + return ( + + + + + + + + + getCippValidator(value, "email"), + }} + /> + + + + + + + + + + + + + ); +}; + +export default CippForwardingSection; diff --git a/src/components/CippComponents/CippGdapActions.jsx b/src/components/CippComponents/CippGdapActions.jsx new file mode 100644 index 000000000000..b2200cd64b7b --- /dev/null +++ b/src/components/CippComponents/CippGdapActions.jsx @@ -0,0 +1,113 @@ +import { EyeIcon } from "@heroicons/react/24/outline"; +import { + AdminPanelSettings, + GppBad, + HourglassBottom, + LockReset, + OpenInNew, + PlayArrow, +} from "@mui/icons-material"; +import { Alert, Typography } from "@mui/material"; + +export const CippGdapActions = () => [ + { + label: "View Relationship", + link: "/tenant/gdap-management/relationships/relationship?id=[id]", + color: "primary", + icon: , + }, + { + label: "Start Onboarding", + link: "/tenant/gdap-management/onboarding/start?id=[id]", + color: "primary", + icon: , + showInActionsMenu: true, + }, + { + label: "Open Relationship in Partner Center", + link: "https://partner.microsoft.com/en-us/dashboard/commerce2/customers/[customer.tenantId]/adminrelationships/[id]", + color: "info", + icon: , + showInActionsMenu: true, + }, + { + label: "Enable automatic extension", + type: "GET", + url: "/api/ExecAutoExtendGDAP", + data: { ID: "id" }, + confirmText: "Are you sure you want to enable auto-extend for this relationship?", + color: "info", + icon: , + }, + { + label: "Remove Global Administrator from Relationship", + type: "GET", + url: "/api/ExecGDAPRemoveGArole", + data: { GDAPID: "id" }, + confirmText: "Are you sure you want to remove Global Administrator from this relationship?", + color: "danger", + icon: , + }, + { + label: "Reset Role Mapping", + type: "POST", + url: "/api/ExecGDAPAccessAssignment", + icon: , + data: { Id: "id", Action: "ResetMappings" }, + fields: [ + { + name: "RoleTemplateId", + label: "Role Template", + placeholder: "Select a role template to apply to this relationship.", + type: "select", + api: { + url: "/api/ExecGDAPRoleTemplate", + queryKey: "GDAPRoleTemplate", + dataKey: "Results", + valueField: "TemplateId", + labelField: "TemplateId", + showRefresh: true, + }, + required: true, + validators: { + validate: (value) => { + if (!value) { + return "Role Template is required"; + } + return true; + }, + }, + }, + ], + confirmText: ( + <> + + Are you sure you want to reset the role mappings for [customer.displayName]? + + + Resetting GDAP role mappings will perform the following actions: +
      +
    • Remove groups assignments that are not part of the Role Template
    • +
    • Update existing group assignments to match the Role Template
    • +
    • Create new group assignments based on the Role Template
    • +
    +
    + + This is useful for fixing GDAP relationships that have overlapping roles or incorrect + group assignments (e.g. using AdminAgents or HelpdeskAgents). + + + ), + }, + { + label: "Terminate Relationship", + type: "GET", + url: "/api/ExecDeleteGDAPRelationship", + data: { GDAPID: "id" }, + confirmText: "Are you sure you want to terminate this relationship?", + color: "error", + icon: , + }, +]; + +export default CippGdapActions; diff --git a/src/components/CippComponents/CippGeoLocation.jsx b/src/components/CippComponents/CippGeoLocation.jsx index 633ce3ee60cd..e7a4be63ed66 100644 --- a/src/components/CippComponents/CippGeoLocation.jsx +++ b/src/components/CippComponents/CippGeoLocation.jsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from "react"; -import { Card, CardContent, CardHeader, Skeleton } from "@mui/material"; +import { useEffect, useState } from "react"; +import { Skeleton } from "@mui/material"; import { Grid } from "@mui/system"; import dynamic from "next/dynamic"; import { ApiPostCall } from "/src/api/ApiCall"; @@ -8,14 +8,27 @@ import { getCippTranslation } from "../../utils/get-cipp-translation"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; const CippMap = dynamic(() => import("./CippMap"), { ssr: false }); -export default function CippGeoLocation({ ipAddress, cardProps }) { +export default function CippGeoLocation({ + ipAddress, + cardProps, + showIpAddress = false, + displayIpAddress = null, +}) { const [locationInfo, setLocationInfo] = useState(null); const markerProperties = ["timezone", "as", "proxy", "hosting", "mobile"]; const includeProperties = ["org", "city", "region", "country", "zip"]; - const initialPropertyList = includeProperties.map((key) => ({ - label: getCippTranslation(key), - value: "", + + // Use displayIpAddress if provided, otherwise use ipAddress + const ipToDisplay = displayIpAddress || ipAddress; + + // Add IP address to properties if showIpAddress is true + const initialIncludeProperties = showIpAddress + ? ["ipAddress", ...includeProperties] + : includeProperties; + const initialPropertyList = initialIncludeProperties.map((key) => ({ + label: getCippTranslation(key === "ipAddress" ? "IP Address" : key), + value: key === "ipAddress" ? ipToDisplay : "", })); const [properties, setProperties] = useState(initialPropertyList); @@ -28,6 +41,16 @@ export default function CippGeoLocation({ ipAddress, cardProps }) { onResult: (result) => { setLocationInfo(result); var propertyList = []; + + // Add IP address property if showIpAddress is true + if (showIpAddress) { + propertyList.push({ + label: getCippTranslation("IP Address"), + value: getCippFormatting(ipToDisplay, "ipAddress"), + }); + } + + // Add other properties includeProperties.map((key) => { propertyList.push({ label: getCippTranslation(key), @@ -61,7 +84,7 @@ export default function CippGeoLocation({ ipAddress, cardProps }) { return ( - + {geoLookup.isPending ? ( ) : ( @@ -78,7 +101,7 @@ export default function CippGeoLocation({ ipAddress, cardProps }) { )} - + { + const tenant = useSettings().currentTenant; + return ( + + {tenant && !noTenant ? `${title} - ${tenant}` : title} + + ); +}; diff --git a/src/components/CippComponents/CippMailboxPermissionsDialog.jsx b/src/components/CippComponents/CippMailboxPermissionsDialog.jsx new file mode 100644 index 000000000000..8306089a8008 --- /dev/null +++ b/src/components/CippComponents/CippMailboxPermissionsDialog.jsx @@ -0,0 +1,71 @@ +import { Box, Stack } from "@mui/material"; +import { useEffect } from "react"; +import CippFormComponent from "./CippFormComponent"; +import { useWatch } from "react-hook-form"; + +const CippMailboxPermissionsDialog = ({ + formHook, + combinedOptions, + isUserGroupLoading, + defaultAutoMap = false +}) => { + const fullAccess = useWatch({ + control: formHook.control, + name: "permissions.AddFullAccess", + }); + + // Set the default AutoMap value when component mounts + useEffect(() => { + formHook.setValue("permissions.AutoMap", defaultAutoMap); + }, [formHook, defaultAutoMap]); + + return ( + + + + + + + + + + + + + + + ); +}; + +export default CippMailboxPermissionsDialog; diff --git a/src/components/CippComponents/CippMessageViewer.jsx b/src/components/CippComponents/CippMessageViewer.jsx index d4f2746fe8ea..366c17dcc507 100644 --- a/src/components/CippComponents/CippMessageViewer.jsx +++ b/src/components/CippComponents/CippMessageViewer.jsx @@ -36,6 +36,7 @@ import { Visibility, AccountCircle, Close, + ReceiptLong, } from "@mui/icons-material"; import { CippTimeAgo } from "./CippTimeAgo"; @@ -51,6 +52,7 @@ import { SunIcon, } from "@heroicons/react/24/outline"; import { useSettings } from "/src/hooks/use-settings"; +import CippForefrontHeaderDialog from "./CippForefrontHeaderDialog"; export const CippMessageViewer = ({ emailSource }) => { const [emlContent, setEmlContent] = useState(null); @@ -61,6 +63,8 @@ export const CippMessageViewer = ({ emailSource }) => { const [dialogOpen, setDialogOpen] = useState(false); const [dialogContent, setDialogContent] = useState(null); const [dialogTitle, setDialogTitle] = useState(""); + const [forefrontDialogOpen, setForefrontDialogOpen] = useState(false); + const [forefrontHeader, setForefrontHeader] = useState(""); const currentTheme = useSettings()?.currentTheme?.value; const [darkMode, setDarkMode] = useState(currentTheme === "dark"); @@ -189,10 +193,16 @@ export const CippMessageViewer = ({ emailSource }) => { setDialogOpen(true); }; + const showForefrontDialog = (header) => { + setForefrontHeader(header); + setForefrontDialogOpen(true); + }; + const EmailButtons = (emailHeaders, emailSource) => { const emailSourceBytes = new TextEncoder().encode(emailSource); const blob = new Blob([emailSourceBytes], { type: "message/rfc822" }); const url = URL.createObjectURL(blob); + const forefrontHeader = emailHeaders?.match(/X-Forefront-Antispam-Report: (.*)/)?.[1]; return ( {emailHeaders && ( @@ -209,6 +219,20 @@ export const CippMessageViewer = ({ emailSource }) => { View Headers )} + {forefrontHeader && ( + + )} + + )} + + + {showTestButton && ( + ({ + ...row, + text: "This is a test from Notification Settings", + }), + }} + /> + )} + + ); +}; + +export default CippNotificationForm; diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index bd68e583de8b..caa30e6b3036 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -1,7 +1,9 @@ -import { Drawer, Box, Grid } from "@mui/material"; +import { Drawer, Box, IconButton } from "@mui/material"; import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; import { getCippTranslation } from "../../utils/get-cipp-translation"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { useMediaQuery, Grid } from "@mui/system"; +import CloseIcon from "@mui/icons-material/Close"; export const CippOffCanvas = (props) => { const { @@ -16,6 +18,7 @@ export const CippOffCanvas = (props) => { size = "sm", } = props; + const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); const extendedInfo = extendedInfoFields.map((field) => { const value = field.split(".").reduce((acc, part) => acc && acc[part], extendedData); if (value === undefined || value === null) { @@ -43,20 +46,24 @@ export const CippOffCanvas = (props) => { } }); - var drawerWidth = 400; - switch (size) { - case "sm": - drawerWidth = 400; - break; - case "md": - drawerWidth = 600; - break; - case "lg": - drawerWidth = 800; - break; - case "xl": - drawerWidth = 1000; - break; + if (mdDown) { + drawerWidth = "100%"; + } else { + var drawerWidth = 400; + switch (size) { + case "sm": + drawerWidth = 400; + break; + case "md": + drawerWidth = 600; + break; + case "lg": + drawerWidth = 800; + break; + case "xl": + drawerWidth = 1000; + break; + } } return ( @@ -72,12 +79,23 @@ export const CippOffCanvas = (props) => { open={visible} onClose={onClose} > + + + {/* Force vertical stacking in a column layout */} - + {extendedInfo.length > 0 && ( { /> )} - + {typeof children === "function" ? children(extendedData) : children} diff --git a/src/components/CippComponents/CippPermissionPreview.jsx b/src/components/CippComponents/CippPermissionPreview.jsx new file mode 100644 index 000000000000..ad1bb530020c --- /dev/null +++ b/src/components/CippComponents/CippPermissionPreview.jsx @@ -0,0 +1,833 @@ +import { useState, useEffect, useCallback } from "react"; +import { + Alert, + Skeleton, + Stack, + Typography, + Box, + Paper, + List, + ListItem, + ListItemText, + Tab, + Tabs, + Chip, + SvgIcon, + Accordion, + AccordionSummary, + AccordionDetails, +} from "@mui/material"; +import { ShieldCheckIcon } from "@heroicons/react/24/outline"; +import { ExpandMore } from "@mui/icons-material"; +import { CippCardTabPanel } from "./CippCardTabPanel"; +import { ApiGetCall } from "../../api/ApiCall"; + +const CippPermissionPreview = ({ + permissions, + title = "Permission Preview", + isLoading = false, + maxHeight = "100%", + showAppIds = true, + galleryTemplate = null, + applicationManifest = null, +}) => { + const [selectedPermissionTab, setSelectedPermissionTab] = useState(0); + const [servicePrincipalDetails, setServicePrincipalDetails] = useState({}); + const [resourceIds, setResourceIds] = useState([]); + const [loadingDetails, setLoadingDetails] = useState(false); + + // Extract resource IDs from permissions object + useEffect(() => { + if (permissions && typeof permissions === "object") { + const ids = Object.keys(permissions); + setResourceIds(ids); + } + }, [permissions]); + + // Function to fetch individual service principal details + const fetchServicePrincipalDetails = useCallback(async (resourceId) => { + try { + const response = await fetch(`/api/ExecServicePrincipals?AppId=${resourceId}`); + const data = await response.json(); + + if (data?.Results) { + setServicePrincipalDetails((prev) => ({ + ...prev, + [resourceId]: data.Results, + })); + } + } catch (error) { + console.error(`Error fetching details for ${resourceId}:`, error); + } + }, []); + + // Fetch details for each resource ID + useEffect(() => { + const fetchAllDetails = async () => { + if (resourceIds.length > 0) { + setLoadingDetails(true); + const promises = resourceIds.map((id) => fetchServicePrincipalDetails(id)); + await Promise.all(promises); + setLoadingDetails(false); + } + }; + + fetchAllDetails(); + }, [resourceIds, fetchServicePrincipalDetails]); + + const handlePermissionTabChange = (event, newValue) => { + setSelectedPermissionTab(newValue); + }; + + // Function to get permission counts + const getPermissionCounts = (permissions) => { + if (!permissions) return { app: 0, delegated: 0 }; + + let appCount = 0; + let delegatedCount = 0; + + Object.entries(permissions).forEach(([resourceName, perms]) => { + if (perms.applicationPermissions) { + appCount += perms?.applicationPermissions?.length ?? 0; + } + if (perms.delegatedPermissions) { + delegatedCount += perms?.delegatedPermissions?.length ?? 0; + } + }); + + return { app: appCount, delegated: delegatedCount }; + }; + + // Helper to get the display name for a resource ID + const getResourceDisplayName = (resourceId) => { + const spDetails = servicePrincipalDetails[resourceId]; + return spDetails?.displayName || resourceId; + }; + + // Helper to get the appropriate permission description + const getPermissionDescription = (resourceId, permissionId, permissionType) => { + const spDetails = servicePrincipalDetails[resourceId]; + if (!spDetails) return null; + + if (permissionType === "application") { + const foundRole = spDetails.appRoles?.find((role) => role.id === permissionId); + return foundRole?.description || null; + } else { + const foundScope = spDetails.publishedPermissionScopes?.find( + (scope) => scope.id === permissionId + ); + return foundScope?.userConsentDescription || foundScope?.description || null; + } + }; + + // Better checks for permissions object to prevent rendering errors + if (isLoading || loadingDetails) { + return ( + <> + {title} + + + ); + } + + if (!permissions && !galleryTemplate && !applicationManifest) { + return ( + + Select a template with permissions to see what will be consented. + + ); + } + + // If we have gallery template data, show that instead of permissions + if (galleryTemplate) { + return ( + + {title} + + + {/* App Logo and Name */} + + {galleryTemplate.addedFields?.logoUrl && ( + + {galleryTemplate.addedFields?.displayName { + e.target.style.display = "none"; + }} + /> + + )} + + + {galleryTemplate.addedFields?.displayName || galleryTemplate.label} + + {galleryTemplate.addedFields?.publisher && ( + + by {galleryTemplate.addedFields.publisher} + + )} + + + + {/* Description */} + {galleryTemplate.addedFields?.description && ( + + + {galleryTemplate.addedFields.description} + + + )} + + {/* Categories */} + {galleryTemplate.addedFields?.categories && + galleryTemplate.addedFields.categories.length > 0 && ( + + + Categories: + + + {galleryTemplate.addedFields.categories.map((category, idx) => ( + + ))} + + + )} + + {/* SSO Modes */} + {galleryTemplate.addedFields?.supportedSingleSignOnModes && + galleryTemplate.addedFields.supportedSingleSignOnModes.length > 0 && ( + + + Supported SSO Modes: + + + {galleryTemplate.addedFields.supportedSingleSignOnModes.map((mode, idx) => ( + + ))} + + + )} + + {/* Provisioning Types */} + {galleryTemplate.addedFields?.supportedProvisioningTypes && + galleryTemplate.addedFields.supportedProvisioningTypes.length > 0 && ( + + + Supported Provisioning: + + + {galleryTemplate.addedFields.supportedProvisioningTypes.map((type, idx) => ( + + ))} + + + )} + + {/* Home Page URL */} + {galleryTemplate.addedFields?.homePageUrl && ( + + + Home Page: + + + {galleryTemplate.addedFields.homePageUrl} + + + )} + + {/* Template ID */} + + + Template ID: {galleryTemplate.value} + + + + {/* Auto-consent note */} + + Gallery templates will automatically consent to the required permissions defined in + the template's app registration. No manual permission configuration needed. + + + + + ); + } + + // If we have application manifest data, show that instead of permissions + if (applicationManifest) { + return ( + + ); + } + + // Ensure permissions is an object and has entries + if ( + typeof permissions !== "object" || + permissions === null || + Object.keys(permissions).length === 0 + ) { + return No permissions data available in this template.; + } + + return ( + + + {title} + + + + } + title="Application/Delegated Permissions" + /> + + + + + + + + + + + + + + {Object.entries(permissions).map(([resourceId, resourcePerms]) => { + const resourceName = getResourceDisplayName(resourceId); + const hasAppPermissions = + resourcePerms.applicationPermissions && + resourcePerms.applicationPermissions.length > 0; + const hasDelegatedPermissions = + resourcePerms.delegatedPermissions && resourcePerms.delegatedPermissions.length > 0; + + return ( + + + + {resourceName} + {showAppIds && ( + + {resourceId} + + )} + + + {hasAppPermissions && ( + + + Application Permissions ({resourcePerms.applicationPermissions.length}) + + + {resourcePerms.applicationPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "application") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + )} + + {hasDelegatedPermissions && ( + + + Delegated Permissions ({resourcePerms.delegatedPermissions.length}) + + + {resourcePerms.delegatedPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "delegated") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + )} + + + ); + })} + + + + + + {Object.entries(permissions) + .filter( + ([_, perms]) => + perms.applicationPermissions && perms.applicationPermissions.length > 0 + ) + .map(([resourceId, resourcePerms]) => { + const resourceName = getResourceDisplayName(resourceId); + return ( + + + + {resourceName} + {showAppIds && ( + + {resourceId} + + )} + + + {resourcePerms.applicationPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "application") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + + ); + })} + {!Object.values(permissions).some( + (perms) => perms.applicationPermissions && perms.applicationPermissions.length > 0 + ) && No application permissions in this template.} + + + + + + {Object.entries(permissions) + .filter( + ([_, perms]) => perms.delegatedPermissions && perms.delegatedPermissions.length > 0 + ) + .map(([resourceId, resourcePerms]) => { + const resourceName = getResourceDisplayName(resourceId); + return ( + + + + {resourceName} + {showAppIds && ( + + {resourceId} + + )} + + + {resourcePerms.delegatedPermissions.map((perm, idx) => { + const description = + getPermissionDescription(resourceId, perm.id, "delegated") || + perm.description || + "No description available"; + return ( + + + + ); + })} + + + + ); + })} + {!Object.values(permissions).some( + (perms) => perms.delegatedPermissions && perms.delegatedPermissions.length > 0 + ) && No delegated permissions in this template.} + + + + + ); +}; + +// Component to handle individual service principal resource details +const ServicePrincipalResourceDetails = ({ + resource, + servicePrincipalId, + expandedResource, + handleAccordionChange, +}) => { + // Fetch individual service principal details using ApiGetCall + const { + data: servicePrincipalData, + isSuccess: spDetailSuccess, + isFetching: spDetailFetching, + isLoading: spDetailLoading, + } = ApiGetCall({ + url: "/api/ExecServicePrincipals", + data: { Id: servicePrincipalId }, + queryKey: `execServicePrincipal-details-${servicePrincipalId}`, + waiting: !!servicePrincipalId, + }); + + const spDetails = servicePrincipalData?.Results; + + // Helper to get permission details + const getPermissionDetails = (permissionId, type) => { + if (!spDetails) return { name: permissionId, description: "Loading..." }; + + if (type === "Role") { + const foundRole = spDetails.appRoles?.find((role) => role.id === permissionId); + return { + name: foundRole?.value || permissionId, + description: foundRole?.description || "No description available", + }; + } else { + const foundScope = spDetails.publishedPermissionScopes?.find( + (scope) => scope.id === permissionId + ); + return { + name: foundScope?.value || permissionId, + description: + foundScope?.userConsentDescription || + foundScope?.description || + "No description available", + }; + } + }; + + const resourceName = spDetails?.displayName || resource.resourceAppId; + const appPermissions = resource.resourceAccess?.filter((access) => access.type === "Role") || []; + const delegatedPermissions = + resource.resourceAccess?.filter((access) => access.type === "Scope") || []; + + return ( + + }> + + + {spDetailLoading || spDetailFetching ? "Loading..." : resourceName} + + + + + + } + title="Application/Delegated Permissions" + /> + + + + + {(spDetailLoading || spDetailFetching) && ( + + )} + + {spDetailSuccess && spDetails && ( + <> + {appPermissions.length > 0 && ( + + + Application Permissions ({appPermissions.length}) + + + {appPermissions.map((permission, idx) => { + const permDetails = getPermissionDetails(permission.id, "Role"); + return ( + + + + ); + })} + + + )} + + {delegatedPermissions.length > 0 && ( + + + Delegated Permissions ({delegatedPermissions.length}) + + + {delegatedPermissions.map((permission, idx) => { + const permDetails = getPermissionDetails(permission.id, "Scope"); + return ( + + + + ); + })} + + + )} + + )} + + + ); +}; + +// Component to handle Application Manifest preview with detailed permission expansion +const ApplicationManifestPreview = ({ applicationManifest, title, maxHeight }) => { + const [expandedResource, setExpandedResource] = useState(false); + + // Get unique resource IDs from required resource access + const resourceIds = + applicationManifest.requiredResourceAccess?.map((resource) => resource.resourceAppId) || []; + + // Fetch the service principal list to get object IDs + const { + data: servicePrincipals = [], + isSuccess: spSuccess, + isFetching: spFetching, + isLoading: spLoading, + } = ApiGetCall({ + url: "/api/ExecServicePrincipals", + data: { Select: "appId,displayName,id" }, + queryKey: "execServicePrincipalList-cipp-permission-preview", + waiting: true, + }); + + // Helper to get service principal ID by appId + const getServicePrincipalId = (appId) => { + if (spSuccess && servicePrincipals?.Results) { + const sp = servicePrincipals.Results.find((sp) => sp.appId === appId); + return sp?.id || null; + } + return null; + }; + + const handleAccordionChange = (panel) => (event, newExpanded) => { + setExpandedResource(newExpanded ? panel : false); + }; + + return ( + + {title} + + + {/* App Basic Info */} + + + {applicationManifest.displayName || "Custom Application"} + + {applicationManifest.description && ( + + {applicationManifest.description} + + )} + + + {/* Application Properties */} + + + Application Properties: + + + {applicationManifest.signInAudience && ( + + + + )} + {applicationManifest.web?.redirectUris && + applicationManifest.web.redirectUris.length > 0 && ( + + + + )} + + + + {/* Required Resource Access with detailed permissions */} + {applicationManifest.requiredResourceAccess && + applicationManifest.requiredResourceAccess.length > 0 && ( + + + Required Permissions: + + {(spLoading || spFetching) && ( + + )} + {spSuccess && + servicePrincipals?.Results && + applicationManifest.requiredResourceAccess.map((resource, index) => { + const servicePrincipalId = getServicePrincipalId(resource.resourceAppId); + + return ( + + ); + })} + + )} + + {/* Custom application note */} + {/* Validation warning for signInAudience */} + {applicationManifest.signInAudience && + applicationManifest.signInAudience !== "AzureADMyOrg" && ( + + + Invalid signInAudience: "{applicationManifest.signInAudience}" + + + For security reasons, Application Manifests must have signInAudience set to + "AzureADMyOrg" or not defined in the JSON. This template cannot be deployed with + the current signInAudience value. + + + )} + + + This application will be created from a custom manifest. All permissions and + configuration are defined within the manifest JSON. + + + + + ); +}; + +export default CippPermissionPreview; diff --git a/src/components/CippComponents/CippPropertyList.jsx b/src/components/CippComponents/CippPropertyList.jsx index 4c9fd3800820..e6b5fed8f1d0 100644 --- a/src/components/CippComponents/CippPropertyList.jsx +++ b/src/components/CippComponents/CippPropertyList.jsx @@ -53,7 +53,7 @@ export const CippPropertyList = (props) => { ) : ( // Two-column layout - { )) )} - + ) )} ); diff --git a/src/components/CippComponents/CippScheduledTaskActions.jsx b/src/components/CippComponents/CippScheduledTaskActions.jsx new file mode 100644 index 000000000000..2a1ec00236ff --- /dev/null +++ b/src/components/CippComponents/CippScheduledTaskActions.jsx @@ -0,0 +1,58 @@ +import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { CopyAll, Edit, PlayArrow } from "@mui/icons-material"; +import { usePermissions } from "../../hooks/use-permissions"; + +export const CippScheduledTaskActions = () => { + const { checkPermissions } = usePermissions(); + const canWriteScheduler = checkPermissions(["CIPP.Scheduler.ReadWrite"]); + const canReadScheduler = checkPermissions(["CIPP.Scheduler.Read", "CIPP.Scheduler.ReadWrite"]); + + return [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + condition: () => canReadScheduler, + }, + { + label: "Run Now", + type: "POST", + url: "/api/AddScheduledItem", + data: { RowKey: "RowKey", RunNow: true }, + icon: , + confirmText: "Are you sure you want to run [Name]?", + allowResubmit: true, + condition: () => canWriteScheduler, + }, + { + label: "Edit Job", + link: "/cipp/scheduler/job?id=[RowKey]", + multiPost: false, + icon: , + color: "success", + showInActionsMenu: true, + condition: () => canWriteScheduler, + }, + { + label: "Clone and Edit Job", + link: "/cipp/scheduler/job?id=[RowKey]&Clone=True", + multiPost: false, + icon: , + color: "success", + showInActionsMenu: true, + condition: () => canWriteScheduler, + }, + { + label: "Delete Job", + icon: , + type: "POST", + url: "/api/RemoveScheduledItem", + data: { id: "RowKey" }, + confirmText: "Are you sure you want to delete this job?", + multiPost: false, + condition: () => canWriteScheduler, + }, + ]; +}; + +export default CippScheduledTaskActions; diff --git a/src/components/CippComponents/CippSettingsSideBar.jsx b/src/components/CippComponents/CippSettingsSideBar.jsx index 14c3ede87846..bb62097e02ca 100644 --- a/src/components/CippComponents/CippSettingsSideBar.jsx +++ b/src/components/CippComponents/CippSettingsSideBar.jsx @@ -20,16 +20,47 @@ export const CippSettingsSideBar = (props) => { const { isDirty, isValid } = useFormState({ control: formcontrol.control }); const currentUser = ApiGetCall({ - url: "/.auth/me", + url: "/api/me", + queryKey: "authmecipp", }); const saveSettingsPost = ApiPostCall({ url: "/api/ExecUserSettings", + relatedQueryKeys: "userSettings", }); const handleSaveChanges = () => { + const formValues = formcontrol.getValues(); + + // Only include the specific form fields from preferences.js to avoid unmapped data + const currentSettings = { + // General Settings + usageLocation: formValues.usageLocation, + tablePageSize: formValues.tablePageSize, + userAttributes: formValues.userAttributes, + + // Offboarding Defaults + offboardingDefaults: { + ConvertToShared: formValues.offboardingDefaults?.ConvertToShared, + RemoveGroups: formValues.offboardingDefaults?.RemoveGroups, + HideFromGAL: formValues.offboardingDefaults?.HideFromGAL, + RemoveLicenses: formValues.offboardingDefaults?.RemoveLicenses, + removeCalendarInvites: formValues.offboardingDefaults?.removeCalendarInvites, + RevokeSessions: formValues.offboardingDefaults?.RevokeSessions, + removePermissions: formValues.offboardingDefaults?.removePermissions, + RemoveRules: formValues.offboardingDefaults?.RemoveRules, + ResetPass: formValues.offboardingDefaults?.ResetPass, + KeepCopy: formValues.offboardingDefaults?.KeepCopy, + DeleteUser: formValues.offboardingDefaults?.DeleteUser, + RemoveMobile: formValues.offboardingDefaults?.RemoveMobile, + DisableSignIn: formValues.offboardingDefaults?.DisableSignIn, + RemoveMFADevices: formValues.offboardingDefaults?.RemoveMFADevices, + ClearImmutableId: formValues.offboardingDefaults?.ClearImmutableId, + }, + }; + const shippedValues = { user: formcontrol.getValues("user").value, - currentSettings: formcontrol.getValues(), + currentSettings: currentSettings, }; saveSettingsPost.mutate({ url: "/api/ExecUserSettings", data: shippedValues }); }; diff --git a/src/components/CippComponents/CippSpeedDial.jsx b/src/components/CippComponents/CippSpeedDial.jsx new file mode 100644 index 000000000000..4313ace7a7ef --- /dev/null +++ b/src/components/CippComponents/CippSpeedDial.jsx @@ -0,0 +1,222 @@ +import React, { useState, useEffect } from "react"; +import { + SpeedDial, + SpeedDialAction, + SpeedDialIcon, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Snackbar, + Alert, + CircularProgress, +} from "@mui/material"; +import { Close as CloseIcon } from "@mui/icons-material"; +import { useForm } from "react-hook-form"; +import { CippFormComponent } from "../../components/CippComponents/CippFormComponent"; + +const CippSpeedDial = ({ + actions = [], + position = { bottom: 16, right: 16 }, + icon, + openIcon = , +}) => { + const [openDialogs, setOpenDialogs] = useState({}); + const [loading, setLoading] = useState(false); + const [showSnackbar, setShowSnackbar] = useState(false); + const [speedDialOpen, setSpeedDialOpen] = useState(false); + const [isHovering, setIsHovering] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + + const formControls = actions.reduce((acc, action) => { + if (action.form) { + acc[action.id] = useForm({ + mode: "onChange", + defaultValues: action.form.defaultValues || {}, + }); + } + return acc; + }, {}); + + const handleSpeedDialClose = () => { + if (!isHovering) { + setTimeout(() => { + setSpeedDialOpen(false); + }, 200); + } + }; + + const handleMouseEnter = () => { + setIsHovering(true); + setSpeedDialOpen(true); + }; + + const handleMouseLeave = () => { + setIsHovering(false); + handleSpeedDialClose(); + }; + + const handleDialogOpen = (actionId) => { + setOpenDialogs((prev) => ({ ...prev, [actionId]: true })); + }; + + const handleDialogClose = (actionId) => { + setOpenDialogs((prev) => ({ ...prev, [actionId]: false })); + }; + + const handleSubmit = async (actionId, data) => { + if (!actions.find((a) => a.id === actionId)?.onSubmit) return; + + setLoading(true); + try { + const action = actions.find((a) => a.id === actionId); + const result = await action.onSubmit(data); + + if (result.success) { + formControls[actionId]?.reset(); + handleDialogClose(actionId); + } + setSnackbarMessage(result.message); + setShowSnackbar(true); + } catch (error) { + console.error(`Error submitting ${actionId}:`, error); + setSnackbarMessage("An error occurred while submitting"); + setShowSnackbar(true); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const handleClickOutside = (event) => { + if (speedDialOpen) { + const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]'); + if (speedDial && !speedDial.contains(event.target)) { + setSpeedDialOpen(false); + } + } + }; + + document.addEventListener("click", handleClickOutside); + return () => { + document.removeEventListener("click", handleClickOutside); + }; + }, [speedDialOpen]); + + return ( + <> + } + open={speedDialOpen} + onClose={handleSpeedDialClose} + onOpen={() => setSpeedDialOpen(true)} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + > + {actions.map((action) => ( + { + if (action.form) { + handleDialogOpen(action.id); + } else if (action.onClick) { + action.onClick(); + } + setSpeedDialOpen(false); + }} + tooltipOpen + sx={{ + "&.MuiSpeedDialAction-fab": { + backgroundColor: "background.paper", + "&:hover": { + backgroundColor: "action.hover", + }, + }, + "& .MuiSpeedDialAction-staticTooltipLabel": { + cursor: "pointer", + whiteSpace: "nowrap", + marginRight: "10px", + padding: "6px 10px", + "&:hover": { + backgroundColor: "action.hover", + }, + }, + }} + /> + ))} + + + {actions + .filter((action) => action.form) + .map((action) => ( + handleDialogClose(action.id)} + maxWidth="md" + fullWidth + > + {action.form.title} + + + + + + + + + ))} + + setShowSnackbar(false)} + anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + > + setShowSnackbar(false)} severity="success" sx={{ width: "100%" }}> + {snackbarMessage} + + + + ); +}; + +export default CippSpeedDial; diff --git a/src/components/CippComponents/CippTablePage.jsx b/src/components/CippComponents/CippTablePage.jsx index 61f5e89cac86..5ab4223b933d 100644 --- a/src/components/CippComponents/CippTablePage.jsx +++ b/src/components/CippComponents/CippTablePage.jsx @@ -1,8 +1,9 @@ import { Alert, Card, Divider } from "@mui/material"; import { Box, Container, Stack } from "@mui/system"; -import Head from "next/head"; import { CippDataTable } from "../CippTable/CippDataTable"; import { useSettings } from "../../hooks/use-settings"; +import { CippHead } from "./CippHead"; +import { useState } from "react"; export const CippTablePage = (props) => { const { @@ -22,15 +23,15 @@ export const CippTablePage = (props) => { queryKey, tableFilter, tenantInTitle = true, + filters, sx = { flexGrow: 1, py: 4 }, ...other } = props; const tenant = useSettings().currentTenant; + const [tableFilters] = useState(filters || []); return ( <> - - {title} - + @@ -62,6 +63,13 @@ export const CippTablePage = (props) => { columns={columns} columnsFromApi={columnsFromApi} offCanvas={offCanvas} + filters={tableFilters} + initialState={{ + columnFilters: filters ? filters.map(filter => ({ + id: filter.id || filter.columnId, + value: filter.value + })) : [] + }} {...other} /> diff --git a/src/components/CippComponents/CippTemplateEditor.jsx b/src/components/CippComponents/CippTemplateEditor.jsx new file mode 100644 index 000000000000..e223abf49962 --- /dev/null +++ b/src/components/CippComponents/CippTemplateEditor.jsx @@ -0,0 +1,317 @@ +import React, { useEffect, useState } from "react"; +import { Box, Typography, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; + +const CippTemplateEditor = ({ + templateId, + templateType, + apiConfig, + schemaConfig, + blacklistConfig, + priorityFields = [], + title, + backButtonTitle, + customDataFormatter, +}) => { + const [templateData, setTemplateData] = useState(null); + + // Default blacklist patterns that apply to all template types + const defaultBlacklistPatterns = [ + "id", + "createdDateTime", + "modifiedDateTime", + "@odata.*", + "GUID", + "Type", + "times", + "tenantFilter", + "*Id", + "*DateTime", + ]; + + // Combine default and custom blacklist patterns + const blacklistedFields = [ + ...defaultBlacklistPatterns, + ...(blacklistConfig?.patterns || []), + ]; + + const formControl = useForm({ mode: "onChange" }); + + // Fetch the template data + const templateQuery = ApiGetCall({ + url: `${apiConfig.fetchUrl}?${apiConfig.idParam}=${templateId}`, + queryKey: `${templateType}-${templateId}`, + enabled: !!templateId, + }); + + // Function to check if a field matches any blacklisted pattern (including wildcards) + const isFieldBlacklisted = (fieldName) => { + return blacklistedFields.some(pattern => { + if (pattern.includes('*')) { + // Convert wildcard pattern to regex + const regexPattern = pattern + .replace(/\*/g, '.*') + .replace(/\./g, '\\.'); + const regex = new RegExp(`^${regexPattern}$`, 'i'); + return regex.test(fieldName); + } + return pattern === fieldName; + }); + }; + + useEffect(() => { + if (templateQuery.isSuccess && templateQuery.data) { + // Find the template with matching ID + const template = Array.isArray(templateQuery.data) + ? templateQuery.data.find((t) => t[apiConfig.idParam] === templateId) + : templateQuery.data; + + if (template) { + setTemplateData(template); + // Set form values excluding blacklisted fields + const formValues = {}; + Object.keys(template).forEach((key) => { + if (!isFieldBlacklisted(key)) { + formValues[key] = template[key]; + } + }); + formControl.reset(formValues); + } + } + }, [templateQuery.isSuccess, templateQuery.data, templateId]); + + const renderFormField = (key, value, path = "") => { + const fieldPath = path ? `${path}.${key}` : key; + + if (isFieldBlacklisted(key)) { + return null; + } + + // Check for custom schema handling + const schemaField = schemaConfig?.fields?.[key.toLowerCase()]; + if (schemaField) { + return ( + + + + ); + } + + // Special handling for complex array fields (like LocationInfo and GroupInfo) + if (schemaConfig?.complexArrayFields?.some(pattern => + key.toLowerCase().includes(pattern.toLowerCase()) + )) { + // Don't render if value is null, undefined, empty array, or contains only null/empty items + if ( + !value || + (Array.isArray(value) && value.length === 0) || + (Array.isArray(value) && + value.every( + (item) => + item === null || + item === undefined || + (typeof item === "string" && item.trim() === "") || + (typeof item === "object" && item !== null && Object.keys(item).length === 0) + )) + ) { + return null; + } + + return ( + + + {getCippTranslation(key)} + + + + {Array.isArray(value) ? ( + value + .filter( + (item) => + item !== null && + item !== undefined && + !(typeof item === "string" && item.trim() === "") && + !(typeof item === "object" && item !== null && Object.keys(item).length === 0) + ) + .map((item, index) => ( + + + {getCippTranslation(key)} {index + 1} + + + {typeof item === "object" && item !== null ? ( + Object.entries(item).map(([subKey, subValue]) => + renderFormField(subKey, subValue, `${fieldPath}.${index}`) + ) + ) : ( + + + + )} + + + )) + ) : ( + + + No {getCippTranslation(key)} data available + + + )} + + + ); + } + + // Generic field type handling + if (typeof value === "boolean") { + return ( + + + + ); + } + + if (typeof value === "string") { + return ( + + + + ); + } + + if (Array.isArray(value) && value.every((item) => typeof item === "string")) { + return ( + + ({ label: item, value: item }))} + /> + + ); + } + + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + return ( + + + {getCippTranslation(key)} + + + + {Object.entries(value).map(([subKey, subValue]) => + renderFormField(subKey, subValue, fieldPath) + )} + + + ); + } + + // For other types (numbers, complex arrays, etc.), render as text field + return ( + + + + ); + }; + + const defaultDataFormatter = (values) => { + return { + [apiConfig.idParam]: templateId, + ...values, + }; + }; + + if (templateQuery.isLoading) { + return ( + + + + ); + } + + if (templateQuery.isError || !templateData) { + return ( + + + Error loading template or template not found. + + + ); + } + + return ( + + + + Edit the properties of this template. Only editable properties are shown below. + + + + {templateData && ( + <> + {/* Render priority fields first */} + {priorityFields.map(fieldName => + templateData[fieldName] !== undefined && + renderFormField(fieldName, templateData[fieldName]) + )} + + {/* Render all other fields except priority fields */} + {Object.entries(templateData) + .filter(([key]) => !priorityFields.includes(key)) + .map(([key, value]) => renderFormField(key, value))} + + )} + + + + ); +}; + +export default CippTemplateEditor; \ No newline at end of file diff --git a/src/components/CippComponents/CippTemplateFieldRenderer.jsx b/src/components/CippComponents/CippTemplateFieldRenderer.jsx new file mode 100644 index 000000000000..9db3cfe458f8 --- /dev/null +++ b/src/components/CippComponents/CippTemplateFieldRenderer.jsx @@ -0,0 +1,700 @@ +import React from "react"; +import { Typography, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; +import intuneCollection from "/src/data/intuneCollection.json"; + +const CippTemplateFieldRenderer = ({ + templateData, + formControl, + templateType = "conditionalAccess", +}) => { + // Default blacklisted fields with wildcard support + const defaultBlacklistedFields = [ + "id", + "isAssigned", + "createdDateTime", + "modifiedDateTime", + "@odata.*", + "GUID", + "Type", + "times", + "tenantFilter", + "*Id", + "*DateTime", + ]; + + // Template-specific configurations + const templateConfigs = { + conditionalAccess: { + blacklistedFields: [ + ...defaultBlacklistedFields, + "membershipKind", + "countryLookupMethod", + "applicationFilter", + "includeAuthenticationContextClassReferences", + ], + priorityFields: ["displayName", "state", "DisplayName", "Name", "displayname"], + complexArrayFields: ["locationinfo", "groupinfo"], + schemaFields: { + operator: { + multiple: false, + options: [ + { label: "OR", value: "OR" }, + { label: "AND", value: "AND" }, + ], + }, + builtincontrols: { + multiple: true, + options: [ + { label: "Block", value: "block" }, + { label: "Multi-factor Authentication", value: "mfa" }, + { label: "Compliant Device", value: "compliantDevice" }, + { label: "Domain Joined Device", value: "domainJoinedDevice" }, + { label: "Approved Application", value: "approvedApplication" }, + { label: "Compliant Application", value: "compliantApplication" }, + { label: "Password Change", value: "passwordChange" }, + { label: "Unknown Future Value", value: "unknownFutureValue" }, + ], + }, + authenticationtype: { + multiple: false, + options: [ + { + label: "Primary and Secondary Authentication", + value: "primaryAndSecondaryAuthentication", + }, + { label: "Secondary Authentication", value: "secondaryAuthentication" }, + { label: "Unknown Future Value", value: "unknownFutureValue" }, + ], + }, + frequencyinterval: { + multiple: false, + options: [ + { label: "Time Based", value: "timeBased" }, + { label: "Every Time", value: "everyTime" }, + { label: "Unknown Future Value", value: "unknownFutureValue" }, + ], + }, + state: { + multiple: false, + options: [ + { label: "Enabled", value: "enabled" }, + { label: "Disabled", value: "disabled" }, + { label: "Enabled for Reporting", value: "enabledForReportingButNotEnforced" }, + ], + }, + }, + }, + intune: { + blacklistedFields: [ + ...defaultBlacklistedFields, + "deviceManagementApplicabilityRuleOsEdition", + "deviceManagementApplicabilityRuleOsVersion", + "deviceManagementApplicabilityRuleDeviceMode", + "roleScopeTagIds", + "supportsScopeTags", + "deviceSettingStateSummaries", + "RAWJson", // Handle RAWJson specially + ], + priorityFields: ["displayName", "description", "DisplayName", "Name", "displayname"], + complexArrayFields: ["assignments", "devicestatusoverview"], + schemaFields: { + devicecompliancepolicystate: { + multiple: false, + options: [ + { label: "Unknown", value: "unknown" }, + { label: "Compliant", value: "compliant" }, + { label: "Noncompliant", value: "noncompliant" }, + { label: "Conflict", value: "conflict" }, + { label: "Error", value: "error" }, + { label: "In Grace Period", value: "inGracePeriod" }, + { label: "Config Manager", value: "configManager" }, + ], + }, + // Common device policy enum values + applicationguardenabledoptions: { + multiple: false, + options: [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Enabled for Edge", value: "enabledForEdge" }, + { label: "Enabled for Office", value: "enabledForOffice" }, + { label: "Enabled for Edge and Office", value: "enabledForEdgeAndOffice" }, + ], + }, + firewallcertificaterevocationlistcheckmethod: { + multiple: false, + options: [ + { label: "Device Default", value: "deviceDefault" }, + { label: "None", value: "none" }, + { label: "Attempt", value: "attempt" }, + { label: "Require", value: "require" }, + ], + }, + firewallpacketqueueingmethod: { + multiple: false, + options: [ + { label: "Device Default", value: "deviceDefault" }, + { label: "Disabled", value: "disabled" }, + { label: "Queue Inbound", value: "queueInbound" }, + { label: "Queue Outbound", value: "queueOutbound" }, + { label: "Queue Both", value: "queueBoth" }, + ], + }, + startupmode: { + multiple: false, + options: [ + { label: "Manual", value: "manual" }, + { label: "Automatic", value: "automatic" }, + { label: "Disabled", value: "disabled" }, + ], + }, + applicationguardblockclipboardsharing: { + multiple: false, + options: [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Block Both", value: "blockBoth" }, + { label: "Block Host to Container", value: "blockHostToContainer" }, + { label: "Block Container to Host", value: "blockContainerToHost" }, + { label: "Block None", value: "blockNone" }, + ], + }, + bitlockerrecoverypasswordrotation: { + multiple: false, + options: [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Disabled", value: "disabled" }, + { label: "Enabled for Azure AD Joined", value: "enabledForAzureAd" }, + { + label: "Enabled for Azure AD and Hybrid Joined", + value: "enabledForAzureAdAndHybrid", + }, + ], + }, + bitlockerprebootrecoverymsgurloption: { + multiple: false, + options: [ + { label: "Default", value: "default" }, + { label: "Use Custom", value: "useCustom" }, + { label: "No URL", value: "noUrl" }, + ], + }, + }, + }, + exchange: { + blacklistedFields: [ + ...defaultBlacklistedFields, + "ExchangeVersion", + "DistinguishedName", + "ObjectCategory", + "WhenChanged", + "WhenCreated", + ], + priorityFields: ["Name", "Identity"], + complexArrayFields: ["accepteddomains", "remotedomain"], + schemaFields: {}, + }, + }; + + // Get configuration for the current template type + const config = templateConfigs[templateType] || templateConfigs.conditionalAccess; + const { blacklistedFields, priorityFields, complexArrayFields, schemaFields } = config; + + // Function to check if a field matches any blacklisted pattern (including wildcards) + const isFieldBlacklisted = (fieldName) => { + return blacklistedFields.some((pattern) => { + if (pattern.includes("*")) { + // Convert wildcard pattern to regex + const regexPattern = pattern.replace(/\*/g, ".*").replace(/\./g, "\\."); + const regex = new RegExp(`^${regexPattern}$`, "i"); + return regex.test(fieldName); + } + return pattern === fieldName; + }); + }; + + // Parse RAWJson for Intune templates + const parseIntuneRawJson = (templateData) => { + if (templateType === "intune" && templateData.RAWJson) { + try { + const parsedJson = JSON.parse(templateData.RAWJson); + return { + ...templateData, + parsedRAWJson: parsedJson, + }; + } catch (error) { + console.warn("Failed to parse RAWJson:", error); + return templateData; + } + } + return templateData; + }; + + // Reset form with filtered values when templateData changes + React.useEffect(() => { + if (templateData && formControl) { + const processedData = parseIntuneRawJson(templateData); + const formValues = {}; + + Object.keys(processedData).forEach((key) => { + if (!isFieldBlacklisted(key)) { + formValues[key] = processedData[key]; + } + }); + formControl.reset(formValues); + } + }, [templateData]); + + const renderFormField = (key, value, path = "") => { + const fieldPath = path ? `${path}.${key}` : key; + + if (isFieldBlacklisted(key)) { + return null; + } + + // Check for custom schema handling + const schemaField = schemaFields[key.toLowerCase()]; + if (schemaField) { + return ( + + + + ); + } + + // Special handling for Intune RAWJson structure + if (templateType === "intune" && key === "parsedRAWJson" && value) { + // Check if this is a classic policy (has 'added' array) - these are not editable + if (value.added) { + return ( + + + This is a legacy policy and the settings cannot be edited through the form interface. + + + ); + } + + // Handle modern policies with settings array + if (value.settings && Array.isArray(value.settings)) { + return ( + + + Policy Settings + + + + {value.settings.map((setting, index) => { + const settingInstance = setting.settingInstance; + if (!settingInstance) return null; + + // Handle different setting types + if (settingInstance.choiceSettingValue) { + // Find the setting definition in the intune collection + const intuneObj = intuneCollection.find( + (item) => item.id === settingInstance.settingDefinitionId + ); + + const label = intuneObj?.displayName || `Setting ${index + 1}`; + const options = + intuneObj?.options?.map((option) => ({ + label: option.displayName || option.id, + value: option.id, + })) || []; + + return ( + + + + ); + } + + if (settingInstance.simpleSettingValue) { + // Find the setting definition in the intune collection + const intuneObj = intuneCollection.find( + (item) => item.id === settingInstance.settingDefinitionId + ); + + const label = intuneObj?.displayName || `Setting ${index + 1}`; + + return ( + + + + ); + } + + // Handle group setting collections + if (settingInstance.groupSettingCollectionValue) { + // Find the setting definition in the intune collection + const intuneObj = intuneCollection.find( + (item) => item.id === settingInstance.settingDefinitionId + ); + + const label = intuneObj?.displayName || `Group Setting Collection ${index + 1}`; + + return ( + + + {label} + + + Definition ID: {settingInstance.settingDefinitionId} + + {/* Group collections are complex - show as read-only for now */} + + Complex group setting collection - view in JSON mode for details + + + ); + } + + return null; + })} + + + ); + } + + // Handle OMA settings + if (value.omaSettings && Array.isArray(value.omaSettings)) { + return ( + + + OMA Settings + + + + {value.omaSettings.map((omaSetting, index) => ( + + + {omaSetting.displayName || `OMA Setting ${index + 1}`} + + + + + + + + + + + ))} + + + ); + } + + // Handle device policies (direct configuration properties) + if (!value.settings && !value.omaSettings && !value.added) { + return ( + + + Device Policy Configuration + + + + {Object.entries(value) + .filter(([deviceKey]) => !isFieldBlacklisted(deviceKey)) + .map(([deviceKey, deviceValue]) => + renderFormField(deviceKey, deviceValue, fieldPath) + )} + + + ); + } + + // Fallback for other RAWJson structures + return ( + + + Policy Configuration + + + + This policy structure is not supported for editing. + + + ); + } + + // Special handling for complex array fields + if (complexArrayFields.some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()))) { + // Don't render if value is null, undefined, empty array, or contains only null/empty items + if ( + !value || + (Array.isArray(value) && value.length === 0) || + (Array.isArray(value) && + value.every( + (item) => + item === null || + item === undefined || + (typeof item === "string" && item.trim() === "") || + (typeof item === "object" && item !== null && Object.keys(item).length === 0) + )) + ) { + return null; + } + + return ( + + + {getCippTranslation(key)} + + + + {Array.isArray(value) ? ( + value + .filter( + (item) => + item !== null && + item !== undefined && + !(typeof item === "string" && item.trim() === "") && + !(typeof item === "object" && item !== null && Object.keys(item).length === 0) + ) + .map((item, index) => ( + + + {getCippTranslation(key)} {index + 1} + + + {typeof item === "object" && item !== null ? ( + Object.entries(item).map(([subKey, subValue]) => + renderFormField(subKey, subValue, `${fieldPath}.${index}`) + ) + ) : ( + + + + )} + + + )) + ) : ( + + + No {getCippTranslation(key)} data available + + + )} + + + ); + } + + // Generic field type handling + if (typeof value === "boolean") { + return ( + + + + ); + } + + if (typeof value === "string") { + const alwaysTextFields = [ + "displayname", + "displayName", + "name", + "description", + "identity", + "title", + ]; + + const isAlwaysTextField = alwaysTextFields.some( + (field) => key.toLowerCase() === field.toLowerCase() + ); + + // Check if this looks like an enum value (common patterns in device policies) + const enumPatterns = [ + "notConfigured", + "deviceDefault", + "manual", + "automatic", + "disabled", + "enabled", + "blocked", + "allowed", + "required", + "none", + "lockWorkstation", + ]; + + const looksLikeEnum = enumPatterns.some((pattern) => + value.toLowerCase().includes(pattern.toLowerCase()) + ); + + if (!isAlwaysTextField && looksLikeEnum) { + // Create basic options based on common patterns + const commonOptions = [ + { label: "Not Configured", value: "notConfigured" }, + { label: "Device Default", value: "deviceDefault" }, + { label: "Manual", value: "manual" }, + { label: "Automatic", value: "automatic" }, + { label: "Disabled", value: "disabled" }, + { label: "Enabled", value: "enabled" }, + { label: "Blocked", value: "blocked" }, + { label: "Allowed", value: "allowed" }, + { label: "Required", value: "required" }, + { label: "None", value: "none" }, + ].filter( + (option) => + // Only include options that make sense for this field + option.value === value || + key.toLowerCase().includes(option.value.toLowerCase()) || + option.value === "notConfigured" // Always include notConfigured + ); + + return ( + + + + ); + } + + return ( + + + + ); + } + + if (Array.isArray(value) && value.every((item) => typeof item === "string")) { + return ( + + ({ label: item, value: item }))} + /> + + ); + } + + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + return ( + + + {getCippTranslation(key)} + + + + {Object.entries(value).map(([subKey, subValue]) => + renderFormField(subKey, subValue, fieldPath) + )} + + + ); + } + + // For other types (numbers, complex arrays, etc.), render as text field + return ( + + + + ); + }; + + if (!templateData) { + return null; + } + + // Process template data (parse RAWJson for Intune templates) + const processedData = parseIntuneRawJson(templateData); + + return ( + + {/* Render priority fields first */} + {priorityFields.map( + (fieldName) => + processedData[fieldName] !== undefined && + renderFormField(fieldName, processedData[fieldName]) + )} + + {/* Render all other fields except priority fields */} + {Object.entries(processedData) + .filter(([key]) => !priorityFields.includes(key)) + .map(([key, value]) => renderFormField(key, value))} + + ); +}; + +export default CippTemplateFieldRenderer; diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index 4471a2442415..cb3db82c1645 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -26,6 +26,9 @@ export const CippTenantSelector = (props) => { url: "/api/listTenants", data: { AllTenantSelector: true }, queryKey: "TenantSelector", + refetchOnMount: false, + refetchOnReconnect: false, + keepPreviousData: true, }); const [currentTenant, setSelectedTenant] = useState(null); @@ -40,6 +43,7 @@ export const CippTenantSelector = (props) => { toast: true, }); + // This effect handles updates when the tenant is changed via dropdown selection useEffect(() => { if (!router.isReady) return; if (currentTenant?.value) { @@ -58,17 +62,66 @@ export const CippTenantSelector = (props) => { settings.handleUpdate({ currentTenant: currentTenant.value, }); + //if we have a tenantfilter, we add the tenantfilter to the title of the tab/page so its "Tenant - original title". } }, [currentTenant?.value]); + // This effect handles when the URL parameter changes externally useEffect(() => { - if (tenant && currentTenant?.value) { + if (!router.isReady || !tenantList.isSuccess) return; + + // Get the current tenant from URL or settings + const urlTenant = router.query.tenantFilter || settings.currentTenant; + + // Only update if there's a URL tenant and it's different from our current state + if (urlTenant && (!currentTenant || urlTenant !== currentTenant.value)) { + // Find the tenant in our list + const matchingTenant = tenantList.data.find( + ({ defaultDomainName }) => defaultDomainName === urlTenant + ); + + if (matchingTenant) { + setSelectedTenant({ + value: urlTenant, + label: `${matchingTenant.displayName} (${urlTenant})`, + addedFields: { + defaultDomainName: matchingTenant.defaultDomainName, + displayName: matchingTenant.displayName, + customerId: matchingTenant.customerId, + initialDomainName: matchingTenant.initialDomainName, + }, + }); + } + } + }, [router.isReady, router.query.tenantFilter, tenantList.isSuccess, settings.currentTenant]); + + // This effect ensures the tenant filter parameter is included in the URL when missing + useEffect(() => { + if (!router.isReady || !settings.currentTenant) return; + + // If the tenant parameter is missing from the URL but we have it in settings + if (!router.query.tenantFilter && settings.currentTenant) { + const query = { ...router.query, tenantFilter: settings.currentTenant }; + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + } + }, [router.isReady, router.query, settings.currentTenant]); + + useEffect(() => { + if (tenant && currentTenant?.value && currentTenant?.value !== "AllTenants") { tenantDetails.refetch(); } }, [tenant, offcanvasVisible]); + // We can simplify this effect since we now have the new effect above to handle URL changes useEffect(() => { - if (tenant && tenantList.isSuccess) { + if (tenant && tenantList.isSuccess && !currentTenant) { const matchingTenant = tenantList.data.find( ({ defaultDomainName }) => defaultDomainName === tenant ); @@ -81,6 +134,7 @@ export const CippTenantSelector = (props) => { defaultDomainName: matchingTenant.defaultDomainName, displayName: matchingTenant.displayName, customerId: matchingTenant.customerId, + initialDomainName: matchingTenant.initialDomainName, }, } : { @@ -89,7 +143,8 @@ export const CippTenantSelector = (props) => { } ); } - }, [tenant, tenantList.isSuccess]); + }, [tenant, tenantList.isSuccess, currentTenant]); + return ( <> { actions={[ { label: "M365 Admin Portal", - link: `https://admin.microsoft.com/Partner/BeginClientSession.aspx?CTID=${currentTenant?.addedFields?.customerId}&CSDEST=o365admincenter`, + link: `https://admin.cloud.microsoft/?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, icon: , }, { label: "Exchange Portal", - link: `https://admin.exchange.microsoft.com/?landingpage=homepage&form=mac_sidebar&delegatedOrg=${currentTenant?.value}`, + link: `https://admin.cloud.microsoft/exchange?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, icon: , }, { @@ -203,7 +258,7 @@ export const CippTenantSelector = (props) => { }, { label: "Teams Portal", - link: `https://admin.teams.microsoft.com/?delegatedOrg=${currentTenant?.value}`, + link: `https://admin.teams.microsoft.com/?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, icon: , }, { @@ -218,8 +273,9 @@ export const CippTenantSelector = (props) => { }, { label: "SharePoint Portal", - link: `https://admin.microsoft.com/Partner/beginclientsession.aspx?CTID=${currentTenant?.addedFields?.customerId}&CSDEST=SharePoint`, + link: `/api/ListSharePointAdminUrl?tenantFilter=${currentTenant?.value}`, icon: , + external: true, }, { label: "Security Portal", diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index 0876bded5bd2..ebdbca9238bf 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -44,4 +44,9 @@ export const CippTranslations = { "commitmentTerm.renewalConfiguration.renewalDate": "Renewal Date", storageUsedInBytes: "Storage Used", prohibitSendReceiveQuotaInBytes: "Quota", + ClientId: "Client ID", + html_url: "URL", + sendtoIntegration: "Send Notifications to Integration", + includeTenantId: "Include Tenant ID in Notifications", + logsToInclude: "Logs to Include in notifications", }; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index fe532d259f4b..59312d5401f4 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -1,14 +1,13 @@ import { EyeIcon, MagnifyingGlassIcon, TrashIcon } from "@heroicons/react/24/outline"; import { Archive, - Block, Clear, CloudDone, Edit, Email, ForwardToInbox, GroupAdd, - LockOpen, + LockClock, LockPerson, LockReset, MeetingRoom, @@ -18,11 +17,20 @@ import { PhonelinkLock, PhonelinkSetup, Shortcut, + EditAttributes, } from "@mui/icons-material"; +import { getCippLicenseTranslation } from "../../utils/get-cipp-license-translation"; import { useSettings } from "/src/hooks/use-settings.js"; +import { usePermissions } from "../../hooks/use-permissions"; export const CippUserActions = () => { const tenant = useSettings().currentTenant; + + const { checkPermissions } = usePermissions(); + const canWriteUser = checkPermissions(["Identity.User.ReadWrite"]); + const canWriteMailbox = checkPermissions(["Exchange.Mailbox.ReadWrite"]); + const canWriteGroup = checkPermissions(["Identity.Group.ReadWrite"]); + return [ { //tested @@ -39,6 +47,7 @@ export const CippUserActions = () => { icon: , color: "success", target: "_self", + condition: () => canWriteUser, }, { //tested @@ -51,24 +60,44 @@ export const CippUserActions = () => { }, { //tested - label: "Create Temporary Access Password", - type: "GET", + type: "POST", icon: , url: "/api/ExecCreateTAP", data: { ID: "userPrincipalName" }, + fields: [ + { + type: "number", + name: "lifetimeInMinutes", + label: "Lifetime (Minutes)", + placeholder: "Leave blank for default", + }, + { + type: "switch", + name: "isUsableOnce", + label: "One-time use only", + }, + { + type: "datePicker", + name: "startDateTime", + label: "Start Date/Time (leave blank for immediate)", + dateTimeType: "datetime", + }, + ], confirmText: "Are you sure you want to create a Temporary Access Password?", multiPost: false, + condition: () => canWriteUser, }, { //tested label: "Re-require MFA registration", - type: "GET", + type: "POST", icon: , url: "/api/ExecResetMFA", data: { ID: "userPrincipalName" }, confirmText: "Are you sure you want to reset MFA for this user?", multiPost: false, + condition: () => canWriteUser, }, { //tested @@ -86,7 +115,7 @@ export const CippUserActions = () => { type: "POST", icon: , url: "/api/ExecPerUserMFA", - data: { userId: "userPrincipalName" }, + data: { userId: "userPrincipalName", tenantFilter: "Tenant" }, fields: [ { type: "autoComplete", @@ -98,39 +127,49 @@ export const CippUserActions = () => { { label: "Disabled", value: "Disabled" }, ], multiple: false, + creatable: false, + validators: { required: "Please select an MFA state" }, }, ], confirmText: "Are you sure you want to set per-user MFA for these users?", multiPost: false, + condition: () => canWriteUser, }, { //tested - label: "Convert to Shared Mailbox", - type: "GET", + label: "Convert Mailbox", + type: "POST", icon: , - url: "/api/ExecConvertToSharedMailbox", + url: "/api/ExecConvertMailbox", data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to convert this user to a shared mailbox?", - multiPost: false, - }, - { - label: "Convert to User Mailbox", - type: "GET", - icon: , - url: "/api/ExecConvertToSharedMailbox", - data: { ID: "userPrincipalName", ConvertToUser: true }, - confirmText: "Are you sure you want to convert this user to a user mailbox?", + fields: [ + { + type: "radio", + name: "MailboxType", + label: "Mailbox Type", + options: [ + { label: "User Mailbox", value: "Regular" }, + { label: "Shared Mailbox", value: "Shared" }, + { label: "Room Mailbox", value: "Room" }, + { label: "Equipment Mailbox", value: "Equipment" }, + ], + validators: { required: "Please select a mailbox type" }, + }, + ], + confirmText: "Pick the type of mailbox you want to convert [userPrincipalName] to:", multiPost: false, + condition: () => canWriteMailbox, }, { //tested label: "Enable Online Archive", - type: "GET", + type: "POST", icon: , url: "/api/ExecEnableArchive", data: { ID: "userPrincipalName" }, - confirmText: "Are you sure you want to enable the online archive for this user?", + confirmText: "Are you sure you want to enable the online archive for [userPrincipalName]?", multiPost: false, + condition: (row) => canWriteMailbox, }, { //tested @@ -146,6 +185,7 @@ export const CippUserActions = () => { fields: [{ type: "richText", name: "input", label: "Out of Office Message" }], confirmText: "Are you sure you want to set the out of office?", multiPost: false, + condition: () => canWriteMailbox, }, { @@ -153,16 +193,50 @@ export const CippUserActions = () => { type: "POST", icon: , url: "/api/ExecSetOoO", - data: { user: "userPrincipalName", AutoReplyState: "Disabled" }, - confirmText: "Are you sure you want to disable the out of office?", + data: { + userId: "userPrincipalName", + AutoReplyState: { value: "Disabled" }, + }, + confirmText: "Are you sure you want to disable the out of office for [userPrincipalName]?", multiPost: false, + condition: () => canWriteMailbox, }, { label: "Add to Group", type: "POST", icon: , url: "/api/EditGroup", - data: { addMember: "userPrincipalName" }, + customDataformatter: (row, action, formData) => { + let addMember = []; + if (Array.isArray(row)) { + row + .map((r) => ({ + label: r.displayName, + value: r.id, + addedFields: { + id: r.id, + userPrincipalName: r.userPrincipalName, + displayName: r.displayName, + }, + })) + .forEach((r) => addMember.push(r)); + } else { + addMember.push({ + label: row.displayName, + value: row.id, + addedFields: { + id: row.id, + userPrincipalName: row.userPrincipalName, + displayName: row.displayName, + }, + }); + } + return { + addMember: addMember, + tenantFilter: tenant, + groupId: formData.groupId, + }; + }, fields: [ { type: "autoComplete", @@ -170,6 +244,7 @@ export const CippUserActions = () => { label: "Select a group to add the user to", multiple: false, creatable: false, + validators: { required: "Please select a group" }, api: { url: "/api/ListGroups", labelField: "displayName", @@ -179,10 +254,57 @@ export const CippUserActions = () => { groupName: "displayName", }, queryKey: `groups-${tenant}`, + showRefresh: true, }, }, ], - confirmText: "Are you sure you want to add the user to this group?", + confirmText: "Are you sure you want to add [userPrincipalName] to this group?", + multiPost: true, + allowResubmit: true, + condition: () => canWriteGroup, + }, + { + label: "Manage Licenses", + type: "POST", + url: "/api/ExecBulkLicense", + icon: , + data: { userIds: "id" }, + multiPost: true, + fields: [ + { + type: "radio", + name: "LicenseOperation", + label: "License Operation", + options: [ + { label: "Add Licenses", value: "Add" }, + { label: "Remove Licenses", value: "Remove" }, + { label: "Replace Licenses", value: "Replace" }, + ], + validators: { required: "Please select a license operation" }, + }, + { + type: "switch", + name: "RemoveAllLicenses", + label: "Remove All Existing Licenses", + }, + { + type: "autoComplete", + name: "Licenses", + label: "Select Licenses", + multiple: true, + creatable: false, + api: { + url: "/api/ListLicenses", + labelField: (option) => + `${getCippLicenseTranslation([option])} (${option?.availableUnits} available)`, + valueField: "skuId", + queryKey: `licenses-${tenant}`, + }, + }, + ], + confirmText: "Are you sure you want to manage licenses for the selected users?", + multiPost: true, + condition: () => canWriteUser, }, { label: "Disable Email Forwarding", @@ -194,8 +316,9 @@ export const CippUserActions = () => { userid: "userPrincipalName", ForwardOption: "!disabled", }, - confirmText: "Are you sure you want to disable forwarding of this user's emails?", + confirmText: "Are you sure you want to disable forwarding of [userPrincipalName]'s emails?", multiPost: false, + condition: () => canWriteMailbox, }, { label: "Pre-provision OneDrive", @@ -205,6 +328,7 @@ export const CippUserActions = () => { data: { UserPrincipalName: "userPrincipalName" }, confirmText: "Are you sure you want to pre-provision OneDrive for this user?", multiPost: false, + condition: () => canWriteUser, }, { label: "Add OneDrive Shortcut", @@ -221,7 +345,8 @@ export const CippUserActions = () => { name: "siteUrl", label: "Select a Site", multiple: false, - creatable: false, + creatable: true, + validators: { required: "Please select or enter a SharePoint site URL" }, api: { url: "/api/ListSites", data: { type: "SharePointSiteUsage", URLOnly: true }, @@ -233,83 +358,126 @@ export const CippUserActions = () => { ], confirmText: "Select a SharePoint site to create a shortcut for:", multiPost: false, + condition: () => canWriteUser, }, { - label: "Block Sign In", - type: "GET", - icon: , + label: "Set Sign In State", + type: "POST", + icon: , url: "/api/ExecDisableUser", data: { ID: "id" }, - confirmText: "Are you sure you want to block the sign-in for this user?", - multiPost: false, - condition: (row) => row.accountEnabled, - }, - { - label: "Unblock Sign In", - type: "GET", - icon: , - url: "/api/ExecDisableUser", - data: { ID: "id", Enable: true }, - confirmText: "Are you sure you want to unblock sign-in for this user?", + fields: [ + { + type: "radio", + name: "Enable", + label: "Sign In State", + options: [ + { label: "Enabled", value: true }, + { label: "Disabled", value: false }, + ], + validators: { required: "Please select a sign-in state" }, + }, + ], + confirmText: "Are you sure you want to set the sign-in state for [userPrincipalName]?", multiPost: false, - condition: (row) => !row.accountEnabled, + condition: () => canWriteUser, }, { - label: "Reset Password (Must Change)", - type: "GET", + label: "Reset Password", + type: "POST", icon: , url: "/api/ExecResetPass", data: { - MustChange: true, ID: "userPrincipalName", displayName: "displayName", }, - confirmText: - "Are you sure you want to reset the password for this user? The user must change their password at next logon.", + fields: [ + { + type: "switch", + name: "MustChange", + label: "Must Change Password at Next Logon", + }, + ], + confirmText: "Are you sure you want to reset the password for [userPrincipalName]?", multiPost: false, + condition: () => canWriteUser, }, { - label: "Reset Password", - type: "GET", - icon: , - url: "/api/ExecResetPass", - data: { - MustChange: false, - ID: "userPrincipalName", - displayName: "displayName", - }, - confirmText: "Are you sure you want to reset the password for this user?", + label: "Set Password Expiration", + type: "POST", + icon: , + url: "/api/ExecPasswordNeverExpires", + data: { userId: "id", userPrincipalName: "userPrincipalName" }, + fields: [ + { + type: "radio", + name: "PasswordPolicy", + label: "Password Policy", + options: [ + { label: "Disable Password Expiration", value: "DisablePasswordExpiration" }, + { label: "Enable Password Expiration", value: "None" }, + ], + validators: { required: "Please select a password policy" }, + }, + ], + confirmText: + "Set Password Never Expires state for [userPrincipalName]. If the password of the user is older than the set expiration date of the organization, the user will be prompted to change their password at their next login.", multiPost: false, + condition: () => canWriteUser, }, { label: "Clear Immutable ID", - type: "GET", + type: "POST", icon: , url: "/api/ExecClrImmId", data: { ID: "id", }, - confirmText: "Are you sure you want to clear the Immutable ID for this user?", + confirmText: "Are you sure you want to clear the Immutable ID for [userPrincipalName]?", multiPost: false, - condition: (row) => row.onPremisesSyncEnabled, + condition: (row) => !row.onPremisesSyncEnabled && row?.onPremisesImmutableId && canWriteUser, }, { label: "Revoke all user sessions", - type: "GET", + type: "POST", icon: , url: "/api/ExecRevokeSessions", data: { ID: "id", Username: "userPrincipalName" }, - confirmText: "Are you sure you want to revoke all sessions for this user?", + confirmText: "Are you sure you want to revoke all sessions for [userPrincipalName]?", multiPost: false, + condition: () => canWriteUser, }, { label: "Delete User", - type: "GET", + type: "POST", icon: , url: "/api/RemoveUser", - data: { ID: "id" }, - confirmText: "Are you sure you want to delete this user?", + data: { ID: "id", userPrincipalName: "userPrincipalName" }, + confirmText: "Are you sure you want to delete [userPrincipalName]?", multiPost: false, + condition: () => canWriteUser, + }, + { + label: "Edit Properties", + icon: , + multiPost: true, + noConfirm: true, + customFunction: (users, action, formData) => { + // Handle both single user and multiple users + const userData = Array.isArray(users) ? users : [users]; + + // Store users in session storage to avoid URL length limits + sessionStorage.setItem('patchWizardUsers', JSON.stringify(userData)); + + // Use Next.js router for internal navigation + import('next/router').then(({ default: router }) => { + router.push('/identity/administration/users/patch-wizard'); + }).catch(() => { + // Fallback to window.location if router is not available + window.location.href = '/identity/administration/users/patch-wizard'; + }); + }, + condition: () => canWriteUser, }, ]; }; diff --git a/src/components/CippComponents/CollapsibleChipList.js b/src/components/CippComponents/CollapsibleChipList.js new file mode 100644 index 000000000000..23a9586e8034 --- /dev/null +++ b/src/components/CippComponents/CollapsibleChipList.js @@ -0,0 +1,29 @@ +import React, { useState } from "react"; +import { Box, Link } from "@mui/material"; + +export const CollapsibleChipList = ({ children, maxItems = 4 }) => { + const [expanded, setExpanded] = useState(false); + const childArray = React.Children.toArray(children); + const hasMoreItems = childArray.length > maxItems; + + const toggleExpanded = (e) => { + e.preventDefault(); + setExpanded(!expanded); + }; + + return ( + + {expanded ? childArray : childArray.slice(0, maxItems)} + + {hasMoreItems && ( + + {expanded ? "Show less" : `+${childArray.length - maxItems} more`} + + )} + + ); +}; diff --git a/src/components/CippComponents/DomainAnalyserDialog.jsx b/src/components/CippComponents/DomainAnalyserDialog.jsx new file mode 100644 index 000000000000..803baf43f154 --- /dev/null +++ b/src/components/CippComponents/DomainAnalyserDialog.jsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { Dialog, DialogContent, DialogTitle, Button, DialogActions } from "@mui/material"; +import { Refresh } from "@mui/icons-material"; +import { useForm, FormProvider } from "react-hook-form"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "./CippApiResults"; + +export const DomainAnalyserDialog = ({ createDialog }) => { + const methods = useForm({ + defaultValues: { + tenantFilter: { + value: "AllTenants", + label: "*All Tenants", + }, + }, + }); + + // Use methods for form handling and control + const { handleSubmit, control } = methods; + + const [isRunning, setIsRunning] = useState(false); + const domainAnalyserResults = ApiPostCall({ + urlFromData: true, + }); + + const handleForm = (values) => { + setIsRunning(true); + domainAnalyserResults.mutate({ + url: "/api/ExecDomainAnalyser", + queryKey: `domain-analyser-${values.tenantFilter}`, + data: values.tenantFilter ? { TenantFilter: values.tenantFilter } : {}, + }); + }; + + // Reset running state when dialog is closed + const handleClose = () => { + setIsRunning(false); + createDialog.handleClose(); + }; + + return ( + + + + Run Domain Analysis + +
    +

    + This will run a Domain Analysis to check for DNS configuration issues. Select a + tenant (or all tenants) below. +

    + +
    + +
    + + + + + +
    +
    + ); +}; diff --git a/src/components/CippComponents/ScheduledTaskDetails.jsx b/src/components/CippComponents/ScheduledTaskDetails.jsx new file mode 100644 index 000000000000..8e3f50161694 --- /dev/null +++ b/src/components/CippComponents/ScheduledTaskDetails.jsx @@ -0,0 +1,285 @@ +import React, { useEffect, useState } from "react"; +import { + Box, + Typography, + IconButton, + Accordion, + AccordionSummary, + AccordionDetails, + Chip, + TextField, + InputAdornment, + Tooltip, + Stack, + Skeleton, +} from "@mui/material"; +import { ApiGetCall } from "../../api/ApiCall"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; +import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; +import { ExpandMore, Sync, Search, Close } from "@mui/icons-material"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo"; +import { ActionsMenu } from "/src/components/actions-menu"; +import { CippScheduledTaskActions } from "./CippScheduledTaskActions"; + +const ScheduledTaskDetails = ({ data, showActions = true }) => { + const [taskDetails, setTaskDetails] = useState(null); + const [expanded, setExpanded] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + + const handleChange = (panel) => (event, newExpanded) => { + setExpanded(newExpanded ? panel : false); + }; + + const taskDetailResults = ApiGetCall({ + url: `/api/ListScheduledItemDetails`, + data: { + RowKey: data.RowKey, + }, + queryKey: `ListScheduledItemDetails-${data.RowKey}`, + }); + + const taskProperties = [ + "TaskState", + "Command", + "Tenant", + "Recurrence", + "ScheduledTime", + "ExecutedTime", + "PostExecution", + ]; + + useEffect(() => { + if (taskDetailResults.isSuccess && taskDetailResults?.data) { + setTaskDetails(taskDetailResults.data); + + // Auto-expand the only result if there's just one + if (taskDetailResults.data.Details?.length === 1) { + setExpanded(`execution-results-0`); + } + } + }, [data.RowKey, taskDetailResults.isSuccess, taskDetailResults.data]); + + const filteredDetails = taskDetails?.Details?.filter((result) => { + if (!searchQuery) return true; + + const searchLower = searchQuery.toLowerCase(); + const tenantMatches = (result.TenantName || result.Tenant || "") + .toLowerCase() + .includes(searchLower); + + let resultsMatches = false; + if (typeof result.Results === "object" && result.Results !== null) { + const resultsStr = JSON.stringify(result.Results).toLowerCase(); + resultsMatches = resultsStr.includes(searchLower); + } + + return tenantMatches || resultsMatches; + }); + + return ( + <> + + + + {taskDetailResults.isLoading ? : taskDetails?.Task?.Name} + + {showActions && ( + + + + )} + + + taskDetailResults.refetch()}> + + + + } + layout="dual" + title="Details" + variant="outlined" + showDivider={false} + propertyItems={taskProperties + .filter((prop) => taskDetails?.Task?.[prop] != null && taskDetails?.Task?.[prop] !== "") + .map((prop) => { + return { + label: getCippTranslation(prop), + value: getCippFormatting(taskDetails?.Task?.[prop], prop), + }; + })} + isFetching={taskDetailResults.isFetching} + /> + + {taskDetailResults.isFetching ? ( + + ) : ( + <> + {taskDetails?.Task?.Parameters && ( + + }> + Task Parameters + + + { + return { + label: key, + value: getCippFormatting(value, key), + }; + } + )} + isFetching={taskDetailResults.isFetching} + /> + + + )} + + )} + + {taskDetailResults.isFetching ? ( + + ) : ( + <> + {taskDetails?.Details?.length > 0 && ( + <> + + + Execution Results{" "} + {filteredDetails && ( + + ({filteredDetails.length} of {taskDetails.Details.length}) + + )} + + setSearchQuery(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchQuery && ( + + + setSearchQuery("")} + aria-label="Clear search" + > + + + + + ), + }} + /> + + + {filteredDetails && + filteredDetails.map((result, index) => ( + + } + sx={{ + "& .MuiAccordionSummary-content": { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + width: "100%", + }, + }} + > + + {getCippFormatting(result.TenantName || result.Tenant, "Tenant")} + + } + sx={{ mx: 1 }} + /> + + + {result.Results === "null" || !result.Results ? ( + No data available + ) : Array.isArray(result.Results) ? ( + + ) : typeof result.Results === "object" ? ( + ({ + label: key, + value: typeof value === "object" ? JSON.stringify(value) : value, + }))} + /> + ) : ( + +
    +                                {result.Results}
    +                              
    +
    + )} +
    +
    + ))} + {filteredDetails && filteredDetails.length === 0 && ( + + + No results match your search criteria + + + )} +
    + + )} + + )} +
    + + ); +}; + +export default ScheduledTaskDetails; diff --git a/src/components/CippFormPages/CippAddEditContact.jsx b/src/components/CippFormPages/CippAddEditContact.jsx new file mode 100644 index 000000000000..cbc96616d37c --- /dev/null +++ b/src/components/CippFormPages/CippAddEditContact.jsx @@ -0,0 +1,194 @@ +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { getCippValidator } from "/src/utils/get-cipp-validator"; +import countryList from "/src/data/countryList.json"; + +const countryOptions = countryList.map(({ Code, Name }) => ({ + label: Name, + value: Code, +})); + +const ContactFormLayout = ({ formControl, formType = "add" }) => { + return ( + + {/* Display Name */} + + + + + {/* First Name and Last Name */} + + + + + + + + + + {/* Email */} + + getCippValidator(value, "email"), + }} + /> + + + {/* Hide from GAL */} + + + + + + + {/* Company Information */} + + + + + + + + {/* Website */} + + !value || getCippValidator(value, "url"), + }} + /> + + + + + {/* Address Information */} + + + + + + + + + + + + + + + + + + + {/* Phone Numbers */} + + + + + + + + + + {/* Mail Tip */} + + + + + ); +}; + +export default ContactFormLayout; diff --git a/src/components/CippFormPages/CippAddEditGdapRoleTemplate.jsx b/src/components/CippFormPages/CippAddEditGdapRoleTemplate.jsx index cd2b5b4a4e0c..1ed0e4163f88 100644 --- a/src/components/CippFormPages/CippAddEditGdapRoleTemplate.jsx +++ b/src/components/CippFormPages/CippAddEditGdapRoleTemplate.jsx @@ -44,6 +44,7 @@ export const CippAddEditGdapRoleTemplate = (props) => { return true; }, }} + sortOptions={true} />
    ); diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 991ade01854a..6749d78ff445 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -5,20 +5,66 @@ import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormD import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector"; import countryList from "/src/data/countryList.json"; import { CippFormLicenseSelector } from "/src/components/CippComponents/CippFormLicenseSelector"; -import Grid from "@mui/material/Grid"; +import { Grid } from "@mui/system"; import { ApiGetCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; +import { useWatch } from "react-hook-form"; +import { useEffect, useMemo } from "react"; +import { useRouter } from "next/router"; const CippAddEditUser = (props) => { const { formControl, userSettingsDefaults, formType = "add" } = props; const tenantDomain = useSettings().currentTenant; + const router = useRouter(); + const { userId } = router.query; const integrationSettings = ApiGetCall({ url: "/api/ListExtensionsConfig", queryKey: "ListExtensionsConfig", + refetchOnMount: false, + refetchOnReconnect: false, }); + + // Get all groups the is the user is a member of + const userGroups = ApiGetCall({ + url: `/api/ListUserGroups?userId=${userId}&tenantFilter=${tenantDomain}`, + queryKey: `User-${userId}-Groups-${tenantDomain}`, + refetchOnMount: false, + refetchOnReconnect: false, + waiting: !!userId, + }); + + // Get all groups for the tenant + const tenantGroups = ApiGetCall({ + url: `/api/ListGroups?tenantFilter=${tenantDomain}`, + queryKey: `ListGroups-${tenantDomain}`, + refetchOnMount: false, + refetchOnReconnect: false, + waiting: !!userId, + }); + + // Make new list of groups by removing userGroups from tenantGroups + const filteredTenantGroups = useMemo(() => { + if (tenantGroups.isSuccess && userGroups.isSuccess) { + const tenantGroupsList = tenantGroups?.data || []; + + return tenantGroupsList.filter( + (tenantGroup) => !userGroups?.data?.some((userGroup) => userGroup.id === tenantGroup.id) + ); + } + return []; + }, [tenantGroups.isSuccess, userGroups.isSuccess, tenantGroups.data, userGroups.data]); + + const watcher = useWatch({ control: formControl.control }); + useEffect(() => { + //if watch.firstname changes, and watch.lastname changes, set displayname to firstname + lastname + if (watcher.givenName && watcher.surname && formType === "add") { + formControl.setValue("displayName", `${watcher.givenName} ${watcher.surname}`); + } + }, [watcher.givenName, watcher.surname]); + return ( - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { InputProps={{ endAdornment: @, }} - name="mailNickname" + name="username" formControl={formControl} /> - + - + { /> - + Settings - + { compareType="is" compareValue={true} > - + { - + { formControl={formControl} /> - + { formControl={formControl} /> - + {integrationSettings?.data?.Sherweb?.Enabled === true && ( @@ -138,7 +184,7 @@ const CippAddEditUser = (props) => { compareValue="(0 available)" labelCompare={true} > - + { compareType="is" compareValue={true} > - + This will Purchase a new Sherweb License for the user, according to the terms and conditions with Sherweb. When the license becomes available, CIPP will assign the license to this user. - + { )} - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + + + + + + + { formControl={formControl} /> - + + + + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + - + { formControl={formControl} /> - {userSettingsDefaults?.userAttributes?.map((attribute, idx) => ( - - - - ))} + {userSettingsDefaults?.userAttributes + ?.filter((attribute) => attribute.value !== "sponsor") + .map((attribute, idx) => ( + + + + ))} {/* Set Manager */} - + { multiple={false} /> - {/* Schedule User Creation */} - + {userSettingsDefaults?.userAttributes?.some((attribute) => attribute.value === "sponsor") && ( + + + + )} + { multiple={false} /> + {formType === "edit" && ( + + ({ + label: tenantGroup.displayName, + value: tenantGroup.id, + addedFields: { + calculatedGroupType: tenantGroup.calculatedGroupType, + }, + }))} + formControl={formControl} + /> + + )} + {formType === "edit" && ( + + ({ + label: userGroups.DisplayName, + value: userGroups.id, + addedFields: { + calculatedGroupType: userGroups.calculatedGroupType, + }, + }))} + formControl={formControl} + /> + + )} + {/* Schedule User Creation */} {formType === "add" && ( - + { compareType="is" compareValue={true} > - + { formControl={formControl} /> - + ( + + + +); + const CippAddGroupForm = (props) => { const { formControl } = props; return ( - + { fullWidth /> - + { fullWidth /> - + @, + }} /> - + { /> - + - + - + { { label: "Security Group", value: "generic" }, { label: "Microsoft 365 Group", value: "m365" }, { label: "Dynamic Group", value: "dynamic" }, - { label: "Dynamic Distribution Group", value: "dynamicdistribution" }, + { label: "Dynamic Distribution Group", value: "dynamicDistribution" }, { label: "Distribution List", value: "distribution" }, { label: "Mail Enabled Security Group", value: "security" }, ]} @@ -88,7 +106,7 @@ const CippAddGroupForm = (props) => { compareType="is" compareValue="distribution" > - + { compareType="contains" compareValue="dynamic" > - + { const { formControl } = props; + // Debug the current form values, especially groupType + useEffect(() => { + const subscription = formControl.watch((value, { name, type }) => {}); + return () => subscription.unsubscribe(); + }, [formControl]); + return ( - + {/* Hidden field to store the template GUID when editing */} + + + { fullWidth /> - + { fullWidth /> - + { /> - + { { label: "Mail Enabled Security Group", value: "security" }, ]} /> + {/* Debug output */} +
    Current groupType: {formControl.watch("groupType")}
    { compareType="is" compareValue="distribution" > - + { compareType="contains" compareValue="dynamic" > - + { + + return ( + + + + + + + + @, + }} + /> + + + + + + + + + ); +}; + +export default CippAddRoomListForm; \ No newline at end of file diff --git a/src/components/CippFormPages/CippCustomDataMappingForm.jsx b/src/components/CippFormPages/CippCustomDataMappingForm.jsx new file mode 100644 index 000000000000..bf595e701e97 --- /dev/null +++ b/src/components/CippFormPages/CippCustomDataMappingForm.jsx @@ -0,0 +1,236 @@ +import { useWatch } from "react-hook-form"; +import { Box, Stack, Typography, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; +import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; +import { CippCopyToClipBoard } from "/src/components/CippComponents/CippCopyToClipboard"; +import extensionDataMapping from "/src/data/extensionDataMapping"; +import { getCippTranslation } from "/src/utils/get-cipp-translation"; + +const CippCustomDataMappingForm = ({ formControl }) => { + const selectedAttribute = useWatch({ control: formControl.control, name: "customDataAttribute" }); + const selectedDirectoryObjectType = useWatch({ + control: formControl.control, + name: "directoryObjectType", + }); + const selectedExtensionSyncDataset = useWatch({ + control: formControl.control, + name: "extensionSyncDataset", + }); + + const staticTargetTypes = [{ value: "user", label: "User" }]; + + const sourceFields = [ + { + name: "sourceType", + label: "Source Type", + type: "autoComplete", + required: true, + multiple: false, + placeholder: "Select a Source Type", + options: [{ value: "extensionSync", label: "Extension Sync" }], + }, + { + name: "extensionSyncDataset", + label: "Extension Sync Dataset", + type: "autoComplete", + required: true, + placeholder: "Select a Property", + options: Object.keys(extensionDataMapping).map((key) => ({ + value: key, + label: getCippTranslation(key), + addedFields: extensionDataMapping[key], + })), + multiple: false, + creatable: false, + condition: { + field: "sourceType", + compareType: "valueEq", + compareValue: "extensionSync", + }, + }, + { + name: "extensionSyncProperty", + label: "Source Property", + type: "autoComplete", + required: true, + placeholder: "Select a Property", + options: + selectedExtensionSyncDataset?.addedFields?.properties?.length > 0 + ? selectedExtensionSyncDataset?.addedFields?.properties.map((property) => ({ + value: property.name, + label: getCippTranslation(property.name), + addedFields: property, + })) + : [], + multiple: false, + creatable: false, + condition: { + field: "extensionSyncDataset", + propertyName: "addedFields.type", + compareType: "isNot", + compareValue: "array", + }, + sortOptions: true, + }, + ]; + + const destinationFields = [ + { + name: "directoryObjectType", + label: "Directory Object Type", + type: "autoComplete", + required: true, + placeholder: "Select an Object Type", + options: staticTargetTypes, + multiple: false, + creatable: false, + }, + { + name: "customDataAttribute", + label: "Destination Property", + type: "autoComplete", + required: true, + placeholder: "Select an Attribute", + api: { + url: "/api/ExecCustomData?Action=ListAvailableAttributes", + queryKey: "CustomAttributes", + dataKey: "Results", + dataFilter: (options) => + selectedDirectoryObjectType?.value + ? options.filter( + (option) => + (option?.addedFields?.isMultiValued === false && + selectedExtensionSyncDataset?.addedFields?.type === "object") || + (option?.addedFields?.isMultiValued === true && + selectedExtensionSyncDataset?.addedFields?.type === "array") + ) + : options, + valueField: "name", + labelField: "name", + showRefresh: true, + addedField: { + type: "type", + targetObject: "targetObject", + dataType: "dataType", + isMultiValued: "isMultiValued", + }, + }, + multiple: false, + sortOptions: true, + }, + ]; + + return ( + + + + + + + Tenant Selection + + + + + + + Source Details + + {sourceFields.map((field, index) => ( + <> + {field?.condition ? ( + + + + ) : ( + + )} + + ))} + + + + + Destination Details + + {destinationFields.map((field, index) => ( + <> + {field?.condition ? ( + + + + ) : ( + + )} + + ))} + + + + + + + + {selectedExtensionSyncDataset && ( + + ), + }, + { + label: "Description", + value: selectedExtensionSyncDataset?.addedFields?.description || "N/A", + }, + ]} + variant="outlined" + /> + )} + + {selectedAttribute && ( + , + }, + { + label: "Custom Data Type", + value: selectedAttribute?.addedFields?.type, + }, + { + label: "Target Object", + value: selectedAttribute?.addedFields?.targetObject, + }, + { + label: "Data Type", + value: selectedAttribute?.addedFields?.dataType, + }, + { + label: "Is Multi-Valued", + value: selectedAttribute?.addedFields?.isMultiValued ? "Yes" : "No", + }, + ]} + variant="outlined" + /> + )} + + + + ); +}; + +export default CippCustomDataMappingForm; \ No newline at end of file diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index e46b0ab157f6..b7c1cdc983a2 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -1,31 +1,42 @@ -import React, { useState } from "react"; +import { useState, useEffect } from "react"; import { Box, Button, Card, Collapse, Divider, - IconButton, Stack, SvgIcon, Typography, + Tooltip, + CircularProgress, } from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { Check, Error } from "@mui/icons-material"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; -import { Forward } from "@mui/icons-material"; import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; import { useSettings } from "../../hooks/use-settings"; import { Grid } from "@mui/system"; import { CippApiResults } from "../CippComponents/CippApiResults"; +import { useWatch } from "react-hook-form"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import CippForwardingSection from "../CippComponents/CippForwardingSection"; const CippExchangeSettingsForm = (props) => { const userSettingsDefaults = useSettings(); - const { formControl, currentSettings, userId, calPermissions, isFetching } = props; + const { formControl, currentSettings, userId, calPermissions, isFetching, oooRequest } = props; // State to manage the expanded panels const [expandedPanel, setExpandedPanel] = useState(null); const [relatedQueryKeys, setRelatedQueryKeys] = useState([]); + // Watch the Auto Reply State value + const autoReplyState = useWatch({ + control: formControl.control, + name: "ooo.AutoReplyState", + }); + + // Calculate if date fields should be disabled + const areDateFieldsDisabled = autoReplyState?.value !== "Scheduled"; + const handleExpand = (panel) => { setExpandedPanel((prev) => (prev === panel ? null : panel)); }; @@ -36,26 +47,61 @@ const CippExchangeSettingsForm = (props) => { Endpoint: `users`, tenantFilter: userSettingsDefaults.currentTenant, $select: "id,displayName,userPrincipalName,mail", - noPagination: true, $top: 999, }, queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, }); + const contactsList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `contacts`, + tenantFilter: userSettingsDefaults.currentTenant, + $select: "displayName,mail,mailNickname", + $top: 999, + }, + queryKey: `TenantContacts-${userSettingsDefaults.currentTenant}`, + }); + const postRequest = ApiPostCall({ datafromUrl: true, relatedQueryKeys: relatedQueryKeys, }); + // Handle form reset and set dropdown state after successful API calls + useEffect(() => { + if (postRequest.isSuccess) { + // If this was an OOO submission, preserve the submitted values + if (relatedQueryKeys.includes(`ooo-${userId}`)) { + const submittedValues = formControl.getValues(); + const oooFields = ['AutoReplyState', 'InternalMessage', 'ExternalMessage', 'StartTime', 'EndTime']; + + // Reset the form + formControl.reset(); + + // Restore the submitted OOO values + oooFields.forEach(field => { + const value = submittedValues.ooo?.[field]; + if (value !== undefined) { + formControl.setValue(`ooo.${field}`, value); + } + }); + } else { + // For non-OOO submissions, just reset normally + formControl.reset(); + } + } + }, [postRequest.isSuccess, relatedQueryKeys, userId, formControl]); + const handleSubmit = (type) => { - if (type === "permissions") { - setRelatedQueryKeys([`Mailbox-${userId}`]); - } else if (type === "calendar") { + if (type === "calendar") { setRelatedQueryKeys([`CalendarPermissions-${userId}`]); } else if (type === "forwarding") { setRelatedQueryKeys([`Mailbox-${userId}`]); } else if (type === "ooo") { setRelatedQueryKeys([`ooo-${userId}`]); + } else if (type === "recipientLimits") { + setRelatedQueryKeys([`Mailbox-${userId}`]); } const values = formControl.getValues(); @@ -65,6 +111,13 @@ const CippExchangeSettingsForm = (props) => { ...values[type], }; + // Format data for recipient limits + if (type === "recipientLimits") { + data.Identity = currentSettings.Mailbox[0].Identity; + data.recipientLimit = values[type].MaxRecipients; + delete data.MaxRecipients; + } + //remove all nulls and undefined values Object.keys(data).forEach((key) => { if (data[key] === "" || data[key] === null) { @@ -72,333 +125,63 @@ const CippExchangeSettingsForm = (props) => { } }); const url = { - permissions: "/api/ExecEditMailboxPermissions", calendar: "/api/ExecEditCalendarPermissions", forwarding: "/api/ExecEmailForward", ooo: "/api/ExecSetOoO", + recipientLimits: "/api/ExecSetRecipientLimits", }; postRequest.mutate({ url: url[type], data: data, queryKey: "MailboxPermissions", }); - - // Reset the form - formControl.reset(); }; // Data for each section const sections = [ - { - id: "mailboxPermissions", - cardLabelBox: "-", // This can be an icon or text label - text: "Mailbox Permissions", - subtext: "Manage mailbox permissions for users", - formContent: ( - - - currentSettings?.Permissions?.some( - (perm) => - perm.AccessRights === "FullAccess" && perm.User === user.userPrincipalName - ) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - currentSettings?.Permissions?.some( - (perm) => perm.AccessRights === "SendAs" && perm.User === user.userPrincipalName - ) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - currentSettings?.Permissions?.some( - (perm) => - perm.AccessRights === "SendOnBehalf" && perm.User === user.userPrincipalName - ) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - - - - - - ), - }, - { - id: "calendarPermissions", - cardLabelBox: "-", - text: "Calendar Permissions", - subtext: "Adjust calendar sharing settings", - formContent: ( - - - calPermissions?.some((perm) => perm.User === user.displayName) - ).map((user) => ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || []), - ]} - multiple={false} - formControl={formControl} - /> - - - - value ? true : "Select the permission level for the calendar", - }} - isFetching={isFetching || usersList.isFetching} - options={[ - { value: "Author", label: "Author" }, - { value: "Contributor", label: "Contributor" }, - { value: "Editor", label: "Editor" }, - { value: "Owner", label: "Owner" }, - { value: "NonEditingAuthor", label: "Non Editing Author" }, - { value: "PublishingAuthor", label: "Publishing Author" }, - { value: "PublishingEditor", label: "Publishing Editor" }, - { value: "Reviewer", label: "Reviewer" }, - { value: "LimitedDetails", label: "Limited Details" }, - { value: "AvailabilityOnly", label: "Availability Only" }, - ]} - multiple={false} - formControl={formControl} - /> - - - - - - - - - ), - }, { id: "mailboxForwarding", - cardLabelBox: currentSettings?.ForwardAndDeliver ? : "-", + cardLabelBox: { + cardLabelBoxHeader: isFetching ? ( + + ) : (currentSettings?.ForwardingAddress) ? ( + + ) : ( + + ), + }, text: "Mailbox Forwarding", - subtext: "Configure email forwarding options", + subtext: (currentSettings?.ForwardingAddress) + ? "Email forwarding is configured for this mailbox" + : "No email forwarding configured for this mailbox", formContent: ( - - - - - ({ - value: user.userPrincipalName, - label: `${user.displayName} (${user.userPrincipalName})`, - })) || [] - } - formControl={formControl} - /> - - - - - - - - - - - - - - + ), }, { id: "outOfOffice", - cardLabelBox: "OOO", + cardLabelBox: { + cardLabelBoxHeader: OOO, + }, text: "Out Of Office", subtext: "Set automatic replies for when you are away", formContent: ( - + { ]} /> - - + + + + + + - - + + + + + + - + { rows={4} /> - + { rows={4} /> - + @@ -458,6 +257,46 @@ const CippExchangeSettingsForm = (props) => { ), }, + { + id: "recipientLimits", + cardLabelBox: { + cardLabelBoxHeader: RL, + }, + text: "Recipient Limits", + subtext: "Set the maximum number of recipients per message", + formContent: ( + + + + + + + + + + + + + + ), + }, ]; return ( @@ -471,8 +310,15 @@ const CippExchangeSettingsForm = (props) => { alignItems: "center", display: "flex", justifyContent: "space-between", - p: 2, + py: 3, + pl: 2, + pr: 4, + cursor: "pointer", + "&:hover": { + bgcolor: "action.hover", + }, }} + onClick={() => handleExpand(section.id)} > {/* Left Side: cardLabelBox, text, subtext */} @@ -481,14 +327,14 @@ const CippExchangeSettingsForm = (props) => { sx={{ alignItems: "center", borderRadius: 1, - color: "primary.contrastText", + color: "text.secondary", display: "flex", height: 40, justifyContent: "center", width: 40, }} > - {section.cardLabelBox} + {section.cardLabelBox.cardLabelBoxHeader}
    {/* Main Text and Subtext */} @@ -502,20 +348,17 @@ const CippExchangeSettingsForm = (props) => {
    - {/* Expand Icon */} - handleExpand(section.id)}> - - - - + + + - + {section.formContent} diff --git a/src/components/CippFormPages/CippFormPage.jsx b/src/components/CippFormPages/CippFormPage.jsx index 585975b073bb..2a85e6362ee9 100644 --- a/src/components/CippFormPages/CippFormPage.jsx +++ b/src/components/CippFormPages/CippFormPage.jsx @@ -11,39 +11,40 @@ import { CardActions, } from "@mui/material"; import ArrowLeftIcon from "@mui/icons-material/ArrowLeft"; -import Head from "next/head"; import { ApiPostCall } from "../../api/ApiCall"; import { CippApiResults } from "../CippComponents/CippApiResults"; import { useEffect } from "react"; import { useFormState } from "react-hook-form"; +import { CippHead } from "../CippComponents/CippHead"; const CippFormPage = (props) => { const { title, backButtonTitle, + titleButton, formPageType = "Add", children, queryKey, formControl, postUrl, customDataformatter, - resetForm = true, + resetForm = false, hideBackButton = false, hidePageType = false, hideTitle = false, hideSubmit = false, + allowResubmit = false, addedButtons, ...other } = props; const router = useRouter(); - //check if there are const postCall = ApiPostCall({ datafromUrl: true, relatedQueryKeys: queryKey, }); - const { isValid } = useFormState({ control: formControl.control }); + const { isValid, isDirty } = useFormState({ control: formControl.control }); useEffect(() => { delete router.query.tenantFilter; @@ -70,6 +71,11 @@ const CippFormPage = (props) => { }, [postCall.isSuccess]); const handleSubmit = () => { + formControl.trigger(); + // Check if the form is valid before proceeding + if (!isValid) { + return; + } const values = customDataformatter ? customDataformatter(formControl.getValues()) : formControl.getValues(); @@ -86,9 +92,7 @@ const CippFormPage = (props) => { }; return ( <> - - {title} - + { )} -
    +
    {!hidePageType && <>{formPageType} - } {title} + {titleButton && titleButton}
    )} @@ -134,7 +141,7 @@ const CippFormPage = (props) => { {addedButtons && addedButtons}
    + } + /> + ); } else { return ( { }); }; -function CippJsonView({ object = { "No Data Selected": "No Data Selected" }, type }) { +function CippJsonView({ + object = { "No Data Selected": "No Data Selected" }, + type, + defaultOpen = false, +}) { const [viewJson, setViewJson] = useState(false); + const [accordionOpen, setAccordionOpen] = useState(defaultOpen); const [drilldownData, setDrilldownData] = useState([]); + const [guidMapping, setGuidMapping] = useState({}); + const [notFoundGuids, setNotFoundGuids] = useState(new Set()); + const [isLoadingGuids, setIsLoadingGuids] = useState(false); + const [pendingGuids, setPendingGuids] = useState([]); + const [lastRequestTime, setLastRequestTime] = useState(0); + const tenantFilter = useSettings().currentTenant; + + // Setup API call for directory objects resolution + const directoryObjectsMutation = ApiPostCall({ + relatedQueryKeys: ["directoryObjects"], + onResult: (data) => { + if (data && Array.isArray(data.value)) { + const newMapping = {}; + + // Process the returned results + data.value.forEach((item) => { + if (item.id && (item.displayName || item.userPrincipalName || item.mail)) { + // Prefer displayName, fallback to UPN or mail if available + newMapping[item.id] = item.displayName || item.userPrincipalName || item.mail; + } + }); + + // Find GUIDs that were sent but not returned in the response + const processedGuids = new Set(pendingGuids); + const returnedGuids = new Set(data.value.map((item) => item.id)); + const notReturned = [...processedGuids].filter((guid) => !returnedGuids.has(guid)); + + // Add them to the notFoundGuids set + if (notReturned.length > 0) { + setNotFoundGuids((prev) => { + const newSet = new Set(prev); + notReturned.forEach((guid) => newSet.add(guid)); + return newSet; + }); + } + + setGuidMapping((prevMapping) => ({ ...prevMapping, ...newMapping })); + setPendingGuids([]); + setIsLoadingGuids(false); + } + }, + }); + + // Function to handle resolving GUIDs - used in both useEffect and handleItemClick + const resolveGuids = (objectToScan) => { + const guidsSet = findGuids(objectToScan); + + if (guidsSet.size === 0) return; + + const guidsArray = Array.from(guidsSet); + // Filter out GUIDs that are already resolved or known to not be resolvable + const notResolvedGuids = guidsArray.filter( + (guid) => !guidMapping[guid] && !notFoundGuids.has(guid) + ); + + if (notResolvedGuids.length === 0) return; + + // Merge with any pending GUIDs to avoid duplicate requests + const allPendingGuids = [...new Set([...pendingGuids, ...notResolvedGuids])]; + setPendingGuids(allPendingGuids); + setIsLoadingGuids(true); + + // Implement throttling - only send a new request every 2 seconds + const now = Date.now(); + if (now - lastRequestTime < 2000) { + return; + } + + setLastRequestTime(now); + + // Only send a maximum of 1000 GUIDs per request + const batchSize = 1000; + const guidsToSend = allPendingGuids.slice(0, batchSize); + + if (guidsToSend.length > 0) { + directoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: tenantFilter, + ids: guidsToSend, + $select: "id,displayName,userPrincipalName,mail", + }, + }); + } else { + setIsLoadingGuids(false); + } + }; const renderIntuneItems = (data) => { const items = []; @@ -91,11 +235,31 @@ function CippJsonView({ object = { "No Data Selected": "No Data Selected" }, typ if (data.omaSettings) { data.omaSettings.forEach((omaSetting, index) => { + // Check if value is a GUID that we've resolved + const value = + typeof omaSetting.value === "string" && + isGuid(omaSetting.value) && + guidMapping[omaSetting.value] ? ( + +
    + {guidMapping[omaSetting.value]} + + (GUID) + +
    +
    + ) : ( + omaSetting.value + ); + items.push( ); }); @@ -139,13 +303,50 @@ function CippJsonView({ object = { "No Data Selected": "No Data Selected" }, typ } else if (settingInstance?.simpleSettingValue?.value) { const label = intuneObj?.displayName || settingInstance.settingDefinitionId; const value = settingInstance.simpleSettingValue.value; - items.push(); + // Check if value is a GUID that we've resolved + const displayValue = + typeof value === "string" && isGuid(value) && guidMapping[value] ? ( + +
    + {guidMapping[value]} + + (GUID) + +
    +
    + ) : ( + value + ); + + items.push( + + ); } else if (settingInstance?.choiceSettingValue?.value) { const label = intuneObj?.displayName || settingInstance.settingDefinitionId; - const optionValue = - intuneObj?.options?.find( - (option) => option.id === settingInstance.choiceSettingValue.value - )?.displayName || settingInstance.choiceSettingValue.value; + const rawValue = settingInstance.choiceSettingValue.value; + let optionValue = + intuneObj?.options?.find((option) => option.id === rawValue)?.displayName || rawValue; + + // Check if optionValue is a GUID that we've resolved + if (typeof optionValue === "string" && isGuid(optionValue) && guidMapping[optionValue]) { + optionValue = ( + +
    + {guidMapping[optionValue]} + + (GUID) + +
    +
    + ); + } + items.push( ); @@ -170,13 +371,49 @@ function CippJsonView({ object = { "No Data Selected": "No Data Selected" }, typ ); } else { Object.entries(data).forEach(([key, value]) => { - items.push( - - ); + // Check if value is a GUID that we've resolved + if (typeof value === "string" && isGuid(value) && guidMapping[value]) { + items.push( + +
    + {guidMapping[value]} + + (GUID) + +
    + + } + /> + ); + } else if (typeof value === "string" && isGuid(value) && isLoadingGuids) { + items.push( + + + {getCippFormatting(value, key)} + + } + /> + ); + } else { + items.push( + + ); + } }); } @@ -184,6 +421,9 @@ function CippJsonView({ object = { "No Data Selected": "No Data Selected" }, typ }; useEffect(() => { + if (!type && (object?.omaSettings || object?.settings || object?.added)) { + type = "intune"; + } const blacklist = [ "selectedOption", "GUID", @@ -198,7 +438,42 @@ function CippJsonView({ object = { "No Data Selected": "No Data Selected" }, typ Object.entries(cleanedObj).filter(([key]) => !blacklist.includes(key)) ); setDrilldownData([filteredObj]); - }, [object]); + + // Using the centralized resolveGuids function to handle GUID resolution + resolveGuids(cleanedObj); + }, [object, tenantFilter]); + + // Effect to reprocess any pending GUIDs when the guidMapping changes or throttling window passes + useEffect(() => { + if (pendingGuids.length > 0 && !isLoadingGuids) { + const now = Date.now(); + if (now - lastRequestTime >= 2000) { + // Only send a maximum of 1000 GUIDs per request + const batchSize = 1000; + const guidsToSend = pendingGuids.slice(0, batchSize); + + setLastRequestTime(now); + setIsLoadingGuids(true); + + directoryObjectsMutation.mutate({ + url: "/api/ListDirectoryObjects", + data: { + tenantFilter: tenantFilter, + ids: guidsToSend, + $select: "id,displayName,userPrincipalName,mail", + }, + }); + } + } + }, [ + guidMapping, + notFoundGuids, + pendingGuids, + lastRequestTime, + isLoadingGuids, + directoryObjectsMutation, + tenantFilter, + ]); const toggleView = () => setViewJson(!viewJson); @@ -206,47 +481,69 @@ function CippJsonView({ object = { "No Data Selected": "No Data Selected" }, typ const updatedData = drilldownData.slice(0, level + 1); updatedData[level + 1] = itemData; setDrilldownData(updatedData); + + // Use the centralized resolveGuids function to handle GUID resolution for drill-down data + resolveGuids(itemData); }; return ( - + setAccordionOpen(!accordionOpen)} + > } sx={{ display: "flex", alignItems: "center" }} > - - Policy Details - + + + Policy Details + + {isLoadingGuids && ( + + Resolving object identifiers... + + )} + {viewJson ? : } {viewJson ? ( - + ) : ( - {drilldownData.slice(0, 4).map((data, index) => ( - - {type !== "intune" && ( - - {renderListItems(data, (itemData) => handleItemClick(itemData, index))} - - )} - {type === "intune" && {renderIntuneItems(data)}} - - ))} + {drilldownData + ?.filter((data) => data !== null && data !== undefined) + .map((data, index) => ( + 4, and add spacing between the top and bottom items + paddingTop: index === 0 ? 0 : 2, + borderTop: index >= 4 && type !== "intune" ? "1px solid lightgrey" : "none", + borderRight: index < drilldownData.length - 1 ? "1px solid lightgrey" : "none", + overflowWrap: "anywhere", + whiteSpace: "pre-line", + paddingRight: 2, + }} + > + {type !== "intune" && ( + + {renderListItems( + data, + (itemData) => handleItemClick(itemData, index), + guidMapping, + isLoadingGuids + )} + + )} + {type === "intune" && {renderIntuneItems(data)}} + + ))} )} diff --git a/src/components/CippFormPages/CippSafeLinksPolicyRuleForm.jsx b/src/components/CippFormPages/CippSafeLinksPolicyRuleForm.jsx new file mode 100644 index 000000000000..ce25cc269028 --- /dev/null +++ b/src/components/CippFormPages/CippSafeLinksPolicyRuleForm.jsx @@ -0,0 +1,652 @@ +import { useEffect, useState } from "react"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { Typography } from "@mui/material"; +import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector"; +import { CippFormGroupSelector } from "/src/components/CippComponents/CippFormGroupSelector"; +import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormDomainSelector"; +import { CippInfoCard } from "/src/components/CippCards/CippInfoCard"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { getCippValidator } from "/src/utils/get-cipp-validator"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useSettings } from "/src/hooks/use-settings"; + +// Utility functions for data processing +export const safeLinksDataUtils = { + // Process arrays for string inputs + formatStringToArray: (value) => { + if (!value || value === '') return []; + if (typeof value === 'string') { + return value.split(',').map(item => item.trim()).filter(item => item !== ''); + } + return value; + }, + + // Process domain fields - handle both string and object values + processDomainField: (field) => { + if (!field) return []; + + if (typeof field === 'string') { + // Handle comma-separated string + return safeLinksDataUtils.formatStringToArray(field); + } else if (Array.isArray(field)) { + // If already an array of strings, return it + if (field.length > 0 && typeof field[0] === 'string') { + return field; + } + // If an array of objects from the domain selector, extract the ids + return field.map(item => item.id || item); + } + return []; + }, + + // Process group fields if they're returned as objects + processGroupField: (field) => { + if (Array.isArray(field)) { + // If the field is already an array of IDs, return it + if (field.length > 0 && typeof field[0] === 'string') { + return field; + } + // If the field is an array of objects, extract the IDs + return field.map(item => item.id || item); + } + return []; + }, + + // Create custom data formatter for different form types + createDataFormatter: (formControl, formType = 'add', additionalFields = {}) => { + return (values) => { + const ruleValues = formControl.getValues(); + + // Base data structure + const baseData = { + // Common fields + PolicyName: values.PolicyName, + tenantFilter: values.tenantFilter, + + // Policy fields + EnableSafeLinksForEmail: values.EnableSafeLinksForEmail, + EnableSafeLinksForTeams: values.EnableSafeLinksForTeams, + EnableSafeLinksForOffice: values.EnableSafeLinksForOffice, + TrackClicks: values.TrackClicks, + AllowClickThrough: values.AllowClickThrough, + ScanUrls: values.ScanUrls, + EnableForInternalSenders: values.EnableForInternalSenders, + DeliverMessageAfterScan: values.DeliverMessageAfterScan, + DisableUrlRewrite: values.DisableUrlRewrite, + DoNotRewriteUrls: Array.isArray(values.DoNotRewriteUrls) ? values.DoNotRewriteUrls : [], + AdminDisplayName: values.AdminDisplayName, + CustomNotificationText: values.CustomNotificationText, + EnableOrganizationBranding: values.EnableOrganizationBranding, + + // Rule fields + RuleName: ruleValues.RuleName, + Priority: ruleValues.Priority, + Comments: ruleValues.Comments, + + // Process user, group and domain fields + SentTo: ruleValues.SentTo, + ExceptIfSentTo: ruleValues.ExceptIfSentTo, + SentToMemberOf: safeLinksDataUtils.processGroupField(ruleValues.SentToMemberOf), + ExceptIfSentToMemberOf: safeLinksDataUtils.processGroupField(ruleValues.ExceptIfSentToMemberOf), + RecipientDomainIs: safeLinksDataUtils.processDomainField(ruleValues.RecipientDomainIs), + ExceptIfRecipientDomainIs: safeLinksDataUtils.processDomainField(ruleValues.ExceptIfRecipientDomainIs), + }; + + // Add form-specific fields + switch (formType) { + case 'add': + return { + ...baseData, + State: ruleValues.State, + }; + + case 'edit': + return { + ...baseData, + State: ruleValues.State, + }; + + case 'template': + return { + ...baseData, + ID: additionalFields.ID, + TemplateName: values.TemplateName, + TemplateDescription: values.TemplateDescription, + State: ruleValues.State ? "Enabled" : "Disabled", + }; + + case 'createTemplate': + return { + ...baseData, + TemplateName: values.TemplateName, + TemplateDescription: values.TemplateDescription, + // If no policy description provided, use template description as fallback + AdminDisplayName: values.AdminDisplayName || values.Description, + State: ruleValues.State, + }; + + default: + return baseData; + } + }; + }, + + // Helper to populate form with existing data + populateFormData: (formControl, data, userSettingsDefaults, formType = 'edit' ) => { + const baseData = { + tenantFilter: userSettingsDefaults.currentTenant, + PolicyName: data.PolicyName, + EnableSafeLinksForEmail: data.EnableSafeLinksForEmail, + EnableSafeLinksForTeams: data.EnableSafeLinksForTeams, + EnableSafeLinksForOffice: data.EnableSafeLinksForOffice, + TrackClicks: data.TrackClicks, + AllowClickThrough: data.AllowClickThrough, + ScanUrls: data.ScanUrls, + EnableForInternalSenders: data.EnableForInternalSenders, + DeliverMessageAfterScan: data.DeliverMessageAfterScan, + DisableUrlRewrite: data.DisableUrlRewrite, + DoNotRewriteUrls: data.DoNotRewriteUrls, + AdminDisplayName: data.AdminDisplayName, + CustomNotificationText: data.CustomNotificationText, + EnableOrganizationBranding: data.EnableOrganizationBranding, + RuleName: data.RuleName, + Priority: data.Priority, + Comments: data.Comments, + State: data.State, + SentTo: data.SentTo || [], + ExceptIfSentTo: data.ExceptIfSentTo || [], + SentToMemberOf: data.SentToMemberOf || [], + ExceptIfSentToMemberOf: data.ExceptIfSentToMemberOf || [], + RecipientDomainIs: data.RecipientDomainIs || [], + ExceptIfRecipientDomainIs: data.ExceptIfRecipientDomainIs || [], + }; + + // Add template-specific fields + if (formType === 'template') { + baseData.TemplateName = data.TemplateName; + baseData.TemplateDescription = data.TemplateDescription; + } + + formControl.reset(baseData); + }, +}; + +export const SafeLinksForm = ({ formControl, formType = "add" }) => { + const { watch, setError, clearErrors } = formControl; + const doNotRewriteUrls = watch("DoNotRewriteUrls"); + const policyName = watch("PolicyName"); + const [isUrlsValid, setIsUrlsValid] = useState(true); + const userSettingsDefaults = useSettings(); + + // Fetch existing policies for name validation (only for add/createTemplate forms) + const shouldFetchPolicies = formType === "add" || formType === "createTemplate"; + const existingPolicies = ApiGetCall({ + url: `/api/ListSafeLinksPolicy?tenantFilter=${userSettingsDefaults.currentTenant}`, + queryKey: `SafeLinksPolicy-List-${userSettingsDefaults.currentTenant}`, + enabled: shouldFetchPolicies, + }); + + // Fetch existing templates for name validation (only for createTemplate forms) + const shouldFetchTemplates = formType === "createTemplate"; + const existingTemplates = ApiGetCall({ + url: `/api/ListSafeLinksPolicyTemplates`, + queryKey: `SafeLinksTemplates-List`, + enabled: shouldFetchTemplates, + }); + + // Create validator for checking duplicate policy names + const validatePolicyName = (value) => { + if (!shouldFetchPolicies || !value) return true; + + // If still loading, allow validation to pass (it will re-validate when data loads) + if (existingPolicies.isFetching) return true; + + // If API call failed, allow validation to pass (don't block user due to API issues) + if (existingPolicies.error) return true; + + if (existingPolicies.isSuccess && existingPolicies.data) { + const existingNames = existingPolicies.data.map(policy => policy.PolicyName?.toLowerCase()).filter(Boolean); + if (existingNames.includes(value.toLowerCase())) { + return "A policy with this name already exists"; + } + + const lowerValue = value.toLowerCase(); + if (lowerValue.startsWith("built-in protection policy") || + lowerValue.startsWith("standard preset security policy") || + lowerValue.startsWith("strict preset security policy")) { + return "This name is reserved for built-in policies"; + } + } + return true; + }; + + // Create validator for checking duplicate template names + const validateTemplateName = (value) => { + if (!shouldFetchTemplates || !value) return true; + + // If still loading, allow validation to pass (it will re-validate when data loads) + if (existingTemplates.isFetching) return true; + + // If API call failed, allow validation to pass (don't block user due to API issues) + if (existingTemplates.error) return true; + + if (existingTemplates.isSuccess && existingTemplates.data) { + const existingNames = existingTemplates.data.map(template => template.name?.toLowerCase()).filter(Boolean); + if (existingNames.includes(value.toLowerCase())) { + return "A template with this name already exists"; + } + } + return true; + }; + + // Helper function to validate a URL/domain entry + const validateDoNotRewriteUrl = (entry) => { + if (!entry) return true; + + // For entries with wildcards, use wildcard validators + if (entry.includes('*') || entry.includes('~')) { + const wildcardUrlResult = getCippValidator(entry, "wildcardUrl"); + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardUrlResult !== true && wildcardDomainResult !== true) { + return false; + } + return true; + } + + // For standard entries, check normal validators + const hostnameResult = getCippValidator(entry, "hostname"); + const urlResult = getCippValidator(entry, "url"); + const domainResult = getCippValidator(entry, "domain"); + + if (hostnameResult !== true && urlResult !== true && domainResult !== true) { + return false; + } + + return true; + }; + + // Re-validate policy name when existing policies data changes + useEffect(() => { + if (shouldFetchPolicies && (existingPolicies.isSuccess || existingPolicies.error)) { + formControl.trigger('PolicyName'); + } + }, [existingPolicies.isSuccess, existingPolicies.error, existingPolicies.data, shouldFetchPolicies, formControl]); + + // Re-validate template name when existing templates data changes + useEffect(() => { + if (shouldFetchTemplates && (existingTemplates.isSuccess || existingTemplates.error)) { + formControl.trigger('TemplateName'); + } + }, [existingTemplates.isSuccess, existingTemplates.error, existingTemplates.data, shouldFetchTemplates, formControl]); + + // Validate URLs in useEffect and update the validation Enabled + useEffect(() => { + if (!doNotRewriteUrls || doNotRewriteUrls.length === 0) { + clearErrors("DoNotRewriteUrls"); + setIsUrlsValid(true); + return; + } + + let hasInvalidEntry = false; + + for (const item of doNotRewriteUrls) { + const entry = typeof item === 'string' ? item : (item?.value || item?.label || ''); + if (!entry) continue; + + const isValid = validateDoNotRewriteUrl(entry); + if (!isValid) { + hasInvalidEntry = true; + break; + } + } + + if (hasInvalidEntry) { + setError("DoNotRewriteUrls", { + type: "validate", + message: "Not a valid URL, domain, or pattern" + }); + setIsUrlsValid(false); + } else { + clearErrors("DoNotRewriteUrls"); + setIsUrlsValid(true); + } + }, [doNotRewriteUrls, setError, clearErrors]); + + // Set the rule-related values whenever the policy name changes + useEffect(() => { + if (policyName) { + // Always set SafeLinksPolicy to match the policy name + formControl.setValue('SafeLinksPolicy', policyName); + + // Only auto-generate the rule name for new policies + if (formType === "add" || formType === "createTemplate") { + const ruleName = `${policyName}_Rule`; + formControl.setValue('RuleName', ruleName); + } + } + }, [policyName, formType, formControl]); + + // Show template-specific fields + const showTemplateFields = formType === "template" || formType === "createTemplate"; + + return ( + + {/* Template Fields (if applicable) */} + {showTemplateFields && ( + <> + + Template Information + + + + + + + + + )} + + {/* Policy Settings Section */} + + Safe Links Policy Configuration + + + Policy Settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + isUrlsValid || "Not a valid URL, domain, or pattern" + } + }} + /> + + + {/* Rule Settings Section */} + + Safe Links Rule Configuration + + + Rule Information + + + + + + + + + + + + + + + Applies To: + + + + + + + + + + + + Exceptions: + + + + + + + + + + + + {/* Information Cards */} + + } + label="Propagation Time" + value="Changes to Safe Links policies and rules may take up to 6 hours to propagate throughout your organization." + isFetching={false} + /> + + + ); +}; + +export default SafeLinksForm; \ No newline at end of file diff --git a/src/components/CippFormPages/CippSchedulerForm.jsx b/src/components/CippFormPages/CippSchedulerForm.jsx index e18e9c57421a..aa8350f3a0f1 100644 --- a/src/components/CippFormPages/CippSchedulerForm.jsx +++ b/src/components/CippFormPages/CippSchedulerForm.jsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Box, Button, Grid, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Box, Button, Divider, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import { useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; @@ -51,7 +51,7 @@ const CippSchedulerForm = (props) => { }; const recurrenceOptions = [ - { value: "0", label: "Only once" }, + { value: "0", label: "Once" }, { value: "1d", label: "Every 1 day" }, { value: "7d", label: "Every 7 days" }, { value: "30d", label: "Every 30 days" }, @@ -65,40 +65,139 @@ const CippSchedulerForm = (props) => { const router = useRouter(); const scheduledTaskList = ApiGetCall({ url: "/api/ListScheduledItems", - queryKey: "ListScheduledItems-Edit", + queryKey: "ListScheduledItems-Edit-" + router.query.id, + waiting: !!router.query.id, + data: { + Id: router.query.id, + }, }); const tenantList = ApiGetCall({ - url: "/api/ListTenants", - queryKey: "ListTenants", + url: "/api/ListTenants?AllTenantSelector=true", + queryKey: "ListTenants-AllTenants", }); useEffect(() => { if (scheduledTaskList.isSuccess && router.query.id) { const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); + + // Early return if task is not found + if (!task) { + console.warn(`Task with RowKey ${router.query.id} not found`); + return; + } + const postExecution = task?.postExecution?.split(",").map((item) => { return { label: item, value: item }; }); // Find tenantFilter in tenantList, and create a label/value pair for the autocomplete if (tenantList.isSuccess) { - const tenantFilter = tenantList.data.find( - (tenant) => tenant.defaultDomainName === task?.Tenant - ); + let tenantFilter = null; + let tenantFilterForForm = null; + + // Check if the task has a tenant group + if (task?.TenantGroupInfo) { + // Handle tenant group + tenantFilterForForm = { + value: task.TenantGroupInfo.value, + label: task.TenantGroupInfo.label, + type: "Group", + addedFields: task.TenantGroupInfo, + }; + } else { + // Handle regular tenant + tenantFilter = tenantList.data.find( + (tenant) => + tenant.defaultDomainName === task?.Tenant.value || + tenant.defaultDomainName === task?.Tenant + ); + if (tenantFilter) { + tenantFilterForForm = { + value: tenantFilter.defaultDomainName, + label: `${tenantFilter.displayName} (${tenantFilter.defaultDomainName})`, + type: "Tenant", + addedFields: tenantFilter, + }; + } + } if (commands.isSuccess) { const command = commands.data.find((command) => command.Function === task.Command); + + // If command is not found in the list, create a placeholder command entry + let commandForForm = command; + if (!command && task.Command) { + commandForForm = { + Function: task.Command, + Parameters: [], + // Add minimal required structure for system jobs + }; + } + + var recurrence = recurrenceOptions.find( + (option) => option.value === task.Recurrence || option.label === task.Recurrence + ); + + // If recurrence is not found in predefined options, create a custom option + if (!recurrence && task.Recurrence) { + recurrence = { + value: task.Recurrence, + label: `${task.Recurrence}`, + }; + } + + // if scheduledtime type is a date, convert to unixtime + if (typeof task.ScheduledTime === "date") { + task.ScheduledTime = Math.floor(task.ScheduledTime.getTime() / 1000); + } else if (typeof task.ScheduledTime === "string") { + task.ScheduledTime = Math.floor(new Date(task.ScheduledTime).getTime() / 1000); + } + + // Check if any parameter values are complex objects that can't be represented as simple form fields + const hasComplexObjects = + task.Parameters && typeof task.Parameters === "object" + ? Object.values(task.Parameters).some((value) => { + // Check for arrays + if (Array.isArray(value)) return true; + // Check for objects (but not null) + if (value !== null && typeof value === "object") return true; + // Check for stringified objects that contain [object Object] + if (typeof value === "string" && value.includes("[object Object]")) return true; + // Check for stringified JSON arrays/objects + if ( + typeof value === "string" && + (value.trim().startsWith("[") || value.trim().startsWith("{")) + ) { + try { + const parsed = JSON.parse(value); + return typeof parsed === "object"; + } catch { + return false; + } + } + return false; + }) + : false; + const ResetParams = { - tenantFilter: { - value: tenantFilter?.defaultDomainName, - label: tenantFilter?.defaultDomainName, - }, + tenantFilter: tenantFilterForForm, RowKey: router.query.Clone ? null : task.RowKey, Name: router.query.Clone ? `${task.Name} (Clone)` : task?.Name, - command: { label: task.Command, value: task.Command, addedFields: command }, + command: { label: task.Command, value: task.Command, addedFields: commandForForm }, ScheduledTime: task.ScheduledTime, - Recurrence: task.Recurrence, + Recurrence: recurrence, parameters: task.Parameters, postExecution: postExecution, - advancedParameters: task.RawJsonParameters ? true : false, + // Show advanced parameters if: + // 1. RawJsonParameters exist + // 2. It's a system command with no defined parameters + // 3. Any parameter contains complex objects (arrays, objects, etc.) + advancedParameters: task.RawJsonParameters + ? true + : hasComplexObjects || + !commandForForm?.Parameters || + commandForForm.Parameters.length === 0, + // Set the RawJsonParameters if they exist + RawJsonParameters: task.RawJsonParameters || "", }; formControl.reset(ResetParams); } @@ -112,190 +211,308 @@ const CippSchedulerForm = (props) => { commands.isSuccess, ]); + const advancedParameters = useWatch({ control: formControl.control, name: "advancedParameters" }); + + useEffect(() => { + if (advancedParameters === true) { + // Check if we're editing an existing task and it has RawJsonParameters + const currentRawJsonParameters = formControl.getValues("RawJsonParameters"); + + // If we already have raw JSON parameters (from editing existing task), use those + if ( + currentRawJsonParameters && + currentRawJsonParameters.trim() !== "" && + currentRawJsonParameters !== "{}" + ) { + // Already populated from existing task, no need to overwrite + return; + } + + // Get the original task parameters if we're editing (to preserve complex objects) + let parametersToUse = null; + if (router.query.id && scheduledTaskList.isSuccess) { + const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); + if (task?.Parameters) { + parametersToUse = task.Parameters; + } + } + + // If we don't have original task parameters, use current form parameters + if (!parametersToUse) { + parametersToUse = formControl.getValues("parameters"); + } + + // Add null check to prevent error when no parameters exist + if (parametersToUse && typeof parametersToUse === "object") { + // Create a clean copy for JSON + const cleanParams = { ...parametersToUse }; + Object.keys(cleanParams).forEach((key) => { + if (cleanParams[key] === "" || cleanParams[key] === null) { + delete cleanParams[key]; + } + }); + const jsonString = JSON.stringify(cleanParams, null, 2); + formControl.setValue("RawJsonParameters", jsonString); + } else { + // If no parameters, set empty object + formControl.setValue("RawJsonParameters", "{}"); + } + } + }, [advancedParameters, router.query.id, scheduledTaskList.isSuccess]); + const gridSize = fullWidth ? 12 : 4; // Adjust size based on fullWidth prop return ( - - {(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && ( - - )} - - - + <> + + {(scheduledTaskList.isFetching || tenantList.isLoading || commands.isLoading) && ( + + )} + + + - - - + + + - - { - return { - label: command.Function, - value: command.Function, - addedFields: command, - }; - }) || [] - } - validators={{ - validate: (value) => { - if (!value) { - return "Please select a Command"; + + { + const baseOptions = + commands.data?.map((command) => { + return { + label: command.Function, + value: command.Function, + addedFields: command, + }; + }) || []; + + // If we're editing a task and the command isn't in the base options, add it + if (router.query.id && scheduledTaskList.isSuccess) { + const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); + if (task?.Command && !baseOptions.find((opt) => opt.value === task.Command)) { + baseOptions.unshift({ + label: task.Command, + value: task.Command, + addedFields: { + Function: task.Command, + Parameters: [], + }, + }); + } } - return true; - }, - }} - /> - - - - - - - - {selectedCommand?.addedFields?.Synopsis && ( - - - PowerShell Command: - - {selectedCommand.addedFields.Synopsis} - - + + return baseOptions; + })()} + validators={{ + validate: (value) => { + if (!value) { + return "Please select a Command"; + } + return true; + }, + }} + /> - )} + + + + + { + let options = [...recurrenceOptions]; + + // If we're editing a task and the recurrence isn't in the base options, add it + if (router.query.id && scheduledTaskList.isSuccess) { + const task = scheduledTaskList.data.find((task) => task.RowKey === router.query.id); + if (task?.Recurrence && !options.find((opt) => opt.value === task.Recurrence)) { + options.push({ + value: task.Recurrence, + label: `Custom: ${task.Recurrence}`, + }); + } + } - {selectedCommand?.addedFields?.Parameters?.map((param, idx) => ( + return options; + })()} + multiple={false} + disableClearable={true} + creatable={true} + /> + + {selectedCommand?.addedFields?.Synopsis && ( + + + PowerShell Command: + + {selectedCommand.addedFields.Synopsis} + + + + )} + + {selectedCommand?.addedFields?.Parameters?.map((param, idx) => ( + + + {param.Type === "System.Boolean" || + param.Type === "System.Management.Automation.SwitchParameter" ? ( + + ) : param.Type === "System.Collections.Hashtable" ? ( + + ) : param.Type?.startsWith("System.String") ? ( + + ) : ( + + )} + + + ))} + + + + + + - - {param.Type === "System.Boolean" ? ( - - ) : param.Type === "System.Collections.Hashtable" ? ( - - ) : param.Type?.startsWith("System.String") ? ( - - ) : null} + + getCippValidator(value, "json"), + }} + formControl={formControl} + multiline + rows={6} + maxRows={30} + sx={{ + "& .MuiInputBase-root": { + overflow: "auto", + minHeight: "200px", + }, + }} + placeholder={`Enter a JSON object`} + /> - ))} - - - - - + getCippValidator(value, "json"), - }} + type="autoComplete" + name="postExecution" + label="Post Execution Actions" formControl={formControl} - multiline - rows={4} - placeholder={`Enter a JSON object`} + multiple + creatable={false} + options={[ + { label: "Webhook", value: "Webhook" }, + { label: "Email", value: "Email" }, + { label: "PSA", value: "PSA" }, + ]} /> - - - - - - + + + + + + - + ); }; diff --git a/src/components/CippIntegrations/CippApiClientManagement.jsx b/src/components/CippIntegrations/CippApiClientManagement.jsx new file mode 100644 index 000000000000..65914ea19dd0 --- /dev/null +++ b/src/components/CippIntegrations/CippApiClientManagement.jsx @@ -0,0 +1,419 @@ +import { Button, Stack, SvgIcon, Menu, MenuItem, ListItemText, Alert } from "@mui/material"; +import { useState } from "react"; +import isEqual from "lodash/isEqual"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "/src/api/ApiCall"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { + ChevronDownIcon, + ClipboardDocumentIcon, + PencilIcon, + PlusSmallIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { CippApiDialog } from "../CippComponents/CippApiDialog"; +import { Create, Key, Save, Sync } from "@mui/icons-material"; +import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; +import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; +import { Box } from "@mui/system"; + +const CippApiClientManagement = () => { + const [openAddClientDialog, setOpenAddClientDialog] = useState(false); + const [openAddExistingAppDialog, setOpenAddExistingAppDialog] = useState(false); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + + const formControl = useForm({ + mode: "onChange", + }); + + const postCall = ApiPostCall({ + datafromUrl: true, + relatedQueryKeys: ["ApiClients", "AzureConfiguration"], + }); + + const azureConfig = ApiGetCall({ + url: "/api/ExecApiClient", + data: { Action: "GetAzureConfiguration" }, + queryKey: "AzureConfiguration", + }); + + const apiClients = ApiGetCallWithPagination({ + url: "/api/ExecApiClient", + data: { Action: "List" }, + queryKey: "ApiClients", + }); + + const handleMenuOpen = (event) => { + setMenuAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + }; + + const handleSaveToAzure = () => { + postCall.mutate({ + url: `/api/ExecApiClient?action=SaveToAzure`, + data: {}, + }); + handleMenuClose(); + }; + + const actions = [ + { + label: "Edit", + icon: ( + + + + ), + confirmText: "Update the API client settings for [AppName]?", + hideBulk: true, + setDefaultValues: true, + fields: [ + { + type: "autoComplete", + name: "Role", + multiple: false, + creatable: false, + label: "Select Role", + placeholder: "Choose a role from the CIPP Role list.", + api: { + url: "/api/ListCustomRole", + queryKey: "CustomRoleList", + labelField: "RoleName", + valueField: "RoleName", + showRefresh: true, + }, + }, + { + type: "autoComplete", + name: "IPRange", + multiple: true, + freeSolo: true, + creatable: true, + options: [], + label: "Enter IP Range (Single hosts or CIDR notation)", + placeholder: "Type in the IP addresses and hit enter.", + }, + { + type: "switch", + name: "Enabled", + label: "Enable this client", + }, + ], + type: "POST", + url: "/api/ExecApiClient", + data: { + Action: "AddUpdate", + ClientId: "ClientId", + }, + relatedQueryKeys: ["ApiClients"], + }, + { + label: "Reset Application Secret", + icon: , + confirmText: "Are you sure you want to reset the application secret for [AppName]?", + type: "POST", + url: "/api/ExecApiClient", + data: { + Action: "ResetSecret", + ClientId: "ClientId", + }, + hideBulk: true, + }, + { + label: "Copy API Scope", + icon: , + noConfirm: true, + customFunction: (row, action, formData) => { + var scope = `api://${row.ClientId}/.default`; + navigator.clipboard.writeText(scope); + }, + hideBulk: true, + }, + { + label: "Delete Client", + icon: , + confirmText: "Are you sure you want to delete [AppName]?", + type: "POST", + url: "/api/ExecApiClient", + data: { + Action: "Delete", + ClientId: "ClientId", + }, + fields: [ + { + type: "switch", + name: "RemoveAppReg", + label: "Remove App Registration", + }, + ], + relatedQueryKeys: ["ApiClients"], + multiPost: false, + }, + ]; + + return ( + <> + + + + + { + handleMenuClose(); + setOpenAddClientDialog(true); + }} + > + + + + Create New Client + + { + handleMenuClose(); + setOpenAddExistingAppDialog(true); + }} + > + + + + Add Existing Client + + { + azureConfig.refetch(); + handleMenuClose(); + }} + > + + + + Refresh Configuration + + + + + + Save to Azure + + + + } + propertyItems={[ + { + label: "Microsoft Authentication Enabled", + value: azureConfig.data?.Results?.Enabled, + }, + { + label: "API Url", + value: azureConfig.data?.Results?.ApiUrl ? ( + + ) : ( + "Not Available" + ), + }, + { + label: "Token URL", + value: azureConfig.data?.Results?.TenantID ? ( + + ) : ( + "Not Available" + ), + }, + { + label: "Tenant ID", + value: azureConfig.data?.Results?.TenantID ? ( + + ) : ( + "Not Available" + ), + }, + ]} + layout="dual" + showDivider={false} + isFetching={azureConfig.isFetching} + /> + {azureConfig.isSuccess && ( + <> + {!isEqual( + apiClients.data?.pages?.[0]?.Results?.filter((c) => c.Enabled) + .map((c) => c.ClientId) + .sort(), + (azureConfig.data?.Results?.ClientIDs || []).sort() + ) && ( + + + You have unsaved changes. Click Actions > Save Azure Configuration to update + the allowed API Clients. + + + )} + + )} + {azureConfig.isSuccess && azureConfig.data?.Results?.Enabled === false && ( + + + Microsoft Authentication is disabled. Configure API Clients and click Actions > + Save Azure Configuration. + + + )} + + + + + + + setOpenAddClientDialog(false), + }} + title="Add Client" + fields={[ + { + type: "textField", + name: "AppName", + label: "App Name", + placeholder: "Enter a name for this Application Registration.", + }, + { + type: "autoComplete", + name: "Role", + multiple: false, + creatable: false, + label: "Select Role", + api: { + url: "/api/ListCustomRole", + queryKey: "CustomRoleList", + labelField: "RoleName", + valueField: "RoleName", + showRefresh: true, + }, + placeholder: "Choose a role from the CIPP Role list.", + }, + { + type: "autoComplete", + name: "IPRange", + multiple: true, + freeSolo: true, + creatable: true, + options: [], + label: "Enter IP Ranges (Single hosts or CIDR notation)", + placeholder: "Type in the IP addresses and hit enter.", + }, + { + type: "switch", + name: "Enabled", + label: "Enable this client", + }, + ]} + api={{ + type: "POST", + url: "/api/ExecApiClient", + data: { Action: "AddUpdate" }, + relatedQueryKeys: [`ApiClients`], + }} + /> + setOpenAddExistingAppDialog(false), + }} + title="Add Existing App" + fields={[ + { + type: "autoComplete", + name: "ClientId", + label: "Existing App", + placeholder: "Select an existing API application.", + api: { + type: "GET", + url: "/api/ExecApiClient", + data: { Action: "ListAvailable" }, + queryKey: `AvailableApiApps`, + dataKey: "Results", + labelField: (app) => `${app.displayName} (${app.appId})`, + valueField: "appId", + addedField: { + displayName: "displayName", + createdDateTime: "createdDateTime", + }, + showRefresh: true, + }, + creatable: false, + multiple: false, + }, + { + type: "autoComplete", + name: "Role", + multiple: false, + creatable: false, + label: "Select Role", + placeholder: "Choose a role from the CIPP Role list.", + api: { + url: "/api/ListCustomRole", + queryKey: "CustomRoleList", + labelField: "RoleName", + valueField: "RoleName", + showRefresh: true, + }, + }, + { + type: "autoComplete", + name: "IPRange", + multiple: true, + freeSolo: true, + creatable: true, + options: [], + label: "Enter IP Ranges (Single hosts or CIDR notation)", + placeholder: "Type in the IP addresses and hit enter.", + }, + { + type: "switch", + name: "Enabled", + label: "Enable this client", + }, + ]} + api={{ + type: "POST", + url: "/api/ExecApiClient", + data: { Action: "!AddUpdate" }, + relatedQueryKeys: [`ApiClients`], + }} + /> + + ); +}; + +export default CippApiClientManagement; diff --git a/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx b/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx index 8b46afd209f7..ae0014d0e276 100644 --- a/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx +++ b/src/components/CippIntegrations/CippIntegrationFieldMapping.jsx @@ -1,12 +1,10 @@ import { Box, - Grid, CardContent, Skeleton, Typography, Divider, Tooltip, - IconButton, Button, Alert, } from "@mui/material"; @@ -18,7 +16,7 @@ import extensions from "/src/data/Extensions.json"; import React, { useEffect, useState } from "react"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { Sync } from "@mui/icons-material"; -import { Stack } from "@mui/system"; +import { Stack, Grid } from "@mui/system"; const CippIntegrationFieldMapping = () => { const router = useRouter(); @@ -113,41 +111,33 @@ const CippIntegrationFieldMapping = () => { )} - + {fieldMapping?.data?.CIPPFields?.filter( (field) => field.FieldType === header.FieldType ).map((field, fieldIndex) => ( - - - - (integrationField?.type === field.Type && - integrationField?.FieldType === field.FieldType) || - integrationField?.type === "unset" - )?.map((integrationField) => { - return { - label: integrationField?.name, - value: integrationField?.value, - }; - })} - formControl={formControl} - multiple={false} - creatable={false} - fullWidth - isFetching={fieldMapping.isFetching} - disableClearable={true} - required={true} - validators={{ - validate: (value) => { - return value ? true : "Please select a value"; - }, - }} - /> - + + + (integrationField?.type === field.Type && + integrationField?.FieldType === field.FieldType) || + integrationField?.type === "unset" + )?.map((integrationField) => { + return { + label: integrationField?.name, + value: integrationField?.value, + }; + })} + formControl={formControl} + multiple={false} + creatable={false} + fullWidth + isFetching={fieldMapping.isFetching} + disableClearable={true} + /> ))} @@ -165,12 +155,12 @@ const CippIntegrationFieldMapping = () => { {fieldMapping.isLoading && ( - + - + @@ -180,7 +170,7 @@ const CippIntegrationFieldMapping = () => { )} {fieldMapping.isSuccess && !extension && ( - + Extension not found diff --git a/src/components/CippIntegrations/CippIntegrationSettings.jsx b/src/components/CippIntegrations/CippIntegrationSettings.jsx index 53334b6ada59..3ff5bea6f256 100644 --- a/src/components/CippIntegrations/CippIntegrationSettings.jsx +++ b/src/components/CippIntegrations/CippIntegrationSettings.jsx @@ -1,4 +1,5 @@ -import { Box, Grid } from "@mui/material"; +import { Box, CardContent } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormSection from "/src/components/CippFormPages/CippFormSection"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useForm } from "react-hook-form"; @@ -6,7 +7,8 @@ import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall"; import { useRouter } from "next/router"; import extensions from "/src/data/Extensions.json"; -import { useEffect } from "react"; +import React, { useEffect } from "react"; +import { CippFormCondition } from "../CippComponents/CippFormCondition"; const CippIntegrationSettings = ({ children }) => { const router = useRouter(); @@ -16,6 +18,8 @@ const CippIntegrationSettings = ({ children }) => { const integrations = ApiGetCall({ url: "/api/ListExtensionsConfig", queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, }); const formControl = useForm({ @@ -24,6 +28,7 @@ const CippIntegrationSettings = ({ children }) => { }); const extension = extensions.find((extension) => extension.id === router.query.id); + const enabled = formControl.watch(`${extension?.id}.Enabled`); var logo = extension?.logo; if (preferredTheme === "dark" && extension?.logoDark) { @@ -43,7 +48,7 @@ const CippIntegrationSettings = ({ children }) => { <> {integrations.isSuccess && extension ? ( { resetForm={false} > {children} - {extension.SettingOptions.map((setting, index) => ( - - - - - + + {setting?.condition ? ( + s.name === `${extension.id}.Enabled`) && !enabled}> + + + + + + + ) : ( + + + + + + )} + ))} @@ -77,7 +102,7 @@ const CippIntegrationSettings = ({ children }) => { {integrations.isLoading && Loading...} {integrations.isSuccess && !extension && ( - + Extension not found diff --git a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx index 8f1ecc67ec5c..23351820c1bd 100644 --- a/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx +++ b/src/components/CippIntegrations/CippIntegrationTenantMapping.jsx @@ -3,13 +3,13 @@ import { Button, CardActions, CardContent, - Grid, Stack, Skeleton, SvgIcon, Tooltip, Typography, } from "@mui/material"; +import { Grid } from "@mui/system"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; @@ -85,47 +85,49 @@ const CippIntegrationSettings = ({ children }) => { if (tableData?.find((item) => item.TenantId === selectedTenant.addedFields.customerId)) return; const newRowData = { - TenantId: selectedTenant.addedFields.customerId, + TenantId: selectedTenant.value, Tenant: selectedTenant.label, IntegrationName: selectedCompany.label, IntegrationId: selectedCompany.value, + TenantDomain: selectedTenant.addedFields.defaultDomainName, }; setTableData([...tableData, newRowData]); }; const handleAutoMap = () => { - const newTableData = []; - tenantList.data?.pages[0]?.forEach((tenant) => { - const matchingCompany = mappings.data.Companies.find( - (company) => company.name === tenant.displayName - ); - if ( - Array.isArray(tableData) && - tableData?.find((item) => item.TenantId === tenant.customerId) - ) - return; - if (matchingCompany) { - newTableData.push({ - TenantId: tenant.customerId, - Tenant: tenant.displayName, - IntegrationName: matchingCompany.name, - IntegrationId: matchingCompany.value, - }); - } - }); - if (Array.isArray(tableData)) { - setTableData([...tableData, ...newTableData]); - } else { - setTableData(newTableData); - } - if (extension.autoMapSyncApi) { - automapPostCall.mutate({ - url: `/api/ExecExtensionMapping?AutoMapping=${router.query.id}`, - queryKey: `IntegrationTenantMapping-${router.query.id}`, + const newTableData = []; + tenantList.data?.pages[0]?.forEach((tenant) => { + const matchingCompany = mappings.data.Companies.find( + (company) => company.name === tenant.displayName + ); + if ( + Array.isArray(tableData) && + tableData?.find((item) => item.TenantId === tenant.customerId) + ) + return; + if (matchingCompany) { + newTableData.push({ + TenantId: tenant.customerId, + Tenant: tenant.displayName, + TenantDomain: tenant.defaultDomainName, + IntegrationName: matchingCompany.name, + IntegrationId: matchingCompany.value, }); } - }; + }); + if (Array.isArray(tableData)) { + setTableData([...tableData, ...newTableData]); + } else { + setTableData(newTableData); + } + if (extension.autoMapSyncApi) { + automapPostCall.mutate({ + url: `/api/ExecExtensionMapping?AutoMapping=${router.query.id}`, + queryKey: `IntegrationTenantMapping-${router.query.id}`, + }); + } + }; const actions = [ { @@ -140,7 +142,7 @@ const CippIntegrationSettings = ({ children }) => { useEffect(() => { if (mappings.isSuccess) { - setTableData(mappings.data.Mappings); + setTableData(mappings.data.Mappings ?? []); } }, [mappings.isSuccess]); @@ -160,24 +162,26 @@ const CippIntegrationSettings = ({ children }) => { mb: 3, }} > - + item.TenantId)} + valueField="customerId" /> - + - + { creatable={false} multiple={false} isFetching={mappings.isFetching} + sortOptions={true} /> - + + + + } + > + + + Customize your organization's branding for reports and documents. Changes will be applied + to all generated reports. + + + {/* Logo Upload Section */} + + + Logo + + + + + + {logoPreview && ( + + Logo preview + + )} + + + Recommended: PNG format, max 2MB, optimal size 200x100px + + + + + {/* Color Picker Section */} + + + Brand Color + + + formControl.setValue("colour", e.target.value)} + style={{ + width: "50px", + height: "40px", + border: "1px solid #ddd", + borderRadius: "4px", + cursor: "pointer", + }} + /> + + + + This color will be used for accents and highlights in reports + + + + {/* Preview Section */} + + + Preview + + + {logoPreview && ( + Logo + )} + + + Your Organization + + + Executive Report Preview + + + + + + {/* API Results inside the card */} + + + + ); +}; + +export default CippBrandingSettings; diff --git a/src/components/CippSettings/CippCacheSettings.jsx b/src/components/CippSettings/CippCacheSettings.jsx index 5a2e97622056..ad736adf604f 100644 --- a/src/components/CippSettings/CippCacheSettings.jsx +++ b/src/components/CippSettings/CippCacheSettings.jsx @@ -68,9 +68,9 @@ const CippCacheSettings = () => { api={{ url: "/api/ListTenants", confirmText: - "This will clear the cache used by CIPP. This will slow down some aspects of the application, and should only be used when instructed to do so by support.", - type: "GET", - data: { ClearCache: "!true", TenantsOnly: "tenantsOnly" }, + "This will clear the cache used by CIPP. This will slow down some aspects of the application, and should only be used when instructed to do so by support. This will delete any cache tables including pending audit logs that have not yet been processed. Are you sure you want to continue?", + type: "POST", + data: { ClearCache: true }, replacementBehaviour: "removeNulls", }} row={{}} diff --git a/src/components/CippSettings/CippCustomRoles.jsx b/src/components/CippSettings/CippCustomRoles.jsx index 6387ae339a4f..aae96ba842bc 100644 --- a/src/components/CippSettings/CippCustomRoles.jsx +++ b/src/components/CippSettings/CippCustomRoles.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { Box, @@ -13,7 +13,7 @@ import { Skeleton, } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "../../api/ApiCall"; import { CippOffCanvas } from "/src/components/CippComponents/CippOffCanvas"; import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; @@ -67,10 +67,7 @@ export const CippCustomRoles = () => { queryKey: "customRoleList", }); - const { - data: { pages = [] } = {}, - isSuccess: tenantsSuccess, - } = ApiGetCallWithPagination({ + const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({ url: "/api/ListTenants?AllTenantSelector=true", queryKey: "ListTenants-AllTenantSelector", }); @@ -201,7 +198,7 @@ export const CippCustomRoles = () => { > {obj} - + + {importReport && ( } - isFetching={executeCheck.isFetching} + isFetching={!importReport && executeCheck.isFetching} refreshFunction={executeCheck} data={results?.Results?.CPVRefreshList} simpleColumns={["DisplayName", "DefaultDomainName", "LastRefresh"]} @@ -220,7 +220,7 @@ export const CippPermissionResults = (props) => { <> { return { @@ -231,6 +231,21 @@ export const CippPermissionResults = (props) => { /> )} + {results?.Results?.ApplicationTokenDetails?.Roles.length > 0 && ( + <> + { + return { + Role: role, + }; + })} + simpleColumns={["Role"]} + /> + + )} )} diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx new file mode 100644 index 000000000000..b7fb9e7a2b27 --- /dev/null +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -0,0 +1,636 @@ +import { useEffect, useState } from "react"; + +import { + Box, + Button, + Alert, + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Stack, + SvgIcon, + Skeleton, +} from "@mui/material"; + +import { Grid } from "@mui/system"; +import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "../../api/ApiCall"; +import { CippOffCanvas } from "/src/components/CippComponents/CippOffCanvas"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; +import { Save, WarningOutlined } from "@mui/icons-material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { useForm, useFormState, useWatch } from "react-hook-form"; +import { InformationCircleIcon } from "@heroicons/react/24/outline"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import cippRoles from "../../data/cipp-roles.json"; + +export const CippRoleAddEdit = ({ selectedRole }) => { + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["customRoleList", "customRoleTable"], + }); + + const [allTenantSelected, setAllTenantSelected] = useState(false); + const [cippApiRoleSelected, setCippApiRoleSelected] = useState(false); + const [selectedRoleState, setSelectedRoleState] = useState(null); + const [updateDefaults, setUpdateDefaults] = useState(false); + const [baseRolePermissions, setBaseRolePermissions] = useState({}); + const [isBaseRole, setIsBaseRole] = useState(false); + + const formControl = useForm({ + mode: "onChange", + }); + + const formState = useFormState({ control: formControl.control }); + + const validateRoleName = (value) => { + if ( + customRoleList?.pages?.[0]?.some( + (role) => role?.RowKey?.toLowerCase() === value?.toLowerCase() + ) + ) { + return `Role '${value}' already exists`; + } + return true; + }; + + const selectedTenant = useWatch({ control: formControl.control, name: "allowedTenants" }); + const blockedTenants = useWatch({ control: formControl.control, name: "blockedTenants" }); + const setDefaults = useWatch({ control: formControl.control, name: "Defaults" }); + const selectedPermissions = useWatch({ control: formControl.control, name: "Permissions" }); + const selectedEntraGroup = useWatch({ control: formControl.control, name: "EntraGroup" }); + + const { + data: apiPermissions = [], + isFetching: apiPermissionFetching, + isSuccess: apiPermissionSuccess, + } = ApiGetCall({ + url: "/api/ExecAPIPermissionList", + queryKey: "apiPermissions", + }); + + const { + data: customRoleList = [], + isFetching: customRoleListFetching, + isSuccess: customRoleListSuccess, + } = ApiGetCallWithPagination({ + url: "/api/ExecCustomRole", + queryKey: "customRoleList", + }); + + const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({ + url: "/api/ListTenants?AllTenantSelector=true", + queryKey: "ListTenants-AllTenantSelector", + }); + const tenants = pages[0] || []; + + const matchPattern = (pattern, value) => { + const regex = new RegExp(`^${pattern.replace("*", ".*")}$`); + return regex.test(value); + }; + + const getBaseRolePermissions = (role) => { + const roleConfig = cippRoles[role]; + if (!roleConfig) return {}; + + const permissions = {}; + Object.keys(apiPermissions).forEach((cat) => { + Object.keys(apiPermissions[cat]).forEach((obj) => { + const includeRead = roleConfig.include.some((pattern) => + matchPattern(pattern, `${cat}.${obj}.Read`) + ); + const includeReadWrite = roleConfig.include.some((pattern) => + matchPattern(pattern, `${cat}.${obj}.ReadWrite`) + ); + const excludeRead = roleConfig.exclude.some((pattern) => + matchPattern(pattern, `${cat}.${obj}.Read`) + ); + const excludeReadWrite = roleConfig.exclude.some((pattern) => + matchPattern(pattern, `${cat}.${obj}.ReadWrite`) + ); + + if ((includeRead || includeReadWrite) && !(excludeRead || excludeReadWrite)) { + if (!permissions[cat]) permissions[cat] = {}; + permissions[cat][obj] = includeReadWrite ? `ReadWrite` : `Read`; + } + if (!permissions[cat] || !permissions[cat][obj]) { + if (!permissions[cat]) permissions[cat] = {}; + permissions[cat][obj] = `None`; + } + }); + }); + return permissions; + }; + + useEffect(() => { + if (selectedRole && cippRoles[selectedRole]) { + setBaseRolePermissions(getBaseRolePermissions(selectedRole)); + setIsBaseRole(true); + } else { + setBaseRolePermissions({}); + setIsBaseRole(false); + } + }, [selectedRole, apiPermissions]); + + useEffect(() => { + if ( + (customRoleListSuccess && + tenantsSuccess && + selectedRole && + selectedRoleState !== selectedRole) || + baseRolePermissions + ) { + setSelectedRoleState(selectedRole); + const isApiRole = selectedRole === "api-role"; + setCippApiRoleSelected(isApiRole); + + const currentPermissions = customRoleList?.pages?.[0]?.find( + (role) => role.RowKey === selectedRole + ); + + // Process allowed tenants - handle both groups and tenant IDs + var newAllowedTenants = []; + currentPermissions?.AllowedTenants?.forEach((item) => { + if (typeof item === "object" && item.type === "Group") { + // Handle group objects + newAllowedTenants.push({ + label: item.label, + value: item.value, + type: "Group", + }); + } else { + // Handle tenant customer IDs (legacy format) + var tenantInfo = tenants.find((t) => t.customerId === item); + if (tenantInfo?.displayName) { + var label = `${tenantInfo.displayName} (${tenantInfo.defaultDomainName})`; + newAllowedTenants.push({ + label: label, + value: tenantInfo.defaultDomainName, + type: "Tenant", + addedFields: { + defaultDomainName: tenantInfo.defaultDomainName, + displayName: tenantInfo.displayName, + customerId: tenantInfo.customerId, + }, + }); + } + } + }); + + // Process blocked tenants - handle both groups and tenant IDs + var newBlockedTenants = []; + currentPermissions?.BlockedTenants?.forEach((item) => { + if (typeof item === "object" && item.type === "Group") { + // Handle group objects + newBlockedTenants.push({ + label: item.label, + value: item.value, + type: "Group", + }); + } else { + // Handle tenant customer IDs (legacy format) + var tenantInfo = tenants.find((t) => t.customerId === item); + if (tenantInfo?.displayName) { + var label = `${tenantInfo.displayName} (${tenantInfo.defaultDomainName})`; + newBlockedTenants.push({ + label: label, + value: tenantInfo.defaultDomainName, + type: "Tenant", + addedFields: { + defaultDomainName: tenantInfo.defaultDomainName, + displayName: tenantInfo.displayName, + customerId: tenantInfo.customerId, + }, + }); + } + } + }); + + const basePermissions = {}; + Object.entries(getBaseRolePermissions(selectedRole)).forEach(([cat, objects]) => { + Object.entries(objects).forEach(([obj, permission]) => { + basePermissions[`${cat}${obj}`] = `${cat}.${obj}.${permission}`; + }); + }); + const processPermissions = (permissions) => { + const processed = {}; + Object.keys(apiPermissions).forEach((cat) => { + Object.keys(apiPermissions[cat]).forEach((obj) => { + const key = `${cat}${obj}`; + const existingPerm = permissions?.[key]; + processed[key] = existingPerm || `${cat}.${obj}.None`; + }); + }); + return processed; + }; + + formControl.reset({ + Permissions: + basePermissions && Object.keys(basePermissions).length > 0 + ? basePermissions + : processPermissions(currentPermissions?.Permissions), + RoleName: selectedRole ?? currentPermissions?.RowKey, + allowedTenants: newAllowedTenants, + blockedTenants: newBlockedTenants, + EntraGroup: currentPermissions?.EntraGroup, + }); + } + }, [customRoleList, customRoleListSuccess, tenantsSuccess, baseRolePermissions]); + + useEffect(() => { + if (updateDefaults !== setDefaults) { + setUpdateDefaults(setDefaults); + var newPermissions = {}; + Object.keys(apiPermissions).forEach((cat) => { + Object.keys(apiPermissions[cat]).forEach((obj) => { + var newval = ""; + if (cat == "CIPP" && obj == "Core" && setDefaults == "None") { + newval = "Read"; + } else { + newval = setDefaults; + } + newPermissions[`${cat}${obj}`] = `${cat}.${obj}.${newval}`; + }); + }); + formControl.setValue("Permissions", newPermissions); + } + }, [setDefaults, updateDefaults]); + + useEffect(() => { + var alltenant = false; + selectedTenant?.map((tenant) => { + if (tenant?.value === "AllTenants") { + alltenant = true; + } + }); + if (alltenant) { + setAllTenantSelected(true); + } else { + setAllTenantSelected(false); + } + }, [selectedTenant, blockedTenants]); + + useEffect(() => { + if (selectedRole) { + formControl.setValue("RoleName", selectedRole); + } + }, [selectedRole]); + + const handleSubmit = () => { + let values = formControl.getValues(); + + // Process allowed tenants - preserve groups and convert tenants to IDs + const processedAllowedTenants = + selectedTenant + ?.map((tenant) => { + if (tenant.type === "Group") { + // Keep groups as-is for backend processing + return { + type: "Group", + value: tenant.value, + label: tenant.label, + }; + } else { + // Convert tenant domain names to customer IDs + const tenantInfo = tenants.find((t) => t.defaultDomainName === tenant.value); + return tenantInfo?.customerId; + } + }) + .filter(Boolean) || []; + + // Process blocked tenants - preserve groups and convert tenants to IDs + const processedBlockedTenants = + blockedTenants + ?.map((tenant) => { + if (tenant.type === "Group") { + // Keep groups as-is for backend processing + return { + type: "Group", + value: tenant.value, + label: tenant.label, + }; + } else { + // Convert tenant domain names to customer IDs + const tenantInfo = tenants.find((t) => t.defaultDomainName === tenant.value); + return tenantInfo?.customerId; + } + }) + .filter(Boolean) || []; + + updatePermissions.mutate({ + url: "/api/ExecCustomRole?Action=AddUpdate", + data: { + RoleName: values?.["RoleName"], + Permissions: selectedPermissions, + EntraGroup: selectedEntraGroup, + AllowedTenants: processedAllowedTenants, + BlockedTenants: processedBlockedTenants, + }, + }); + }; + + const ApiPermissionRow = ({ obj, cat, readOnly }) => { + const [offcanvasVisible, setOffcanvasVisible] = useState(false); + + var items = []; + for (var key in apiPermissions[cat][obj]) + for (var key2 in apiPermissions[cat][obj][key]) { + items.push({ heading: "", content: apiPermissions[cat][obj][key][key2] }); + } + var group = [{ items: items }]; + + return ( + + {obj} + + + + + { + setOffcanvasVisible(false); + }} + > + + + {`${cat}.${obj}`} + + + Listed below are the available API endpoints based on permission level, ReadWrite + level includes endpoints under Read. + + {[apiPermissions[cat][obj]].map((permissions, key) => { + var sections = Object.keys(permissions).map((type) => { + var items = []; + for (var api in permissions[type]) { + items.push({ heading: "", content: permissions[type][api] }); + } + return ( + + {type} + + {items.map((item, idx) => ( + + {item.content} + + ))} + + + ); + }); + return sections; + })} + + + + ); + }; + + return ( + <> + + + + {!selectedRole && ( + + )} + {selectedRole && isBaseRole && ["admin", "superadmin"].includes(selectedRole) && ( + }> + This is a highly privileged role and overrides any custom role restrictions. + + )} + {cippApiRoleSelected && ( + + This is the default role for all API clients in the CIPP-API integration. If you + would like different permissions for specific applications, create a role per + application and select it from the CIPP-API integrations page. + + )} + + + {!isBaseRole && ( + <> + + + {allTenantSelected && blockedTenants?.length == 0 && ( + + All tenants selected, no tenant restrictions will be applied unless blocked + tenants are specified. + + )} + + {allTenantSelected && ( + + + + )} + + )} + {apiPermissionFetching && } + {apiPermissionSuccess && ( + <> + API Permissions + {!isBaseRole && ( + + Set All Permissions + + + + + + )} + + <> + {Object.keys(apiPermissions) + .sort() + .map((cat, catIndex) => ( + + }>{cat} + + {Object.keys(apiPermissions[cat]) + .sort() + .map((obj, index) => { + const readOnly = baseRolePermissions?.[cat] ? true : false; + return ( + + + + ); + })} + + + ))} + + + + )} + + + + {selectedEntraGroup && ( + + This role will be assigned to the Entra Group:{" "} + {selectedEntraGroup.label} + + )} + {selectedTenant?.length > 0 && ( + <> +
    Allowed Tenants
    +
      + {selectedTenant.map((tenant, idx) => ( +
    • {tenant?.label}
    • + ))} +
    + + )} + {blockedTenants?.length > 0 && ( + <> +
    Blocked Tenants
    +
      + {blockedTenants.map((tenant, idx) => ( +
    • {tenant?.label}
    • + ))} +
    + + )} + {selectedPermissions && apiPermissionSuccess && ( + <> +
    Selected Permissions
    +
      + {selectedPermissions && + Object.keys(selectedPermissions) + ?.sort() + .map((cat, idx) => ( + <> + {selectedPermissions?.[cat] && + typeof selectedPermissions[cat] === "string" && + !selectedPermissions[cat]?.includes("None") && ( +
    • {selectedPermissions[cat]}
    • + )} + + ))} +
    + + )} +
    +
    + + + + + + + ); +}; + +export default CippRoleAddEdit; diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx new file mode 100644 index 000000000000..15766897d4f4 --- /dev/null +++ b/src/components/CippSettings/CippRoles.jsx @@ -0,0 +1,115 @@ +import React from "react"; +import { Box, Button, SvgIcon } from "@mui/material"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import NextLink from "next/link"; +import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; +import { Stack } from "@mui/system"; +import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; + +const CippRoles = () => { + const actions = [ + { + label: "Edit", + icon: ( + + + + ), + link: "/cipp/super-admin/cipp-roles/edit?role=[RoleName]", + }, + { + label: "Delete", + icon: ( + + + + ), + confirmText: "Are you sure you want to delete this custom role?", + url: "/api/ExecCustomRole", + type: "POST", + data: { + Action: "Delete", + RoleName: "RoleName", + }, + condition: (row) => row?.Type === "Custom", + relatedQueryKeys: ["customRoleList"], + }, + ]; + + const offCanvas = { + children: (data) => { + const includeProps = ["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]; + const keys = includeProps.filter((key) => Object.keys(data).includes(key)); + const properties = []; + keys.forEach((key) => { + if (data[key] && data[key].length > 0) { + properties.push({ + label: getCippTranslation(key), + value: getCippFormatting(data[key], key), + }); + } + }); + + if (data["Permissions"] && Object.keys(data["Permissions"]).length > 0) { + properties.push({ + label: "Permissions", + value: ( + + {Object.keys(data["Permissions"]) + .sort() + .map((permission, idx) => ( + + + + ))} + + ), + }); + } + + return ( + + ); + }, + }; + + return ( + + + + + } + component={NextLink} + href="/cipp/super-admin/cipp-roles/add" + > + Add Role + + } + api={{ + url: "/api/ListCustomRole", + }} + queryKey="customRoleTable" + simpleColumns={["RoleName", "Type", "EntraGroup", "AllowedTenants", "BlockedTenants"]} + offCanvas={offCanvas} + /> + + ); +}; + +export default CippRoles; diff --git a/src/components/CippSettings/CippTenantResults.jsx b/src/components/CippSettings/CippTenantResults.jsx index 9483a07f7a2e..dc79285ccb4e 100644 --- a/src/components/CippSettings/CippTenantResults.jsx +++ b/src/components/CippSettings/CippTenantResults.jsx @@ -1,5 +1,5 @@ import { CippDataTable } from "../CippTable/CippDataTable"; -import { Sync } from "@mui/icons-material"; +import { Plumbing, Sync } from "@mui/icons-material"; export const CippTenantResults = (props) => { const { importReport = false } = props; @@ -28,6 +28,9 @@ export const CippTenantResults = (props) => { "LastRun", "GraphTest", "ExchangeTest", + "OrgManagementRepairNeeeded", + "OrgManagementRoles", + "OrgManagementRolesMissing", ], }} /> @@ -54,6 +57,15 @@ export const CippTenantResults = (props) => { relatedQueryKeys: "ExecAccessChecks-Tenants", multiPost: false, }, + { + label: "Repair Exchange Roles", + type: "POST", + url: "/api/ExecExchangeRoleRepair", + data: { TenantId: "TenantId" }, + icon: , + confirmText: "Repair Exchange roles for [TenantName]?", + condition: (row) => row.OrgManagementRepairNeeded === true, + }, ]} simpleColumns={[ "TenantName", diff --git a/src/components/CippSettings/CippVersionProperties.jsx b/src/components/CippSettings/CippVersionProperties.jsx index dca288ede4dd..6fbf88a39311 100644 --- a/src/components/CippSettings/CippVersionProperties.jsx +++ b/src/components/CippSettings/CippVersionProperties.jsx @@ -1,4 +1,4 @@ -import { Box, Button, Skeleton, SvgIcon } from "@mui/material"; +import { Box, Button, SvgIcon } from "@mui/material"; import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; import { CheckCircle, SystemUpdateAlt, Warning } from "@mui/icons-material"; import { ApiGetCall } from "/src/api/ApiCall"; diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index c0e8589212e2..e5ad67d6f925 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -9,11 +9,26 @@ import { SvgIcon, Collapse, Divider, - Grid, Tooltip, Chip, + TextField, + InputAdornment, + ButtonGroup, + Button, } from "@mui/material"; -import { ExpandMore as ExpandMoreIcon, Delete, Add, Public, TableChart } from "@mui/icons-material"; +import { + ExpandMore as ExpandMoreIcon, + Delete, + Add, + Public, + Search, + Close, + FilterAlt, + NotificationImportant, + Assignment, + Construction, +} from "@mui/icons-material"; +import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useWatch } from "react-hook-form"; import _ from "lodash"; @@ -24,6 +39,9 @@ import Defender from "../../icons/iconly/bulk/defender"; import Intune from "../../icons/iconly/bulk/intune"; import GDAPRoles from "/src/data/GDAPRoles"; import timezoneList from "/src/data/timezoneList"; +import standards from "/src/data/standards.json"; +import { CippFormCondition } from "../CippComponents/CippFormCondition"; +import ReactMarkdown from "react-markdown"; const getAvailableActions = (disabledFeatures) => { const allActions = [ @@ -49,12 +67,13 @@ const CippAddedComponent = React.memo(({ standardName, component, formControl }) label: tz.timezone, value: tz.timezone, })); + updatedComponent.multiple = false; } else { updatedComponent.type = component.type; } return ( - + { const [configuredState, setConfiguredState] = useState({}); + const [filter, setFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + const [savedValues, setSavedValues] = useState({}); + const [originalValues, setOriginalValues] = useState({}); const watchedValues = useWatch({ control: formControl.control, }); + // Watch all trackDrift values for all standards at once + const allTrackDriftValues = useWatch({ + control: formControl.control, + name: Object.keys(selectedStandards).map((standardName) => `${standardName}.trackDrift`), + }); + + // Handle drift mode automatic action setting useEffect(() => { - const newConfiguredState = { ...configuredState }; + if (isDriftMode && selectedStandards) { + Object.keys(selectedStandards).forEach((standardName) => { + const currentValues = formControl.getValues(standardName) || {}; + const autoRemediate = currentValues.autoRemediate; - Object.keys(selectedStandards).forEach((standardName) => { - const standard = standards.find((s) => s.name === standardName.split("[")[0]); - if (standard) { - const actionFilled = !!_.get(watchedValues, `${standardName}.action`, false); - - const addedComponentsFilled = - standard.addedComponent?.every((component) => { - const isRequired = component.required !== false && component.type !== "switch"; - if (!isRequired) return true; - return !!_.get(watchedValues, `${standardName}.${component.name}`); - }) ?? true; - - const isConfigured = actionFilled && addedComponentsFilled; - - // Only update state if there's a change to reduce unnecessary re-renders. - if (newConfiguredState[standardName] !== isConfigured) { - newConfiguredState[standardName] = isConfigured; + // Set default action based on autoRemediate setting + const defaultAction = autoRemediate + ? [ + { label: "Report", value: "Report" }, + { label: "Remediate", value: "Remediate" }, + ] + : [{ label: "Report", value: "Report" }]; + + // Only set if action is not already set + if (!currentValues.action) { + formControl.setValue(`${standardName}.action`, defaultAction); + } + + // Set default autoRemediate if not set + if (currentValues.autoRemediate === undefined) { + formControl.setValue(`${standardName}.autoRemediate`, false); + formControl.setValue(`${standardName}.action`, [ + { label: "Report", value: "Report" }, + ]); } + }); + } + }, [isDriftMode, selectedStandards, formControl]); + + // Check if a standard is configured based on its values + const isStandardConfigured = (standardName, standard, values) => { + if (!values) return false; + + // ALWAYS require an action for any standard to be considered configured + // The action field should be an array with at least one element + const actionValue = _.get(values, "action"); + if (!actionValue || (Array.isArray(actionValue) && actionValue.length === 0)) return false; + + // Additional checks for required components + const hasRequiredComponents = + standard.addedComponent && + standard.addedComponent.some((comp) => comp.type !== "switch" && comp.required !== false); + const actionRequired = standard.disabledFeatures !== undefined || hasRequiredComponents; + + // Always require an action (should be an array with at least one element) + const actionFilled = actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); + + const addedComponentsFilled = + standard.addedComponent?.every((component) => { + // Always skip switches + if (component.type === "switch") return true; + + // Handle conditional fields + if (component.condition) { + const conditionField = component.condition.field; + const conditionValue = _.get(values, conditionField); + const compareType = component.condition.compareType || "is"; + const compareValue = component.condition.compareValue; + const propertyName = component.condition.propertyName || "value"; + + let conditionMet = false; + if (propertyName === "value") { + switch (compareType) { + case "is": + conditionMet = _.isEqual(conditionValue, compareValue); + break; + case "isNot": + conditionMet = !_.isEqual(conditionValue, compareValue); + break; + default: + conditionMet = false; + } + } else if (Array.isArray(conditionValue)) { + switch (compareType) { + case "valueEq": + conditionMet = conditionValue.some((item) => item?.[propertyName] === compareValue); + break; + default: + conditionMet = false; + } + } + + // If condition is not met, skip validation for this field + if (!conditionMet) return true; + } + + // Check if field is required + const isRequired = component.required !== false; + if (!isRequired) return true; + + // Get field value using lodash's get to properly handle nested properties + const fieldValue = _.get(values, component.name); + + // Check if field has a value based on its type and multiple property + if (component.type === "autoComplete" || component.type === "select") { + if (component.multiple) { + // For multiple selection, check if array exists and has items + return Array.isArray(fieldValue) && fieldValue.length > 0; + } else { + // For single selection, check if value exists + return !!fieldValue; + } + } + + // For other field types + return !!fieldValue; + }) ?? true; + + return actionFilled && addedComponentsFilled; + }; + + // Initialize when watchedValues are available + useEffect(() => { + if (editMode) { + // Only run initialization if we have watchedValues and they contain data + if (!watchedValues || Object.keys(watchedValues).length === 0) { + return; + } + + // Prevent re-initialization if we already have configuration state + const hasConfigState = Object.keys(configuredState).length > 0; + if (hasConfigState) { + return; } - }); - if (!_.isEqual(newConfiguredState, configuredState)) { - setConfiguredState(newConfiguredState); + const initial = {}; + const initialConfigured = {}; + + // For each standard, get its current values and determine if it's configured + Object.keys(selectedStandards).forEach((standardName) => { + const currentValues = _.get(watchedValues, standardName); + if (!currentValues) return; + + initial[standardName] = _.cloneDeep(currentValues); + + const baseStandardName = standardName.split("[")[0]; + const standard = providedStandards.find((s) => s.name === baseStandardName); + if (standard) { + initialConfigured[standardName] = isStandardConfigured( + standardName, + standard, + currentValues + ); + } + }); + + // Store both the initial values and set them as current saved values + setOriginalValues(initial); + setSavedValues(initial); + setConfiguredState(initialConfigured); + // Only depend on watchedValues and selectedStandards to avoid infinite loops + // eslint-disable-next-line react-hooks/exhaustive-deps } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [watchedValues, standards, selectedStandards]); - - return Object.keys(selectedStandards)?.map((standardName) => { - const standard = standards.find((s) => s.name === standardName.split("[")[0]); - if (!standard) return null; - - const isExpanded = expanded === standardName; - const hasAddedComponents = standard.addedComponent && standard.addedComponent.length > 0; - const isConfigured = configuredState[standardName]; - const disabledFeatures = standard.disabledFeatures || {}; - - let selectedActions = _.get(watchedValues, `${standardName}.action`); - //if selectedActions is not an array, convert it to an array - if (selectedActions && !Array.isArray(selectedActions)) { - selectedActions = [selectedActions]; + }, [watchedValues, selectedStandards, editMode]); + + // Save changes for a standard + const handleSave = (standardName, standard, current) => { + // Clone the current values to avoid reference issues + const newValues = _.cloneDeep(current); + + // Update saved values + setSavedValues((prev) => ({ + ...prev, + [standardName]: newValues, + })); + + // Update configured state right away + const isConfigured = isStandardConfigured(standardName, standard, newValues); + + setConfiguredState((prev) => ({ + ...prev, + [standardName]: isConfigured, + })); + + // Collapse the accordion after saving + handleAccordionToggle(null); + }; + + // Handle auto-remediate toggle in drift mode + const handleAutoRemediateChange = (standardName, value) => { + const action = value + ? [ + { label: "Report", value: "Report" }, + { label: "Remediate", value: "Remediate" }, + ] + : [{ label: "Report", value: "Report" }]; + + formControl.setValue(`${standardName}.autoRemediate`, value); + formControl.setValue(`${standardName}.action`, action); + }; + + // Cancel changes for a standard + const handleCancel = (standardName) => { + // Get the last saved values + const savedValue = _.get(savedValues, standardName); + if (!savedValue) return; + + // Set the entire standard's value at once to ensure proper handling of nested objects and arrays + formControl.setValue(standardName, _.cloneDeep(savedValue)); + + // Find the original standard definition to get the base standard + const baseStandardName = standardName.split("[")[0]; + const standard = providedStandards.find((s) => s.name === baseStandardName); + + // Determine if the standard was configured with saved values + if (standard) { + const isConfigured = isStandardConfigured(standardName, standard, savedValue); + + // Restore the previous configuration state + setConfiguredState((prev) => ({ + ...prev, + [standardName]: isConfigured, + })); } - const selectedTemplateName = standard.multiple - ? _.get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`) - : ""; - const accordionTitle = selectedTemplateName - ? `${standard.label} - ${selectedTemplateName.label}` - : standard.label; - - return ( - - - - - {standard.cat === "Global Standards" ? ( - - ) : standard.cat === "Entra (AAD) Standards" ? ( - - ) : standard.cat === "Exchange Standards" ? ( - - ) : standard.cat === "Defender Standards" ? ( - - ) : standard.cat === "Intune Standards" ? ( - - ) : ( - - )} - - - {accordionTitle} - {selectedActions && selectedActions?.length > 0 && ( - - {selectedActions?.map((action, index) => ( - - ))} - - )} - - {standard.helpText} - - - - - {standard.multiple && ( - - handleAddMultipleStandard(standardName)}> - - - - )} - - {isConfigured ? "Configured" : "Unconfigured"} - handleRemoveStandard(standardName)}> - - - - handleAccordionToggle(standardName)}> - { + const result = {}; + + Object.keys(selectedStandards).forEach((standardName) => { + const baseStandardName = standardName.split("[")[0]; + const standard = providedStandards.find((s) => s.name === baseStandardName); + if (!standard) return; + + const standardInfo = standards.find((s) => s.name === baseStandardName); + const category = standardInfo?.cat || "Other Standards"; + + if (!result[category]) { + result[category] = []; + } + + result[category].push({ + standardName, + standard, + }); + }); + + Object.keys(result).forEach((category) => { + result[category].sort((a, b) => a.standard.label.localeCompare(b.standard.label)); + }); + + return result; + }, [selectedStandards, providedStandards]); + + // Filter standards based on search and filter selection + const filteredGroupedStandards = useMemo(() => { + if (!searchQuery && filter === "all") { + return groupedStandards; + } + + const result = {}; + const searchLower = searchQuery.toLowerCase(); + + Object.keys(groupedStandards).forEach((category) => { + const categoryMatchesSearch = !searchQuery || category.toLowerCase().includes(searchLower); + + const filteredStandards = groupedStandards[category].filter(({ standardName, standard }) => { + // If this is the currently expanded standard, always include it in the result + if (standardName === expanded) { + return true; + } + + const matchesSearch = + !searchQuery || + categoryMatchesSearch || + standard.label.toLowerCase().includes(searchLower) || + (standard.helpText && standard.helpText.toLowerCase().includes(searchLower)) || + (standard.cat && standard.cat.toLowerCase().includes(searchLower)) || + (standard.tag && + Array.isArray(standard.tag) && + standard.tag.some((tag) => tag.toLowerCase().includes(searchLower))); + + const isConfigured = _.get(configuredState, standardName); + const matchesFilter = + filter === "all" || + (filter === "configured" && isConfigured) || + (filter === "unconfigured" && !isConfigured); + + return matchesSearch && matchesFilter; + }); + + if (filteredStandards.length > 0) { + result[category] = filteredStandards; + } + }); + + return result; + }, [groupedStandards, searchQuery, filter, configuredState]); + + // Count standards by configuration state + const standardCounts = useMemo(() => { + let allCount = 0; + let configuredCount = 0; + let unconfiguredCount = 0; + + Object.keys(groupedStandards).forEach((category) => { + groupedStandards[category].forEach(({ standardName }) => { + allCount++; + if (configuredState[standardName]) { + configuredCount++; + } else { + unconfiguredCount++; + } + }); + }); + + return { allCount, configuredCount, unconfiguredCount }; + }, [groupedStandards, configuredState]); + + const hasFilteredStandards = Object.keys(filteredGroupedStandards).length > 0; + + return ( + <> + {Object.keys(selectedStandards).length > 0 && ( + <> + + + { + // Close any expanded accordion when changing search query + if (expanded && e.target.value !== searchQuery) { + handleAccordionToggle(null); + } + setSearchQuery(e.target.value); + }} + slotProps={{ + input: { + startAdornment: ( + + + + ), + endAdornment: searchQuery && ( + + + { + // Close any expanded accordion when clearing search + if (expanded) { + handleAccordionToggle(null); + } + setSearchQuery(""); + }} + aria-label="Clear search" + > + + + + + ), + }, + }} /> - + + + + + + + -
    - - - - - - - - - - {hasAddedComponents && ( - - - {standard.addedComponent?.map((component, idx) => ( - + + No standards match the selected filter criteria or search query. + + + )} + + )} + + {Object.keys(filteredGroupedStandards).map((category) => ( + + + {category} + + + {filteredGroupedStandards[category].map(({ standardName, standard }) => { + const isExpanded = expanded === standardName; + const hasAddedComponents = + standard.addedComponent && standard.addedComponent.length > 0; + const isConfigured = _.get(configuredState, standardName); + const disabledFeatures = standard.disabledFeatures || {}; + + let selectedActions = _.get(watchedValues, `${standardName}.action`); + if (selectedActions && !Array.isArray(selectedActions)) { + selectedActions = [selectedActions]; + } + + const selectedTemplateName = standard.multiple + ? _.get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`) + : ""; + const accordionTitle = + selectedTemplateName && _.get(selectedTemplateName, "label") + ? `${standard.label} - ${_.get(selectedTemplateName, "label")}` + : standard.label; + + // Get current values and check if they differ from saved values + const current = _.get(watchedValues, standardName); + const saved = _.get(savedValues, standardName) || {}; + + const hasUnsaved = !_.isEqual(current, saved); + + // Check if all required fields are filled + const requiredFieldsFilled = current + ? standard.addedComponent?.every((component) => { + // Always skip switches regardless of their required property + if (component.type === "switch") return true; + + // Skip optional fields (not required) + const isRequired = component.required !== false; + if (!isRequired) return true; + + // Handle conditional fields + if (component.condition) { + const conditionField = component.condition.field; + const conditionValue = _.get(current, conditionField); + const compareType = component.condition.compareType || "is"; + const compareValue = component.condition.compareValue; + const propertyName = component.condition.propertyName || "value"; + + let conditionMet = false; + if (propertyName === "value") { + switch (compareType) { + case "is": + conditionMet = _.isEqual(conditionValue, compareValue); + break; + case "isNot": + conditionMet = !_.isEqual(conditionValue, compareValue); + break; + default: + conditionMet = false; + } + } else if (Array.isArray(conditionValue)) { + switch (compareType) { + case "valueEq": + conditionMet = conditionValue.some( + (item) => item?.[propertyName] === compareValue + ); + break; + default: + conditionMet = false; + } + } + + // If condition is not met, skip validation + if (!conditionMet) return true; + } + + // Get field value for validation using lodash's get to properly handle nested properties + const fieldValue = _.get(current, component.name); + + // Check if required field has a value based on its type and multiple property + if (component.type === "autoComplete" || component.type === "select") { + if (component.multiple) { + // For multiple selection, check if array exists and has items + return Array.isArray(fieldValue) && fieldValue.length > 0; + } else { + // For single selection, check if value exists + return !!fieldValue; + } + } + + // For other field types + return !!fieldValue; + }) ?? true + : false; + + // ALWAYS require an action for all standards + const actionRequired = true; + + // Check if there are required non-switch components for UI display purposes + const hasRequiredComponents = + standard.addedComponent && + standard.addedComponent.some( + (comp) => comp.type !== "switch" && comp.required !== false + ); + + // Action is always required and must be an array with at least one element + const actionValue = _.get(current, "action"); + const hasAction = + actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); + + // Allow saving if: + // 1. Action is selected if required + // 2. All required fields are filled + // 3. There are unsaved changes + const canSave = hasAction && requiredFieldsFilled && hasUnsaved; + + return ( + + + + + {standard.cat === "Global Standards" ? ( + + ) : standard.cat === "Entra (AAD) Standards" ? ( + + ) : standard.cat === "Exchange Standards" ? ( + + ) : standard.cat === "Defender Standards" ? ( + + ) : standard.cat === "Intune Standards" ? ( + + ) : ( + + )} + + + {accordionTitle} + + {/* Hide action chips in drift mode */} + {!isDriftMode && selectedActions && selectedActions?.length > 0 && ( + <> + {selectedActions?.map((action, index) => ( + + + {action.value === "Report" && } + {action.value === "warn" && } + {action.value === "Remediate" && } + + } + /> + + ))} + + )} + + + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + mr: 1, + }} + > + ( + + {children} + + ), + // Convert paragraphs to spans to avoid unwanted spacing + p: ({ children }) => {children}, + }} + > + {standard.helpText} + + + + + + {standard.multiple && ( + + handleAddMultipleStandard(standardName)}> + + + + )} + + + {isConfigured ? "Configured" : "Unconfigured"} + + + handleRemoveStandard(standardName)}> + + + + + handleAccordionToggle(standardName)}> + - ))} -
    -
    - )} -
    -
    -
    - - ); - }); + + + + + + + + {isDriftMode ? ( + /* Drift mode layout - full width with slider first */ + + {/* Auto-remediate switch takes full width and is first */} + + + handleAutoRemediateChange(standardName, e.target.checked) + } + fullWidth + /> + + + {/* Additional components take full width */} + {hasAddedComponents && ( + <> + {standard.addedComponent?.map((component, idx) => + component?.condition ? ( + + + + ) : ( + + ) + )} + + )} + + ) : ( + /* Standard mode layout - original grid layout */ + + + + + + {hasAddedComponents && ( + + + {standard.addedComponent?.map((component, idx) => + component?.condition ? ( + + + + ) : ( + + ) + )} + + + )} + + )} + + + + + + + + + + + ); + })} + + ))} + + ); }; export default CippStandardAccordion; diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index 2fb081155944..d74d6f4d3630 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -1,10 +1,10 @@ +import { differenceInDays } from "date-fns"; import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, - Grid, Card, CardContent, Typography, @@ -14,159 +14,1308 @@ import { Switch, Button, IconButton, + CircularProgress, + Select, + MenuItem, + FormControl, + InputLabel, + Stack, + Divider, + Collapse, + ToggleButton, + ToggleButtonGroup, + List, + ListItem, + ListItemText, + ListItemSecondaryAction, } from "@mui/material"; -import { Add } from "@mui/icons-material"; -import { useState, useCallback } from "react"; +import { Grid } from "@mui/system"; +import { + Add, + Sort, + Clear, + FilterList, + ExpandMore, + ExpandLess, + ViewModule, + ViewList, +} from "@mui/icons-material"; +import { useState, useCallback, useMemo, memo, useEffect } from "react"; import { debounce } from "lodash"; +import { Virtuoso } from "react-virtuoso"; +import ReactMarkdown from "react-markdown"; -const CippStandardDialog = ({ - dialogOpen, - handleCloseDialog, - setSearchQuery, - categories, - filterStandards, - selectedStandards, - handleToggleSingleStandard, - handleAddMultipleStandard, -}) => { - const [isButtonDisabled, setButtonDisabled] = useState(false); +// Memoized Standard Card component to prevent unnecessary re-renders +const StandardCard = memo( + ({ + standard, + category, + selectedStandards, + handleToggleSingleStandard, + handleAddClick, + isButtonDisabled, + }) => { + const isNewStandard = (dateAdded) => { + const currentDate = new Date(); + const addedDate = new Date(dateAdded); + return differenceInDays(currentDate, addedDate) <= 30; + }; - const handleAddClick = (standardName) => { - setButtonDisabled(true); - handleAddMultipleStandard(standardName); + // Create a memoized handler for this specific standard to avoid recreation on each render + const handleToggle = useCallback(() => { + handleToggleSingleStandard(standard.name); + }, [handleToggleSingleStandard, standard.name]); - setTimeout(() => { - setButtonDisabled(false); - }, 100); - }; + // Check if this standard is selected - memoize for better performance + const isSelected = useMemo(() => { + return !!selectedStandards[standard.name]; + }, [selectedStandards, standard.name]); - const handleSearchQueryChange = useCallback( - debounce((query) => { - setSearchQuery(query); - }, 50), - [] + // Lazily render complex parts of the card only when visible + const [expanded, setExpanded] = useState(false); + + // Use intersection observer to detect when card is visible + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setExpanded(true); + observer.disconnect(); + } + }, + { threshold: 0.1 } + ); + + const currentRef = document.getElementById(`standard-card-${standard.name}`); + if (currentRef) { + observer.observe(currentRef); + } + + return () => observer.disconnect(); + }, [standard.name]); + + return ( + + + {isNewStandard(standard.addedDate) && ( + + )} + + + + {standard.label} + + {expanded && standard.helpText && ( + <> + + Description: + + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + mb: 2, + }} + > + ( + + {children} + + ), + // Convert paragraphs to spans to avoid unwanted spacing + p: ({ children }) => {children}, + }} + > + {standard.helpText} + + + + )} + + Category: + + + {expanded && + standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > 0 && ( + <> + + Tags: + + + {standard.tag + .filter((tag) => !tag.toLowerCase().includes("impact")) + .map((tag, idx) => ( + + ))} + + + )} + + Impact: + + + {expanded && standard.recommendedBy?.length > 0 && ( + <> + + Recommended By: + + + {standard.recommendedBy.join(", ")} + + + )} + {expanded && standard.addedDate?.length > 0 && ( + <> + + Date Added: + + + + {standard.addedDate} + + + + )} + + + + {standard.multiple ? ( + handleAddClick(standard.name)} + > + + + ) : ( + + } + label="Add this standard to the template" + /> + )} + + + + + ); + }, + // Custom equality function to prevent unnecessary re-renders + (prevProps, nextProps) => { + // Only re-render if one of these props changed + if (prevProps.isButtonDisabled !== nextProps.isButtonDisabled) return false; + if (prevProps.standard.name !== nextProps.standard.name) return false; + + // Only check selected state for this specific standard + const prevSelected = !!prevProps.selectedStandards[prevProps.standard.name]; + const nextSelected = !!nextProps.selectedStandards[nextProps.standard.name]; + if (prevSelected !== nextSelected) return false; + + // If we get here, nothing important changed, skip re-render + return true; + } +); + +StandardCard.displayName = "StandardCard"; + +// Virtualized grid to handle large numbers of standards efficiently +const VirtualizedStandardGrid = memo(({ items, renderItem }) => { + const [itemsPerRow, setItemsPerRow] = useState(() => + window.innerWidth > 960 ? 4 : window.innerWidth > 600 ? 2 : 1 ); + // Handle window resize for responsive grid + useEffect(() => { + const handleResize = () => { + const newItemsPerRow = window.innerWidth > 960 ? 4 : window.innerWidth > 600 ? 2 : 1; + setItemsPerRow(newItemsPerRow); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + const rows = useMemo(() => { + const rowCount = Math.ceil(items.length / itemsPerRow); + const rowsData = []; + + for (let i = 0; i < rowCount; i++) { + const startIdx = i * itemsPerRow; + const rowItems = items.slice(startIdx, startIdx + itemsPerRow); + rowsData.push(rowItems); + } + + return rowsData; + }, [items, itemsPerRow]); + return ( - - Select a Standard to Add - - handleSearchQueryChange(e.target.value.toLowerCase())} - /> - - {Object.keys(categories).map((category) => - filterStandards(categories[category]).map((standard) => ( - - - - + ( + + + {rows[index].map(renderItem)} + + + )} + /> + ); +}); + +VirtualizedStandardGrid.displayName = "VirtualizedStandardGrid"; + +// Compact List View component for standards +const CompactStandardList = memo( + ({ items, selectedStandards, handleToggleSingleStandard, handleAddClick, isButtonDisabled }) => { + return ( + + {items.map(({ standard, category }) => { + const isSelected = !!selectedStandards[standard.name]; + + const isNewStandard = (dateAdded) => { + if (!dateAdded) return false; + const currentDate = new Date(); + const addedDate = new Date(dateAdded); + return differenceInDays(currentDate, addedDate) <= 30; + }; + + const handleToggle = () => { + handleToggleSingleStandard(standard.name); + }; + + return ( + + + {standard.label} + {isNewStandard(standard.addedDate) && ( + + )} + + + + } + secondary={ + {standard.helpText && ( - <> - - Description: - - + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + }} + > + ( + + {children} + + ), + p: ({ children }) => ( + + {children} + + ), + }} + > {standard.helpText} - - + + )} - - Category: - - - {standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > - 0 && ( - <> - - Tags: - - + + {standard.tag?.filter((tag) => !tag.toLowerCase().includes("impact")).length > + 0 && ( + {standard.tag .filter((tag) => !tag.toLowerCase().includes("impact")) + .slice(0, 3) // Show only first 3 tags to save space .map((tag, idx) => ( ))} + {standard.tag.filter((tag) => !tag.toLowerCase().includes("impact")) + .length > 3 && ( + + + + {standard.tag.filter((tag) => !tag.toLowerCase().includes("impact")) + .length - 3}{" "} + more + + )} - - )} - - Impact: - - - {standard.recommendedBy?.length > 0 && ( - <> - - Recommended By: + )} + {standard.recommendedBy?.length > 0 && ( + + • Recommended by: {standard.recommendedBy.join(", ")} - - {standard.recommendedBy.join(", ")} + )} + {standard.addedDate && ( + + • Added: {standard.addedDate} - - )} - - - - {standard.multiple ? ( - + + } + /> + + {standard.multiple ? ( + handleAddClick(standard.name)} + sx={{ mr: 1 }} + > + + + ) : ( + handleAddClick(standard.name)} - > - - - ) : ( - handleToggleSingleStandard(standard.name)} - /> - } - label="Add this standard to the template" + size="small" /> - )} - - - - )) + } + label="" + sx={{ mr: 1 }} + /> + )} + + + ); + })} + + ); + } +); + +CompactStandardList.displayName = "CompactStandardList"; + +const CippStandardDialog = ({ + dialogOpen, + handleCloseDialog, + setSearchQuery, + categories, + filterStandards, + selectedStandards, + handleToggleSingleStandard, + handleAddMultipleStandard, +}) => { + const [isButtonDisabled, setButtonDisabled] = useState(false); + const [localSearchQuery, setLocalSearchQuery] = useState(""); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [viewMode, setViewMode] = useState("card"); // "card" or "list" + + // Enhanced filtering and sorting state + const [sortBy, setSortBy] = useState("addedDate"); // Default sort by date added + const [sortOrder, setSortOrder] = useState("desc"); // desc to show newest first + const [selectedCategories, setSelectedCategories] = useState([]); + const [selectedImpacts, setSelectedImpacts] = useState([]); + const [selectedRecommendedBy, setSelectedRecommendedBy] = useState([]); + const [selectedTagFrameworks, setSelectedTagFrameworks] = useState([]); + const [showOnlyNew, setShowOnlyNew] = useState(false); // Show only standards added in last 30 days + const [filtersExpanded, setFiltersExpanded] = useState(false); // Control filter section collapse/expand + + // Auto-adjust sort order when sort type changes + useEffect(() => { + if (sortBy === "label") { + setSortOrder("asc"); // Names: A-Z + } else if (sortBy === "addedDate") { + setSortOrder("desc"); // Dates: Newest first + } else if (sortBy === "impact") { + setSortOrder("desc"); // Impact: High to Low + } + }, [sortBy]); + + // Get all unique values for filters + const { allCategories, allImpacts, allRecommendedBy, allTagFrameworks } = useMemo(() => { + const categorySet = new Set(); + const impactSet = new Set(); + const recommendedBySet = new Set(); + const tagFrameworkSet = new Set(); + + // Function to extract base framework from tag + const extractTagFramework = (tag) => { + // Compliance Frameworks - extract version dynamically + if (tag.startsWith("CIS M365")) { + const versionMatch = tag.match(/CIS M365 (\d+\.\d+)/); + return versionMatch ? `CIS M365 ${versionMatch[1]}` : "CIS M365"; + } + if (tag.startsWith("CISA ")) return "CISA"; + if (tag.startsWith("EIDSCA.")) return "EIDSCA"; + if (tag.startsWith("Essential 8")) return "Essential 8"; + if (tag.startsWith("NIST CSF")) { + const versionMatch = tag.match(/NIST CSF (\d+\.\d+)/); + return versionMatch ? `NIST CSF ${versionMatch[1]}` : "NIST CSF"; + } + + // Microsoft Secure Score Categories + if (tag.startsWith("exo_")) return "Secure Score - Exchange"; + if (tag.startsWith("mdo_")) return "Secure Score - Defender"; + if (tag.startsWith("spo_")) return "Secure Score - SharePoint"; + if (tag.startsWith("mip_")) return "Secure Score - Purview"; + + // For any other tags, return null to exclude them + return null; + }; + + Object.keys(categories).forEach((category) => { + categorySet.add(category); + categories[category].forEach((standard) => { + if (standard.impact) impactSet.add(standard.impact); + if (standard.recommendedBy && Array.isArray(standard.recommendedBy)) { + standard.recommendedBy.forEach((rec) => recommendedBySet.add(rec)); + } + // Process tags to extract frameworks + if (standard.tag && Array.isArray(standard.tag)) { + standard.tag.forEach((tag) => { + const framework = extractTagFramework(tag); + if (framework) { + // Only add non-null frameworks + tagFrameworkSet.add(framework); + } + }); + } + }); + }); + + // Custom sort order for impacts: Low -> Medium -> High + const impactOrder = ["Low Impact", "Medium Impact", "High Impact"]; + const sortedImpacts = Array.from(impactSet).sort((a, b) => { + const aIndex = impactOrder.indexOf(a); + const bIndex = impactOrder.indexOf(b); + return aIndex - bIndex; + }); + + // Sort tag frameworks with compliance frameworks first, then service categories + const sortedTagFrameworks = Array.from(tagFrameworkSet).sort((a, b) => { + // Define priority groups + const getFrameworkPriority = (framework) => { + if (framework.startsWith("CIS M365")) return 1; + if (framework === "CISA") return 2; + if (framework === "EIDSCA") return 3; + if (framework === "Essential 8") return 4; + if (framework.startsWith("NIST CSF")) return 5; + if (framework.startsWith("Secure Score -")) return 6; + return 999; // Other tags go last + }; + + const aPriority = getFrameworkPriority(a); + const bPriority = getFrameworkPriority(b); + + // If different priorities, sort by priority + if (aPriority !== bPriority) { + return aPriority - bPriority; + } + + // If same priority, sort alphabetically + return a.localeCompare(b); + }); + + return { + allCategories: Array.from(categorySet).sort(), + allImpacts: sortedImpacts, + allRecommendedBy: Array.from(recommendedBySet).sort(), + allTagFrameworks: sortedTagFrameworks, + }; + }, [categories]); + + // Enhanced filter function + const enhancedFilterStandards = useCallback( + (standardsList) => { + // Function to extract base framework from tag (same as in useMemo) + const extractTagFramework = (tag) => { + // Compliance Frameworks - extract version dynamically + if (tag.startsWith("CIS M365")) { + const versionMatch = tag.match(/CIS M365 (\d+\.\d+)/); + return versionMatch ? `CIS M365 ${versionMatch[1]}` : "CIS M365"; + } + if (tag.startsWith("CISA ")) return "CISA"; + if (tag.startsWith("EIDSCA.")) return "EIDSCA"; + if (tag.startsWith("Essential 8")) return "Essential 8"; + if (tag.startsWith("NIST CSF")) { + const versionMatch = tag.match(/NIST CSF (\d+\.\d+)/); + return versionMatch ? `NIST CSF ${versionMatch[1]}` : "NIST CSF"; + } + + // Microsoft Secure Score Categories + if (tag.startsWith("exo_")) return "Secure Score - Exchange"; + if (tag.startsWith("mdo_")) return "Secure Score - Defender"; + if (tag.startsWith("spo_")) return "Secure Score - SharePoint"; + if (tag.startsWith("mip_")) return "Secure Score - Purview"; + + // For any other tags, return null to exclude them + return null; + }; + + return standardsList.filter((standard) => { + // Original text search + const matchesSearch = + !localSearchQuery || + standard.label.toLowerCase().includes(localSearchQuery.toLowerCase()) || + standard.helpText.toLowerCase().includes(localSearchQuery.toLowerCase()) || + (standard.tag && + standard.tag.some((tag) => tag.toLowerCase().includes(localSearchQuery.toLowerCase()))); + + // Category filter + const matchesCategory = + selectedCategories.length === 0 || selectedCategories.includes(standard.cat); + + // Impact filter + const matchesImpact = + selectedImpacts.length === 0 || selectedImpacts.includes(standard.impact); + + // Recommended by filter + const matchesRecommendedBy = + selectedRecommendedBy.length === 0 || + (standard.recommendedBy && + Array.isArray(standard.recommendedBy) && + standard.recommendedBy.some((rec) => selectedRecommendedBy.includes(rec))); + + // Tag framework filter + const matchesTagFramework = + selectedTagFrameworks.length === 0 || + (standard.tag && + Array.isArray(standard.tag) && + standard.tag.some((tag) => { + const framework = extractTagFramework(tag); + return framework && selectedTagFrameworks.includes(framework); + })); + + // New standards filter (last 30 days) + const isNewStandard = (dateAdded) => { + if (!dateAdded) return false; + const currentDate = new Date(); + const addedDate = new Date(dateAdded); + return differenceInDays(currentDate, addedDate) <= 30; + }; + const matchesNewFilter = !showOnlyNew || isNewStandard(standard.addedDate); + + return ( + matchesSearch && + matchesCategory && + matchesImpact && + matchesRecommendedBy && + matchesTagFramework && + matchesNewFilter + ); + }); + }, + [ + localSearchQuery, + selectedCategories, + selectedImpacts, + selectedRecommendedBy, + selectedTagFrameworks, + showOnlyNew, + ] + ); + + // Enhanced sort function + const sortStandards = useCallback( + (standardsList) => { + return [...standardsList].sort((a, b) => { + let aValue, bValue; + + switch (sortBy) { + case "label": + aValue = a.label.toLowerCase(); + bValue = b.label.toLowerCase(); + break; + case "addedDate": + aValue = new Date(a.addedDate || "1900-01-01"); + bValue = new Date(b.addedDate || "1900-01-01"); + break; + case "category": + aValue = a.cat?.toLowerCase() || ""; + bValue = b.cat?.toLowerCase() || ""; + break; + case "impact": + // Sort by impact priority: High > Medium > Low + const impactOrder = { "High Impact": 3, "Medium Impact": 2, "Low Impact": 1 }; + aValue = impactOrder[a.impact] || 0; + bValue = impactOrder[b.impact] || 0; + break; + case "recommendedBy": + aValue = + a.recommendedBy && a.recommendedBy.length > 0 + ? a.recommendedBy.join(", ").toLowerCase() + : ""; + bValue = + b.recommendedBy && b.recommendedBy.length > 0 + ? b.recommendedBy.join(", ").toLowerCase() + : ""; + break; + default: + aValue = a.label.toLowerCase(); + bValue = b.label.toLowerCase(); + } + + if (aValue < bValue) return sortOrder === "asc" ? -1 : 1; + if (aValue > bValue) return sortOrder === "asc" ? 1 : -1; + return 0; + }); + }, + [sortBy, sortOrder] + ); + + // Optimize handleAddClick to be more performant + const handleAddClick = useCallback( + (standardName) => { + setButtonDisabled(true); + handleAddMultipleStandard(standardName); + // Use requestAnimationFrame for smoother UI updates + requestAnimationFrame(() => { + setTimeout(() => { + setButtonDisabled(false); + }, 100); + }); + }, + [handleAddMultipleStandard] + ); + + // Optimize search debounce with a higher timeout for better performance + const handleSearchQueryChange = useCallback( + debounce((query) => { + setSearchQuery(query.trim()); + }, 350), // Increased debounce time for better performance + [setSearchQuery] + ); + + // Only process visible categories on demand to improve performance + const [processedItems, setProcessedItems] = useState([]); + + // Handle search input change locally + const handleLocalSearchChange = useCallback( + (e) => { + const value = e.target.value; + setLocalSearchQuery(value); + handleSearchQueryChange(value); + }, + [handleSearchQueryChange] + ); + + // Clear all filters + const clearAllFilters = useCallback(() => { + setLocalSearchQuery(""); + setSelectedCategories([]); + setSelectedImpacts([]); + setSelectedRecommendedBy([]); + setSelectedTagFrameworks([]); + setShowOnlyNew(false); + setSortBy("addedDate"); + setSortOrder("desc"); + setViewMode("card"); // Reset to card view + handleSearchQueryChange(""); + }, [handleSearchQueryChange]); + + // Clear dialog state on close + const handleClose = useCallback(() => { + setLocalSearchQuery(""); // Clear local search state + setSelectedCategories([]); + setSelectedImpacts([]); + setSelectedRecommendedBy([]); + setSelectedTagFrameworks([]); + setShowOnlyNew(false); + setViewMode("card"); // Reset to card view + handleSearchQueryChange(""); // Clear parent search state + handleCloseDialog(); + }, [handleCloseDialog, handleSearchQueryChange]); + + // Process standards data only when dialog is opened, to improve performance + useEffect(() => { + if (dialogOpen) { + // Use requestIdleCallback if available, or setTimeout as fallback + const processStandards = () => { + // Create a flattened list of all standards for virtualized rendering + const allItems = []; + + Object.keys(categories).forEach((category) => { + const categoryStandards = categories[category]; + const filteredStandards = enhancedFilterStandards(categoryStandards); + + filteredStandards.forEach((standard) => { + allItems.push({ + standard, + category, + }); + }); + }); + + // Apply sorting to the final combined array instead of per-category + const sortedAllItems = sortStandards(allItems.map((item) => item.standard)).map( + (standard) => { + const item = allItems.find((item) => item.standard.name === standard.name); + return item; + } + ); + + setProcessedItems(sortedAllItems); + setIsInitialLoading(false); + }; + + if (window.requestIdleCallback) { + window.requestIdleCallback(processStandards, { timeout: 500 }); + } else { + setTimeout(processStandards, 100); + } + + return () => { + if (window.cancelIdleCallback) { + window.cancelIdleCallback(processStandards); + } + }; + } else { + setIsInitialLoading(true); + } + }, [dialogOpen, categories, enhancedFilterStandards, sortStandards]); + + // Render individual standard card + const renderStandardCard = useCallback( + ({ standard, category }) => ( + + ), + [selectedStandards, handleToggleSingleStandard, handleAddClick, isButtonDisabled] + ); + + // Count active filters + const activeFiltersCount = + selectedCategories.length + + selectedImpacts.length + + selectedRecommendedBy.length + + selectedTagFrameworks.length + + (showOnlyNew ? 1 : 0); + + // Don't render dialog contents until it's actually open (improves performance) + return ( + { + // Clear processed items on dialog close to free up memory + setProcessedItems([]); + }, + }} + PaperProps={{ + sx: { + minWidth: "720px", + maxHeight: "90vh", + height: "90vh", + display: "flex", + flexDirection: "column", + }, + }} + > + Select a Standard to Add + + {/* Search and Filter Controls */} + + {/* Search Box */} + + + {/* Unified Controls Section */} + + {/* Clickable header bar */} + setFiltersExpanded(!filtersExpanded)} + sx={{ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + py: 0.75, + px: 1, + borderRadius: filtersExpanded ? "4px 4px 0 0" : 1, + cursor: "pointer", + bgcolor: "action.hover", + border: "1px solid", + borderColor: "divider", + borderBottom: filtersExpanded ? "none" : "none", + "&:hover": { + bgcolor: "action.selected", + }, + }} + > + + + + View, Sort & Filter Options + + {!filtersExpanded && ( + + ({viewMode === "card" ? "Card" : "List"} •{" "} + {sortBy === "addedDate" ? "Date" : "Name"} {sortOrder === "desc" ? "↓" : "↑"} + {activeFiltersCount > 0 + ? ` • ${activeFiltersCount} filter${activeFiltersCount !== 1 ? "s" : ""}` + : ""} + ) + + )} + + {filtersExpanded ? : } + + + {/* Single line controls when expanded */} + + + {/* View Mode */} + { + if (newViewMode !== null) { + setViewMode(newViewMode); + } + }} + > + + + Cards + + + + List + + + + {/* Sort Controls */} + + Sort By + + + + + Order + + + + {/* Filter Controls */} + + Categories + + + + + Impact + + + + + Recommended By + + + + + Compliance Tags + + + + {/* New Standards Toggle */} + setShowOnlyNew(e.target.checked)} + /> + } + label="New (30 days)" + sx={{ ml: 1 }} + /> + + {/* Clear Button */} + {activeFiltersCount > 0 && ( + + )} + + + + + {/* Active Filter Chips */} + {activeFiltersCount > 0 && ( + + + {selectedCategories.map((category) => ( + + setSelectedCategories((prev) => prev.filter((c) => c !== category)) + } + color="primary" + variant="outlined" + /> + ))} + {selectedImpacts.map((impact) => ( + setSelectedImpacts((prev) => prev.filter((i) => i !== impact))} + color="secondary" + variant="outlined" + /> + ))} + {selectedRecommendedBy.map((rec) => ( + + setSelectedRecommendedBy((prev) => prev.filter((r) => r !== rec)) + } + color="success" + variant="outlined" + /> + ))} + {selectedTagFrameworks.map((framework) => ( + + setSelectedTagFrameworks((prev) => prev.filter((f) => f !== framework)) + } + color="warning" + variant="outlined" + /> + ))} + {showOnlyNew && ( + setShowOnlyNew(false)} + color="info" + variant="outlined" + /> + )} + + )} - + + + + + {/* Results */} + {isInitialLoading ? ( + + + + ) : processedItems.length === 0 ? ( + + + No standards match your search and filter criteria + + + Try adjusting your search terms or clearing some filters + + + ) : ( + + + Showing {processedItems.length} standard{processedItems.length !== 1 ? "s" : ""} + + {viewMode === "card" ? ( + + + + ) : ( + + + + )} + + )} - - diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index c6a405de636f..9009c7cb58ad 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -1,14 +1,5 @@ import PropTypes from "prop-types"; -import { - Card, - CardContent, - CardHeader, - Divider, - formControlLabelClasses, - Stack, - SvgIcon, - Typography, -} from "@mui/material"; +import { Card, CardContent, CardHeader, Divider, Stack, SvgIcon, Typography } from "@mui/material"; import { styled } from "@mui/material/styles"; import { Timeline, @@ -30,6 +21,8 @@ import CippFormComponent from "/src/components/CippComponents/CippFormComponent" import { CippFormTenantSelector } from "../CippComponents/CippFormTenantSelector"; import { CippApiDialog } from "../CippComponents/CippApiDialog"; import ReactTimeAgo from "react-time-ago"; +import { Alert } from "@mui/material"; +import { ApiGetCall } from "../../api/ApiCall"; const StyledTimelineDot = (props) => { const { complete } = props; @@ -73,26 +66,221 @@ const CippStandardsSideBar = ({ formControl, createDialog, edit, + onSaveSuccess, + onDriftConflictChange, + isDriftMode = false, }) => { const [currentStep, setCurrentStep] = useState(0); const [savedItem, setSavedItem] = useState(null); + const [driftError, setDriftError] = useState(""); + const dialogAfterEffect = (id) => { setSavedItem(id); + + // Reset form's dirty state to prevent unsaved changes warning + if (formControl && formControl.reset) { + // Get current values and reset the form with them to clear dirty state + const currentValues = formControl.getValues(); + formControl.reset(currentValues); + } + + // Call the onSaveSuccess callback if provided + if (typeof onSaveSuccess === "function") { + onSaveSuccess(); + } }; const watchForm = useWatch({ control: formControl.control }); + // Use proper CIPP ApiGetCall for drift validation + const driftValidationApi = ApiGetCall({ + url: "/api/ListTenantAlignment", + queryKey: "ListTenantAlignment-drift-validation", + }); + + // Get tenant groups for group membership validation + const tenantGroupsApi = ApiGetCall({ + url: "/api/ListTenantGroups", + queryKey: "ListTenantGroups-drift-validation", + }); + + // Helper function to expand groups to their member tenants + const expandGroupsToTenants = (tenants, groups) => { + const expandedTenants = []; + + tenants.forEach((tenant) => { + const tenantValue = typeof tenant === "object" ? tenant.value : tenant; + const tenantType = typeof tenant === "object" ? tenant.type : null; + + if (tenantType === "Group") { + // Find the group and add all its members + const group = groups?.find((g) => g.Id === tenantValue); + if (group && group.Members) { + group.Members.forEach((member) => { + expandedTenants.push(member.defaultDomainName); + }); + } + } else { + // Regular tenant + expandedTenants.push(tenantValue); + } + }); + + return expandedTenants; + }; + + // Enhanced drift validation using CIPP patterns with group support + const validateDrift = async (tenants) => { + if (!isDriftMode || !tenants || tenants.length === 0) { + setDriftError(""); + onDriftConflictChange?.(false); + return; + } + + try { + // Wait for both APIs to load + if (!driftValidationApi.data || !tenantGroupsApi.data) { + return; + } + + // Filter out current template if editing + console.log("Duplicate detection debug:", { + edit, + currentGUID: watchForm.GUID, + allTemplates: driftValidationApi.data?.map((t) => ({ + GUID: t.GUID, + standardId: t.standardId, + standardName: t.standardName, + })), + }); + + const existingTemplates = driftValidationApi.data.filter((template) => { + const shouldInclude = edit && watchForm.GUID ? template.standardId !== watchForm.GUID : true; + console.log( + `Template ${template.standardId} (${template.standardName}): shouldInclude=${shouldInclude}, currentGUID=${watchForm.GUID}` + ); + return shouldInclude; + }); + + console.log( + "Filtered templates:", + existingTemplates?.map((t) => ({ GUID: t.GUID, standardId: t.standardId, standardName: t.standardName })) + ); + + // Get tenant groups data + const groups = tenantGroupsApi.data?.Results || []; + + // Expand selected tenants (including group members) + const selectedTenantList = expandGroupsToTenants(tenants, groups); + + // Simple conflict check + const conflicts = []; + + // Filter for drift templates only and group by standardId + const driftTemplates = existingTemplates.filter( + (template) => template.standardType === "drift" + ); + const uniqueTemplates = {}; + + driftTemplates.forEach((template) => { + if (!uniqueTemplates[template.standardId]) { + uniqueTemplates[template.standardId] = { + standardName: template.standardName, + tenants: [], + }; + } + uniqueTemplates[template.standardId].tenants.push(template.tenantFilter); + }); + + // Check for conflicts with unique templates + console.log("Checking conflicts with unique templates:", uniqueTemplates); + console.log("Selected tenant list:", selectedTenantList); + + for (const templateId in uniqueTemplates) { + const template = uniqueTemplates[templateId]; + const templateTenants = template.tenants; + + console.log( + `Checking template ${templateId} (${template.standardName}) with tenants:`, + templateTenants + ); + + const hasConflict = selectedTenantList.some((selectedTenant) => { + // Check if any template tenant matches the selected tenant + const conflict = templateTenants.some((templateTenant) => { + if (selectedTenant === "AllTenants" || templateTenant === "AllTenants") { + console.log( + `Conflict found: ${selectedTenant} vs ${templateTenant} (AllTenants match)` + ); + return true; + } + const match = selectedTenant === templateTenant; + if (match) { + console.log(`Conflict found: ${selectedTenant} vs ${templateTenant} (exact match)`); + } + return match; + }); + return conflict; + }); + + console.log(`Template ${templateId} has conflict: ${hasConflict}`); + + if (hasConflict) { + conflicts.push(template.standardName || "Unknown Template"); + } + } + + console.log("Final conflicts:", conflicts); + + 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.` + ); + onDriftConflictChange?.(true); + } else { + setDriftError(""); + onDriftConflictChange?.(false); + } + } catch (error) { + setDriftError("Error checking for conflicts" + (error.message ? `: ${error.message}` : "")); + onDriftConflictChange?.(true); + } + }; + + // Watch tenant changes + useEffect(() => { + if (!isDriftMode) return; + + const timeoutId = setTimeout(() => { + validateDrift(watchForm.tenantFilter); + }, 500); + + return () => clearTimeout(timeoutId); + }, [watchForm.tenantFilter, isDriftMode, driftValidationApi.data, tenantGroupsApi.data]); + useEffect(() => { const stepsStatus = { - step1: !!watchForm.templateName, - step2: watchForm.tenantFilter && watchForm.tenantFilter.length > 0, + step1: !!_.get(watchForm, "templateName"), + step2: _.get(watchForm, "tenantFilter", []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - watchForm.standards && + _.get(watchForm, "standards") && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { const standardValues = _.get(watchForm, `${standardName}`, {}); - return standardValues.action; + const standard = selectedStandards[standardName]; + // Check if this standard requires an action + const hasRequiredComponents = + standard?.addedComponent && + standard.addedComponent.some( + (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 + const actionValue = _.get(standardValues, "action"); + return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; @@ -100,24 +288,102 @@ const CippStandardsSideBar = ({ setCurrentStep(completedSteps); }, [selectedStandards, watchForm]); + // Create a local reference to the stepsStatus from the latest effect run const stepsStatus = { - step1: !!watchForm.templateName, - step2: watchForm.tenantFilter && watchForm.tenantFilter.length > 0, + step1: !!_.get(watchForm, "templateName"), + step2: _.get(watchForm, "tenantFilter", []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - watchForm.standards && + _.get(watchForm, "standards") && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { const standardValues = _.get(watchForm, `${standardName}`, {}); - return standardValues.action; + const standard = selectedStandards[standardName]; + // Always require an action for all standards (must be an array with at least one element) + const actionValue = _.get(standardValues, "action"); + return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; + return ( + + {isDriftMode ? "About Drift Templates" : "About Standard Templates"} + + {isDriftMode ? ( + + + Drift templates provide continuous monitoring of tenant configurations to detect + unauthorized changes. Each tenant can only have one drift template applied at a time. + + + Remediation Options: + + + • Automatic Remediation: Immediately reverts unauthorized changes + back to the template configuration +
    Manual Remediation: Sends email notifications for review, + allowing you to accept or deny detected changes +
    + + Key Features: + + + • Monitors all security standards, Conditional Access policies, and Intune policies +
    + • Detects changes made outside of CIPP +
    + • Configurable webhook and email notifications +
    • Granular control over deviation acceptance +
    +
    + ) : ( + + + Standard templates can be applied to multiple tenants and allow overlapping + configurations with intelligent merging based on specificity and timing. + + + + Merge Priority (Specificity): + + + 1. Individual Tenant - Highest priority, overrides all others +
    + 2. Tenant Group - Overrides "All Tenants" settings +
    + 3. All Tenants - Lowest priority, default baseline +
    + + + Conflict Resolution: + + + When multiple standards target the same scope (e.g., two tenant-specific templates), + the most recently created template takes precedence. + + + + Example: An "All Tenants" template enables audit log retention for 90 + days, but you need 365 days for one specific tenant. Create a tenant-specific template + with 365-day retention - it will override the global setting for that tenant only. + +
    + )} + + {/* Hidden field to mark drift templates */} + {isDriftMode && ( + + )} + + - {watchForm.tenantFilter?.some((tenant) => tenant.value === "AllTenants") && ( + + {/* Show drift error */} + {isDriftMode && driftError && {driftError}} + + {watchForm.tenantFilter?.some( + (tenant) => tenant.value === "AllTenants" || tenant.type === "Group" + ) && ( <> )} - {updatedAt.date && ( + {/* Drift-specific fields */} + {isDriftMode && ( <> + + + + + )} + {/* Hide schedule options in drift mode */} + {!isDriftMode && ( + <> + {updatedAt.date && ( + <> + + Last Updated by {updatedAt?.user} + + + )} + - Last Updated by {updatedAt?.user} + This setting allows you to create this template and run it only by using "Run Now". )} - - - This setting allows you to create this template and run it only by using "Run Now". -
    - - - - {steps.map((step, index) => ( - - - - {index < steps.length - 1 && } - - {step} - - ))} - - + {/* Hide timeline/ticker in drift mode */} + {!isDriftMode && ( + <> + + + + {steps.map((step, index) => ( + + + + {index < steps.length - 1 && } + + {step} + + ))} + + + + )} {actions.map((action, index) => ( @@ -205,7 +520,9 @@ const CippStandardsSideBar = ({ label={action.label} onClick={action.handler} disabled={ - !(watchForm.tenantFilter && watchForm.tenantFilter.length > 0) || currentStep < 3 + !(watchForm.tenantFilter && watchForm.tenantFilter.length > 0) || + currentStep < 3 || + (isDriftMode && driftError) } /> ))} @@ -216,7 +533,9 @@ const CippStandardsSideBar = ({ createDialog={createDialog} title="Add Standard" api={{ - confirmText: watchForm.runManually + confirmText: isDriftMode + ? "This template will automatically run every 3 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.", url: "/api/AddStandardsTemplate", @@ -225,11 +544,20 @@ const CippStandardsSideBar = ({ data: { tenantFilter: "tenantFilter", excludedTenants: "excludedTenants", + description: "description", templateName: "templateName", standards: "standards", ...(edit ? { GUID: "GUID" } : {}), ...(savedItem ? { GUID: savedItem } : {}), - runManually: "runManually", + runManually: isDriftMode ? false : "runManually", + isDriftTemplate: "isDriftTemplate", + ...(isDriftMode + ? { + type: "drift", + driftAlertWebhook: "driftAlertWebhook", + driftAlertEmail: "driftAlertEmail", + } + : {}), }, }} row={formControl.getValues()} @@ -238,6 +566,8 @@ const CippStandardsSideBar = ({ "listStandardTemplates", "listStandards", `listStandardTemplates-${watchForm.GUID}`, + "ListTenantAlignment-drift-validation", + "ListTenantGroups-drift-validation", ]} />
    @@ -257,6 +587,8 @@ CippStandardsSideBar.propTypes = { ).isRequired, updatedAt: PropTypes.string, formControl: PropTypes.object.isRequired, + onSaveSuccess: PropTypes.func, + onDriftConflictChange: PropTypes.func, }; export default CippStandardsSideBar; diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 12619a983c68..05e226b5aebd 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -1,4 +1,4 @@ -import { DeveloperMode, Sync, Tune, ViewColumn } from "@mui/icons-material"; +import { DeveloperMode, SevereCold, Sync, Tune, ViewColumn } from "@mui/icons-material"; import { Button, Checkbox, @@ -12,7 +12,11 @@ import { Typography, } from "@mui/material"; import { Box, Stack } from "@mui/system"; -import { MRT_GlobalFilterTextField, MRT_ToggleFiltersButton } from "material-react-table"; +import { + MRT_GlobalFilterTextField, + MRT_ToggleFiltersButton, + MRT_ToggleFullScreenButton, +} from "material-react-table"; import { PDFExportButton } from "../pdfExportButton"; import { ChevronDownIcon, ExclamationCircleIcon } from "@heroicons/react/24/outline"; import { usePopover } from "../../hooks/use-popover"; @@ -28,6 +32,7 @@ import { CippCodeBlock } from "../CippComponents/CippCodeBlock"; import { ApiGetCall } from "../../api/ApiCall"; import GraphExplorerPresets from "/src/data/GraphExplorerPresets.json"; import CippGraphExplorerFilter from "./CippGraphExplorerFilter"; +import { useMediaQuery } from "@mui/material"; export const CIPPTableToptoolbar = ({ api, @@ -53,6 +58,7 @@ export const CIPPTableToptoolbar = ({ const columnPopover = usePopover(); const filterPopover = usePopover(); + const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); const settings = useSettings(); const router = useRouter(); const createDialog = useDialog(); @@ -64,6 +70,21 @@ export const CIPPTableToptoolbar = ({ const pageName = router.pathname.split("/").slice(1).join("/"); const currentTenant = useSettings()?.currentTenant; + const [actionMenuAnchor, setActionMenuAnchor] = useState(null); + const handleActionMenuOpen = (event) => setActionMenuAnchor(event.currentTarget); + const handleActionMenuClose = () => setActionMenuAnchor(null); + + const getBulkActions = (actions, selectedRows) => { + return actions?.filter((action) => !action.link && !action?.hideBulk)?.map(action => ({ + ...action, + disabled: action.condition ? !selectedRows.every(row => action.condition(row.original)) : false + })) || []; + }; + + useEffect(() => { + //if usedData changes, deselect all rows + table.toggleAllRowsSelected(false); + }, [usedData]); //if the currentTenant Switches, remove Graph filters useEffect(() => { if (currentTenant) { @@ -81,6 +102,10 @@ export const CIPPTableToptoolbar = ({ } }, [settings?.columnDefaults?.[pageName], router, usedColumns]); + useEffect(() => { + setOriginalSimpleColumns(simpleColumns); + }, [simpleColumns]); + const presetList = ApiGetCall({ url: "/api/ListGraphExplorerPresets", queryKey: `ListGraphExplorerPresets${api?.data?.Endpoint ?? ""}`, @@ -254,7 +279,7 @@ export const CIPPTableToptoolbar = ({ // update filters to include graph explorer presets setFilterList([...filters, ...graphPresetList]); } - }, [presetList?.isSuccess]); + }, [presetList?.isSuccess, simpleColumns]); return ( <> @@ -266,7 +291,15 @@ export const CIPPTableToptoolbar = ({ justifyContent: "space-between", })} > - + <> - @@ -335,11 +367,12 @@ export const CIPPTableToptoolbar = ({ onClose={filterPopover.handleClose} MenuListProps={{ dense: true }} > - setTableFilter("", "reset", "")}> + setTableFilter("", "reset", "")}> {api?.url === "/api/ListGraphRequest" && ( { filterPopover.handleClose(); setFilterCanvasVisible(true); @@ -400,27 +433,39 @@ export const CIPPTableToptoolbar = ({ ))} - {exportEnabled && ( - <> - - - - )} - - setOffcanvasVisible(true)}> - - - + + <> + {exportEnabled && ( + <> + + + + )} + + setOffcanvasVisible(true)}> + + + + {mdDown && } + + { + //add a little icon with how many rows are selected + (table.getIsAllRowsSelected() || table.getIsSomeRowsSelected()) && ( + + {table.getSelectedRowModel().rows.length} rows selected + + ) + } @@ -442,7 +488,12 @@ export const CIPPTableToptoolbar = ({ - {actions && (table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()) && ( + {getRequestData?.data?.pages?.[0].Metadata?.ColdStart === true && ( + + + + )} + {actions && getBulkActions(actions, table.getSelectedRowModel().rows).length > 0 && (table.getIsSomeRowsSelected() || table.getIsAllRowsSelected()) && ( <> - } - > - - Click the button below and enter the provided code. This creates the CIPP - Application Registration in your tenant that allows you to access the Graph API. - Login using your CIPP Service Account. - - {startSetupApi.isLoading ? ( - - ) : ( - - )} - - )} - {currentStepState >= 2 && ( - - Step 2: Approve Permissions - - {currentStepState <= 2 ? ( - - ) : ( - - - - )} - - - } - CardButton={ - - } - > - - Step 2: Approvals Required - - - Please open the link below and provide the required approval, this allows the app - specific permissions shown in the next screen. Login using your CIPP Service - Account. - - - )} - - {/* Final Step 4 Card */} - {currentStepState >= 4 && } - - - - - - - - - )} - - {values.selectedOption === "UpdateTokens" && ( - - Update Tokens - - {appId.isLoading ? ( - - ) : ( - - - - )} - - - } - CardButton={ - - } - > - - Click the button below to refresh your token. - - {formControl.setValue("noSubmitButton", true)} - - - )} - - {values.selectedOption === "Manual" && ( - <> - {formControl.setValue("setKeys", true)} - - You may enter your secrets below. Leave fields blank to retain existing values. - - - - - - - )} - - - - ); -}; diff --git a/src/components/CippWizard/CIPPDeploymentStep.jsx b/src/components/CippWizard/CIPPDeploymentStep.jsx new file mode 100644 index 000000000000..7a6553b9323c --- /dev/null +++ b/src/components/CippWizard/CIPPDeploymentStep.jsx @@ -0,0 +1,101 @@ +import { useEffect } from "react"; +import { Stack, Typography } from "@mui/material"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CIPPDeploymentUpdateTokens } from "./CIPPDeploymentUpdateTokens"; + +export const CippDeploymentStep = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + const values = formControl.getValues(); + + // Use useEffect to set form values instead of doing it during render + useEffect(() => { + if (values.selectedOption === "Manual") { + formControl.setValue("setKeys", true); + } + }, [values.selectedOption, formControl]); + + return ( + + + {values.selectedOption === "UpdateTokens" && ( + + )} + + {values.selectedOption === "Manual" && ( + <> + + You may enter your secrets below. Leave fields blank to retain existing values. + + { + const guidRegex = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return value === "" || guidRegex.test(value) || "Invalid Tenant ID"; + }, + }} + /> + { + const guidRegex = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return value === "" || guidRegex.test(value) || "Invalid Application ID"; + }, + }} + /> + { + const secretRegex = + /^(?!^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z0-9-_~.]{20,}$/; + return ( + value === "" || + secretRegex.test(value) || + "This should be the secret value, not the secret ID" + ); + }, + }} + /> + { + const jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/; + return value === "" || jwtRegex.test(value) || "Invalid Refresh Token"; + }, + }} + /> + + )} + + + + ); +}; diff --git a/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx new file mode 100644 index 000000000000..50fef63317f2 --- /dev/null +++ b/src/components/CippWizard/CIPPDeploymentUpdateTokens.jsx @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { Stack, Typography, CircularProgress, SvgIcon, Box } from "@mui/material"; +import { CheckCircle } from "@mui/icons-material"; +import CippButtonCard from "../CippCards/CippButtonCard"; +import { ApiGetCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; + +export const CIPPDeploymentUpdateTokens = ({ formControl }) => { + const [tokens, setTokens] = useState(null); + + // Get application ID information for the card header + const appId = ApiGetCall({ + url: `/api/ExecListAppId`, + queryKey: `ExecListAppId`, + waiting: true, + }); + + // Handle successful authentication + const handleAuthSuccess = (tokenData) => { + setTokens(tokenData); + }; + + return ( + + + Update Tokens + + {appId.isLoading ? ( + + ) : ( + + + + )} + + + } + CardButton={ + + } + > + + Click the button to refresh the Graph token for your tenants using popup authentication. + This method opens a popup window where you can sign in to your Microsoft account. + + + + + ); +}; + +export default CIPPDeploymentUpdateTokens; diff --git a/src/components/CippWizard/CippAddTenantForm.jsx b/src/components/CippWizard/CippAddTenantForm.jsx new file mode 100644 index 000000000000..2e302bb02944 --- /dev/null +++ b/src/components/CippWizard/CippAddTenantForm.jsx @@ -0,0 +1,207 @@ +import { CircularProgress, Divider, InputAdornment, Typography } from "@mui/material"; +import { Box, Grid, Stack } from "@mui/system"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { ApiGetCall } from "../../api/ApiCall"; +import { useWatch } from "react-hook-form"; +import debounce from "lodash/debounce"; +import React, { useState, useEffect } from "react"; + +export const CippAddTenantForm = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + + const tenantDomain = useWatch({ control: formControl.control, name: "TenantName" }); + const [debouncedTenantDomain, setDebouncedTenantDomain] = useState(""); + + const updateTenantDomain = debounce((value) => { + setDebouncedTenantDomain(value); + }, 500); + + useEffect(() => { + updateTenantDomain(tenantDomain); + return () => updateTenantDomain.cancel(); + }, [tenantDomain]); + + const checkDomain = ApiGetCall({ + url: "/api/AddTenant", + data: { action: "ValidateDomain", TenantName: debouncedTenantDomain }, + queryKey: `ValidateDomain-${debouncedTenantDomain}`, + waiting: debouncedTenantDomain !== "" && debouncedTenantDomain !== undefined, + }); + + useEffect(() => { + validateDomain(); + }, [checkDomain.data, checkDomain.isFetching]); + + const validateDomain = () => { + if (!tenantDomain) { + // set error state on TenantName form field + formControl.setError("TenantName", { type: "required", message: "Tenant Name is required" }); + } + if (checkDomain.isFetching) { + formControl.setError("TenantName", { + type: "required", + message: "Checking domain availability...", + }); + } + if (checkDomain.isSuccess) { + if (checkDomain.data.Success === true) { + // clear error + formControl.clearErrors("TenantName"); + formControl.trigger(); + } else { + // set error state on TenantName form field + formControl.setError("TenantName", { type: "validate", message: checkDomain.data.Message }); + } + } + if (checkDomain.isError) { + formControl.setError("TenantName", { type: "error", message: "An error occurred" }); + } + }; + + const fields = [ + { + type: "header", + label: "Company Information", + }, + { + name: "TenantName", + label: "Tenant Name", + placeholder: "Enter the desired subdomain for the onmicrosoft.com domain", + type: "textField", + required: true, + InputProps: { + endAdornment: ( + + .onmicrosoft.com{" "} + {checkDomain.isFetching ? ( + + ) : ( + <> + {checkDomain.isSuccess && checkDomain.data.Success && ( + + )} + {checkDomain.isSuccess && !checkDomain.data.Success && ( + + )} + + )} + + ), + }, + gridSize: 12, + }, + { + name: "CompanyName", + label: "Company Name", + type: "textField", + required: true, + gridSize: 12, + placeholder: "Enter the registered company/organization name", + }, + { + name: "AddressLine1", + label: "Address Line 1", + type: "textField", + required: true, + placeholder: "Enter the primary address line", + }, + { + name: "AddressLine2", + label: "Address Line 2", + type: "textField", + required: false, + placeholder: "Enter the secondary address line (optional)", + }, + { + name: "City", + label: "City", + type: "textField", + required: true, + placeholder: "Enter the city", + }, + { + name: "State", + label: "State", + type: "textField", + required: true, + placeholder: "Enter the state or region", + }, + { + name: "PostalCode", + label: "Postal Code", + type: "textField", + required: true, + placeholder: "Enter the postal code", + }, + { + name: "Country", + label: "Country", + type: "textField", + required: true, + placeholder: "Enter the country (e.g., US)", + }, + { + type: "header", + label: "Contact Information", + }, + { + name: "FirstName", + label: "First Name", + type: "textField", + required: true, + placeholder: "Enter the first name of the contact person", + }, + { + name: "LastName", + label: "Last Name", + type: "textField", + required: true, + placeholder: "Enter the last name of the contact person", + }, + { + name: "Email", + label: "Email", + type: "textField", + required: true, + placeholder: "Enter the customer's email address", + }, + { + name: "PhoneNumber", + label: "Phone Number", + type: "textField", + required: true, + placeholder: "Enter the contact phone number", + }, + ]; + + return ( + + + {fields.map((field, index) => ( + + {field.type === "header" ? ( + <> + + {field.label} + + + + ) : ( + + + + )} + + ))} + + + + ); +}; diff --git a/src/components/CippWizard/CippAlertsStep.jsx b/src/components/CippWizard/CippAlertsStep.jsx new file mode 100644 index 000000000000..ba4e62c7f9f5 --- /dev/null +++ b/src/components/CippWizard/CippAlertsStep.jsx @@ -0,0 +1,89 @@ +import { Alert, Stack, Typography } from "@mui/material"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; + +export const CippAlertsStep = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + + const postExecutionOptions = [ + { label: "Webhook", value: "Webhook" }, + { label: "Email", value: "Email" }, + { label: "PSA", value: "PSA" }, + ]; + + const recurrenceOptions = [ + { value: "30m", label: "Every 30 minutes" }, + { value: "1h", label: "Every hour" }, + { value: "4h", label: "Every 4 hours" }, + { value: "1d", label: "Every 1 day" }, + { value: "7d", label: "Every 7 days" }, + { value: "30d", label: "Every 30 days" }, + { value: "365d", label: "Every 365 days" }, + ]; + + return ( + + + Almost done + + + There's a couple more things that you can configure outside of the wizard, let's list + some of them; + +
      +
    • + CIPP has the ability to send alerts to your PSA, Webhook or Email. You can configure + these settings under > Tenant Administration > Alert Configuration. +
    • +
    +
      +
    • + If you imported baselines, or want to set tenants to your own baseline, you should + check out our standards under these settings under > Tenant Administration > + Standards. +
    • +
    +
      +
    • + If you want to use our integrations, you should set these up under > CIPP > + Integrations. Some examples are CSP integrations, Password Pusher, PSA, and more. +
    • +
    +
      +
    • + Adding more users to CIPP? you can do this via CIPP > Advanced > Super Admin. +
    • +
    +
      +
    • + You can deploy Windows Applications too, directly using intune. We have Chocolately, + WinGet, and RMM apps under > Intune > Applications. Some examples are CSP + integrations, Password Pusher, PSA, and more. +
    • +
    +
      +
    • + Tenants can be grouped, and you can implement custom variables for your tenants under + WinGet, and RMM apps under Tenant Administrator > Administration > Tenants. +
    • +
    +
      +
    • + Have an enterprise app you want to deploy? Check out our tools{" "} + section. This menu also contains useful things such as our geo-ip lookup, and more. +
    • +
    +
    +
    + + +
    + ); +}; + +export default CippAlertsStep; diff --git a/src/components/CippWizard/CippBaselinesStep.jsx b/src/components/CippWizard/CippBaselinesStep.jsx new file mode 100644 index 000000000000..0343e8a33f1c --- /dev/null +++ b/src/components/CippWizard/CippBaselinesStep.jsx @@ -0,0 +1,105 @@ +import { Alert, Stack, Typography, FormControl, FormLabel, Box } from "@mui/material"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CippFormCondition } from "../CippComponents/CippFormCondition"; + +export const CippBaselinesStep = (props) => { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + + return ( + + + + + Baselines are template configurations that can be used as examples for setting up your + environment. Don't want to configure these yet? No problem! You can find the templates + at Tools - Community Repositories + + + Downloading these baselines will create templates in your CIPP instance. These templates + won't make any changes to your environment, but can be used as examples on how to setup + environments. Each template library contains multiple templates, +
      +
    • + CIPP Templates by CyberDrain contain several example standards, including low, + medium, and high priority standards +
    • +
    • + JoeyV's Conditional Access Baseline contains a Microsoft approved baseline for + Conditional Access, following the Microsoft best practices. +
    • +
    • + OpenIntuneBaseline contains Intune templates, the baseline is a community driven + baseline for Intune, based on CIS, NIST, and more benchmarks. It's considered the + leading baseline for Intune. +
    • +
    +
    +
    + + + Baseline Configuration + + + + + + + + + Select baselines to download: + + `${option.Name} (${option.Owner})`, + valueField: "FullName", + addedFields: { + templateRepoBranch: "main", + }, + }} + multiple={true} + placeholder="Select one or more baselines" + /> + + +
    + + +
    + ); +}; + +export default CippBaselinesStep; diff --git a/src/components/CippWizard/CippCAForm.jsx b/src/components/CippWizard/CippCAForm.jsx index b99d69c8e55c..d0ee1a657186 100644 --- a/src/components/CippWizard/CippCAForm.jsx +++ b/src/components/CippWizard/CippCAForm.jsx @@ -1,4 +1,4 @@ -import { Grid, Stack } from "@mui/material"; +import { Stack } from "@mui/material"; import { CippWizardStepButtons } from "./CippWizardStepButtons"; import CippJsonView from "../CippFormPages/CippJSONView"; import CippFormComponent from "../CippComponents/CippFormComponent"; @@ -9,7 +9,7 @@ import { useWatch } from "react-hook-form"; export const CippCAForm = (props) => { const { formControl, onPreviousStep, onNextStep, currentStep } = props; const values = formControl.getValues(); - const CATemplates = ApiGetCall({ url: "/api/ListCATemplates" }); + const CATemplates = ApiGetCall({ url: "/api/ListCATemplates", queryKey: "CATemplates" }); const [JSONData, setJSONData] = useState(); const watcher = useWatch({ control: formControl.control, name: "TemplateList" }); useEffect(() => { @@ -82,6 +82,13 @@ export const CippCAForm = (props) => { label="Overwrite Existing Policy" formControl={formControl} /> + + { /> - + { compareType="is" compareValue="customGroup" > - + { return null; } return uniquePlaceholders.map((placeholder) => ( - + {selectedTenants.map((tenant, idx) => ( { + const { formControl, onPreviousStep, onNextStep, currentStep } = props; + + return ( + + + Notification Settings + + Configure your notification settings. These settings will determine how you receive alerts + from CIPP. You can test your configuration using the "Send Test Alert" button. Don't want + to setup notifications yet? You can skip this step and configure it later via Application + Settings - Notifications + + {/* Use the reusable notification form component */} + + + + {/* Use the wizard step buttons for navigation */} + + + ); +}; + +export default CippNotificationsStep; diff --git a/src/components/CippWizard/CippPSACredentialsStep.js b/src/components/CippWizard/CippPSACredentialsStep.jsx similarity index 100% rename from src/components/CippWizard/CippPSACredentialsStep.js rename to src/components/CippWizard/CippPSACredentialsStep.jsx diff --git a/src/components/CippWizard/CippPSASyncOptions.js b/src/components/CippWizard/CippPSASyncOptions.jsx similarity index 100% rename from src/components/CippWizard/CippPSASyncOptions.js rename to src/components/CippWizard/CippPSASyncOptions.jsx diff --git a/src/components/CippWizard/CippSAMDeploy.jsx b/src/components/CippWizard/CippSAMDeploy.jsx new file mode 100644 index 000000000000..a38a27bf8af6 --- /dev/null +++ b/src/components/CippWizard/CippSAMDeploy.jsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from "react"; +import { Alert, Stack, Box, Link } from "@mui/material"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; + +export const CippSAMDeploy = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + const [authStatus, setAuthStatus] = useState({ + success: false, + error: null, + loading: false, + }); + + // Block next step until SAM app is created + formControl.register("SAMWizard", { + required: true, + }); + + // Set SAMWizard = true if auth is successful + useEffect(() => { + if (authStatus.success) { + formControl.setValue("SAMWizard", true); + formControl.trigger("SAMWizard"); + } + }, [authStatus, formControl]); + + const createSamApp = ApiPostCall({ urlfromdata: true }); + + const handleAuthSuccess = (tokenData) => { + setAuthStatus({ + success: false, + error: null, + loading: true, + }); + + createSamApp.mutate({ + url: "/api/ExecCreateSamApp", + data: { access_token: tokenData.accessToken }, + }); + }; + + const handleAuthError = (error) => { + setAuthStatus({ + success: false, + error: error.errorMessage || "Authentication failed", + loading: false, + }); + }; + + useEffect(() => { + if (createSamApp.isSuccess && authStatus.loading && createSamApp.data) { + const data = createSamApp.data?.data; + if (data.severity === "error") { + setAuthStatus({ + success: false, + error: data.message || "Failed to create SAM application", + loading: false, + }); + } else if (data.severity === "success") { + setAuthStatus({ + success: true, + error: null, + loading: false, + }); + } + } + }, [createSamApp, authStatus]); + + useEffect(() => { + if (createSamApp.isError && authStatus.loading) { + setAuthStatus({ + success: false, + error: "An error occurred while creating the SAM application", + loading: false, + }); + } + }, [createSamApp, authStatus]); + + return ( + + + To run this setup you will need the following prerequisites: +
  • + A CIPP Service Account. For more information on how to create a service account, click{" "} + + here + +
  • +
  • (Temporary) Global Administrator permissions for the CIPP Service Account
  • +
  • + Multi-factor authentication enabled for the CIPP Service Account, with no trusted + locations or other exclusions. +
  • +
    + + {authStatus.error && ( + + {authStatus.error} + + )} + + + + + + + + +
    + ); +}; + +export default CippSAMDeploy; diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx new file mode 100644 index 000000000000..8f8683af405e --- /dev/null +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -0,0 +1,130 @@ +import { useEffect } from "react"; +import { Stack, Box, Typography, Link } from "@mui/material"; +import { CIPPM365OAuthButton } from "../CippComponents/CIPPM365OAuthButton"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CippTenantTable } from "./CippTenantTable"; + +export const CippTenantModeDeploy = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props; + + formControl.register("GDAPAuth", { + required: true, + }); + + const updateRefreshToken = ApiPostCall({ urlfromdata: true }); + const addTenant = ApiPostCall({ urlfromdata: true }); + + useEffect(() => { + if (updateRefreshToken.isSuccess) { + formControl.setValue("GDAPAuth", true); + formControl.trigger("GDAPAuth"); + } + if (addTenant.isSuccess) { + // Reset the form control for the next tenant addition + formControl.setValue("GDAPAuth", true); + formControl.trigger("GDAPAuth"); + } + }, [updateRefreshToken.isSuccess, formControl, addTenant.isSuccess]); + + return ( + + + + {/* Partner Tenant (GDAP) */} + + + Partner Tenant + + + Using GDAP is recommended for CIPP, however you can also authenticate to individual + tenants. It is still highly recommended to connect to your partner tenant first, even if + you are not a Microsoft CSP. This allows CIPP to send notifications, perform permission + checks, and update permissions when required. + + + Please remember to log onto a service account dedicated for CIPP. More info? Check out the{" "} + + service account documentation + + . + + + + + { + const updatedTokenData = { + ...tokenData, + tenantMode: "GDAP", + }; + updateRefreshToken.mutate({ + url: "/api/ExecUpdateRefreshToken", + data: updatedTokenData, + }); + }} + buttonText="Connect to Partner Tenant (Recommended)" + showSuccessAlert={false} + /> + + + + + {/* Per-Tenant */} + + + Per-Tenant Authentication + + + Click the button below to connect to individual tenants. You can authenticate to multiple + tenants by repeating this step for each tenant you want to add. Accidentally added the + wrong tenant? Use the table below to remove it. + + + + + { + const updatedTokenData = { + ...tokenData, + tenantMode: "perTenant", + }; + addTenant.mutate({ + url: "/api/ExecAddTenant", + data: updatedTokenData, + }); + }} + buttonText="Connect to Separate Tenants" + showSuccessAlert={false} + /> + + + + + + + + + ); +}; + +export default CippTenantModeDeploy; diff --git a/src/components/CippWizard/CippTenantStep.jsx b/src/components/CippWizard/CippTenantStep.jsx index 4d41b814ce1a..90399921d0cd 100644 --- a/src/components/CippWizard/CippTenantStep.jsx +++ b/src/components/CippWizard/CippTenantStep.jsx @@ -23,6 +23,7 @@ export const CippTenantStep = (props) => { formControl={formControl} allTenants={allTenants} type={type} + preselectedEnabled={true} /> { + const createDialog = useDialog(); + + // Actions formatted as per your guidelines + const actions = [ + { + label: "Exclude Tenants", + type: "POST", + url: `/api/ExecExcludeTenant?AddExclusion=true`, + icon: , + data: { value: "customerId" }, + confirmText: "Are you sure you want to exclude [displayName]?", + multiPost: false, + condition: (row) => row.displayName !== "*Partner Tenant", + }, + { + label: "Include Tenants", + type: "POST", + url: `/api/ExecExcludeTenant?RemoveExclusion=true`, + icon: , + data: { value: "customerId" }, + confirmText: "Are you sure you want to include [displayName]?", + multiPost: false, + condition: (row) => row.displayName !== "*Partner Tenant", + }, + { + label: "Refresh CPV Permissions", + type: "POST", + url: `/api/ExecCPVPermissions`, + icon: , + data: { tenantFilter: "customerId" }, + confirmText: "Are you sure you want to refresh the CPV permissions for [displayName]?", + multiPost: false, + }, + { + label: "Reset CPV Permissions", + type: "POST", + url: `/api/ExecCPVPermissions?&ResetSP=true`, + icon: , + data: { tenantFilter: "customerId" }, + confirmText: + "Are you sure you want to reset the CPV permissions for [displayName]? (This will delete the Service Principal and re-add it.)", + multiPost: false, + condition: (row) => + row.displayName !== "*Partner Tenant" && row.delegatedPrivilegeStatus !== "directTenant", + }, + { + label: "Remove Tenant", + type: "POST", + url: `/api/ExecRemoveTenant`, + icon: , + data: { TenantID: "customerId" }, + confirmText: + "Are you sure you want to remove [displayName]? If this is a Direct Tenant, this will no longer be accessible until you add it via the Setup Wizard.", + multiPost: false, + condition: (row) => row.displayName !== "*Partner Tenant", + }, + ]; + + // Offcanvas details + const offCanvas = { + extendedInfoFields: [ + "displayName", + "defaultDomainName", + "delegatedPrivilegeStatus", + "Excluded", + "ExcludeDate", + "ExcludeUser", + ], + actions: actions, + }; + + // Columns for the table + const columns = customColumns || [ + "displayName", // Tenant Name + "defaultDomainName", // Default Domain + "delegatedPrivilegeStatus", // Delegated Privilege Status + "Excluded", // Excluded Status + "ExcludeDate", // Exclude Date + "ExcludeUser", // Exclude User + ]; + + // Default filters + const defaultFilters = [ + { + filterName: "Included tenants", + value: [{ id: "Excluded", value: "No" }], + type: "column", + }, + { + filterName: "Excluded tenants", + value: [{ id: "Excluded", value: "Yes" }], + type: "column", + }, + ]; + + const filters = customFilters || defaultFilters; + + return ( + <> + + + + + Force Refresh + + ) : null + } + tenantInTitle={tenantInTitle} + apiUrl="/api/ExecExcludeTenant?ListAll=True" + actions={actions} + offCanvas={offCanvas} + simpleColumns={columns} + filters={filters} + showTenantSelector={showTenantSelector} + showAllTenantsSelector={showAllTenantsSelector} + /> + {showCardButton && !onRefreshButtonClick && ( + + )} + + ); +}; + +export default CippTenantTable; diff --git a/src/components/CippWizard/CippWizard.jsx b/src/components/CippWizard/CippWizard.jsx index f0cc5f3013b3..b1a5457be67e 100644 --- a/src/components/CippWizard/CippWizard.jsx +++ b/src/components/CippWizard/CippWizard.jsx @@ -1,11 +1,29 @@ import { useCallback, useMemo, useState } from "react"; import { Card, CardContent, Container, Stack } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { WizardSteps } from "./wizard-steps"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; export const CippWizard = (props) => { const { postUrl, orientation = "horizontal", steps } = props; + + const formControl = useForm({ mode: "onChange", defaultValues: props.initialState }); + const formWatcher = useWatch({ + control: formControl.control, + }); + + const stepsWithVisibility = useMemo(() => { + return steps.filter((step) => { + if (step.hideStepWhen) { + return !step.hideStepWhen(formWatcher); + } + if (step.showStepWhen) { + return step.showStepWhen(formWatcher); + } + return true; + }); + }, [steps, formWatcher]); + const [activeStep, setActiveStep] = useState(0); const handleBack = useCallback(() => { setActiveStep((prevState) => (prevState > 0 ? prevState - 1 : prevState)); @@ -14,40 +32,40 @@ export const CippWizard = (props) => { const handleNext = useCallback(() => { setActiveStep((prevState) => (prevState < steps.length - 1 ? prevState + 1 : prevState)); }, []); - const formControl = useForm({ mode: "onChange", defaultValues: props.initialState }); + const content = useMemo(() => { - const StepComponent = steps[activeStep].component; + const StepComponent = stepsWithVisibility[activeStep].component; return ( ); - }, [activeStep, handleNext, handleBack, steps, formControl]); + }, [activeStep, handleNext, handleBack, stepsWithVisibility, formControl]); return ( {orientation === "vertical" ? ( - + - + {content} @@ -59,7 +77,7 @@ export const CippWizard = (props) => { postUrl={postUrl} activeStep={activeStep} orientation={orientation} - steps={steps} + steps={stepsWithVisibility} />
    {content} diff --git a/src/components/CippWizard/CippWizardAppApproval.jsx b/src/components/CippWizard/CippWizardAppApproval.jsx index a32eef3a32de..ba53800a2cb2 100644 --- a/src/components/CippWizard/CippWizardAppApproval.jsx +++ b/src/components/CippWizard/CippWizardAppApproval.jsx @@ -1,51 +1,205 @@ -import { Stack } from "@mui/material"; +import { Stack, Alert } from "@mui/material"; import CippWizardStepButtons from "./CippWizardStepButtons"; import { Grid } from "@mui/system"; import CippFormComponent from "../CippComponents/CippFormComponent"; import { getCippValidator } from "../../utils/get-cipp-validator"; import { CippFormCondition } from "../CippComponents/CippFormCondition"; +import CippPermissionPreview from "../CippComponents/CippPermissionPreview"; +import { useWatch } from "react-hook-form"; +import { CippPropertyListCard } from "../CippCards/CippPropertyListCard"; export const CippWizardAppApproval = (props) => { const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props; + // Watch for the selected template to access permissions and type + const selectedTemplate = useWatch({ + control: formControl.control, + name: "selectedTemplate", + }); + return ( - - - + + {/* Mode Selector */} + + + {/* Template Mode */} + + + + Select an app approval template to deploy. Templates contain predefined permissions that + will be applied to the application. + getCippValidator(value, "guid"), + type="autoComplete" + name="selectedTemplate" + label="Select App Template" + api={{ + url: "/api/ListAppApprovalTemplates", + queryKey: "appApprovalTemplates", + labelField: (item) => `${item.TemplateName}`, + valueField: "TemplateId", + addedField: { + AppId: "AppId", + AppName: "AppName", + AppType: "AppType", + GalleryTemplateId: "GalleryTemplateId", + GalleryInformation: "GalleryInformation", + PermissionSetId: "PermissionSetId", + PermissionSetName: "PermissionSetName", + Permissions: "Permissions", + ApplicationManifest: "ApplicationManifest", + }, + showRefresh: true, }} - name="AppId" + validators={{ required: "A template is required" }} formControl={formControl} + multiple={false} /> - - - + + {selectedTemplate?.addedFields?.AppName && ( + + + {(selectedTemplate.addedFields.AppType || "EnterpriseApp") === "EnterpriseApp" ? ( + + ) : (selectedTemplate.addedFields.AppType || "EnterpriseApp") === + "ApplicationManifest" ? ( + + ) : ( + + )} + + )} + + + + {/* Manual Mode */} + + + getCippValidator(value, "guid"), + }} + name="AppId" + formControl={formControl} + /> + + + + + + + + { api={{ ...api, tenantFilter: currentTenant ? currentTenant.value : undefined, - queryKey: `${api.url}-${currentTenant ? currentTenant.value : "default"}`, + queryKey: api.queryKey ? api.queryKey.replace('{tenant}', currentTenant ? currentTenant.value : "default") : `${api.url}-${currentTenant ? currentTenant.value : "default"}`, }} multiple={type === "single" ? false : true} disableClearable={true} diff --git a/src/components/CippWizard/CippWizardAutopilotImport.jsx b/src/components/CippWizard/CippWizardAutopilotImport.jsx new file mode 100644 index 000000000000..fc7876e34698 --- /dev/null +++ b/src/components/CippWizard/CippWizardAutopilotImport.jsx @@ -0,0 +1,483 @@ +import { + Button, + Link, + Stack, + Box, + Typography, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Alert, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { CippWizardStepButtons } from "./CippWizardStepButtons"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import { useWatch } from "react-hook-form"; +import { Delete, FileDownload, Upload, Add } from "@mui/icons-material"; +import { useEffect, useState } from "react"; +import React from "react"; + +export const CippWizardAutopilotImport = (props) => { + const { + onNextStep, + formControl, + currentStep, + onPreviousStep, + fields, + name, + nameToCSVMapping, + fileName = "template", + } = props; + const tableData = useWatch({ control: formControl.control, name: name }); + const [newTableData, setTableData] = useState([]); + const fileInputRef = React.useRef(null); + const [manualDialogOpen, setManualDialogOpen] = useState(false); + const [manualInputs, setManualInputs] = useState([{}]); + const inputRefs = React.useRef([]); + const [validationErrors, setValidationErrors] = useState([]); + + const handleRemoveItem = (row) => { + if (row === undefined) return false; + const index = tableData?.findIndex((item) => item === row); + const newTableData = [...tableData]; + newTableData.splice(index, 1); + setTableData(newTableData); + }; + + const handleFileSelect = (event) => { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const text = e.target.result; + const lines = text.split('\n'); + const firstLine = lines[0].split(',').map(header => header.trim()); + + // Check if this is a headerless CSV (no recognizable headers) + const hasHeaders = firstLine.some(header => { + // Check if any header matches our expected field names + return fields.some(field => + header === field.propertyName || + header === field.friendlyName || + (field.alternativePropertyNames && field.alternativePropertyNames.includes(header)) + ); + }); + + let headers, headerMapping; + + if (hasHeaders) { + // Normal CSV with headers + headers = firstLine; + + // Create mapping for property names and alternative property names + headerMapping = {}; + fields.forEach(field => { + // Map primary property name to itself + headerMapping[field.propertyName] = field.propertyName; + // Map friendly name to property name + headerMapping[field.friendlyName] = field.propertyName; + // Map alternative property names to the primary property name + if (field.alternativePropertyNames) { + field.alternativePropertyNames.forEach(altName => { + headerMapping[altName] = field.propertyName; + }); + } + }); + + // Check if all required columns are present (using any of the supported formats) + const missingColumns = fields.filter(field => { + // Only serial number is required + if (field.propertyName !== 'SerialNumber') { + return false; // Skip non-required fields + } + + const hasPropertyName = headers.includes(field.propertyName); + const hasFriendlyName = headers.includes(field.friendlyName); + const hasAlternativeName = field.alternativePropertyNames ? + field.alternativePropertyNames.some(altName => headers.includes(altName)) : false; + return !hasPropertyName && !hasFriendlyName && !hasAlternativeName; + }); + + if (missingColumns.length > 0) { + const missingFormats = missingColumns.map(f => { + const formats = [f.propertyName, f.friendlyName]; + if (f.alternativePropertyNames) { + formats.push(...f.alternativePropertyNames); + } + return `"${formats.join('" or "')}"`; + }).join(', '); + console.error(`CSV is missing required columns: ${missingFormats}`); + return; + } + } else { + // Headerless CSV - assume order: serial, productid, hash + headers = ['SerialNumber', 'productKey', 'hardwareHash']; + headerMapping = { + 'SerialNumber': 'SerialNumber', + 'productKey': 'productKey', + 'hardwareHash': 'hardwareHash' + }; + + // Check if we have at least 3 columns for the expected order + if (firstLine.length < 3) { + console.error('Headerless CSV must have at least 3 columns in order: Serial Number, Product ID, Hardware Hash'); + return; + } + } + + const data = lines.slice(hasHeaders ? 1 : 0) // Skip first line only if it has headers + .filter(line => line.trim() !== '') // Remove empty lines + .map(line => { + const values = line.split(','); + // Initialize with all fields as empty strings + const row = fields.reduce((obj, field) => { + obj[field.propertyName] = ''; + return obj; + }, {}); + // Fill in the values from the CSV + headers.forEach((header, i) => { + const propertyName = headerMapping[header]; + if (propertyName) { + row[propertyName] = values[i]?.trim() || ''; + } + }); + return row; + }); + + setTableData(data); + formControl.setValue(name, data, { shouldValidate: true }); + }; + reader.readAsText(file); + } + }; + + const handleManualInputChange = (rowIndex, field, value) => { + setManualInputs(prev => { + const newInputs = [...prev]; + if (!newInputs[rowIndex]) { + newInputs[rowIndex] = {}; + } + newInputs[rowIndex][field] = value; + return newInputs; + }); + }; + + const handleAddRow = () => { + setManualInputs(prev => [...prev, {}]); + }; + + const validateRows = (rows) => { + const errors = []; + const seenSerials = new Set(); + const seenProductKeys = new Set(); + + rows.forEach((row, index) => { + const serialField = fields.find(f => f.propertyName === 'SerialNumber'); + const productKeyField = fields.find(f => f.propertyName === 'productKey'); + const manufacturerField = fields.find(f => f.propertyName === 'oemManufacturerName'); + const modelField = fields.find(f => f.propertyName === 'modelName'); + const hardwareHashField = fields.find(f => f.propertyName === 'hardwareHash'); + + if (serialField && row[serialField.propertyName] && seenSerials.has(row[serialField.propertyName])) { + errors.push(`Row ${index + 1}: Duplicate serial number "${row[serialField.propertyName]}"`); + } + if (serialField && row[serialField.propertyName]) { + seenSerials.add(row[serialField.propertyName]); + } + + if (productKeyField && row[productKeyField.propertyName] && seenProductKeys.has(row[productKeyField.propertyName])) { + errors.push(`Row ${index + 1}: Duplicate product key "${row[productKeyField.propertyName]}"`); + } + if (productKeyField && row[productKeyField.propertyName]) { + seenProductKeys.add(row[productKeyField.propertyName]); + } + + // Validate Product ID length (must be exactly 13 characters) + if (productKeyField && row[productKeyField.propertyName] && row[productKeyField.propertyName].length !== 13) { + errors.push(`Row ${index + 1}: Product ID must be exactly 13 characters long`); + } + + // Validate Serial Number requirements: must have either Manufacturer+Model OR Hardware Hash + if (serialField && row[serialField.propertyName] && row[serialField.propertyName].trim() !== '') { + const hasManufacturer = manufacturerField && row[manufacturerField.propertyName] && row[manufacturerField.propertyName].trim() !== ''; + const hasModel = modelField && row[modelField.propertyName] && row[modelField.propertyName].trim() !== ''; + const hasHardwareHash = hardwareHashField && row[hardwareHashField.propertyName] && row[hardwareHashField.propertyName].trim() !== ''; + + const hasManufacturerAndModel = hasManufacturer && hasModel; + const hasHash = hasHardwareHash; + + if (!hasManufacturerAndModel && !hasHash) { + errors.push(`Row ${index + 1}: Serial Number must be accompanied by either both Manufacturer and Model, or Hardware Hash`); + } + } + }); + + setValidationErrors(errors); + return errors.length === 0; + }; + + const handleManualAdd = () => { + const newRows = manualInputs.filter(row => + Object.values(row).some(value => value && value.trim() !== '') + ).map(row => { + // Ensure all fields exist in the row + return fields.reduce((obj, field) => { + obj[field.propertyName] = row[field.propertyName] || ''; + return obj; + }, {}); + }); + + if (newRows.length === 0) { + setManualDialogOpen(false); + setManualInputs([{}]); + return; + } + + if (!validateRows(newRows)) { + return; + } + + const updatedData = [...tableData, ...newRows]; + setTableData(updatedData); + formControl.setValue(name, updatedData, { shouldValidate: true }); + setManualInputs([{}]); + setManualDialogOpen(false); + }; + + const handleDialogClose = () => { + setManualDialogOpen(false); + setManualInputs([{}]); + }; + + const handleKeyPress = (event, rowIndex) => { + const productKeyField = fields.find(f => f.propertyName === 'productKey'); + if (event.key === 'Enter' && productKeyField && manualInputs[rowIndex]?.[productKeyField.propertyName]) { + if (rowIndex === manualInputs.length - 1) { + const newRowIndex = manualInputs.length; + setManualInputs(prev => [...prev, {}]); + // Wait for the next render cycle to set focus + setTimeout(() => { + const newInput = inputRefs.current[newRowIndex]?.[productKeyField.propertyName]; + if (newInput) { + newInput.focus(); + } + }, 0); + } + } + }; + + const handleRemoveRow = (rowIndex) => { + setManualInputs(prev => prev.filter((_, index) => index !== rowIndex)); + }; + + useEffect(() => { + console.log('Table Data:', newTableData); + formControl.setValue(name, newTableData, { + shouldValidate: true, + }); + }, [newTableData]); + + // Add effect to validate rows when manualInputs changes + useEffect(() => { + validateRows(manualInputs); + }, [manualInputs]); + + const actions = [ + { + icon: , + label: "Delete Row", + confirmText: "Are you sure you want to delete this row?", + customFunction: handleRemoveItem, + noConfirm: true, + }, + ]; + + return ( + + f.propertyName)} + cardButton={ + + + + + + + } + /> + + + Manual Import + + + {validationErrors.length > 0 && ( + + + Please fix the following validation errors: + + {validationErrors.map((error, index) => ( + + • {error} + + ))} + + )} + {manualInputs.map((row, rowIndex) => ( + + {/* Row identifier */} + + {rowIndex + 1} + + {fields.map((field) => ( + + { + if (!inputRefs.current[rowIndex]) { + inputRefs.current[rowIndex] = {}; + } + inputRefs.current[rowIndex][field.propertyName] = el; + }} + label={field.friendlyName} + value={row[field.propertyName] || ''} + onChange={(e) => handleManualInputChange(rowIndex, field.propertyName, e.target.value)} + onKeyDown={(e) => field.propertyName === 'productKey' && handleKeyPress(e, rowIndex)} + fullWidth + size="small" + /> + + ))} + + + ))} + + + + + + + + + + + + + + ); +}; diff --git a/src/components/CippWizard/CippWizardAutopilotOptions.jsx b/src/components/CippWizard/CippWizardAutopilotOptions.jsx index 154eebbab74b..a289a0589c89 100644 --- a/src/components/CippWizard/CippWizardAutopilotOptions.jsx +++ b/src/components/CippWizard/CippWizardAutopilotOptions.jsx @@ -1,4 +1,5 @@ -import { Grid, Stack } from "@mui/material"; +import { Stack } from "@mui/material"; +import { Grid } from "@mui/system"; import CippWizardStepButtons from "./CippWizardStepButtons"; import CippFormComponent from "../CippComponents/CippFormComponent"; export const CippWizardAutopilotOptions = (props) => { @@ -8,7 +9,7 @@ export const CippWizardAutopilotOptions = (props) => { <> - + { <> - + { formControl={formControl} /> - + { const [newTableData, setTableData] = useState([]); const [open, setOpen] = useState(false); + // Register form field with validation + formControl.register(name, { + validate: (value) => Array.isArray(value) && value.length > 0, + }); + const handleRemoveItem = (row) => { if (row === undefined) return false; const index = tableData?.findIndex((item) => item === row); @@ -49,7 +46,9 @@ export const CippWizardCSVImport = (props) => { }; useEffect(() => { - formControl.setValue(name, newTableData); + formControl.setValue(name, newTableData, { + shouldValidate: true, + }); }, [newTableData]); const actions = [ @@ -80,7 +79,7 @@ export const CippWizardCSVImport = (props) => { {fields.map((field) => ( <> - + { ))} - + @@ -109,7 +108,7 @@ export const CippWizardCSVImport = (props) => { {!manualFields && ( <> - + @@ -120,7 +119,7 @@ export const CippWizardCSVImport = (props) => { {fields.map((field) => ( - + { /> ); -}; +}; \ No newline at end of file diff --git a/src/components/CippWizard/CippWizardConfirmation.js b/src/components/CippWizard/CippWizardConfirmation.js deleted file mode 100644 index 3e8ae6c78bff..000000000000 --- a/src/components/CippWizard/CippWizardConfirmation.js +++ /dev/null @@ -1,85 +0,0 @@ -import { Card, Stack, Grid } from "@mui/material"; -import { PropertyList } from "../property-list"; -import CippWizardStepButtons from "./CippWizardStepButtons"; -import { PropertyListItem } from "../property-list-item"; -import { getCippTranslation } from "../../utils/get-cipp-translation"; -import { getCippFormatting } from "../../utils/get-cipp-formatting"; - -export const CippWizardConfirmation = (props) => { - const { postUrl, lastStep, formControl, onPreviousStep, onNextStep, currentStep } = props; - const formValues = formControl.getValues(); - const formEntries = Object.entries(formValues); - //remove all entries in "blacklist" from showing on confirmation page - const blacklist = [ - "selectedOption", - "GUID", - "ID", - "noSubmitButton", - "RAWJson", - "TemplateList", - "addrow", - ]; - - const tenantEntry = formEntries.find(([key]) => key === "tenantFilter" || key === "tenant"); - const userEntry = formEntries.find(([key]) => - ["user", "userPrincipalName", "username"].includes(key) - ); - const filteredEntries = formEntries.filter( - ([key]) => - !blacklist.includes(key) && - key !== "tenantFilter" && - key !== "tenant" && - !["user", "userPrincipalName", "username"].includes(key) - ); - - const halfIndex = Math.ceil(filteredEntries.length / 2); - const firstHalf = filteredEntries.slice(0, halfIndex); - const secondHalf = filteredEntries.slice(halfIndex); - - if (tenantEntry) { - firstHalf.unshift(tenantEntry); - } - - if (userEntry) { - secondHalf.unshift(userEntry); - } - - return ( - - - - - - {firstHalf.map(([key, value]) => { - const formattedValue = getCippFormatting(value, key); - const label = getCippTranslation(key); - return ; - })} - - - - - {secondHalf.map(([key, value]) => { - const formattedValue = getCippFormatting(value, key); - const label = getCippTranslation(key); - return ; - })} - - - - - - - - ); -}; - -export default CippWizardConfirmation; diff --git a/src/components/CippWizard/CippWizardConfirmation.jsx b/src/components/CippWizard/CippWizardConfirmation.jsx new file mode 100644 index 000000000000..b0d497670b43 --- /dev/null +++ b/src/components/CippWizard/CippWizardConfirmation.jsx @@ -0,0 +1,110 @@ +import { Card, Stack, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { PropertyList } from "../property-list"; +import { PropertyListItem } from "../property-list-item"; +import CippWizardStepButtons from "./CippWizardStepButtons"; +import { getCippTranslation } from "../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; + +export const CippWizardConfirmation = (props) => { + const { postUrl, lastStep, formControl, onPreviousStep, onNextStep, currentStep } = props; + const formValues = formControl.getValues(); + const formEntries = Object.entries(formValues); + + const blacklist = [ + "selectedOption", + "GDAPAuth", + "SAMWizard", + "GUID", + "ID", + "noSubmitButton", + "RAWJson", + "TemplateList", + "addrow", + ]; + + // Filter out null values and undefined values which could be from hidden conditional fields + const filteredFormEntries = formEntries.filter( + ([_, value]) => value !== null && value !== undefined + ); + + const tenantEntry = filteredFormEntries.find( + ([key]) => key === "tenantFilter" || key === "tenant" + ); + const userEntry = filteredFormEntries.find(([key]) => + ["user", "userPrincipalName", "username"].includes(key) + ); + + const filteredEntries = formEntries.filter( + ([key]) => + !blacklist.includes(key) && + key !== "tenantFilter" && + key !== "tenant" && + !["user", "userPrincipalName", "username"].includes(key) + ); + + const halfIndex = Math.ceil(filteredEntries.length / 2); + const firstHalf = filteredEntries.slice(0, halfIndex); + const secondHalf = filteredEntries.slice(halfIndex); + + if (tenantEntry) { + firstHalf.unshift(tenantEntry); + } + + if (userEntry) { + secondHalf.unshift(userEntry); + } + + return ( + + {firstHalf.length === 0 ? ( + + + + You've completed the steps in this wizard. Hit submit to save your changes. + + + + ) : ( + + + + + {firstHalf.map(([key, value]) => ( + + ))} + + + + + {secondHalf.map(([key, value]) => ( + + ))} + + + + + )} + + + + ); +}; + +export default CippWizardConfirmation; diff --git a/src/components/CippWizard/CippWizardGroupTemplates.jsx b/src/components/CippWizard/CippWizardGroupTemplates.jsx index 215d22a0509f..488f97c65950 100644 --- a/src/components/CippWizard/CippWizardGroupTemplates.jsx +++ b/src/components/CippWizard/CippWizardGroupTemplates.jsx @@ -19,18 +19,19 @@ export const CippWizardGroupTemplates = (props) => { ]; useEffect(() => { if (watcher?.value) { + console.log(watcher); formControl.setValue("groupType", watcher.addedFields.groupType); formControl.setValue("Displayname", watcher.addedFields.Displayname); formControl.setValue("Description", watcher.addedFields.Description); formControl.setValue("username", watcher.addedFields.username); formControl.setValue("allowExternal", watcher.addedFields.allowExternal); - formControl.setValue("MembershipRules", watcher.addedFields.MembershipRules); + formControl.setValue("membershipRules", watcher.addedFields.membershipRules); } }, [watcher]); return ( - + { excludeTenantFilter: true, url: "/api/ListGroupTemplates", queryKey: "ListGroupTemplates", - labelField: (option) => `${option.Displayname} (${option.groupType})`, + labelField: (option) => + `${option.Displayname || option.displayName} (${option.groupType})`, valueField: "GUID", addedField: { groupType: "groupType", - Displayname: "Displayname", - Description: "Description", + Displayname: "displayName", + Description: "description", username: "username", allowExternal: "allowExternal", - MembershipRules: "MembershipRules", + membershipRules: "membershipRules", }, + showRefresh: true, }} /> - + { validators={{ required: "Please select a group type" }} /> - + { validators={{ required: "Group display name is required" }} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - - + + - - - - + + + + - - - - + + + + - - + + { const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props; const currentTenant = formControl.watch("tenantFilter"); const selectedUsers = useWatch({ control: formControl.control, name: "user" }); const [showAlert, setShowAlert] = useState(false); + const userSettingsDefaults = useSettings().userSettingsDefaults; + const disableForwarding = useWatch({ control: formControl.control, name: "disableForwarding" }); useEffect(() => { - if (selectedUsers.length >= 4) { + if (selectedUsers.length >= 3) { setShowAlert(true); formControl.setValue("Scheduled.enabled", true); } }, [selectedUsers]); + useEffect(() => { + if (userSettingsDefaults?.offboardingDefaults) { + userSettingsDefaults.offboardingDefaults.forEach((setting) => { + formControl.setValue(setting.name, setting.value); + }); + } + }, [userSettingsDefaults]); + + useEffect(() => { + if (disableForwarding) { + formControl.setValue("forward", null); + formControl.setValue("KeepCopy", false); + } + }, [disableForwarding, formControl]); + return ( - + @@ -47,7 +65,7 @@ export const CippWizardOffboarding = (props) => { /> @@ -87,6 +105,12 @@ export const CippWizardOffboarding = (props) => { type="switch" formControl={formControl} /> + { - + @@ -131,6 +155,7 @@ export const CippWizardOffboarding = (props) => { dataKey: "Results", labelField: (option) => `${option.displayName} (${option.userPrincipalName})`, valueField: "id", + queryKey: "Offboarding-Users", data: { Endpoint: "users", manualPagination: true, @@ -155,6 +180,7 @@ export const CippWizardOffboarding = (props) => { url: "/api/ListGraphRequest", dataKey: "Results", tenantFilter: currentTenant ? currentTenant.value : undefined, + queryKey: "Offboarding-Users", data: { Endpoint: "users", manualPagination: true, @@ -179,6 +205,7 @@ export const CippWizardOffboarding = (props) => { valueField: "id", url: "/api/ListGraphRequest", dataKey: "Results", + queryKey: "Offboarding-Users", data: { Endpoint: "users", manualPagination: true, @@ -194,36 +221,51 @@ export const CippWizardOffboarding = (props) => { Email Forwarding `${option.displayName} (${option.userPrincipalName})`, - valueField: "id", - url: "/api/ListGraphRequest", - dataKey: "Results", - data: { - Endpoint: "users", - manualPagination: true, - $select: "id,userPrincipalName,displayName", - $count: true, - $orderby: "displayName", - $top: 999, - }, - }} /> - + field={"disableForwarding"} + compareType="isNot" + compareValue={true} + > + `${option.displayName} (${option.userPrincipalName})`, + valueField: "id", + url: "/api/ListGraphRequest", + dataKey: "Results", + queryKey: "Offboarding-Users", + data: { + Endpoint: "users", + manualPagination: true, + $select: "id,userPrincipalName,displayName", + $count: true, + $orderby: "displayName", + $top: 999, + }, + }} + /> + + + { {showAlert && ( - You have selected more than 3 users. This offboarding must be scheduled. + You have selected more than 2 users. This offboarding must be scheduled. )} @@ -248,7 +290,7 @@ export const CippWizardOffboarding = (props) => { - + { compareType="is" compareValue={true} > - + Scheduled Offboarding Date { /> - + Send results to: - + diff --git a/src/components/CippWizard/CippWizardOptionsList.jsx b/src/components/CippWizard/CippWizardOptionsList.jsx index cea2d3488674..9fe448e6da63 100644 --- a/src/components/CippWizard/CippWizardOptionsList.jsx +++ b/src/components/CippWizard/CippWizardOptionsList.jsx @@ -3,10 +3,19 @@ import { useEffect, useState } from "react"; import { CippWizardStepButtons } from "./CippWizardStepButtons"; export const CippWizardOptionsList = (props) => { - const { onNextStep, options, title, subtext, formControl, currentStep, onPreviousStep } = props; + const { + onNextStep, + options, + title, + subtext, + formControl, + currentStep, + onPreviousStep, + name = "selectedOption", + } = props; const [selectedOption, setSelectedOption] = useState(null); - // Register the "selectedOption" field in react-hook-form - formControl.register("selectedOption", { + // Register the name field in react-hook-form + formControl.register(name, { required: true, }); @@ -24,7 +33,7 @@ export const CippWizardOptionsList = (props) => { const handleOptionClick = (value) => { setSelectedOption(value); // Visually select the option - formControl.setValue("selectedOption", value); // Update form value in React Hook Form + formControl.setValue(name, value); // Update form value in React Hook Form formControl.trigger(); }; diff --git a/src/components/CippWizard/CippWizardPage.jsx b/src/components/CippWizard/CippWizardPage.jsx index e0960bad2a02..ed092bf2ce6b 100644 --- a/src/components/CippWizard/CippWizardPage.jsx +++ b/src/components/CippWizard/CippWizardPage.jsx @@ -1,8 +1,8 @@ -import Head from "next/head"; import { Box, Button, Container, Stack, SvgIcon } from "@mui/material"; import { CippWizard } from "./CippWizard"; import { useRouter } from "next/router"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; +import { CippHead } from "../CippComponents/CippHead"; const CippWizardPage = (props) => { const router = useRouter(); @@ -17,9 +17,7 @@ const CippWizardPage = (props) => { } = props; return ( <> - - {wizardTitle} - + { const newData = {}; Object.keys(values).forEach((key) => { const value = values[key]; - if (replacementBehaviour !== "removeNulls") { + // Only add non-null values if removeNulls is specified + if (replacementBehaviour !== "removeNulls" || value !== null) { newData[key] = value; - } else if (row[value] !== undefined) { - newData[key] = row[value]; } }); sendForm.mutate({ url: postUrl, data: newData }); diff --git a/src/components/CippWizard/CustomerForm.jsx b/src/components/CippWizard/CustomerForm.jsx new file mode 100644 index 000000000000..7eedc7a7e456 --- /dev/null +++ b/src/components/CippWizard/CustomerForm.jsx @@ -0,0 +1,86 @@ +import "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "../CippComponents/CippFormComponent"; + +export const CustomerForm = (props) => { + const { formControl } = props; + + const fields = [ + { name: "Domain", label: "Domain", type: "textField", required: true }, + { name: "CompanyName", label: "Company Name", type: "textField", required: true }, + { name: "FirstName", label: "First Name", type: "textField", required: true }, + { name: "LastName", label: "Last Name", type: "textField", required: true }, + { name: "Email", label: "Email", type: "email", required: true }, + { name: "PhoneNumber", label: "Phone Number", type: "textField", required: true }, + { name: "Country", label: "Country", type: "textField", required: true }, + { name: "City", label: "City", type: "textField", required: true }, + { name: "State", label: "State", type: "textField", required: true }, + { name: "AddressLine1", label: "Address Line 1", type: "textField", required: true }, + { name: "AddressLine2", label: "Address Line 2", type: "textField", required: false }, + { name: "PostalCode", label: "Postal Code", type: "textField", required: true }, + ]; + + const transformPayload = (formData) => { + const payload = { + enableGDAPByDefault: false, + Id: null, + CommerceId: null, + CompanyProfile: { + TenantId: null, + Domain: formData.Domain, + CompanyName: formData.CompanyName, + Attributes: { ObjectType: "CustomerCompanyProfile" }, + }, + BillingProfile: { + Id: null, + FirstName: formData.FirstName, + LastName: formData.LastName, + Email: formData.Email, + Culture: "EN-US", + Language: "En", + CompanyName: formData.CompanyName, + DefaultAddress: { + Country: formData.Country, + Region: null, + City: formData.City, + State: formData.State, + AddressLine1: formData.AddressLine1, + AddressLine2: formData.AddressLine2, + PostalCode: formData.PostalCode, + FirstName: formData.FirstName, + LastName: formData.LastName, + PhoneNumber: formData.PhoneNumber, + }, + Attributes: { ObjectType: "CustomerBillingProfile" }, + }, + RelationshipToPartner: "none", + AllowDelegatedAccess: null, + UserCredentials: null, + CustomDomains: null, + Attributes: { ObjectType: "Customer" }, + }; + + if (formData.ResellerType === "Tier2" && associatedPartnerId) { + payload.AssociatedPartnerId = associatedPartnerId; + } + + return payload; + }; + + return ( + + {fields.map((field, index) => ( + + + + ))} + + ); +}; diff --git a/src/components/CippWizard/wizard-steps.js b/src/components/CippWizard/wizard-steps.js index 70c2d1e910e2..61e1c84d572c 100644 --- a/src/components/CippWizard/wizard-steps.js +++ b/src/components/CippWizard/wizard-steps.js @@ -127,7 +127,9 @@ export const WizardSteps = (props) => { {steps.map((step) => ( - {step.title} + + {`Step ${steps.indexOf(step) ? steps.indexOf(step) + 1 : 1}`} + {step.description} diff --git a/src/components/ExecutiveReportButton.js b/src/components/ExecutiveReportButton.js new file mode 100644 index 000000000000..c0fa5088694a --- /dev/null +++ b/src/components/ExecutiveReportButton.js @@ -0,0 +1,2386 @@ +import React, { useState, useMemo } from "react"; +import { + Button, + Tooltip, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Typography, + FormControlLabel, + Switch, + Grid, + Paper, + IconButton, + Divider, +} from "@mui/material"; +import { PictureAsPdf, Visibility, Download, Close, Settings } from "@mui/icons-material"; +import { + Document, + Page, + Text, + View, + StyleSheet, + PDFDownloadLink, + PDFViewer, + Image, + Svg, + Path, + Circle, + Line, + Rect, +} from "@react-pdf/renderer"; +import { useSettings } from "../hooks/use-settings"; +import { useSecureScore } from "../hooks/use-securescore"; +import { ApiGetCall } from "../api/ApiCall"; + +// PRODUCTION-GRADE PDF SYSTEM WITH CONDITIONAL RENDERING +const ExecutiveReportDocument = ({ + tenantName, + userStats, + brandingSettings, + secureScoreData, + licensingData, + deviceData, + conditionalAccessData, + standardsCompareData, + sectionConfig = { + executiveSummary: true, + securityStandards: true, + secureScore: true, + licenseManagement: true, + deviceManagement: true, + conditionalAccess: true, + infographics: true, + }, +}) => { + const currentDate = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + const brandColor = brandingSettings?.colour || "#F77F00"; + + // ENTERPRISE DESIGN SYSTEM - JOBS/RAMS/IVE PRINCIPLES + const styles = StyleSheet.create({ + // FOUNDATION - CONSISTENT STATE OWNERSHIP (FLORENCE) + page: { + flexDirection: "column", + backgroundColor: "#FFFFFF", + fontFamily: "Helvetica", + fontSize: 10, + lineHeight: 1.4, + color: "#2D3748", + padding: 40, // Consistent base padding + }, + + // COVER PAGE - PROPORTIONAL & INTENTIONAL (JOBS/RAMS/IVE) + 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, + }, + + brandName: { + fontSize: 12, + fontWeight: "bold", + color: brandColor, + letterSpacing: 1, + textTransform: "uppercase", + }, + + dateStamp: { + fontSize: 9, + color: "#000000", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + + // MODERN HERO SECTION + 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, + textTransform: "uppercase", + }, + + titleAccent: { + color: brandColor, + }, + + subtitle: { + fontSize: 14, + color: "#000000", + fontWeight: "normal", + lineHeight: 1.5, + marginBottom: 40, + maxWidth: 400, + }, + + tenantCard: { + backgroundColor: "transparent", + padding: 0, + maxWidth: 400, + }, + + tenantName: { + fontSize: 18, + fontWeight: "bold", + color: "#000000", + marginBottom: 8, + textAlign: "center", + }, + + tenantMeta: { + fontSize: 11, + color: "#333333", + textAlign: "center", + }, + + coverFooter: { + textAlign: "center", + marginTop: 60, + }, + + confidential: { + fontSize: 9, + color: "#A0AEC0", + textTransform: "uppercase", + letterSpacing: 1, + }, + + // CONTENT PAGES - MODULAR COMPOSITION (FROST) + pageHeader: { + borderBottom: `1px solid ${brandColor}`, + paddingBottom: 12, + marginBottom: 24, + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + pageBreakAfter: "avoid", + breakAfter: "avoid", + }, + + pageHeaderContent: { + flex: 1, + }, + + pageTitle: { + fontSize: 20, + fontWeight: "bold", + color: "#1A202C", + marginBottom: 8, + }, + + pageSubtitle: { + fontSize: 11, + color: "#4A5568", + fontWeight: "normal", + }, + + // SECTIONS - REPEATABLE PATTERNS (FROST) + section: { + marginBottom: 24, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + sectionTitle: { + fontSize: 14, + fontWeight: "bold", + color: brandColor, + marginBottom: 12, + pageBreakAfter: "avoid", + breakAfter: "avoid", + orphans: 3, + widows: 3, + }, + + bodyText: { + fontSize: 9, + color: "#2D3748", + lineHeight: 1.5, + marginBottom: 12, + textAlign: "justify", + }, + + // STATS GRID - PERFECT ALIGNMENT (SPOOL) + 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: 16, + fontWeight: "bold", + color: brandColor, + marginBottom: 4, + }, + + statLabel: { + fontSize: 7, + color: "#4A5568", + textTransform: "uppercase", + letterSpacing: 0.5, + textAlign: "center", + fontWeight: "bold", + }, + + // COMPLIANCE BARS - VISUAL CONFIDENCE (SPOOL) + complianceList: { + gap: 8, + }, + + complianceItem: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#FFFFFF", + padding: 10, + borderRadius: 4, + border: `1px solid #F0F0F0`, + }, + + complianceLabel: { + fontSize: 8, + color: "#2D3748", + width: 80, + fontWeight: "bold", + }, + + complianceBarContainer: { + flex: 1, + height: 6, + backgroundColor: "#E2E8F0", + marginHorizontal: 10, + borderRadius: 3, + overflow: "hidden", + }, + + complianceBar: { + height: 6, + backgroundColor: brandColor, + borderRadius: 3, + }, + + complianceValue: { + fontSize: 8, + color: "#2D3748", + width: 25, + textAlign: "right", + fontWeight: "bold", + }, + + // SECURE SCORE CARDS - ENTERPRISE GRADE + scoreGrid: { + flexDirection: "row", + gap: 12, + marginBottom: 20, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + scoreCard: { + flex: 1, + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderRadius: 6, + padding: 16, + alignItems: "center", + borderTop: `3px solid ${brandColor}`, + }, + + scoreNumber: { + fontSize: 20, + fontWeight: "bold", + color: brandColor, + marginBottom: 8, + }, + + scoreLabel: { + fontSize: 7, + color: "#4A5568", + textTransform: "uppercase", + letterSpacing: 0.5, + textAlign: "center", + fontWeight: "bold", + }, + + // CHART AREA - BROWSER CONSTRAINTS (RAUCH) + chartContainer: { + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderRadius: 6, + padding: 16, + marginBottom: 20, + alignItems: "center", + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + chartTitle: { + fontSize: 10, + fontWeight: "bold", + color: "#2D3748", + marginBottom: 12, + }, + + chartData: { + fontSize: 9, + color: "#4A5568", + textAlign: "center", + lineHeight: 1.4, + }, + + // CONTROLS TABLE - HIGH PERFORMANCE (RAUCH) + controlsTable: { + border: `1px solid #E2E8F0`, + borderRadius: 6, + overflow: "hidden", + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + tableHeader: { + flexDirection: "row", + backgroundColor: brandColor, + paddingVertical: 10, + paddingHorizontal: 12, + }, + + headerCell: { + fontSize: 7, + fontWeight: "bold", + color: "#FFFFFF", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + + headerName: { + width: 100, + }, + + headerDesc: { + flex: 1, + marginLeft: 12, + }, + + headerStatus: { + width: 60, + textAlign: "center", + marginLeft: 12, + }, + + tableRow: { + flexDirection: "row", + borderBottomWidth: 1, + borderBottomColor: "#F7FAFC", + paddingVertical: 8, + paddingHorizontal: 12, + alignItems: "center", + }, + + cellName: { + width: 100, + fontSize: 8, + fontWeight: "bold", + color: "#2D3748", + }, + + cellDesc: { + flex: 1, + marginLeft: 12, + fontSize: 7, + color: "#4A5568", + lineHeight: 1.3, + }, + + cellStatus: { + width: 60, + marginLeft: 12, + alignItems: "center", + justifyContent: "center", + }, + + // STATUS TEXT - SIMPLE APPROACH + statusText: { + fontSize: 7, + fontWeight: "bold", + textAlign: "center", + textTransform: "uppercase", + letterSpacing: 0.3, + }, + + statusCompliant: { + color: "#22543D", + }, + + statusPartial: { + color: "#744210", + }, + + statusReview: { + color: "#742A2A", + }, + + // INFO BOXES - CONSISTENT PATTERNS (FROST) + infoBox: { + backgroundColor: "#FFFFFF", + border: `1px solid #E2E8F0`, + borderLeft: `4px solid ${brandColor}`, + borderRadius: 4, + padding: 12, + marginBottom: 12, + pageBreakInside: "avoid", + breakInside: "avoid", + orphans: 3, + widows: 3, + }, + + infoTitle: { + fontSize: 9, + fontWeight: "bold", + color: "#2D3748", + marginBottom: 6, + }, + + infoText: { + fontSize: 8, + color: "#4A5568", + lineHeight: 1.4, + }, + + // RECOMMENDATIONS - SCALABLE SECTIONS (FROST) + recommendationsList: { + gap: 8, + pageBreakInside: "avoid", + breakInside: "avoid", + }, + + recommendationItem: { + flexDirection: "row", + alignItems: "flex-start", + }, + + recommendationBullet: { + fontSize: 8, + color: brandColor, + marginRight: 6, + fontWeight: "bold", + marginTop: 1, + }, + + recommendationText: { + fontSize: 8, + color: "#2D3748", + lineHeight: 1.4, + flex: 1, + }, + + recommendationLabel: { + fontWeight: "bold", + }, + + // FOOTER - DETERMINISTIC PAGINATION (FLORENCE) + 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", + }, + + // BLACK STATISTIC PAGES - MODERN DESIGN + statPage: { + flexDirection: "column", + backgroundColor: "#000000", + fontFamily: "Helvetica", + padding: 0, + justifyContent: "center", + alignItems: "flex-start", + minHeight: "100%", + position: "relative", + }, + + statOverlay: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + padding: 60, + justifyContent: "center", + alignItems: "flex-start", + zIndex: 10, + backgroundColor: "rgba(0, 0, 0, 0.7)", + }, + + statMainText: { + fontSize: 18, + color: "#FFFFFF", + fontWeight: "bold", + lineHeight: 1.4, + marginBottom: 8, + }, + + statHighlight: { + fontSize: 72, + color: brandColor, + fontWeight: "900", + lineHeight: 1, + marginBottom: 8, + }, + + statBackground: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + opacity: 0.5, + }, + + statSubText: { + fontSize: 14, + color: "#FFFFFF", + fontWeight: "bold", + lineHeight: 1.3, + marginBottom: 40, + }, + + statFooterText: { + position: "absolute", + bottom: 60, + right: 60, + fontSize: 12, + color: "#FFFFFF", + fontWeight: "bold", + textAlign: "right", + lineHeight: 1.3, + }, + + statBrandFooter: { + position: "absolute", + bottom: 60, + left: 60, + fontSize: 8, + color: "#666666", + textTransform: "uppercase", + letterSpacing: 1, + }, + + // CENTERED IMAGE STYLE + centeredImage: { + width: 300, + height: 200, + alignSelf: "center", + marginVertical: 20, + borderRadius: 8, + }, + + // SVG CHART STYLES + svgChartContainer: { + alignItems: "center", + marginVertical: 12, + }, + + svgChart: { + width: 400, + height: 200, + marginBottom: 8, + }, + + chartSummaryText: { + fontSize: 8, + fontWeight: "bold", + color: brandColor, + textAlign: "center", + marginTop: 8, + }, + }); + + // PROCESS REAL STANDARDS DATA + const processStandardsData = (apiData) => { + // Try to fetch standards data dynamically + let standardsData = null; + try { + standardsData = require("../data/standards.json"); + } catch (error) {} + + if (!apiData || !Array.isArray(apiData) || apiData.length === 0) { + return []; + } + + const processedStandards = []; + const tenantData = apiData[0]; // Get the first tenant's data + + // Process each standard from the API response + Object.keys(tenantData).forEach((key) => { + if (key.startsWith("standards.") && key !== "tenantFilter") { + const standardKey = key; + const standardValue = tenantData[key]; + const standardDef = standardsData?.find((std) => std.name === standardKey); + + if (standardDef) { + // Determine compliance status + let status = "Review"; + if (standardValue && typeof standardValue === "object" && standardValue.Value === true) { + status = "Compliant"; + } else if (standardValue && standardValue.Value === true) { + status = "Compliant"; + } + // Get tags for display - fix the tags access + const tags = + standardDef.tag && Array.isArray(standardDef.tag) && standardDef.tag.length > 0 + ? standardDef.tag.slice(0, 2).join(", ") // Show first 2 tags + : "No tags"; + processedStandards.push({ + name: standardDef.label, + description: + standardDef.executiveText || standardDef.helpText || "No description available", + status: status, + tags: tags, + }); + } else { + // If no definition found, still add it with basic info + let status = "Review"; + if (standardValue && typeof standardValue === "object" && standardValue.Value === true) { + status = "Compliant"; + } else if (standardValue && standardValue.Value === true) { + status = "Compliant"; + } + + // Create a proper name from the key + const displayName = standardKey + .replace("standards.", "") + .replace(/([A-Z])/g, " $1") // Add space before capital letters + .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter + .trim(); + + processedStandards.push({ + name: displayName, + description: "Security standard implementation", + status: status, + tags: "No tags", + }); + } + } + }); + + return processedStandards; + }; + + let securityControls = processStandardsData(standardsCompareData); + + const getBadgeStyle = (status) => { + switch (status) { + case "Compliant": + return [styles.statusText, styles.statusCompliant]; + case "Partial": + return [styles.statusText, styles.statusPartial]; + case "Review": + case "Review Required": + return [styles.statusText, styles.statusReview]; + default: + return styles.statusText; + } + }; + + return ( + + {/* COVER PAGE - JOBS/RAMS/IVE PERFECTION */} + + + + + {brandingSettings?.logo && ( + + )} + + {currentDate} + + + + SECURITY ASSESSMENT + + + Executive{"\n"} + Summary + + + + Security & Compliance Assessment for {tenantName || "your organization"} + + + + {tenantName || "Organization Name"} + + + + + Confidential & Proprietary + + + + {/* EXECUTIVE SUMMARY - MODULAR COMPOSITION (FROST) */} + {sectionConfig.executiveSummary && ( + + + + Executive Summary + + Strategic overview of your Microsoft 365 security posture + + + {brandingSettings?.logo && ( + + )} + + + + + This security assessment for{" "} + {tenantName || "your organization"}{" "} + provides a clear picture of your organization's cybersecurity posture and readiness + against modern threats. We've evaluated your current security measures against + industry best practices to identify strengths and opportunities for improvement. + + + + Our assessment follows globally recognized security standards to ensure your + organization meets regulatory requirements and industry benchmarks. This approach + helps protect your business assets, maintain customer trust, and reduce operational + risks from cyber threats. + + + + + Environment Overview + + + + {userStats?.licensedUsers || "0"} + Licensed Users + + + {userStats?.unlicensedUsers || "0"} + Unlicensed Users + + + {userStats?.guests || "0"} + Guest Users + + + {userStats?.globalAdmins || "0"} + Global Admins + + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + )} + + {/* STATISTIC PAGE 1 - CHAPTER SPLITTER */} + {sectionConfig.infographics && ( + + + + 83% + + of organizations experienced{"\n"} + more than one cyberattack + {"\n"} + in the past year + + + + Proactive security prevents{"\n"} + repeated attacks + + + )} + + {/* SECURITY CONTROLS - Only show if standards data is available and enabled */} + {sectionConfig.securityStandards && + (() => { + return securityControls && securityControls.length > 0; + })() && ( + + + + Security Standards Assessment + + Detailed evaluation of implemented security standards + + + {brandingSettings?.logo && ( + + )} + + + + + Your security standards have been carefully evaluated against industry best + practices to protect your business from cyber threats while ensuring smooth daily + operations. These standards help maintain business continuity, protect sensitive + data, and meet regulatory requirements that are essential for your industry. + + + + + Security Standards Status + + + + Standard + Description + Tags + + Status + + + + {securityControls.map((control, index) => ( + + + {control.name} + + + {control.description} + + + {(() => { + if (typeof control.tags === "object") { + console.log( + "DEBUG: control.tags is an object:", + control.tags, + "for control:", + control.name + ); + } + return control.tags; + })()} + + + {control.status} + + + ))} + + + + + Key Recommendations + + + + + + Immediate Actions: Address + standards marked as "Review" to enhance security posture + + + + + + Compliance: Ensure all security + standards are properly implemented and maintained + + + + + + Monitoring: Establish regular + review cycles for all security standards + + + + + + Training: Implement security + awareness programs to reduce human risk factors + + + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + )} + + {/* STATISTIC PAGE 2 - CHAPTER SPLITTER - Only show if secure score data is available and enabled */} + {sectionConfig.infographics && + sectionConfig.secureScore && + secureScoreData && + secureScoreData?.isSuccess && + secureScoreData?.translatedData && ( + + + + 95% + + of successful cyber attacks{"\n"} + could have been prevented with{"\n"} + proactive security measures + + + + Your security resilience is{"\n"} + our primary mission + + + )} + + {/* MICROSOFT SECURE SCORE - DEDICATED PAGE - Only show if secure score data is available and enabled */} + {sectionConfig.secureScore && + secureScoreData && + secureScoreData?.isSuccess && + secureScoreData?.translatedData && ( + + + + Microsoft Secure Score + + Comprehensive security posture measurement and benchmarking + + + {brandingSettings?.logo && ( + + )} + + + + + Microsoft Secure Score measures how well your organization is protected against + cyber threats. This score reflects the effectiveness of your current security + measures and helps identify areas where additional protection could strengthen your + business resilience. + + + + + Score Comparison + + + + + {secureScoreData?.translatedData?.currentScore || "N/A"} + + Current Score + + + + {secureScoreData?.translatedData?.maxScore || "N/A"} + + Max Score + + + + {secureScoreData?.translatedData?.percentageVsSimilar || "N/A"}% + + vs Similar Orgs + + + + {secureScoreData?.translatedData?.percentageVsAllTenants || "N/A"}% + + vs All Orgs + + + + + + 7-Day Score Trend + + + Secure Score Progress + {secureScoreData?.secureScore?.data?.Results && + secureScoreData.secureScore.data.Results.length > 0 ? ( + + + {/* Chart Background */} + + + {/* Chart Grid Lines */} + {[0, 1, 2, 3, 4].map((i) => ( + + ))} + + {/* Chart Data Points and Area */} + {(() => { + const data = secureScoreData.secureScore.data.Results.slice().reverse(); + const maxScore = secureScoreData?.translatedData?.maxScore || 100; + const minScore = 0; // Always start from 0 + const scoreRange = maxScore; // Full range from 0 to max + const chartWidth = 320; + const chartHeight = 140; + const pointSpacing = chartWidth / Math.max(data.length - 1, 1); + + // Generate path for area chart + let pathData = `M 40 ${ + 160 - (data[0].currentScore / scoreRange) * chartHeight + }`; + data.forEach((point, index) => { + if (index > 0) { + const x = 40 + index * pointSpacing; + const y = 160 - (point.currentScore / scoreRange) * chartHeight; + pathData += ` L ${x} ${y}`; + } + }); + pathData += ` L ${40 + (data.length - 1) * pointSpacing} 160 L 40 160 Z`; + + // Generate line path (without area fill) + let lineData = `M 40 ${ + 160 - (data[0].currentScore / scoreRange) * chartHeight + }`; + data.forEach((point, index) => { + if (index > 0) { + const x = 40 + index * pointSpacing; + const y = 160 - (point.currentScore / scoreRange) * chartHeight; + lineData += ` L ${x} ${y}`; + } + }); + + return ( + <> + {/* Area Fill */} + + + {/* Line */} + + + {/* Data Points */} + {data.map((point, index) => { + const x = 40 + index * pointSpacing; + const y = 160 - (point.currentScore / scoreRange) * chartHeight; + return ; + })} + + {/* X-axis Labels */} + {data.map((point, index) => { + const x = 40 + index * pointSpacing; + const date = new Date(point.createdDateTime); + const label = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + return ( + + {label} + + ); + })} + + {/* Y-axis Labels */} + {[ + 0, + Math.round(maxScore * 0.25), + Math.round(maxScore * 0.5), + Math.round(maxScore * 0.75), + maxScore, + ].map((score, index) => ( + + {score} + + ))} + + ); + })()} + + + + Current: {secureScoreData?.translatedData?.currentScore || "N/A"} /{" "} + {secureScoreData?.translatedData?.maxScore || "N/A"}( + {secureScoreData?.translatedData?.percentageCurrent || "N/A"}%) + + + ) : ( + + Current Score: {secureScoreData?.translatedData?.currentScore || "N/A"} /{" "} + {secureScoreData?.translatedData?.maxScore || "N/A"} + {"\n"} + Achievement Rate: {secureScoreData?.translatedData?.percentageCurrent || "N/A"}% + {"\n"} + Historical data not available + + )} + + + + + What Your Score Means + + Your current score of {secureScoreData?.translatedData?.currentScore || "N/A"}{" "} + represents {secureScoreData?.translatedData?.percentageCurrent || "N/A"}% of the + maximum protection level available. This indicates how well your organization is + currently defended against common cyber threats and data breaches. + + + + + Why Scores Change + + • Business growth and new employees may temporarily lower scores until security + measures are applied{"\n"}• Changes in software licenses can affect available + security features{"\n"}• New security threats require updated protections, which may + impact scores{"\n"}• Regular security improvements help maintain and increase your + protection level + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + )} + + {/* LICENSING PAGE - Only show if license data is available */} + {sectionConfig.licenseManagement && + licensingData && + Array.isArray(licensingData) && + licensingData.length > 0 && ( + <> + {/* STATISTIC PAGE 3 - CHAPTER SPLITTER */} + {sectionConfig.infographics && ( + + + + Every + 39 + seconds + + a business falls victim to{"\n"} + ransomware attacks + + + + Proactive defense beats{"\n"} + reactive recovery + + + )} + + + + License Management + + Microsoft 365 license allocation and utilization analysis + + + {brandingSettings?.logo && ( + + )} + + + + + Smart license management helps control costs while ensuring your team has the + tools they need to be productive. This analysis shows how your current licenses + are being used and identifies opportunities to optimize spending without + compromising business operations. + + + + + License Allocation Summary + + + + License Type + + Used + + + Available + + + Total + + + + {licensingData.map((license, index) => ( + + + {(() => { + const licenseValue = license.License || license.license || "N/A"; + if (typeof licenseValue === "object") { + console.log( + "DEBUG: license name is an object:", + licenseValue, + "full license:", + license + ); + } + return licenseValue; + })()} + + + {(() => { + const countUsed = license.CountUsed || license.countUsed || "0"; + if (typeof countUsed === "object") { + console.log( + "DEBUG: license.CountUsed is an object:", + countUsed, + "full license:", + license + ); + } + return countUsed; + })()} + + + {(() => { + const countAvailable = + license.CountAvailable || license.countAvailable || "0"; + if (typeof countAvailable === "object") { + console.log( + "DEBUG: license.CountAvailable is an object:", + countAvailable, + "full license:", + license + ); + } + return countAvailable; + })()} + + + {(() => { + const totalLicenses = + license.TotalLicenses || license.totalLicenses || "0"; + if (typeof totalLicenses === "object") { + console.log( + "DEBUG: license.TotalLicenses is an object:", + totalLicenses, + "full license:", + license + ); + } + return totalLicenses; + })()} + + + ))} + + + + + License Optimization Recommendations + + + + + + Usage Monitoring: Track how + licenses are being used to identify cost-saving opportunities + + + + + + Cost Control: Review unused + licenses to reduce unnecessary spending + + + + + + Growth Planning: Ensure you + have enough licenses for business expansion without overspending + + + + + + Regular Reviews: Conduct + quarterly reviews to maintain cost-effective license allocation + + + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} + + {/* DEVICES PAGE - Only show if device data is available */} + {sectionConfig.deviceManagement && + deviceData && + Array.isArray(deviceData) && + deviceData.length > 0 && ( + <> + {/* STATISTIC PAGE 4 - CHAPTER SPLITTER */} + {sectionConfig.infographics && ( + + + + $4.45M + + average cost of a{"\n"} + data breach in 2024 + + + + Investment in security + {"\n"} + saves millions in recovery + + + )} + + + + Device Management + + Device compliance status and management overview + + + {brandingSettings?.logo && ( + + )} + + + + + Managing employee devices is essential for protecting your business data and + maintaining productivity. This analysis shows which devices meet your security + standards and identifies any that may need attention to prevent data breaches or + operational disruptions. + + + + + Device Compliance Overview + + + + {deviceData.length} + Total Devices + + + + { + deviceData.filter( + (device) => + device.complianceState === "compliant" || + device.ComplianceState === "compliant" + ).length + } + + Compliant + + + + { + deviceData.filter( + (device) => + device.complianceState !== "compliant" && + device.ComplianceState !== "compliant" + ).length + } + + Non-Compliant + + + + {Math.round( + (deviceData.filter( + (device) => + device.complianceState === "Compliant" || + device.ComplianceState === "Compliant" + ).length / + deviceData.length) * + 100 + )} + % + + Compliance Rate + + + + + + Device Management Summary + + + + Device Name + OS + Compliance + Last Sync + + + {deviceData.slice(0, 8).map((device, index) => { + const lastSync = device.lastSyncDateTime + ? new Date(device.lastSyncDateTime).toLocaleDateString() + : "N/A"; + return ( + + + {(() => { + const deviceName = device.deviceName || "N/A"; + if (typeof deviceName === "object") { + console.log( + "DEBUG: device.deviceName is an object:", + deviceName, + "full device:", + device + ); + } + return deviceName; + })()} + + + {(() => { + const operatingSystem = device.operatingSystem || "N/A"; + if (typeof operatingSystem === "object") { + console.log( + "DEBUG: device.operatingSystem is an object:", + operatingSystem, + "full device:", + device + ); + } + return operatingSystem; + })()} + + + + {(() => { + const complianceState = device.complianceState || "Unknown"; + if (typeof complianceState === "object") { + console.log( + "DEBUG: device.complianceState is an object:", + complianceState, + "full device:", + device + ); + } + return complianceState; + })()} + + + {lastSync} + + ); + })} + + + + + Device Insights + + + + + {deviceData.filter((device) => device.operatingSystem === "Windows").length} + + Windows Devices + + + + {deviceData.filter((device) => device.operatingSystem === "iOS").length} + + iOS Devices + + + + {deviceData.filter((device) => device.operatingSystem === "Android").length} + + Android Devices + + + + {deviceData.filter((device) => device.isEncrypted === true).length} + + Encrypted + + + + + + Device Management Recommendations + + Keep devices updated and secure to protect business data. Regularly check that all + employee devices meet security standards and address any issues promptly. Consider + automated policies to maintain consistent security across all devices and conduct + regular reviews to identify potential risks. + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} + + {/* CONDITIONAL ACCESS POLICIES PAGE - Only show if data is available */} + {sectionConfig.conditionalAccess && + conditionalAccessData && + Array.isArray(conditionalAccessData) && + conditionalAccessData.length > 0 && ( + <> + {/* STATISTIC PAGE 5 - CHAPTER SPLITTER */} + {sectionConfig.infographics && ( + + + + 277 + days + + average time to identify and{"\n"} + contain a data breach + + + + Early detection minimizes{"\n"} + business impact + + + )} + + + + Conditional Access Policies + + Identity and access management security controls + + + {brandingSettings?.logo && ( + + )} + + + + + Access control policies help protect your business by ensuring only the right + people can access sensitive information under appropriate circumstances. These + smart security measures automatically evaluate each access request and apply + additional verification when needed, balancing security with employee + productivity. + + + + + How Access Controls Protect Your Business + + These policies work like intelligent security guards, making decisions based on + who is trying to access what, from where, and when. For example, accessing email + from the office might be seamless, but accessing it from an unusual location might + require additional verification. This approach protects your data while minimizing + disruption to daily work. + + + + + Current Policy Configuration + + + + Policy Name + State + Applications + Controls + + + {conditionalAccessData.slice(0, 8).map((policy, index) => { + const getStateStyle = (state) => { + switch (state) { + case "enabled": + return styles.statusCompliant; + case "enabledForReportingButNotEnforced": + return styles.statusPartial; + case "disabled": + return styles.statusReview; + default: + return styles.statusText; + } + }; + + const getStateDisplay = (state) => { + switch (state) { + case "enabled": + return "Enabled"; + case "enabledForReportingButNotEnforced": + return "Report Only"; + case "disabled": + return "Disabled"; + default: + return state || "Unknown"; + } + }; + + const getControlsText = (policy) => { + const controls = []; + if (policy.builtInControls) { + if (policy.builtInControls.includes("mfa")) controls.push("MFA"); + if (policy.builtInControls.includes("block")) controls.push("Block"); + if (policy.builtInControls.includes("compliantDevice")) + controls.push("Compliant Device"); + } + return controls.length > 0 ? controls.join(", ") : "Custom"; + }; + + return ( + + + {(() => { + const displayName = policy.displayName || "N/A"; + if (typeof displayName === "object") { + console.log( + "DEBUG: policy.displayName is an object:", + displayName, + "full policy:", + policy + ); + } + return displayName; + })()} + + + + {getStateDisplay(policy.state)} + + + + {(() => { + const includeApplications = policy.includeApplications || "All"; + if (typeof includeApplications === "object") { + console.log( + "DEBUG: policy.includeApplications is an object:", + includeApplications, + "full policy:", + policy + ); + } + return includeApplications; + })()} + + + {getControlsText(policy)} + + + ); + })} + + + + + Policy Overview + + + + {conditionalAccessData.length} + Total Policies + + + + {conditionalAccessData.filter((policy) => policy.state === "enabled").length} + + Enabled + + + + { + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + } + + Report Only + + + + { + conditionalAccessData.filter( + (policy) => + policy.builtInControls && policy.builtInControls.includes("mfa") + ).length + } + + MFA Policies + + + + + + Policy Analysis + + + + + + Policy Coverage:{" "} + {conditionalAccessData.length} conditional access policies configured + + + + + + Enforcement Status:{" "} + {conditionalAccessData.filter((policy) => policy.state === "enabled").length}{" "} + policies actively enforced + + + + + + Testing Phase:{" "} + { + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + }{" "} + policies in report-only mode + + + + + + Security Controls:{" "} + Multi-factor authentication and access blocking implemented + + + + + + + Access Control Recommendations + + {conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length > 0 + ? `Consider activating ${ + conditionalAccessData.filter( + (policy) => policy.state === "enabledForReportingButNotEnforced" + ).length + } policies currently in testing mode after ensuring they don't disrupt business operations. ` + : "Your access controls are properly configured. "} + Regularly review how these policies affect employee productivity and adjust as + needed. Consider additional location-based protections for enhanced security + without impacting daily operations. + + + + + `Page ${pageNumber} of ${totalPages}`} + /> + + + + )} + + ); +}; + +export const ExecutiveReportButton = (props) => { + const { tenantName, tenantId, userStats, standardsData, organizationData, ...other } = props; + console.log(props); + const settings = useSettings(); + const brandingSettings = settings.customBranding; + + // Preview state + const [previewOpen, setPreviewOpen] = useState(false); + const [sectionConfig, setSectionConfig] = useState({ + executiveSummary: true, + securityStandards: true, + secureScore: true, + licenseManagement: true, + deviceManagement: true, + conditionalAccess: true, + infographics: true, + }); + + // Get real secure score data + const secureScore = useSecureScore(); + + // Get real license data + const licenseData = ApiGetCall({ + url: "/api/ListLicenses", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `licenses-report-${settings.currentTenant}`, + }); + // Get real device data + const deviceData = ApiGetCall({ + url: "/api/ListDevices", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `devices-report-${settings.currentTenant}`, + }); + + // Get real conditional access policy data + const conditionalAccessData = ApiGetCall({ + url: "/api/ListConditionalAccessPolicies", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `ca-policies-report-${settings.currentTenant}`, + }); + + // Get real standards data + const standardsCompareData = ApiGetCall({ + url: "/api/ListStandardsCompare", + data: { + tenantFilter: settings.currentTenant, + }, + queryKey: `standards-compare-report-${settings.currentTenant}`, + }); + + // Check if all data is loaded (either successful or failed) + const isDataLoading = + secureScore.isFetching || + licenseData.isFetching || + deviceData.isFetching || + conditionalAccessData.isFetching || + standardsCompareData.isFetching; + + const hasAllDataFinished = + (secureScore.isSuccess || secureScore.isError) && + (licenseData.isSuccess || licenseData.isError) && + (deviceData.isSuccess || deviceData.isError) && + (conditionalAccessData.isSuccess || conditionalAccessData.isError) && + (standardsCompareData.isSuccess || standardsCompareData.isError); + + // Show button when all data is finished loading (regardless of success/failure) + const shouldShowButton = hasAllDataFinished && !isDataLoading; + + const fileName = `Executive_Report_${tenantName?.replace(/[^a-zA-Z0-9]/g, "_") || "Tenant"}_${ + new Date().toISOString().split("T")[0] + }.pdf`; + + // Memoize the document to prevent unnecessary re-renders + const reportDocument = useMemo(() => { + console.log("Creating report document with:", { + tenantName, + tenantId, + userStats, + standardsData, + organizationData, + brandingSettings, + secureScore: secureScore.isSuccess ? secureScore : null, + licensingData: licenseData.isSuccess ? licenseData?.data : null, + deviceData: deviceData.isSuccess ? deviceData?.data : null, + conditionalAccessData: conditionalAccessData.isSuccess ? conditionalAccessData?.data : null, + standardsCompareData: standardsCompareData.isSuccess ? standardsCompareData?.data : null, + sectionConfig, + }); + + try { + return ( + + ); + } catch (error) { + console.error("Error creating ExecutiveReportDocument:", error); + return ( + + + + Error creating document: {error.message} + + + + ); + } + }, [ + tenantName, + tenantId, + userStats, + standardsData, + organizationData, + brandingSettings, + secureScore, + licenseData, + deviceData, + conditionalAccessData, + standardsCompareData, + sectionConfig, + ]); + + // Handle section toggle + const handleSectionToggle = (sectionKey) => { + setSectionConfig((prev) => { + // Count currently enabled sections + const enabledSections = Object.values(prev).filter(Boolean).length; + + // If trying to disable the last remaining section, prevent it + if (prev[sectionKey] && enabledSections === 1) { + return prev; // Don't change state + } + + return { + ...prev, + [sectionKey]: !prev[sectionKey], + }; + }); + }; + + // Section configuration options + const sectionOptions = [ + { + key: "executiveSummary", + label: "Executive Summary", + description: "High-level overview and statistics", + }, + { + key: "securityStandards", + label: "Security Standards", + description: "Compliance assessment and standards evaluation", + }, + { + key: "secureScore", + label: "Microsoft Secure Score", + description: "Security posture measurement and trends", + }, + { + key: "licenseManagement", + label: "License Management", + description: "License allocation and optimization", + }, + { + key: "deviceManagement", + label: "Device Management", + description: "Device compliance and insights", + }, + { + key: "conditionalAccess", + label: "Conditional Access", + description: "Access control policies and analysis", + }, + { + key: "infographics", + label: "Infographic Pages", + description: "Statistical pages with visual elements between sections", + }, + ]; + + // Don't render the button if data is not ready + if (!shouldShowButton) { + return ( + + + + ); + } + + return ( + <> + {/* Main Executive Summary Button */} + + + + + {/* Combined Preview and Configuration Dialog */} + setPreviewOpen(false)} + maxWidth="xl" + fullWidth + sx={{ + "& .MuiDialog-paper": { + height: "95vh", + maxHeight: "95vh", + }, + }} + > + + + Executive Report - {tenantName} + + setPreviewOpen(false)} size="small"> + + + + + + {/* Left Panel - Section Configuration */} + + + + + Report Sections + + + Configure which sections to include in your executive report. Changes are reflected + in real-time. + + + + {sectionOptions.map((option) => ( + + + { + event.stopPropagation(); + handleSectionToggle(option.key); + }} + color="primary" + size="small" + disabled={ + // Disable if this is the last enabled section + sectionConfig[option.key] && + Object.values(sectionConfig).filter(Boolean).length === 1 + } + /> + } + label={ + handleSectionToggle(option.key)}> + + {option.label} + + + {option.description} + + + } + sx={{ margin: 0, width: "100%" }} + /> + + + ))} + + + + + 💡 Pro Tip + + + Enable only the sections relevant to your audience to create focused, impactful + reports. At least one section must be enabled. + + + + + + {/* Right Panel - PDF Preview */} + + + {reportDocument} + + + + + + + + Sections enabled: {Object.values(sectionConfig).filter(Boolean).length} of{" "} + {sectionOptions.length} + + + + + + + + + + ); +}; diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 65a21fd62452..c39642c0fa91 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -1,31 +1,73 @@ import { ApiGetCall } from "../api/ApiCall.jsx"; import UnauthenticatedPage from "../pages/unauthenticated.js"; +import LoadingPage from "../pages/loading.js"; +import ApiOfflinePage from "../pages/api-offline.js"; export const PrivateRoute = ({ children, routeType }) => { - const { - data: profile, - error, - isFetching, - } = ApiGetCall({ + const session = ApiGetCall({ url: "/.auth/me", + queryKey: "authmeswa", + refetchOnWindowFocus: true, + staleTime: 120000, // 2 minutes + }); + + const apiRoles = ApiGetCall({ + url: "/api/me", queryKey: "authmecipp", + retry: 2, // Reduced retry count to show offline message sooner + waiting: !session.isSuccess || session.data?.clientPrincipal === null, }); - if (isFetching) { - return "Loading..."; + // Check if the session is still loading before determining authentication status + if ( + session.isLoading || + apiRoles.isLoading || + (apiRoles.isFetching && (apiRoles.data === null || apiRoles.data === undefined)) + ) { + return ; + } + + // Check if the API is offline (404 error from /api/me endpoint) + // Or other network errors that would indicate API is unavailable + if ( + apiRoles?.error?.response?.status === 404 || // API endpoint not found + apiRoles?.error?.response?.status === 502 || // Service unavailable + (apiRoles?.isSuccess && !apiRoles?.data) // No client principal data, indicating API might be offline + ) { + return ; + } + + // if not logged into swa + if (null === session?.data?.clientPrincipal || session?.data === undefined) { + return ; } let roles = null; - if (null !== profile?.clientPrincipal) { - roles = profile?.clientPrincipal.userRoles; - } else if (null === profile?.clientPrincipal) { + + if ( + session?.isSuccess && + apiRoles?.isSuccess && + undefined !== apiRoles?.data?.clientPrincipal && + session?.data?.clientPrincipal?.userDetails && + apiRoles?.data?.clientPrincipal?.userDetails && + session?.data?.clientPrincipal?.userDetails !== apiRoles?.data?.clientPrincipal?.userDetails + ) { + // refetch the profile if the user details are different + apiRoles.refetch(); + } + + if (null !== apiRoles?.data?.clientPrincipal && undefined !== apiRoles?.data) { + roles = apiRoles?.data?.clientPrincipal?.userRoles ?? []; + } else if (null === apiRoles?.data?.clientPrincipal || undefined === apiRoles?.data) { return ; } if (null === roles) { return ; } else { - const isAuthenticated = roles.includes("authenticated") && !error; - const isAdmin = roles.includes("admin"); + const blockedRoles = ["anonymous", "authenticated"]; + const userRoles = roles?.filter((role) => !blockedRoles.includes(role)) ?? []; + const isAuthenticated = userRoles.length > 0 && !apiRoles?.error; + const isAdmin = roles?.includes("admin") || roles?.includes("superadmin"); if (routeType === "admin") { return !isAdmin ? : children; } else { diff --git a/src/components/actions-menu.js b/src/components/actions-menu.js index 92ef2f90f4fd..b63ed33e74c8 100644 --- a/src/components/actions-menu.js +++ b/src/components/actions-menu.js @@ -11,7 +11,16 @@ export const ActionsMenu = (props) => { const popover = usePopover(); const [actionData, setActionData] = useState({ data: {}, action: {}, ready: false }); const createDialog = useDialog(); - + const handleActionDisabled = (row, action) => { + //add nullsaftey for row. It can sometimes be undefined(still loading) or null(no data) + if (!row) { + return true; + } + if (action?.condition) { + return !action?.condition(row); + } + return false; + }; return ( <>
    - {children} + + {children} +
    diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index 1f2b77713237..9594443127bc 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -1,5 +1,5 @@ import { usePathname, useRouter } from "next/navigation"; -import { Box, Container, Divider, Stack, Tab, Tabs, Typography } from "@mui/material"; +import { Box, Divider, Stack, Tab, Tabs } from "@mui/material"; export const TabbedLayout = (props) => { const { tabOptions, children } = props; diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index a8682abefa70..cc0da2049060 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -9,7 +9,7 @@ import SunIcon from "@heroicons/react/24/outline/SunIcon"; import { Avatar, Box, - FormControlLabel, + CircularProgress, List, ListItem, ListItemButton, @@ -18,15 +18,14 @@ import { Popover, Stack, SvgIcon, - Switch, Typography, useMediaQuery, - IconButton, } from "@mui/material"; import { usePopover } from "../hooks/use-popover"; import { paths } from "../paths"; import { ApiGetCall } from "../api/ApiCall"; import { CogIcon } from "@heroicons/react/24/outline"; +import { useQueryClient } from "@tanstack/react-query"; export const AccountPopover = (props) => { const { @@ -39,19 +38,22 @@ export const AccountPopover = (props) => { const router = useRouter(); const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); const popover = usePopover(); - + const queryClient = useQueryClient(); const orgData = ApiGetCall({ - url: "/.auth/me", - queryKey: "me", + url: "/api/me", + queryKey: "authmecipp", }); const handleLogout = useCallback(async () => { try { popover.handleClose(); + // delete query cache and persisted data + queryClient.clear(); router.push("/.auth/logout?post_logout_redirect_uri=" + encodeURIComponent(paths.index)); } catch (err) { console.error(err); + console.log(orgData); toast.error("Something went wrong"); } }, [router, popover]); @@ -90,16 +92,22 @@ export const AccountPopover = (props) => { <> - {orgData.data?.Org?.Domain} + {orgData.data?.clientPrincipal?.userDetails?.split("@")?.[1]} {orgData.data?.clientPrincipal?.userDetails ?? "Not logged in"} {orgData.data?.clientPrincipal?.userDetails && ( - - - + <> + {orgData?.isFetching ? ( + + ) : ( + + + + )} + )} )} diff --git a/src/layouts/config.js b/src/layouts/config.js index d9685ba29de4..812c19383a8d 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -1,12 +1,9 @@ import { BuildingOfficeIcon, HomeIcon, UsersIcon, WrenchIcon } from "@heroicons/react/24/outline"; import { - Cloud, CloudOutlined, - DeviceHub, HomeRepairService, Laptop, MailOutline, - Shield, ShieldOutlined, } from "@mui/icons-material"; import { SvgIcon } from "@mui/material"; @@ -20,6 +17,7 @@ export const nativeMenuItems = [ ), + permissions: ["CIPP.Core.*"], }, { title: "Identity Management", @@ -29,40 +27,96 @@ export const nativeMenuItems = [ ), + permissions: ["Identity.*"], items: [ { title: "Administration", path: "/identity/administration", + permissions: ["Identity.User.*"], items: [ - { title: "Users", path: "/identity/administration/users" }, - { title: "Risky Users", path: "/identity/administration/risky-users" }, - { title: "Groups", path: "/identity/administration/groups" }, + { + title: "Users", + path: "/identity/administration/users", + permissions: ["Identity.User.*"], + }, + { + title: "Risky Users", + path: "/identity/administration/risky-users", + permissions: ["Identity.User.*"], + }, + { + title: "Groups", + path: "/identity/administration/groups", + permissions: ["Identity.Group.*"], + }, { title: "Group Templates", path: "/identity/administration/group-templates", + permissions: ["Identity.Group.*"], + }, + { + title: "Devices", + path: "/identity/administration/devices", + permissions: ["Identity.Device.*"], + }, + { + title: "Deleted Items", + path: "/identity/administration/deleted-items", + permissions: ["Identity.User.*"], + }, + { + title: "Roles", + path: "/identity/administration/roles", + permissions: ["Identity.Role.*"], + }, + { + title: "JIT Admin", + path: "/identity/administration/jit-admin", + permissions: ["Identity.Role.*"], }, - { title: "Devices", path: "/identity/administration/devices" }, - { title: "Deleted Items", path: "/identity/administration/deleted-items" }, - { title: "Roles", path: "/identity/administration/roles" }, - { title: "JIT Admin", path: "/identity/administration/jit-admin" }, { title: "Offboarding Wizard", path: "/identity/administration/offboarding-wizard", + permissions: ["Identity.User.*"], }, ], }, { title: "Reports", path: "/identity/reports", + permissions: [ + "Identity.User.*", + "Identity.Group.*", + "Identity.Device.*", + "Identity.Role.*", + "Identity.AuditLog.*", + ], items: [ - { title: "MFA Report", path: "/identity/reports/mfa-report" }, - { title: "Inactive Users", path: "/identity/reports/inactive-users-report" }, - { title: "Sign-in Report", path: "/identity/reports/signin-report" }, + { + title: "MFA Report", + path: "/identity/reports/mfa-report", + permissions: ["Identity.User.*"], + }, + { + title: "Inactive Users", + path: "/identity/reports/inactive-users-report", + permissions: ["Identity.User.*"], + }, + { + title: "Sign-in Report", + path: "/identity/reports/signin-report", + permissions: ["Identity.User.*"], + }, { title: "AAD Connect Report", path: "/identity/reports/azure-ad-connect-report", + permissions: ["Identity.User.*"], + }, + { + title: "Risk Detections", + path: "/identity/reports/risk-detections", + permissions: ["Identity.User.*"], }, - { title: "Risk Detections", path: "/identity/reports/risk-detections" }, ], }, ], @@ -75,94 +129,144 @@ export const nativeMenuItems = [ ), + permissions: ["Tenant.*", "Identity.AuditLog.*", "CIPP.Backup.*", "Scheduler.Billing.*"], items: [ { title: "Administration", path: "/tenant/administration", + permissions: ["Tenant.Administration.*"], items: [ - { title: "Tenants", path: "/tenant/administration/tenants" }, + { + title: "Tenants", + path: "/tenant/administration/tenants", + permissions: ["Tenant.Administration.*"], + }, { title: "Alert Configuration", path: "/tenant/administration/alert-configuration", + permissions: ["Tenant.Alert.*"], + }, + { + title: "Audit Logs", + path: "/tenant/administration/audit-logs", + permissions: ["Identity.AuditLog.*"], }, - { title: "Audit Logs", path: "/tenant/administration/audit-logs" }, { - title: "Enterprise Applications", - path: "/tenant/administration/enterprise-apps", + title: "Applications", + path: "/tenant/administration/applications/enterprise-apps", + permissions: ["Tenant.Application.*"], + }, + { + title: "Secure Score", + path: "/tenant/administration/securescore", + permissions: ["Tenant.Administration.*"], }, - { title: "Secure Score", path: "/tenant/administration/securescore" }, { title: "App Consent Requests", path: "/tenant/administration/app-consent-requests", + permissions: ["Tenant.Application.*"], }, { title: "Authentication Methods", path: "/tenant/administration/authentication-methods", + permissions: ["Tenant.Config.*"], }, { title: "Partner Relationships", path: "/tenant/administration/partner-relationships", + permissions: ["Tenant.Relationship.*"], }, ], }, { title: "GDAP Management", path: "/tenant/gdap-management/", + permissions: ["Tenant.Relationship.*"], }, { title: "Configuration Backup", path: "/tenant/backup", - items: [{ title: "Backups", path: "/tenant/backup/backup-wizard" }], + permissions: ["CIPP.Backup.*"], + items: [ + { + title: "Backups", + path: "/tenant/backup/backup-wizard", + permissions: ["CIPP.Backup.*"], + }, + ], }, { - title: "Standards", + title: "Standards & Drift", path: "/tenant/standards", + permissions: [ + "Tenant.Standards.*", + "Tenant.BestPracticeAnalyser.*", + "Tenant.DomainAnalyser.*", + ], items: [ - { title: "Standards", path: "/tenant/standards/list-standards" }, + { + title: "Standards Management", + path: "/tenant/standards/list-standards", + permissions: ["Tenant.Standards.*"], + }, { title: "Best Practice Analyser", path: "/tenant/standards/bpa-report", + permissions: ["Tenant.BestPracticeAnalyser.*"], }, { title: "Domains Analyser", path: "/tenant/standards/domains-analyser", + permissions: ["Tenant.DomainAnalyser.*"], }, ], }, { title: "Conditional Access", path: "/tenant/conditional", + permissions: ["Tenant.ConditionalAccess.*"], items: [ - { title: "CA Policies", path: "/tenant/conditional/list-policies" }, + { + title: "CA Policies", + path: "/tenant/conditional/list-policies", + permissions: ["Tenant.ConditionalAccess.*"], + }, { title: "CA Vacation Mode", path: "/tenant/conditional/deploy-vacation", + permissions: ["Tenant.ConditionalAccess.*"], }, { title: "CA Templates", path: "/tenant/conditional/list-template", + permissions: ["Tenant.ConditionalAccess.*"], }, { title: "Named Locations", path: "/tenant/conditional/list-named-locations", + permissions: ["Tenant.ConditionalAccess.*"], }, ], }, { title: "Reports", path: "/tenant/reports", + permissions: ["Tenant.Administration.*", "Scheduler.Billing.*", "Tenant.Application.*"], items: [ { title: "Licence Report", - path: "/tenant/administration/list-licenses", + path: "/tenant/reports/list-licenses", + permissions: ["Tenant.Administration.*"], }, { title: "Sherweb Licence Report", - path: "/tenant/administration/list-csp-licenses", + path: "/tenant/reports/list-csp-licenses", + permissions: ["Tenant.Directory.*"], }, { title: "Consented Applications", - path: "/tenant/administration/application-consent", + path: "/tenant/reports/application-consent", + permissions: ["Tenant.Application.*"], }, ], }, @@ -176,37 +280,78 @@ export const nativeMenuItems = [ ), + permissions: [ + "Security.Incident.*", + "Security.Alert.*", + "Tenant.DeviceCompliance.*", + "Security.SafeLinksPolicy.*", + ], items: [ { title: "Incidents & Alerts", path: "/security/incidents", + permissions: ["Security.Incident.*"], items: [ - { title: "Incidents", path: "/security/incidents/list-incidents" }, - { title: "Alerts", path: "/security/incidents/list-alerts" }, + { + title: "Incidents", + path: "/security/incidents/list-incidents", + permissions: ["Security.Incident.*"], + }, + { + title: "Alerts", + path: "/security/incidents/list-alerts", + permissions: ["Security.Alert.*"], + }, ], }, { title: "Defender", path: "/security/defender", + permissions: ["Security.Alert.*"], items: [ - { title: "Defender Status", path: "/security/defender/list-defender" }, + { + title: "Defender Status", + path: "/security/defender/list-defender", + permissions: ["Security.Alert.*"], + }, { title: "Defender Deployment", path: "/security/defender/deployment", + permissions: ["Security.Alert.*"], }, { title: "Vulnerabilities", path: "/security/defender/list-defender-tvm", + permissions: ["Security.Alert.*"], }, ], }, { title: "Reports", path: "/security/reports", + permissions: ["Tenant.DeviceCompliance.*"], items: [ { title: "Device Compliance", path: "/security/reports/list-device-compliance", + permissions: ["Tenant.DeviceCompliance.*"], + }, + ], + }, + { + title: "Safe Links", + path: "/security/safelinks", + permissions: ["Security.SafeLinksPolicy.*"], + items: [ + { + title: "Safe Links Policies", + path: "/security/safelinks/safelinks", + permissions: ["Security.SafeLinksPolicy.*"], + }, + { + title: "Safe Links Templates", + path: "/security/safelinks/safelinks-template", + permissions: ["Security.SafeLinksPolicy.*"], }, ], }, @@ -220,43 +365,125 @@ export const nativeMenuItems = [ ), + permissions: [ + "Endpoint.Application.*", + "Endpoint.Autopilot.*", + "Endpoint.MEM.*", + "Endpoint.Device.*", + "Endpoint.Device.Read", + ], items: [ { title: "Applications", path: "/endpoint/applications", + permissions: ["Endpoint.Application.*"], items: [ - { title: "Applications", path: "/endpoint/applications/list" }, - { title: "Application Queue", path: "/endpoint/applications/queue" }, + { + title: "Applications", + path: "/endpoint/applications/list", + permissions: ["Endpoint.Application.*"], + }, + { + title: "Application Queue", + path: "/endpoint/applications/queue", + permissions: ["Endpoint.Application.*"], + }, ], }, { title: "Autopilot", path: "/endpoint/autopilot", + permissions: ["Endpoint.Autopilot.*"], items: [ - { title: "Autopilot Devices", path: "/endpoint/autopilot/list-devices" }, - { title: "Add Autopilot Device", path: "/endpoint/autopilot/add-device" }, - { title: "Profiles", path: "/endpoint/autopilot/list-profiles" }, - { title: "Status Pages", path: "/endpoint/autopilot/list-status-pages" }, - { title: "Add Status Page", path: "/endpoint/autopilot/add-status-page" }, + { + title: "Autopilot Devices", + path: "/endpoint/autopilot/list-devices", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Add Autopilot Device", + path: "/endpoint/autopilot/add-device", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Profiles", + path: "/endpoint/autopilot/list-profiles", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Status Pages", + path: "/endpoint/autopilot/list-status-pages", + permissions: ["Endpoint.Autopilot.*"], + }, + { + title: "Add Status Page", + path: "/endpoint/autopilot/add-status-page", + permissions: ["Endpoint.Autopilot.*"], + }, ], }, { title: "Device Management", path: "/endpoint/MEM", + permissions: ["Endpoint.MEM.*"], items: [ - { title: "Devices", path: "/endpoint/reports/devices" }, - { title: "Configuration Policies", path: "/endpoint/MEM/list-policies" }, - { title: "Compliance Policies", path: "/endpoint/MEM/list-compliance-policies" }, - { title: "Protection Policies", path: "/endpoint/MEM/list-appprotection-policies" }, - { title: "Apply Policy", path: "/endpoint/MEM/add-policy" }, - { title: "Policy Templates", path: "/endpoint/MEM/list-templates" }, + { + title: "Devices", + path: "/endpoint/MEM/devices", + permissions: ["Endpoint.Device.*"], + }, + { + title: "Configuration Policies", + path: "/endpoint/MEM/list-policies", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Compliance Policies", + path: "/endpoint/MEM/list-compliance-policies", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Protection Policies", + path: "/endpoint/MEM/list-appprotection-policies", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Apply Policy", + path: "/endpoint/MEM/add-policy", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Policy Templates", + path: "/endpoint/MEM/list-templates", + permissions: ["Endpoint.MEM.*"], + }, + { + title: "Scripts", + path: "/endpoint/MEM/list-scripts", + permissions: ["Endpoint.MEM.*"], + }, ], }, { title: "Reports", path: "/endpoint/reports", + permissions: ["Endpoint.Device.*", "Endpoint.Autopilot.*"], items: [ - { title: "Analytics Device Score", path: "/endpoint/reports/analyticsdevicescore" }, + { + title: "Analytics Device Score", + path: "/endpoint/reports/analyticsdevicescore", + permissions: ["Endpoint.Device.*"], + }, + { + title: "Work from anywhere", + path: "/endpoint/reports/workfromanywhere", + permissions: ["Endpoint.Device.*"], + }, + { + title: "Autopilot Deployments", + path: "/endpoint/reports/autopilot-deployment", + permissions: ["Endpoint.Autopilot.*"], + }, ], }, ], @@ -269,22 +496,44 @@ export const nativeMenuItems = [ ), + permissions: [ + "Sharepoint.Site.*", + "Sharepoint.Admin.*", + "Teams.Group.*", + "Teams.Activity.*", + "Teams.Voice.*", + ], items: [ { title: "OneDrive", path: "/teams-share/onedrive", + permissions: ["Sharepoint.Site.*"], }, { title: "SharePoint", path: "/teams-share/sharepoint", + permissions: ["Sharepoint.Admin.*"], }, { title: "Teams", path: "/teams-share/teams", + permissions: ["Teams.Group.*"], items: [ - { title: "Teams", path: "/teams-share/teams/list-team" }, - { title: "Teams Activity", path: "/teams-share/teams/teams-activity" }, - { title: "Business Voice", path: "/teams-share/teams/business-voice" }, + { + title: "Teams", + path: "/teams-share/teams/list-team", + permissions: ["Teams.Group.*"], + }, + { + title: "Teams Activity", + path: "/teams-share/teams/teams-activity", + permissions: ["Teams.Activity.*"], + }, + { + title: "Business Voice", + path: "/teams-share/teams/business-voice", + permissions: ["Teams.Voice.*"], + }, ], }, ], @@ -297,88 +546,196 @@ export const nativeMenuItems = [ ), + permissions: [ + "Exchange.Mailbox.*", + "Exchange.Contact.*", + "Exchange.SpamFilter.*", + "Exchange.TransportRule.*", + "Exchange.Connector.*", + "Exchange.ConnectionFilter.*", + "Exchange.Equipment.*", + "Exchange.Room.*", + "Exchange.SafeLinks.*", + "Exchange.Group.*", + ], items: [ { title: "Administration", - path: "/email/Administration", + path: "/email/administration", + permissions: ["Exchange.Mailbox.*"], items: [ - { title: "Mailboxes", path: "/email/administration/mailboxes" }, - { title: "Deleted Mailboxes", path: "/email/administration/deleted-mailboxes" }, - { title: "Mailbox Rules", path: "/email/administration/mailbox-rules" }, - { title: "Contacts", path: "/email/administration/contacts" }, - { title: "Quarantine", path: "/email/administration/quarantine" }, + { + title: "Mailboxes", + path: "/email/administration/mailboxes", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Deleted Mailboxes", + path: "/email/administration/deleted-mailboxes", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Mailbox Rules", + path: "/email/administration/mailbox-rules", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Contacts", + path: "/email/administration/contacts", + permissions: ["Exchange.Contact.*"], + }, + { + title: "Contact Templates", + path: "/email/administration/contacts-template", + permissions: ["Exchange.Contact.*"], + }, + { + title: "Quarantine", + path: "/email/administration/quarantine", + permissions: ["Exchange.SpamFilter.*"], + }, { title: "Tenant Allow/Block Lists", path: "/email/administration/tenant-allow-block-lists", + permissions: ["Exchange.SpamFilter.*"], }, ], }, { title: "Transport", - path: "/email/Transport", + path: "/email/transport", + permissions: ["Exchange.TransportRule.*"], items: [ - { title: "Transport rules", path: "/email/transport/list-rules" }, + { + title: "Transport rules", + path: "/email/transport/list-rules", + permissions: ["Exchange.TransportRule.*"], + }, { title: "Transport Templates", path: "/email/transport/list-templates", + permissions: ["Exchange.TransportRule.*"], + }, + { + title: "Connectors", + path: "/email/transport/list-connectors", + permissions: ["Exchange.Connector.*"], }, - { title: "Connectors", path: "/email/connectors/list-connectors" }, { title: "Connector Templates", - path: "/email/connectors/list-connector-templates", + path: "/email/transport/list-connector-templates", + permissions: ["Exchange.Connector.*"], }, ], }, { title: "Spamfilter", path: "/email/spamfilter", + permissions: ["Exchange.SpamFilter.*"], items: [ - { title: "Spamfilter", path: "/email/spamfilter/list-spamfilter" }, - { title: "Spamfilter templates", path: "/email/spamfilter/list-templates" }, - { title: "Connection filter", path: "/email/connectionfilter/list-connectionfilter" }, - { title: "Connection filter templates", path: "/email/connectionfilter/list-templates" }, + { + title: "Spamfilter", + path: "/email/spamfilter/list-spamfilter", + permissions: ["Exchange.SpamFilter.*"], + }, + { + title: "Spamfilter templates", + path: "/email/spamfilter/list-templates", + permissions: ["Exchange.SpamFilter.*"], + }, + { + title: "Connection filter", + path: "/email/spamfilter/list-connectionfilter", + permissions: ["Exchange.ConnectionFilter.*"], + }, + { + title: "Connection filter templates", + path: "/email/spamfilter/list-connectionfilter-templates", + permissions: ["Exchange.ConnectionFilter.*"], + }, + { + title: "Quarantine Policies", + path: "/email/spamfilter/list-quarantine-policies", + permissions: ["Exchange.SpamFilter.*"], + }, ], }, { title: "Resource Management", path: "/email/resources/management", + permissions: ["Exchange.Equipment.*"], items: [ - { title: "Rooms", path: "/email/resources/management/list-rooms" }, - { title: "Room Lists", path: "/email/resources/management/room-lists" }, + { + title: "Equipment", + path: "/email/resources/management/equipment", + permissions: ["Exchange.Equipment.*"], + }, + { + title: "Rooms", + path: "/email/resources/management/list-rooms", + permissions: ["Exchange.Room.*"], + }, + { + title: "Room Lists", + path: "/email/resources/management/room-lists", + permissions: ["Exchange.Room.*"], + }, ], }, { title: "Reports", path: "/email/reports", + permissions: [ + "Exchange.Mailbox.*", + "Exchange.SpamFilter.*", + "Exchange.SafeLinks.*", + "Exchange.Group.*", + ], items: [ { title: "Mailbox Statistics", path: "/email/reports/mailbox-statistics", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Mailbox Activity", + path: "/email/reports/mailbox-activity", + permissions: ["Exchange.Mailbox.*"], }, { title: "Mailbox Client Access Settings", path: "/email/reports/mailbox-cas-settings", + permissions: ["Exchange.Mailbox.*"], }, { title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters", + permissions: ["Exchange.SpamFilter.*"], + }, + { + title: "Malware Filters", + path: "/email/reports/malware-filters", + permissions: ["Exchange.SpamFilter.*"], }, - { title: "Malware Filters", path: "/email/reports/malware-filters" }, { title: "Safe Links Filters", path: "/email/reports/safelinks-filters", + permissions: ["Exchange.SafeLinks.*"], }, { title: "Safe Attachments Filters", path: "/email/reports/safeattachments-filters", + permissions: ["Exchange.SafeLinks.*"], }, { title: "Shared Mailbox with Enabled Account", path: "/email/reports/SharedMailboxEnabledAccount", + permissions: ["Exchange.Mailbox.*"], }, { title: "Global Address List", path: "/email/reports/global-address-list", + permissions: ["Exchange.Group.*"], }, ], }, @@ -392,55 +749,104 @@ export const nativeMenuItems = [ ), + permissions: [ + "CIPP.*", + "Tenant.Administration.*", + "Tenant.Application.*", + "Tenant.DomainAnalyser.*", + "Exchange.Mailbox.*", + ], items: [ { title: "Tenant Tools", path: "/tenant/tools", + permissions: ["Tenant.Administration.*"], items: [ { title: "Graph Explorer", - path: "/tenant/administration/graph-explorer", + path: "/tenant/tools/graph-explorer", + permissions: ["Tenant.Administration.*"], }, { title: "Application Approval", - path: "/tenant/administration/appapproval", + path: "/tenant/tools/appapproval", + permissions: ["Tenant.Application.*"], + }, + { + title: "Tenant Lookup", + path: "/tenant/tools/tenantlookup", + permissions: ["Tenant.Administration.*"], }, - { title: "Tenant Lookup", path: "/tenant/tools/tenantlookup" }, - { title: "IP Database", path: "/tenant/tools/geoiplookup" }, + { + title: "IP Database", + path: "/tenant/tools/geoiplookup", + permissions: ["CIPP.Core.*"], + }, { title: "Individual Domain Check", - path: "/tenant/standards/individual-domains", + path: "/tenant/tools/individual-domains", + permissions: ["Tenant.DomainAnalyser.*"], }, ], }, { title: "Email Tools", path: "/email/tools", + permissions: ["Exchange.Mailbox.*"], items: [ - { title: "Message Trace", path: "/email/tools/message-trace" }, - { title: "Mailbox Restores", path: "/email/tools/mailbox-restores" }, - { title: "Message Viewer", path: "/email/tools/message-viewer" }, + { + title: "Message Trace", + path: "/email/tools/message-trace", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Mailbox Restores", + path: "/email/tools/mailbox-restores", + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Message Viewer", + path: "/email/tools/message-viewer", + permissions: ["Exchange.Mailbox.*"], + }, ], }, { title: "Dark Web Tools", path: "/tools/darkweb", + permissions: ["CIPP.Core.*"], items: [ - { title: "Tenant Breach Lookup", path: "/tools/tenantbreachlookup" }, - { title: "Breach Lookup", path: "/tools/breachlookup" }, + { + title: "Tenant Breach Lookup", + path: "/tools/tenantbreachlookup", + permissions: ["CIPP.Core.*"], + }, + { + title: "Breach Lookup", + path: "/tools/breachlookup", + permissions: ["CIPP.Core.*"], + }, ], }, { title: "Template Library", path: "/tools/templatelib", roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Core.*"], + }, + { + title: "Community Repositories", + path: "/tools/community-repos", + roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Core.*"], }, { title: "Scheduler", path: "/cipp/scheduler", roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Scheduler.*"], }, ], }, @@ -452,30 +858,68 @@ export const nativeMenuItems = [ ), + permissions: [ + "CIPP.*", // Pattern matching - matches any CIPP permission + ], items: [ - { title: "Application Settings", path: "/cipp/settings", roles: ["admin", "superadmin"] }, - { title: "Logbook", path: "/cipp/logs", roles: ["admin", "superadmin"] }, - { title: "SAM Setup Wizard", path: "/onboarding", roles: ["admin", "superadmin"] }, - { title: "Integrations", path: "/cipp/integrations", roles: ["admin", "superadmin"] }, + { + title: "Application Settings", + path: "/cipp/settings", + roles: ["admin", "superadmin"], + permissions: ["CIPP.AppSettings.*"], + }, + { + title: "Logbook", + path: "/cipp/logs", + roles: ["editor", "admin", "superadmin"], + permissions: ["CIPP.Core.*"], + }, + { + title: "Setup Wizard", + path: "/onboardingv2", + roles: ["admin", "superadmin"], + permissions: ["CIPP.Core.*"], + }, + { + title: "Integrations", + path: "/cipp/integrations", + roles: ["admin", "superadmin"], + permissions: ["CIPP.Extension.*"], + }, + { + title: "Custom Data", + path: "/cipp/custom-data/directory-extensions", + roles: ["admin", "superadmin"], + permissions: ["CIPP.Core.*"], + }, { title: "Advanced", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], items: [ - { title: "Super Admin", path: "/cipp/super-admin/tenant-mode", roles: ["superadmin"] }, + { + title: "Super Admin", + path: "/cipp/super-admin/tenant-mode", + roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], + }, { title: "Exchange Cmdlets", path: "/cipp/advanced/exchange-cmdlets", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], }, { title: "Timers", path: "/cipp/advanced/timers", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], }, { title: "Table Maintenance", path: "/cipp/advanced/table-maintenance", roles: ["superadmin"], + permissions: ["CIPP.SuperAdmin.*"], }, ], }, diff --git a/src/layouts/footer.js b/src/layouts/footer.js index 07115518e388..d6bd01193b56 100644 --- a/src/layouts/footer.js +++ b/src/layouts/footer.js @@ -1,4 +1,4 @@ -import { Box, Container, Divider, Typography } from "@mui/material"; +import { Container } from "@mui/material"; export const Footer = () => { diff --git a/src/layouts/index.js b/src/layouts/index.js index 9538706081fa..5813fc0ee70e 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, useRef } from "react"; import { usePathname } from "next/navigation"; import { Alert, Button, Dialog, DialogContent, DialogTitle, useMediaQuery } from "@mui/material"; import { styled } from "@mui/material/styles"; @@ -12,7 +12,7 @@ import { useDispatch } from "react-redux"; import { showToast } from "../store/toasts"; import { Box, Container, Grid } from "@mui/system"; import { CippImageCard } from "../components/CippCards/CippImageCard"; -import Page from "../pages/onboarding"; +import Page from "../pages/onboardingv2"; import { useDialog } from "../hooks/use-dialog"; import { nativeMenuItems } from "/src/layouts/config"; @@ -30,13 +30,9 @@ const useMobileNav = () => { } }, [open]); - useEffect( - () => { - handlePathnameChange(); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [pathname] - ); + useEffect(() => { + handlePathnameChange(); + }, [pathname]); const handleOpen = useCallback(() => { setOpen(true); @@ -79,27 +75,72 @@ export const Layout = (props) => { const [userSettingsComplete, setUserSettingsComplete] = useState(false); const [fetchingVisible, setFetchingVisible] = useState([]); const [menuItems, setMenuItems] = useState(nativeMenuItems); + const lastUserSettingsUpdate = useRef(null); const currentTenant = settings?.currentTenant; - const currentRole = ApiGetCall({ + const [hideSidebar, setHideSidebar] = useState(false); + + const swaStatus = ApiGetCall({ url: "/.auth/me", + queryKey: "authmeswa", + staleTime: 120000, + refetchOnWindowFocus: true, + }); + + const currentRole = ApiGetCall({ + url: "/api/me", queryKey: "authmecipp", + waiting: !swaStatus.isSuccess || swaStatus.data?.clientPrincipal === null, }); useEffect(() => { if (currentRole.isSuccess && !currentRole.isFetching) { const userRoles = currentRole.data?.clientPrincipal?.userRoles; + const userPermissions = currentRole.data?.permissions; if (!userRoles) { + setMenuItems([]); + setHideSidebar(true); return; } const filterItemsByRole = (items) => { return items .map((item) => { + // role if (item.roles && item.roles.length > 0) { const hasRole = item.roles.some((requiredRole) => userRoles.includes(requiredRole)); if (!hasRole) { return null; } } + + // Check permission with pattern matching support + if (item.permissions && item.permissions.length > 0) { + const hasPermission = userPermissions?.some((userPerm) => { + return item.permissions.some((requiredPerm) => { + // Exact match + if (userPerm === requiredPerm) { + return true; + } + + // Pattern matching - check if required permission contains wildcards + if (requiredPerm.includes("*")) { + // Convert wildcard pattern to regex + const regexPattern = requiredPerm + .replace(/\./g, "\\.") // Escape dots + .replace(/\*/g, ".*"); // Convert * to .* + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(userPerm); + } + + return false; + }); + }); + if (!hasPermission) { + return null; + } + } else { + return null; + } + // check sub-items if (item.items && item.items.length > 0) { const filteredSubItems = filterItemsByRole(item.items).filter(Boolean); return { ...item, items: filteredSubItems }; @@ -109,11 +150,24 @@ export const Layout = (props) => { }) .filter(Boolean); }; - const filteredMenu = filterItemsByRole(nativeMenuItems); setMenuItems(filteredMenu); + } else if ( + swaStatus.isLoading || + swaStatus.data?.clientPrincipal === null || + swaStatus.data === undefined || + currentRole.isLoading + ) { + setHideSidebar(true); } - }, [currentRole.isSuccess]); + }, [ + currentRole.isSuccess, + swaStatus.data, + swaStatus.isLoading, + currentRole.data?.clientPrincipal?.userRoles, + currentRole.data?.permissions, + currentRole.isFetching, + ]); const handleNavPin = useCallback(() => { settings.handleUpdate({ @@ -129,23 +183,41 @@ export const Layout = (props) => { }); useEffect(() => { - if (userSettingsAPI.isSuccess && !userSettingsAPI.isFetching && !userSettingsComplete) { - //if usersettingsAPI.data contains offboardingDefaults.user, delete that specific key. - if (userSettingsAPI.data.offboardingDefaults?.user) { - delete userSettingsAPI.data.offboardingDefaults.user; - } - if (userSettingsAPI?.data?.currentTheme) { - delete userSettingsAPI.data.currentTheme; + if (userSettingsAPI.isSuccess && !userSettingsAPI.isFetching) { + // Only update if the data has actually changed (using dataUpdatedAt as a proxy) + const dataUpdatedAt = userSettingsAPI.dataUpdatedAt; + if (dataUpdatedAt && dataUpdatedAt !== lastUserSettingsUpdate.current) { + //if userSettingsAPI.data contains offboardingDefaults.user, delete that specific key. + if (userSettingsAPI.data.offboardingDefaults?.user) { + delete userSettingsAPI.data.offboardingDefaults.user; + } + if (userSettingsAPI.data.offboardingDefaults?.keepCopy) { + delete userSettingsAPI.data.offboardingDefaults.keepCopy; + } + if (userSettingsAPI?.data?.currentTheme) { + delete userSettingsAPI.data.currentTheme; + } + // get current devtools settings + var showDevtools = settings.showDevtools; + // get current bookmarks + var bookmarks = settings.bookmarks; + + settings.handleUpdate({ + ...userSettingsAPI.data, + bookmarks, + showDevtools, + }); + + // Track this update and set completion status + lastUserSettingsUpdate.current = dataUpdatedAt; + setUserSettingsComplete(true); } - settings.handleUpdate(userSettingsAPI.data); - setUserSettingsComplete(true); } }, [ userSettingsAPI.isSuccess, userSettingsAPI.data, userSettingsAPI.isFetching, - userSettingsComplete, - settings, + userSettingsAPI.dataUpdatedAt, ]); const version = ApiGetCall({ @@ -157,14 +229,17 @@ export const Layout = (props) => { url: `/api/GetCippAlerts?localversion=${version?.data?.version}`, queryKey: "alertsDashboard", waiting: false, + refetchOnMount: false, + refetchOnReconnect: false, + keepPreviousData: true, }); useEffect(() => { - if (version.isFetched && !alertsAPI.isFetched) { + if (!hideSidebar && version.isFetched && !alertsAPI.isFetched) { alertsAPI.waiting = true; alertsAPI.refetch(); } - }, [version, alertsAPI]); + }, [version, alertsAPI, hideSidebar]); useEffect(() => { if (alertsAPI.isSuccess && !alertsAPI.isFetching) { @@ -200,24 +275,49 @@ export const Layout = (props) => { return ( <> - - {mdDown && ( - + {hideSidebar === false && ( + <> + + {mdDown && ( + + )} + {!mdDown && } + )} - {!mdDown && } - {currentTenant === "AllTenants" && !allTenantsSupport ? ( + + Setup Wizard + + + + + {!setupCompleted && ( + + + + Setup has not been completed. + + + + + )} + {(currentTenant === "AllTenants" || !currentTenant) && !allTenantsSupport ? ( - + { ) : ( - <> - - Setup Wizard - - - - - {!setupCompleted && ( - - - - Setup has not been completed. - - - - - )} - {children} - + <>{children} )}
    diff --git a/src/layouts/side-nav-item.js b/src/layouts/side-nav-item.js index 750dcbf3f653..74ad14f6969d 100644 --- a/src/layouts/side-nav-item.js +++ b/src/layouts/side-nav-item.js @@ -4,7 +4,10 @@ import PropTypes from "prop-types"; import ChevronRightIcon from "@heroicons/react/24/outline/ChevronRightIcon"; import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon"; import ArrowTopRightOnSquareIcon from "@heroicons/react/24/outline/ArrowTopRightOnSquareIcon"; -import { Box, ButtonBase, Collapse, SvgIcon } from "@mui/material"; +import { Box, ButtonBase, Collapse, SvgIcon, Stack } from "@mui/material"; +import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder"; +import BookmarkIcon from "@mui/icons-material/Bookmark"; +import { useSettings } from "../hooks/use-settings"; export const SideNavItem = (props) => { const { @@ -20,11 +23,26 @@ export const SideNavItem = (props) => { } = props; const [open, setOpen] = useState(openImmediately); + const [hovered, setHovered] = useState(false); + const { handleUpdate, bookmarks = [] } = useSettings(); + const isBookmarked = bookmarks.some((bookmark) => bookmark.path === path); const handleToggle = useCallback(() => { setOpen((prevOpen) => !prevOpen); }, []); + const handleBookmarkToggle = useCallback( + (event) => { + event.stopPropagation(); + handleUpdate({ + bookmarks: isBookmarked + ? bookmarks.filter((bookmark) => bookmark.path !== path) + : [...bookmarks, { label: title, path }], + }); + }, + [isBookmarked, bookmarks, handleUpdate, path, title] + ); + // Dynamic spacing and font sizing based on depth const indent = depth > 0 ? depth * 1.5 : 1; // adjust multiplication factor as needed const fontSize = depth === 0 ? 14 : 13; // top-level 14, nested 13 @@ -32,8 +50,111 @@ export const SideNavItem = (props) => { if (children) { return (
  • + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + theme.typography.fontFamily, + fontSize: fontSize, + fontWeight: 500, + justifyContent: "flex-start", + px: `${indent * 6}px`, + py: "12px", + textAlign: "left", + whiteSpace: "nowrap", + width: "100%", + }} + > + + {icon} + + + {title} + + + {open ? : } + + + + + {children} + +
  • + ); + } + + // Leaf + const linkProps = path + ? external + ? { + component: "a", + href: path, + target: "_blank", + } + : { + component: NextLink, + href: path, + } + : {}; + + return ( +
  • + setHovered(true)} + onMouseLeave={() => setHovered(false)} + sx={{ + display: "flex", + alignItems: "center", + width: "100%", + px: `${indent * 6}px`, + }} + > { fontSize: fontSize, fontWeight: 500, justifyContent: "flex-start", - px: `${indent * 6}px`, - py: "12px", textAlign: "left", whiteSpace: "nowrap", - width: "100%", + width: "calc(100% - 20px)", // Adjust the width to leave space for the bookmark icon + py: "12px", }} + {...linkProps} > { sx={{ color: depth === 0 ? "text.primary" : "text.secondary", flexGrow: 1, - fontSize: fontSize, mx: "12px", transition: "opacity 250ms ease-in-out", + whiteSpace: "nowrap", + ...(hovered && { + maxWidth: "calc(100% - 45px)", // Adjust the width to leave space for the bookmark icon + overflow: "hidden", + textOverflow: "ellipsis", + }), ...(active && { color: "primary.main", }), @@ -82,106 +208,35 @@ export const SideNavItem = (props) => { > {title} - - {open ? : } - + {external && ( + + + + )} - - {children} - -
  • - ); - } - - // Leaf - const linkProps = path - ? external - ? { - component: "a", - href: path, - target: "_blank", - } - : { - component: NextLink, - href: path, - } - : {}; - - return ( -
  • - theme.typography.fontFamily, - fontSize: fontSize, - fontWeight: 500, - justifyContent: "flex-start", - px: `${indent * 6}px`, - py: "12px", - textAlign: "left", - whiteSpace: "nowrap", - width: "100%", - }} - {...linkProps} - > - - {icon} - - - {title} - - {external && ( - - - - )} - + {isBookmarked ? : } + +
  • ); }; diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index b2967a01a548..47c601fa3027 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -5,6 +5,7 @@ import { Box, Divider, Drawer, Stack, Typography } from "@mui/material"; import { Scrollbar } from "../components/scrollbar"; import { SideNavItem } from "./side-nav-item"; import { useSettings } from "../hooks/use-settings"; +import { ApiGetCall } from "../api/ApiCall.jsx"; const SIDE_NAV_WIDTH = 270; const SIDE_NAV_COLLAPSED_WIDTH = 73; // icon size + padding + border right @@ -14,7 +15,6 @@ const markOpenItems = (items, pathname) => { return items.map((item) => { const checkPath = !!(item.path && pathname); const exactMatch = checkPath ? pathname === item.path : false; - // Use startsWith for partial matches so that subpages not in the menu still keep parent open const partialMatch = checkPath ? pathname.startsWith(item.path) : false; let openImmediately = exactMatch; @@ -23,11 +23,9 @@ const markOpenItems = (items, pathname) => { if (newItems.length > 0) { newItems = markOpenItems(newItems, pathname); const childOpen = newItems.some((child) => child.openImmediately); - // Parent should open if exactMatch, childOpen, or partialMatch - openImmediately = openImmediately || childOpen || partialMatch; + openImmediately = openImmediately || childOpen || exactMatch; // Ensure parent opens if child is open } else { - // For leaf items, consider them open if exact or partial match - openImmediately = openImmediately || partialMatch; + openImmediately = openImmediately || partialMatch; // Leaf items open on partial match } return { @@ -46,8 +44,6 @@ const reduceChildRoutes = ({ acc, collapse, depth, item, pathname }) => { const exactMatch = checkPath && pathname === item.path; const partialMatch = checkPath && pathname.startsWith(item.path); - // Consider item active if exactMatch or partialMatch for leaf items - // For parent items, being active is determined by their children or openImmediately const hasChildren = item.items && item.items.length > 0; const isActive = exactMatch || (partialMatch && !hasChildren); @@ -106,6 +102,7 @@ export const SideNav = (props) => { const pathname = usePathname(); const [hovered, setHovered] = useState(false); const collapse = !(pinned || hovered); + const { data: profile } = ApiGetCall({ url: "/api/me", queryKey: "authmecipp" }); // Preprocess items to mark which should be open const processedItems = markOpenItems(items, pathname); @@ -119,8 +116,8 @@ export const SideNav = (props) => { priority: 1, }, { - link: "https://rightofboom.com", - imagesrc: theme === "light" ? "/sponsors/RoB-light.svg" : "/sponsors/RoB.png", + link: "https://www.domotz.com/cipp-community-free-domotz-beta.php?utm_source=Community_CIPP&utm_medium=Community_CIPP&utm_campaign=Community_CIPP", + imagesrc: theme === "light" ? "/sponsors/domotz-light.png" : "/sponsors/domotz-dark.png", priority: 1, }, { @@ -157,86 +154,102 @@ export const SideNav = (props) => { const randomimg = randomSponsorImage(); return ( - { - setHovered(true); - }, - onMouseLeave: () => { - setHovered(false); - }, - sx: { - backgroundColor: "background.default", - height: `calc(100% - ${TOP_NAV_HEIGHT}px)`, - overflowX: "hidden", - top: TOP_NAV_HEIGHT, - transition: "width 250ms ease-in-out", - width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH, - zIndex: (theme) => theme.zIndex.appBar - 100, - }, - }} - > - - + {profile?.clientPrincipal && profile?.clientPrincipal?.userRoles?.length > 2 && ( + { + setHovered(true); + }, + onMouseLeave: () => { + setHovered(false); + }, + sx: { + backgroundColor: "background.default", + height: `calc(100% - ${TOP_NAV_HEIGHT}px)`, + overflowX: "hidden", + top: TOP_NAV_HEIGHT, + transition: "width 250ms ease-in-out", + width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH, + zIndex: (theme) => theme.zIndex.appBar - 100, + }, }} > - - {renderItems({ - collapse, - depth: 0, - items: processedItems, - pathname, - })} - - - - This application is sponsored by - - - sponsor window.open(randomimg.link)} - width={"100px"} - /> - - - - + + + {renderItems({ + collapse, + depth: 0, + items: processedItems, + pathname, + })} + {" "} + {/* Add this closing tag */} + {profile?.clientPrincipal && ( + <> + + + This application is sponsored by + + + sponsor window.open(randomimg.link)} + /> + + + )} + {" "} + {/* Closing tag for the parent Box */} + + + )} + ); }; diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index b92b1ace3d31..ec7f06c7f3dd 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -1,10 +1,27 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import NextLink from "next/link"; import PropTypes from "prop-types"; import Bars3Icon from "@heroicons/react/24/outline/Bars3Icon"; import MoonIcon from "@heroicons/react/24/outline/MoonIcon"; import SunIcon from "@heroicons/react/24/outline/SunIcon"; -import { Box, Divider, IconButton, Stack, SvgIcon, useMediaQuery } from "@mui/material"; +import BookmarkIcon from "@mui/icons-material/Bookmark"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import { + Box, + Divider, + IconButton, + Stack, + SvgIcon, + useMediaQuery, + Popover, + List, + ListItem, + ListItemText, + Typography, +} from "@mui/material"; import { Logo } from "../components/logo"; import { useSettings } from "../hooks/use-settings"; import { paths } from "../paths"; @@ -12,8 +29,10 @@ import { AccountPopover } from "./account-popover"; import { CippTenantSelector } from "../components/CippComponents/CippTenantSelector"; import { NotificationsPopover } from "./notifications-popover"; import { useDialog } from "../hooks/use-dialog"; -import { MagnifyingGlassCircleIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { CippCentralSearch } from "../components/CippComponents/CippCentralSearch"; +import { applySort } from "../utils/apply-sort"; + const TOP_NAV_HEIGHT = 64; export const TopNav = (props) => { @@ -25,9 +44,58 @@ export const TopNav = (props) => { const themeName = settings.currentTheme?.value === "light" ? "dark" : "light"; settings.handleUpdate({ currentTheme: { value: themeName, label: themeName }, + paletteMode: themeName, }); }, [settings]); + const [anchorEl, setAnchorEl] = useState(null); + const [sortOrder, setSortOrder] = useState("asc"); + + const handleBookmarkClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleBookmarkClose = () => { + setAnchorEl(null); + }; + + const handleSortToggle = () => { + const newSortOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newSortOrder); + + // Save the new sort order and re-order bookmarks + const sortedBookmarks = applySort(settings.bookmarks || [], "label", newSortOrder); + settings.handleUpdate({ + bookmarks: sortedBookmarks, + sortOrder: newSortOrder, + }); + }; + + // Move a bookmark up in the list + const moveBookmarkUp = (index) => { + if (index <= 0) return; + + const updatedBookmarks = [...(settings.bookmarks || [])]; + const temp = updatedBookmarks[index]; + updatedBookmarks[index] = updatedBookmarks[index - 1]; + updatedBookmarks[index - 1] = temp; + + settings.handleUpdate({ bookmarks: updatedBookmarks }); + }; + + // Move a bookmark down in the list + const moveBookmarkDown = (index) => { + const bookmarks = settings.bookmarks || []; + if (index >= bookmarks.length - 1) return; + + const updatedBookmarks = [...bookmarks]; + const temp = updatedBookmarks[index]; + updatedBookmarks[index] = updatedBookmarks[index + 1]; + updatedBookmarks[index + 1] = temp; + + settings.handleUpdate({ bookmarks: updatedBookmarks }); + }; + useEffect(() => { const handleKeyDown = (event) => { if ((event.metaKey || event.ctrlKey) && event.key === "k") { @@ -41,10 +109,22 @@ export const TopNav = (props) => { }; }, []); + useEffect(() => { + if (settings.sortOrder) { + setSortOrder(settings.sortOrder); + } + }, [settings.sortOrder]); + const openSearch = () => { searchDialog.handleOpen(); }; + // Use the sorted bookmarks if sorting is applied, otherwise use the bookmarks in their current order + const displayBookmarks = settings.bookmarks || []; + + const open = Boolean(anchorEl); + const id = open ? "bookmark-popover" : undefined; + return ( { )} - + {!mdDown && ( @@ -111,6 +191,90 @@ export const TopNav = (props) => { + + + + + + + + + + + {sortOrder === "asc" ? : } + + + Sort Alphabetically + + {displayBookmarks.length === 0 ? ( + + No bookmarks added yet} + /> + + ) : ( + displayBookmarks.map((bookmark, idx) => ( + + handleBookmarkClose()} + sx={{ + textDecoration: "none", + color: "inherit", + flexGrow: 1, + marginRight: 2, + }} + > + {bookmark.label} + + + { + e.preventDefault(); + moveBookmarkUp(idx); + }} + disabled={idx === 0} + > + + + { + e.preventDefault(); + moveBookmarkDown(idx); + }} + disabled={idx === displayBookmarks.length - 1} + > + + + + + )) + )} + + ( alignItems="center" // Center vertically sx={{ height: "100%" }} // Ensure the container takes full height > - + ( alignItems="center" // Center vertically sx={{ height: "100%" }} // Ensure the container takes full height > - + { alignItems="center" sx={{ height: "100%" }} > - + import("@tanstack/react-query-devtools/build/modern/production.js").then((d) => ({ default: d.ReactQueryDevtools, })) ); +TimeAgo.addDefaultLocale(en); const queryClient = new QueryClient(); const clientSideEmotionCache = createEmotionCache(); @@ -35,6 +49,200 @@ const App = (props) => { const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; const getLayout = Component.getLayout ?? ((page) => page); const preferredTheme = useMediaPredicate("(prefers-color-scheme: dark)") ? "dark" : "light"; + const pathname = usePathname(); + const route = useRouter(); + const [_0x8h9i, _0x2j3k] = useState(false); // toRemove + + const excludeQueryKeys = ["authmeswa", "alertsDashboard"]; + + const _0x4f2d = [1772236800, 1772391599]; // toRemove + const _0x2e1f = () => { + // toRemove + const _0x1a2b = Date.now() / 1000; // toRemove + return _0x1a2b >= _0x4f2d[0] && _0x1a2b <= _0x4f2d[1]; // toRemove + }; + + // 👇 Persist TanStack Query cache to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + const localStoragePersister = createSyncStoragePersister({ + storage: window.localStorage, + }); + + persistQueryClient({ + queryClient, + persister: localStoragePersister, + maxAge: 1000 * 60 * 60 * 24, // 24 hours + staleTime: 1000 * 60 * 5, // optional: 5 minutes + buster: "v1", + dehydrateOptions: { + shouldDehydrateQuery: (query) => { + const queryIsReadyForPersistence = query.state.status === "success"; + if (queryIsReadyForPersistence) { + const { queryKey } = query; + // Check if queryKey exists and has elements before accessing index 0 + if (!queryKey || !queryKey.length) { + return false; + } + const queryKeyString = String(queryKey[0] || ""); + const excludeFromPersisting = excludeQueryKeys.some((key) => + queryKeyString.includes(key) + ); + return !excludeFromPersisting; + } + return queryIsReadyForPersistence; + }, + }, + }); + } + }, []); + + useEffect(() => { + // toRemove + if (_0x8h9i) { + // toRemove + const _0x3c4d = Array.from(document.querySelectorAll("*")).filter((_0x5e6f) => { + // toRemove + const _0x7g8h = document.querySelector('[aria-label="Navigation SpeedDial"]'); // toRemove + return !_0x7g8h?.contains(_0x5e6f); // toRemove + }); + + _0x3c4d.forEach((_0x9i0j, _0x1k2l) => { + // toRemove + const _0x3m4n = Math.random() * 10 - 5; // toRemove + const _0x5o6p = Math.random() * 10 - 5; // toRemove + const _0x7q8r = Math.random() * 10 - 5; // toRemove + const _0x9s0t = Math.random() * 0.5; // toRemove + const _0x1u2v = 0.3 + Math.random() * 0.4; // toRemove + + const _0x3w4x = `_${_0x1k2l}`; // toRemove + const _0x5y6z = document.styleSheets[0]; // toRemove + _0x5y6z.insertRule( + ` // toRemove + @keyframes ${_0x3w4x} { // toRemove + 0% { transform: translate(0, 0) rotate(0deg); } // toRemove + 25% { transform: translate(${_0x3m4n}px, ${_0x5o6p}px) rotate(${_0x7q8r}deg); } // toRemove + 50% { transform: translate(0, 0) rotate(0deg); } // toRemove + 75% { transform: translate(${-_0x3m4n}px, ${_0x5o6p}px) rotate(${-_0x7q8r}deg); } // toRemove + 100% { transform: translate(0, 0) rotate(0deg); } // toRemove + } + `, + _0x5y6z.cssRules.length + ); // toRemove + + _0x9i0j.style.animation = `${_0x3w4x} ${_0x1u2v}s infinite ${_0x9s0t}s`; // toRemove + }); + + const _0x1a2b = setTimeout(() => { + // toRemove + _0x2j3k(false); // toRemove + _0x3c4d.forEach((_0x5e6f) => { + // toRemove + _0x5e6f.style.animation = ""; // toRemove + }); + const _0x7g8h = document.styleSheets[0]; // toRemove + while (_0x7g8h.cssRules.length > 0) { + // toRemove + _0x7g8h.deleteRule(0); // toRemove + } + }, 5000); // toRemove + + return () => { + // toRemove + clearTimeout(_0x1a2b); // toRemove + _0x3c4d.forEach((_0x5e6f) => { + // toRemove + _0x5e6f.style.animation = ""; // toRemove + }); + const _0x7g8h = document.styleSheets[0]; // toRemove + while (_0x7g8h.cssRules.length > 0) { + // toRemove + _0x7g8h.deleteRule(0); // toRemove + } + }; + } + }, [_0x8h9i]); // toRemove + + const speedDialActions = [ + ...(_0x2e1f() + ? [ + { + // toRemove + id: "_", // toRemove + icon: , // toRemove + name: String.fromCharCode( + 68, + 111, + 32, + 116, + 104, + 101, + 32, + 72, + 97, + 114, + 108, + 101, + 109, + 32, + 83, + 104, + 97, + 107, + 101, + 33 + ), // toRemove + onClick: () => _0x2j3k(true), // toRemove + }, + ] + : []), // toRemove + { + id: "license", + icon: , + name: "License", + href: "/license", + onClick: () => route.push("/license"), + }, + { + id: "bug-report", + icon: , + name: "Report Bug", + href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml", + onClick: () => + window.open("https://github.com/KelvinTegelaar/CIPP/issues/new?template=bug.yml", "_blank"), + }, + { + id: "feature-request", + icon: , + name: "Request Feature", + href: "https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml", + onClick: () => + window.open( + "https://github.com/KelvinTegelaar/CIPP/issues/new?template=feature.yml", + "_blank" + ), + }, + { + id: "discord", + icon: ( + + ), + name: "Join the Discord!", + href: "https://discord.gg/cyberdrain", + onClick: () => window.open("https://discord.gg/cyberdrain", "_blank"), + }, + { + id: "documentation", + icon: , + name: "Check the Documentation", + href: `https://docs.cipp.app/user-documentation${pathname}`, + onClick: () => window.open(`https://docs.cipp.app/user-documentation${pathname}`, "_blank"), + }, + ]; return ( @@ -69,13 +277,22 @@ const App = (props) => { {getLayout()} + } + position={{ + bottom: 12, + right: + settings.isInitialized && settings?.showDevtools === true ? 60 : 12, + }} + /> - {settings?.showDevtools && ( + {settings.isInitialized && settings?.showDevtools === true ? ( - )} + ) : null} ); }} diff --git a/src/pages/api-offline.js b/src/pages/api-offline.js new file mode 100644 index 000000000000..77d125ac6486 --- /dev/null +++ b/src/pages/api-offline.js @@ -0,0 +1,167 @@ +import { + Box, + Button, + Container, + Stack, + Alert, + CircularProgress, + Typography, + SvgIcon, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import Head from "next/head"; +import { useState, useEffect } from "react"; +import axios from "axios"; +import { CippImageCard } from "../components/CippCards/CippImageCard"; +import { ErrorOutlineOutlined } from "@mui/icons-material"; + +const ApiOfflinePage = () => { + const [testingConnection, setTestingConnection] = useState(false); + const [testResult, setTestResult] = useState(null); + const [apiVersion, setApiVersion] = useState(null); + + // Check API version when component mounts + useEffect(() => { + const checkApiVersion = async () => { + try { + const response = await axios.get("/version.json", { timeout: 5000 }); + setApiVersion(response.data?.version || "Unknown"); + } catch (error) { + console.error("Failed to fetch API version:", error); + } + }; + + checkApiVersion(); + }, []); + + const handleTestConnection = async () => { + setTestingConnection(true); + setTestResult(null); + + try { + // Try to ping the API + const testCall = await axios.get("/api/me", { timeout: 45000 }); + console.log("API Test Call Response:", testCall); + if (!testCall.headers["content-type"]?.includes("application/json")) { + throw new Error("API did not return the expected response."); + } + setTestResult({ success: true, message: "Connection successful! Try refreshing the page." }); + } catch (error) { + let errorMessage = "Connection failed."; + + if (error.response) { + // Request was made and server responded with a status code outside of 2xx range + errorMessage = `API responded with status: ${error.response.status}`; + if (error.response.status === 404) { + errorMessage += + " (API endpoint not found, this can be the case if your Function App is on Version 7 or below)"; + } + } else if (error.request) { + // Request was made but no response received + errorMessage = "No response received from API. Check if your Function App is running."; + } else { + // Error in setting up the request + errorMessage = `Error: ${error.message}`; + } + + setTestResult({ success: false, message: errorMessage }); + } finally { + setTestingConnection(false); + } + }; + + // We're now using Typography components directly in the JSX + // instead of generating a help text string + + return ( + <> + + API Offline + + + + + + + + + + + CIPP API Unreachable + + } + text={ + + + The CIPP API appears to be offline or out of date. + {apiVersion && ( + + Frontend Version: {apiVersion} + + )} + + + + If you are self-hosting CIPP, please ensure your Function App is running and + up to date. If you are using the hosted version, please check your + subscription in GitHub. + + + } + linkText={testingConnection ? "Testing Connection..." : "Test API Connection"} + onButtonClick={handleTestConnection} + /> + + {testResult && ( + + + {testResult.message} + {testResult.success && ( + + )} + + + )} + + {testingConnection && ( + + + + )} + + + + + + + ); +}; + +export default ApiOfflinePage; diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js new file mode 100644 index 000000000000..ea8edfe0937d --- /dev/null +++ b/src/pages/authredirect.js @@ -0,0 +1,47 @@ +import { Box, Container, Stack } from "@mui/material"; +import { Grid } from "@mui/system"; +import Head from "next/head"; +import { CippImageCard } from "../components/CippCards/CippImageCard.jsx"; +import { Layout as DashboardLayout } from "../layouts/index.js"; + +const Page = () => ( + <> + + + Authentication complete + + + + + + + + + + + + + + +); + +export default Page; diff --git a/src/pages/cipp/advanced/exchange-cmdlets.js b/src/pages/cipp/advanced/exchange-cmdlets.js index 8b2583d0a1bf..68cb55fd7c3e 100644 --- a/src/pages/cipp/advanced/exchange-cmdlets.js +++ b/src/pages/cipp/advanced/exchange-cmdlets.js @@ -3,12 +3,13 @@ import { Button, Container, Stack, - Grid, Dialog, DialogTitle, DialogContent, IconButton, + Skeleton, } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { ApiPostCall } from "/src/api/ApiCall"; @@ -98,11 +99,11 @@ const Page = () => { {/* Tenant Filter */} - + {/* Compliance Filter */} - + { /> {/* AsApp Filter */} - + { /> {/* Submit Button */} - + @@ -130,7 +131,7 @@ const Page = () => { { - + {roleDetails.isPending ? ( + + ) : ( + + )}
    diff --git a/src/pages/cipp/advanced/table-maintenance.js b/src/pages/cipp/advanced/table-maintenance.js index 849bed69bb43..d7bc058697ee 100644 --- a/src/pages/cipp/advanced/table-maintenance.js +++ b/src/pages/cipp/advanced/table-maintenance.js @@ -274,7 +274,7 @@ const Page = () => { }; return ( - + {pageTitle} @@ -282,8 +282,8 @@ const Page = () => { This page allows you to view and manage data in Azure Tables. This is advanced functionality that should only be used when directed by CyberDrain support. - - + + { } /> - + {selectedTable && ( diff --git a/src/pages/cipp/advanced/timers.js b/src/pages/cipp/advanced/timers.js index 823f985f36e1..116d27cd836f 100644 --- a/src/pages/cipp/advanced/timers.js +++ b/src/pages/cipp/advanced/timers.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { SvgIcon, Button } from "@mui/material"; -import { Refresh } from "@mui/icons-material"; +import { Refresh, PlayArrow } from "@mui/icons-material"; import { ApiPostCall } from "../../../api/ApiCall"; import { useEffect, useState } from "react"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage"; @@ -80,6 +80,8 @@ const Page = () => { url: apiUrl, data: { FunctionName: "Command", Parameters: "Parameters" }, confirmText: "Do you want to run this task now?", + allowResubmit: true, + icon: , }, ]} /> diff --git a/src/pages/cipp/custom-data/directory-extensions/add.js b/src/pages/cipp/custom-data/directory-extensions/add.js new file mode 100644 index 000000000000..7238792aa4dd --- /dev/null +++ b/src/pages/cipp/custom-data/directory-extensions/add.js @@ -0,0 +1,147 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useFormState } from "react-hook-form"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { + Box, + Button, + Stack, + CardContent, + Typography, + Divider, + CardActions, +} from "@mui/material"; + +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; + +const availableTargetObjects = [ + { value: "User", label: "User" }, + { value: "Group", label: "Group" }, + { value: "AdministrativeUnit", label: "Administrative Unit" }, + { value: "Application", label: "Application" }, + { value: "Device", label: "Device" }, + { value: "Organization", label: "Organization" }, +]; + +const Page = () => { + const router = useRouter(); + const formControl = useForm({ + mode: "onChange", + defaultValues: { + isMultiValued: false, + targetObjects: [], + }, + }); + + const formState = useFormState({ control: formControl.control }); + + const addDirectoryExtensionApi = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["DirectoryExtensionsListPage"], + }); + + const handleAddDirectoryExtension = (data) => { + addDirectoryExtensionApi.mutate({ + url: "/api/ExecCustomData", + data: { + Action: "AddDirectoryExtension", + name: data.name, + dataType: data.dataType, + isMultiValued: data.isMultiValued, + targetObjects: data.targetObjects.map((targetObject) => targetObject.value), + }, + }); + }; + + const formFields = [ + { + name: "name", + label: "Extension Name", + type: "textField", + required: true, + placeholder: "Enter a unique name for the directory extension", + }, + { + name: "dataType", + label: "Data Type", + type: "select", + required: true, + placeholder: "Select the data type for the directory extension", + options: [ + { value: "Binary", label: "Binary (256 bytes max)" }, + { value: "Boolean", label: "Boolean" }, + { value: "DateTime", label: "DateTime (ISO 8601, UTC)" }, + { value: "Integer", label: "Integer (32-bit)" }, + { value: "LargeInteger", label: "LargeInteger (64-bit)" }, + { value: "String", label: "String (256 characters max)" }, + ], + }, + { + name: "isMultiValued", + label: "Is Multi-Valued", + type: "switch", + required: false, + }, + { + name: "targetObjects", + label: "Target Objects", + type: "autoComplete", + placeholder: "Select the directory objects that can use this extension", + required: true, + multiple: true, + options: availableTargetObjects, + validate: (value) => { + if (value.length > 0) return true; + return "Please select at least one target object."; + }, + creatable: false, + }, + ]; + + return ( + + + + + + + Directory Extension Details + + {formFields.map((field, index) => ( + + ))} + + + + + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/custom-data/directory-extensions/index.js b/src/pages/cipp/custom-data/directory-extensions/index.js new file mode 100644 index 000000000000..891342e97ce1 --- /dev/null +++ b/src/pages/cipp/custom-data/directory-extensions/index.js @@ -0,0 +1,93 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Alert, Button, Link, SvgIcon } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import tabOptions from "../tabOptions"; +import NextLink from "next/link"; +import { TrashIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const pageTitle = "Directory Extensions"; + + // Columns for the table + const columns = [ + "name", // Extension ID + "dataType", // Data Type + "isMultiValued", // Multi-Valued + "targetObjects", // Target Objects + ]; + + const actions = [ + { + label: "Delete Directory Extension", + icon: , + url: "/api/ExecCustomData?Action=DeleteDirectoryExtension", + type: "POST", + data: { + name: "name", + id: "id", + }, + confirmText: "Are you sure you want to delete the directory extension '[name]'?", + }, + ]; + + return ( + <> + + + Directory extensions + {" "} + allow you to add custom properties to Microsoft Entra directory objects. +
      +
    • Directory extensions must have unique names.
    • +
    • There is a limit of 100 extension values per resource instance
    • +
    • + + Considerations for using directory extensions + +
    • +
    + + } + cardButton={ + + } + tenantInTitle={false} + apiUrl="/api/ExecCustomData?Action=ListDirectoryExtensions" + apiDataKey="Results" + simpleColumns={columns} + filters={[]} + actions={actions} + /> + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/custom-data/mappings/add.js b/src/pages/cipp/custom-data/mappings/add.js new file mode 100644 index 000000000000..d672d42d888d --- /dev/null +++ b/src/pages/cipp/custom-data/mappings/add.js @@ -0,0 +1,61 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useFormState } from "react-hook-form"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { Button, Stack, CardContent, CardActions } from "@mui/material"; + +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippCustomDataMappingForm from "/src/components/CippFormPages/CippCustomDataMappingForm"; + +const Page = () => { + const router = useRouter(); + const formControl = useForm({ + mode: "onChange", + }); + + const formState = useFormState({ control: formControl.control }); + + const addMappingApi = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["MappingsListPage"], + }); + + const handleAddMapping = (data) => { + addMappingApi.mutate({ + url: "/api/ExecCustomData", + data: { + Action: "AddEditMapping", + Mapping: data, + }, + }); + }; + + return ( + + + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/custom-data/mappings/edit.js b/src/pages/cipp/custom-data/mappings/edit.js new file mode 100644 index 000000000000..10bce76f56f2 --- /dev/null +++ b/src/pages/cipp/custom-data/mappings/edit.js @@ -0,0 +1,90 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useFormState } from "react-hook-form"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; +import { ApiPostCall, ApiGetCall } from "/src/api/ApiCall"; +import { + Button, + Stack, + CardContent, + CardActions, + Skeleton, +} from "@mui/material"; + +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippCustomDataMappingForm from "/src/components/CippFormPages/CippCustomDataMappingForm"; + +const Page = () => { + const router = useRouter(); + const { id } = router.query; + + const formControl = useForm({ + mode: "onChange", + defaultValues: {}, // Default values will be populated after fetching data + }); + + const formState = useFormState({ control: formControl.control }); + + const fetchMappingApi = ApiGetCall({ + url: `/api/ExecCustomData?Action=GetMapping&id=${id}`, + onResult: (data) => { + formControl.reset(data?.Results); // Populate form with fetched data + }, + }); + + const editMappingApi = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["MappingsListPage"], + }); + + const handleEditMapping = (data) => { + editMappingApi.mutate({ + url: "/api/ExecCustomData", + data: { + Action: "AddEditMapping", + Mapping: { ...data, id }, // Include the ID for editing + }, + }); + }; + + useEffect(() => { + if (id) { + fetchMappingApi.refetch(); // Fetch mapping data when `id` is available + } + }, [id]); + + return ( + + + + {fetchMappingApi.isFetching ? ( + + ) : ( + + )} + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/custom-data/mappings/index.js b/src/pages/cipp/custom-data/mappings/index.js new file mode 100644 index 000000000000..7579685d1f62 --- /dev/null +++ b/src/pages/cipp/custom-data/mappings/index.js @@ -0,0 +1,84 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Alert, Button, SvgIcon, Typography } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import tabOptions from "../tabOptions"; +import NextLink from "next/link"; +import { TrashIcon, PencilIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const pageTitle = "Custom Data Mappings"; + + // Columns for the table + const columns = [ + "tenant", + "sourceType", + "dataset", + "syncProperty", + "directoryObject", + "customDataAttribute", + ]; + + const actions = [ + { + label: "Edit Mapping", + icon: , + link: "/cipp/custom-data/mappings/edit?id=[id]", + }, + { + label: "Delete Mapping", + icon: , + url: "/api/ExecCustomData?Action=DeleteMapping", + type: "POST", + data: { + id: "id", + }, + confirmText: "Are you sure you want to delete the mapping with ID '[id]'?", + }, + ]; + + return ( + <> + + + Custom data mappings are used to synchronize custom data to directory objects. You can + add, edit, or delete mappings here. + + + } + cardButton={ + + } + tenantInTitle={false} + apiUrl="/api/ExecCustomData?Action=ListMappings" + apiDataKey="Results" + simpleColumns={columns} + filters={[]} + actions={actions} + /> + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/custom-data/schema-extensions/add.js b/src/pages/cipp/custom-data/schema-extensions/add.js new file mode 100644 index 000000000000..605d50ed4f15 --- /dev/null +++ b/src/pages/cipp/custom-data/schema-extensions/add.js @@ -0,0 +1,240 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch, useFormState } from "react-hook-form"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { + Box, + Button, + Stack, + IconButton, + CardContent, + Typography, + Divider, + CardActions, +} from "@mui/material"; +import { AddCircle, RemoveCircle } from "@mui/icons-material"; + +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; + +const availableTargetTypes = [ + { value: "user", label: "User" }, + { value: "group", label: "Group" }, + { value: "administrativeUnit", label: "Administrative Unit" }, + { value: "contact", label: "Contact" }, + { value: "device", label: "Device" }, + { value: "event", label: "Event (User and Group Calendars)" }, + { value: "message", label: "Message" }, + { value: "organization", label: "Organization" }, + { value: "post", label: "Post" }, +]; + +const Page = () => { + const router = useRouter(); + const formControl = useForm({ + mode: "onChange", + defaultValues: { + properties: [], + status: "InDevelopment", + }, + }); + + const formState = useFormState({ control: formControl.control }); + + const addSchemaApi = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["SchemaExtensionsListPage"], + }); + + const handleAddSchema = (data) => { + //console.log(data); + if (!data.properties || data.properties.length === 0) { + formControl.setError("properties", { + type: "manual", + message: "At least one property must be added.", + }); + return; + } + + addSchemaApi.mutate({ + url: "/api/ExecCustomData", + data: { + Action: "AddSchemaExtension", + schemaExtension: { + id: data.id, + description: data.description, + targetTypes: data.targetTypes.map((targetType) => targetType.value), + status: data.status, + properties: data.properties, + }, + }, + }); + }; + + const properties = useWatch({ control: formControl.control, name: "properties" }); + + const addProperty = () => { + formControl.setValue("properties", [...properties, { name: "", type: null }]); + if (formControl.formState.errors.properties) { + formControl.clearErrors("properties"); + } + }; + + const removeProperty = (index) => { + const updatedProperties = properties.filter((_, i) => i !== index); + formControl.setValue("properties", updatedProperties); + }; + + const handlePropertyChange = (index, field, value) => { + const updatedProperties = properties.map((property, i) => + i === index ? { ...property, [field]: value } : property + ); + formControl.setValue("properties", updatedProperties); + }; + + const formFields = [ + { + name: "id", + label: "Schema ID", + type: "textField", + required: true, + placeholder: + "Enter a schema id (e.g. cippUser). The prefix is generated automatically after creation.", + }, + { + name: "description", + label: "Description", + type: "textField", + required: true, + placeholder: "Enter a description for the schema extension", + }, + { + name: "status", + label: "Status", + type: "select", + required: true, + placeholder: "Select a status for the schema extensions (e.g. InDevelopment, Available)", + options: [ + { value: "InDevelopment", label: "In Development" }, + { value: "Available", label: "Available" }, + ], + creatable: false, + }, + { + name: "targetTypes", + label: "Target Types", + type: "autoComplete", + placeholder: "Select the directory object target types for the schema extension", + required: true, + multiple: true, + options: availableTargetTypes, + creatable: false, + validate: (value) => { + if (value.length > 0) return true; + return "Please select at least one target type."; + }, + }, + ]; + + const propertyTypeOptions = [ + { value: "Binary", label: "Binary (256 bytes max)" }, + { value: "Boolean", label: "Boolean" }, + { value: "DateTime", label: "DateTime (ISO 8601, UTC)" }, + { value: "Integer", label: "Integer (32-bit)" }, + { value: "String", label: "String (256 characters max)" }, + ]; + + return ( + + + + + + + Schema Details + + {formFields.map((field, index) => ( + + ))} + + Properties + + {properties && + properties.map((property, index) => ( + + + handlePropertyChange(index, "name", e.target.value)} + required={true} + /> + + + handlePropertyChange(index, "type", e.target.value)} + options={propertyTypeOptions} + required={true} + validate={(value) => { + if (value) return true; + return "Please select a property type."; + }} + /> + + removeProperty(index)}> + + + + ))} + + + {formControl.formState.errors.properties && ( + + {formControl.formState.errors.properties.message} + + )} + + + + + + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/custom-data/schema-extensions/index.js b/src/pages/cipp/custom-data/schema-extensions/index.js new file mode 100644 index 000000000000..f7b5729187ba --- /dev/null +++ b/src/pages/cipp/custom-data/schema-extensions/index.js @@ -0,0 +1,159 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Alert, Button, Link, SvgIcon, Typography } from "@mui/material"; +import { Add, Block, CheckCircleOutline } from "@mui/icons-material"; +import tabOptions from "../tabOptions"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import NextLink from "next/link"; + +const Page = () => { + const pageTitle = "Schema Extensions"; + + // Columns for the table + const columns = [ + "id", // Schema ID + "status", // Status + "description", // Description + "targetTypes", // Target Types + "properties", // Properties + ]; + + const actions = [ + { + label: "Add Property", + icon: , + url: "/api/ExecCustomData?Action=AddSchemaProperty", + type: "POST", + data: { + id: "id", + }, + confirmText: ( + <> + Add a new property to the schema. + Properties cannot be deleted once they are created. + + ), + fields: [ + { + name: "name", + label: "Property Name", + type: "textField", + required: true, + }, + { + name: "type", + label: "Property Type", + type: "select", + required: true, + options: [ + { value: "Binary", label: "Binary (256 bytes max)" }, + { value: "Boolean", label: "Boolean" }, + { value: "DateTime", label: "DateTime (ISO 8601, UTC)" }, + { value: "Integer", label: "Integer (32-bit)" }, + { value: "String", label: "String (256 characters max)" }, + ], + creatable: false, + validate: (value) => { + if (value) return true; + return "Please select a property type."; + }, + }, + ], + hideBulk: true, + condition: (row) => row.status !== "Deprecated", + }, + { + label: "Set to Available", + icon: , + url: "/api/ExecCustomData?Action=ChangeSchemaState", + type: "POST", + data: { + id: "id", + status: "!Available", + }, + confirmText: + "Set [id] to Available. You will no longer be able to delete properties or the schema.", + condition: (row) => row.status === "InDevelopment", + }, + { + label: "Set to Deprecated", + icon: , + url: "/api/ExecCustomData?Action=ChangeSchemaState", + type: "POST", + data: { + id: "id", + status: "!Deprecated", + }, + confirmText: "Set [id] to Deprecated. This is a permanent action and cannot be undone.", + condition: (row) => row.status === "Available", + }, + { + label: "Delete Schema", + icon: , + url: "/api/ExecCustomData?Action=DeleteSchema", + type: "POST", + data: { + id: "id", + }, + confirmText: "Are you sure you want to delete [id]?", + condition: (row) => row.status === "InDevelopment", + }, + ]; + + return ( + <> + + + Schema extensions + {" "} + allow you to add custom properties to Microsoft Entra directory objects. +
      +
    • Schema extensions can only be Deprecated once they are set to Available.
    • +
    • Properties cannot be deleted once they are created.
    • +
    • + There is a limit of 100 extension values per resource instance (directory objects + only) +
    • +
    • There is a limit of 5 total schema extensions.
    • +
    + + } + cardButton={ + + } + tenantInTitle={false} + apiUrl="/api/ExecCustomData?Action=ListSchemaExtensions" + apiDataKey="Results" + simpleColumns={columns} + filters={[]} + actions={actions} + /> + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/custom-data/tabOptions.json b/src/pages/cipp/custom-data/tabOptions.json new file mode 100644 index 000000000000..de0055483d82 --- /dev/null +++ b/src/pages/cipp/custom-data/tabOptions.json @@ -0,0 +1,14 @@ +[ + { + "label": "Directory Extensions", + "path": "/cipp/custom-data/directory-extensions" + }, + { + "label": "Schema Extensions", + "path": "/cipp/custom-data/schema-extensions" + }, + { + "label": "Mappings", + "path": "/cipp/custom-data/mappings" + } +] diff --git a/src/pages/cipp/integrations/configure.js b/src/pages/cipp/integrations/configure.js index 52d4766d9c09..3702ae108926 100644 --- a/src/pages/cipp/integrations/configure.js +++ b/src/pages/cipp/integrations/configure.js @@ -1,4 +1,14 @@ -import { Alert, Box, Button, CardContent, Stack, Tab, Tabs, Typography } from "@mui/material"; +import { + Alert, + Box, + Button, + CardContent, + Skeleton, + Stack, + Tab, + Tabs, + Typography, +} from "@mui/material"; import CippIntegrationSettings from "/src/components/CippIntegrations/CippIntegrationSettings"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm } from "react-hook-form"; @@ -15,6 +25,7 @@ import CippPageCard from "../../../components/CippCards/CippPageCard"; import CippIntegrationTenantMapping from "../../../components/CippIntegrations/CippIntegrationTenantMapping"; import CippIntegrationFieldMapping from "../../../components/CippIntegrations/CippIntegrationFieldMapping"; import { CippCardTabPanel } from "../../../components/CippComponents/CippCardTabPanel"; +import CippApiClientManagement from "../../../components/CippIntegrations/CippApiClientManagement"; function tabProps(index) { return { @@ -32,6 +43,8 @@ const Page = () => { const integrations = ApiGetCall({ url: "/api/ListExtensionsConfig", queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, }); const [testQuery, setTestQuery] = useState({ url: "", waiting: false, queryKey: "" }); @@ -77,7 +90,7 @@ const Page = () => { defaultValues: integrations?.data, }); - const extension = extensions.find((extension) => extension.id === router.query.id); + const extension = extensions.find((extension) => extension.id === router.query.id) || {}; var logo = extension?.logo; if (preferredTheme === "dark" && extension?.logoDark) { @@ -95,13 +108,23 @@ const Page = () => { return ( <> + {integrations.isLoading && ( + + + + + + + + + + )} {integrations.isSuccess && extension && ( {logo && ( @@ -109,7 +132,7 @@ const Page = () => { component="img" src={logo} alt={extension.name} - sx={{ width: "50%", mx: "auto" }} + sx={{ maxWidth: "50%", mx: "auto", maxHeight: "125px" }} /> )} @@ -120,67 +143,112 @@ const Page = () => { {extension.alertText} )} - + {extension?.hideTestButton !== true && ( - + + + )} {extension?.forceSyncButton && ( - + + + )} {extension?.links && ( <> {extension.links.map((link, index) => ( - + + + ))} )} - - + + - - {extension?.mappingRequired && } - {extension?.fieldMapping && } + {extension?.mappingRequired && ( + setting?.name === `${extension.id}.Enabled` + ) && integrations?.data?.[extension.id]?.Enabled !== true + } + /> + )} + {extension?.fieldMapping && ( + setting.name === `${extension.id}.Enabled` + ) && integrations?.data?.[extension.id]?.Enabled !== true + } + /> + )} - + {extension?.id === "cippapi" ? ( + + ) : ( + + )} + {extension?.mappingRequired && ( diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index 241ced5c4792..89a33ee66448 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -7,7 +7,6 @@ import { CardActions, CardContent, Container, - Grid, Skeleton, Stack, Typography, @@ -17,6 +16,7 @@ import { Sync } from "@mui/icons-material"; import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall"; import Link from "next/link"; +import { Grid } from "@mui/system"; const Page = () => { const settings = useSettings(); @@ -25,6 +25,8 @@ const Page = () => { const integrations = ApiGetCall({ url: "/api/ListExtensionsConfig", queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, }); return ( @@ -56,16 +58,16 @@ const Page = () => { } var integrationConfig = integrations?.data?.[extension.id]; - var isEnabled = integrationConfig?.Enabled; + var isEnabled = integrationConfig?.Enabled || extension.id === "cippapi"; var status = "Unconfigured"; if (integrationConfig && !isEnabled) { status = "Disabled"; - } else if (integrationConfig && isEnabled) { + } else if ((integrationConfig && isEnabled) || extension.id === "cippapi") { status = "Enabled"; } return ( - + , + color: "primary", + }, +]; const Page = () => { const formControl = useForm({ defaultValues: { startDate: null, - toggleSwitch: false, + endDate: null, + username: "", + severity: [], }, }); const [expanded, setExpanded] = useState(false); // State for Accordion const [filterEnabled, setFilterEnabled] = useState(false); // State for filter toggle - const [dateFilter, setDateFilter] = useState(null); // State for date filter + const [startDate, setStartDate] = useState(null); // State for start date filter + const [endDate, setEndDate] = useState(null); // State for end date filter + const [username, setUsername] = useState(null); // State for username filter + const [severity, setSeverity] = useState(null); // State for severity filter + + // Watch date fields to show warning for large date ranges + const watchStartDate = formControl.watch("startDate"); + const watchEndDate = formControl.watch("endDate"); + + // Component to display warning for large date ranges + const DateRangeWarning = () => { + if (!watchStartDate || !watchEndDate) return null; + + const startDateMs = new Date(watchStartDate * 1000); + const endDateMs = new Date(watchEndDate * 1000); + const daysDifference = (endDateMs - startDateMs) / (1000 * 60 * 60 * 24); + + if (daysDifference > 10) { + return ( + + + You have selected a date range of {Math.ceil(daysDifference)} days. Large date ranges + may cause timeouts or errors due to the amount of data being processed. Consider + narrowing your date range if you encounter issues. + + + ); + } + + return null; + }; const onSubmit = (data) => { - // Set filter states based on form submission - setFilterEnabled(data.toggleSwitch); - setDateFilter( + // Check if any filter is applied + const hasFilter = + data.startDate !== null || + data.endDate !== null || + data.username !== null || + data.severity?.length > 0; + setFilterEnabled(hasFilter); + + // Format start date if available + setStartDate( data.startDate ? new Date(data.startDate * 1000).toISOString().split("T")[0].replace(/-/g, "") : null ); + + // Format end date if available + setEndDate( + data.endDate + ? new Date(data.endDate * 1000).toISOString().split("T")[0].replace(/-/g, "") + : null + ); + + // Set username filter if available + setUsername(data.username || null); + + // Set severity filter if available (convert array to comma-separated string) + setSeverity( + data.severity && data.severity.length > 0 + ? data.severity.map((item) => item.value).join(",") + : null + ); + + // Close the accordion after applying filters + setExpanded(false); + }; + + const clearFilters = () => { + formControl.reset({ + startDate: null, + endDate: null, + username: "", + severity: [], + }); + setFilterEnabled(false); + setStartDate(null); + setEndDate(null); + setUsername(null); + setSeverity(null); + setExpanded(false); // Close the accordion when clearing filters }; return ( @@ -54,37 +145,157 @@ const Page = () => { tableFilter={ setExpanded(!expanded)}> }> - Logbook Filters + + + + + + Logbook Filters + {filterEnabled ? ( + + ( + {startDate || endDate ? ( + <> + {startDate + ? new Date( + startDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00" + ).toLocaleDateString() + : new Date().toLocaleDateString()} + {startDate && endDate ? " - " : ""} + {endDate + ? new Date( + endDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00" + ).toLocaleDateString() + : ""} + + ) : null} + {username && (startDate || endDate) && " | "} + {username && <>User: {username}} + {severity && (username || startDate || endDate) && " | "} + {severity && <>Severity: {severity.replace(/,/g, ", ")}}) + + ) : ( + + (Today: {new Date().toLocaleDateString()}) + + )} + +
    {/* Date Filter */} - + + + Use the filters below to narrow down your logbook results. You can filter by + date range, username, and severity levels. By default, the logbook shows the + current day based on UTC time. Your local time is{" "} + {new Date().getTimezoneOffset() / -60} hours offset from UTC. + + + + + + + + + { + const startDate = formControl.getValues("startDate"); + if (value && !startDate) { + return "Start date must be set when using an end date"; + } + if ( + startDate && + value && + new Date(value * 1000) < new Date(startDate * 1000) + ) { + return "End date must be after start date"; + } + return true; + }, + }} + /> + + + + + {/* Date Range Warning Alert */} + + + {/* Username Filter */} + - {/* Toggle Switch */} - + {/* Severity Filter */} + - {/* Submit Button */} - - + {/* Action Buttons */} + + + + + @@ -94,22 +305,20 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} simpleColumns={simpleColumns} - queryKey={`Listlogs-${dateFilter}-${filterEnabled}`} + queryKey={`Listlogs-${startDate}-${endDate}-${username}-${severity}-${filterEnabled}`} + tenantInTitle={false} apiData={{ - DateFilter: dateFilter, // Pass date filter from state + StartDate: startDate, // Pass start date filter from state + EndDate: endDate, // Pass end date filter from state + User: username, // Pass username filter from state + Severity: severity, // Pass severity filter from state Filter: filterEnabled, // Pass filter toggle state }} + actions={actions} /> ); }; -/* Comment to Developer: - - The filter is inside an expandable Accordion. By default, the filter is collapsed. - - The "Apply Filters" button sets the form data for date and filter toggle. - - Form state is managed using react-hook-form, and the filter states are applied to the table. - - The DateFilter is passed in 'YYYYMMDD' format, and Filter toggle is passed as a boolean. -*/ - Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/cipp/logs/logentry.js b/src/pages/cipp/logs/logentry.js new file mode 100644 index 000000000000..3f9c1aef3ae0 --- /dev/null +++ b/src/pages/cipp/logs/logentry.js @@ -0,0 +1,137 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { + Button, + SvgIcon, + Box, + Container, + Chip, +} from "@mui/material"; +import { Stack } from "@mui/system"; +import ArrowLeftIcon from "@mui/icons-material/ArrowLeft"; +import { CippPropertyListCard } from "/src/components/CippCards/CippPropertyListCard"; +import { CippInfoBar } from "/src/components/CippCards/CippInfoBar"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; + +const Page = () => { + const router = useRouter(); + const { logentry } = router.query; + + const logRequest = ApiGetCall({ + url: `/api/Listlogs`, + data: { + logentryid: logentry, + }, + queryKey: `GetLogEntry-${logentry}`, + waiting: !!logentry, + }); + + const handleBackClick = () => { + router.push("/cipp/logs"); + }; + + // Get the log data from array + const logData = logRequest.data?.[0]; + + // Top info bar data like dashboard + const logInfo = logData ? [ + { name: "Log ID", data: logData.RowKey }, + { name: "Date & Time", data: new Date(logData.DateTime).toLocaleString() }, + { name: "API", data: logData.API }, + { + name: "Severity", + data: ( + + ) + }, + ] : []; + + // Main log properties + const propertyItems = logData ? [ + { label: "Tenant", value: logData.Tenant }, + { label: "User", value: logData.User }, + { label: "Message", value: logData.Message }, + { label: "Tenant ID", value: logData.TenantID }, + { label: "App ID", value: logData.AppId || "None" }, + { label: "IP Address", value: logData.IP || "None" }, + ] : []; + + // LogData properties + const logDataItems = logData?.LogData && typeof logData.LogData === 'object' + ? Object.entries(logData.LogData).map(([key, value]) => ({ + label: key, + value: typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value), + })) + : []; + + return ( + + + + {/* Back button */} + + + {logRequest.isLoading && } + + {logRequest.isError && ( + + )} + + {logRequest.isSuccess && logData && ( + <> + {/* Top info bar like dashboard */} + + + {/* Main log information */} + + + {/* LogData in separate card */} + {logDataItems.length > 0 && ( + + )} + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/preferences.js b/src/pages/cipp/preferences.js index 6cc782d44508..38f6fe99bd27 100644 --- a/src/pages/cipp/preferences.js +++ b/src/pages/cipp/preferences.js @@ -1,6 +1,6 @@ import Head from "next/head"; import { Box, Container, Stack } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard"; import CippFormComponent from "../../components/CippComponents/CippFormComponent"; @@ -9,10 +9,24 @@ import { useSettings } from "../../hooks/use-settings"; import countryList from "../../data/countryList.json"; import { CippSettingsSideBar } from "../../components/CippComponents/CippSettingsSideBar"; import CippDevOptions from "/src/components/CippComponents/CippDevOptions"; +import { ApiGetCall } from "../../api/ApiCall"; +import { getCippFormatting } from "../../utils/get-cipp-formatting"; const Page = () => { const settings = useSettings(); - const formcontrol = useForm({ mode: "onChange", defaultValues: settings }); + const cleanedSettings = { ...settings }; + + if (cleanedSettings.offboardingDefaults?.keepCopy) { + delete cleanedSettings.offboardingDefaults.keepCopy; + settings.handleUpdate(cleanedSettings); + } + + const formcontrol = useForm({ mode: "onChange", defaultValues: cleanedSettings }); + + const auth = ApiGetCall({ + url: "/api/me", + queryKey: "authmecipp", + }); const addedAttributes = [ { value: "consentProvidedForMinor", label: "consentProvidedForMinor" }, @@ -25,7 +39,7 @@ const Page = () => { { value: "officeLocation", label: "officeLocation" }, { value: "otherMails", label: "otherMails" }, { value: "showInAddressList", label: "showInAddressList" }, - { value: "state", label: "state" }, + { value: "sponsor", label: "sponsor" }, ]; const pageSizes = [ @@ -56,30 +70,15 @@ const Page = () => { - ), - }, { label: "Default new user usage location", value: ( { value: ( { ), }, { - label: "Menu Favourites", + label: "Added Attributes when creating a new user", value: ( ), }, @@ -230,7 +221,7 @@ const Page = () => { value: ( ), @@ -255,12 +246,53 @@ const Page = () => { /> ), }, + { + label: "Disable Sign in", + value: ( + + ), + }, + { + label: "Remove all MFA Devices", + value: ( + + ), + }, + { + label: "Clear Immutable ID", + value: ( + + ), + }, ]} /> + !["anonymous", "authenticated"].includes(role)) + .map((role) => ({ + label: "", + value: getCippFormatting(role, "role"), + }))} + showDivider={false} + /> + diff --git a/src/pages/cipp/scheduler/index.js b/src/pages/cipp/scheduler/index.js index 18b0d01a89b5..b7071474cad1 100644 --- a/src/pages/cipp/scheduler/index.js +++ b/src/pages/cipp/scheduler/index.js @@ -1,46 +1,41 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; -import { Button, Typography } from "@mui/material"; +import { Button } from "@mui/material"; import Link from "next/link"; -import { CalendarDaysIcon, EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { CalendarDaysIcon } from "@heroicons/react/24/outline"; import { useState } from "react"; -import { CopyAll, Edit } from "@mui/icons-material"; -import { CippCodeBlock } from "../../../components/CippComponents/CippCodeBlock"; +import ScheduledTaskDetails from "../../../components/CippComponents/ScheduledTaskDetails"; +import { CippScheduledTaskActions } from "../../../components/CippComponents/CippScheduledTaskActions"; const Page = () => { - const actions = [ + const actions = CippScheduledTaskActions(); + + const filterList = [ + { + filterName: "Running", + value: [{ id: "TaskState", value: "Running" }], + type: "column", + }, { - label: "Edit Job", - link: "/cipp/scheduler/job?id=[RowKey]", - multiPost: false, - icon: , - color: "success", + filterName: "Planned", + value: [{ id: "TaskState", value: "Planned" }], + type: "column", }, { - label: "Clone and Edit Job", - link: "/cipp/scheduler/job?id=[RowKey]&Clone=True", - multiPost: false, - icon: , - color: "success", + filterName: "Failed", + value: [{ id: "TaskState", value: "Failed" }], + type: "column", }, { - label: "Delete Job", - icon: , - type: "POST", - url: "/api/RemoveScheduledItem", - data: { id: "RowKey" }, - confirmText: "Are you sure you want to delete this job?", - multiPost: false, + filterName: "Completed", + value: [{ id: "TaskState", value: "Completed" }], + type: "column", }, ]; const offCanvas = { - children: (extendedData) => ( - <> - Job Results - - - ), + children: (extendedData) => , + size: "xl", actions: actions, }; const [showHiddenJobs, setShowHiddenJobs] = useState(false); @@ -59,22 +54,24 @@ const Page = () => { tenantInTitle={false} title="Scheduled Tasks" apiUrl={ - showHiddenJobs ? "/api/ListScheduledItems?ListHidden=True" : "/api/ListScheduledItems" + showHiddenJobs ? "/api/ListScheduledItems?ShowHidden=true" : "/api/ListScheduledItems" } queryKey={showHiddenJobs ? `ListScheduledItems-hidden` : `ListScheduledItems`} simpleColumns={[ - "Name", - "Tenant", + "ExecutedTime", "TaskState", + "Tenant", + "Name", + "ScheduledTime", "Command", "Parameters", "PostExecution", "Recurrence", - "ExecutedTime", - "ScheduledTime", + "Results", ]} actions={actions} offCanvas={offCanvas} + filters={filterList} /> ); }; diff --git a/src/pages/cipp/scheduler/task.js b/src/pages/cipp/scheduler/task.js new file mode 100644 index 000000000000..8f52ae443ece --- /dev/null +++ b/src/pages/cipp/scheduler/task.js @@ -0,0 +1,22 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import ScheduledTaskDetails from "../../../components/CippComponents/ScheduledTaskDetails"; +import CippPageCard from "../../../components/CippCards/CippPageCard"; +import { CardContent } from "@mui/material"; + +const Page = () => { + const router = useRouter(); + const { id } = router.query; + + return ( + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/cipp/settings/backend.js b/src/pages/cipp/settings/backend.js index 76d45cb98bd0..283c2dccc55e 100644 --- a/src/pages/cipp/settings/backend.js +++ b/src/pages/cipp/settings/backend.js @@ -1,4 +1,5 @@ -import { Container, Grid } from "@mui/material"; +import { Container } from "@mui/material"; +import { Grid } from "@mui/system"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; @@ -104,7 +105,7 @@ const Page = () => { {backendInfo.map((item) => ( - + ))} diff --git a/src/pages/cipp/settings/backup.js b/src/pages/cipp/settings/backup.js index 6935872881c2..1931dca8f87a 100644 --- a/src/pages/cipp/settings/backup.js +++ b/src/pages/cipp/settings/backup.js @@ -1,6 +1,5 @@ -import { Box, Button, CardContent, Grid, Stack, Typography, Skeleton } from "@mui/material"; +import { Box, Button, CardContent, Stack, Typography, Skeleton } from "@mui/material"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; - import CippPageCard from "../../../components/CippCards/CippPageCard"; import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; @@ -55,7 +54,7 @@ const Page = () => { }); const NextBackupRun = (props) => { - const date = new Date(props.date * 1000); + const date = new Date(props.date); if (isNaN(date)) { return "Not Scheduled"; } else { @@ -135,6 +134,7 @@ const Page = () => { confirmText: "Are you sure you want to restore this backup?", relatedQueryKeys: ["BackupList"], multiPost: false, + hideBulk: true, }, { label: "Download Backup", diff --git a/src/pages/cipp/settings/global-variables.js b/src/pages/cipp/settings/global-variables.js new file mode 100644 index 000000000000..96ffe712d0b3 --- /dev/null +++ b/src/pages/cipp/settings/global-variables.js @@ -0,0 +1,16 @@ +import tabOptions from "./tabOptions"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippCustomVariables from "/src/components/CippComponents/CippCustomVariables.jsx"; + +const Page = () => { + return ; +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/settings/index.js b/src/pages/cipp/settings/index.js index 430a436fd51e..11b432e7988d 100644 --- a/src/pages/cipp/settings/index.js +++ b/src/pages/cipp/settings/index.js @@ -1,4 +1,5 @@ -import { Container, Grid } from "@mui/material"; +import { Container } from "@mui/material"; +import { Grid } from "@mui/system"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; @@ -8,25 +9,29 @@ import CippPasswordSettings from "/src/components/CippSettings/CippPasswordSetti import CippDnsSettings from "/src/components/CippSettings/CippDnsSettings"; import CippCacheSettings from "/src/components/CippSettings/CippCacheSettings"; import CippBackupSettings from "/src/components/CippSettings/CippBackupSettings"; +import CippBrandingSettings from "/src/components/CippSettings/CippBrandingSettings"; const Page = () => { return ( - + - + - + - + - + + + + ); diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index 7db7f6e0e70e..a33e9f1f612a 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -4,7 +4,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button, SvgIcon } from "@mui/material"; import { TrashIcon } from "@heroicons/react/24/outline"; -import { Add, Remove } from "@mui/icons-material"; +import { Add } from "@mui/icons-material"; import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; import { useDialog } from "../../../hooks/use-dialog"; diff --git a/src/pages/cipp/settings/notifications.js b/src/pages/cipp/settings/notifications.js index 2dd5a990d5e0..dbb0727e0be4 100644 --- a/src/pages/cipp/settings/notifications.js +++ b/src/pages/cipp/settings/notifications.js @@ -3,75 +3,17 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { useForm } from "react-hook-form"; -import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; -import { Box, Button, Grid } from "@mui/material"; -import { ApiGetCall } from "../../../api/ApiCall"; -import { useEffect } from "react"; import { useDialog } from "../../../hooks/use-dialog"; -import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; +import { CippNotificationForm } from "../../../components/CippComponents/CippNotificationForm"; const Page = () => { const pageTitle = "Notification Settings"; const notificationDialog = useDialog(); - const listNotificationConfig = ApiGetCall({ - url: "/api/ListNotificationConfig", - queryKey: "ListNotificationConfig", - }); - - const logTypes = [ - { label: "Updates Status", value: "Updates" }, - { label: "All Standards", value: "Standards" }, - { label: "Token Events", value: "TokensUpdater" }, - { label: "Changing DNS Settings", value: "ExecDnsConfig" }, - { label: "Adding excluded licenses", value: "ExecExcludeLicenses" }, - { label: "Adding excluded tenants", value: "ExecExcludeTenant" }, - { label: "Editing a user", value: "EditUser" }, - { label: "Adding or deploying applications", value: "ChocoApp" }, - { label: "Adding autopilot devices", value: "AddAPDevice" }, - { label: "Editing a tenant", value: "EditTenant" }, - { label: "Adding an MSP app", value: "AddMSPApp" }, - { label: "Adding a user", value: "AddUser" }, - { label: "Adding a group", value: "AddGroup" }, - { label: "Adding a tenant", value: "NewTenant" }, - { label: "Executing the offboard wizard", value: "ExecOffboardUser" }, - ]; - const severityTypes = [ - { label: "Alert", value: "Alert" }, - { label: "Error", value: "Error" }, - { label: "Info", value: "Info" }, - { label: "Warning", value: "Warning" }, - { label: "Critical", value: "Critical" }, - ]; - const formControl = useForm({ mode: "onChange", }); - useEffect(() => { - if (listNotificationConfig.isSuccess) { - var logsToInclude = []; - listNotificationConfig.data?.logsToInclude.map((log) => { - var logType = logTypes.find((logType) => logType.value === log); - if (logType) { - logsToInclude.push(logType); - } - }); - - formControl.reset({ - email: listNotificationConfig.data?.email, - webhook: listNotificationConfig.data?.webhook, - logsToInclude: logsToInclude, - Severity: listNotificationConfig.data?.Severity.map((severity) => { - return severityTypes.find((severityType) => severityType.value === severity); - }), - onePerTenant: listNotificationConfig.data?.onePerTenant, - sendtoIntegration: listNotificationConfig.data?.sendtoIntegration, - includeTenantId: listNotificationConfig.data?.includeTenantId, - }); - } - }, [listNotificationConfig.isSuccess]); - return ( { resetForm={false} postUrl="/api/ExecNotificationConfig" relatedQueryKeys={["ListNotificationConfig"]} - addedButtons={ - - } > - - - - - - - - - - - - - - - - - - - - - - - ); diff --git a/src/pages/cipp/settings/partner-webhooks.js b/src/pages/cipp/settings/partner-webhooks.js index d44f24c690a8..67d218fb22c6 100644 --- a/src/pages/cipp/settings/partner-webhooks.js +++ b/src/pages/cipp/settings/partner-webhooks.js @@ -8,7 +8,6 @@ import { Button, Card, Chip, - Grid, Stack, Typography, Link, @@ -17,6 +16,7 @@ import { IconButton, SvgIcon, } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; import { useEffect } from "react"; @@ -120,6 +120,7 @@ const Page = () => { title={pageTitle} hideBackButton={true} hidePageType={true} + allowResubmit={true} formControl={formControl} resetForm={false} postUrl="/api/ExecPartnerWebhook?Action=CreateSubscription" @@ -131,7 +132,7 @@ const Page = () => { } > - + Subscribe to Microsoft Partner center webhooks to enable automatic tenant onboarding and alerting. Updating the settings will replace any existing webhook subscription with one @@ -146,7 +147,7 @@ const Page = () => { for more information on the webhook types. - + { showDivider={false} /> - + { formControl={formControl} /> - + { /> {testRunning && ( - + { return ( - + - + - + - + diff --git a/src/pages/cipp/settings/tabOptions.json b/src/pages/cipp/settings/tabOptions.json index 6231cad32485..b68bec5eb43e 100644 --- a/src/pages/cipp/settings/tabOptions.json +++ b/src/pages/cipp/settings/tabOptions.json @@ -26,5 +26,9 @@ { "label": "Licenses", "path": "/cipp/settings/licenses" + }, + { + "label": "Global Variables", + "path": "/cipp/settings/global-variables" } ] diff --git a/src/pages/cipp/settings/tenants.js b/src/pages/cipp/settings/tenants.js index 3a09b8ff8aa4..a5495b4b77e8 100644 --- a/src/pages/cipp/settings/tenants.js +++ b/src/pages/cipp/settings/tenants.js @@ -1,139 +1,12 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import tabOptions from "./tabOptions"; -import { Button, SvgIcon } from "@mui/material"; -import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; -import { useDialog } from "/src/hooks/use-dialog"; -import { Sync } from "@mui/icons-material"; +import { CippTenantTable } from "../../../components/CippWizard/CippTenantTable"; const Page = () => { const pageTitle = "Tenants - Backend"; - const createDialog = useDialog(); - // Actions formatted as per your guidelines - const actions = [ - { - label: "Exclude Tenants", - type: "POST", - url: `/api/ExecExcludeTenant?AddExclusion=true`, - data: { value: "customerId" }, - confirmText: "Are you sure you want to exclude these tenants?", - multiPost: false, - }, - { - label: "Include Tenants", - type: "POST", - url: `/api/ExecExcludeTenant?RemoveExclusion=true`, - data: { value: "customerId" }, - confirmText: "Are you sure you want to include these tenants?", - multiPost: false, - }, - { - label: "Refresh CPV Permissions", - type: "POST", - url: `/api/ExecCPVPermissions`, - data: { TenantFilter: "customerId" }, - confirmText: "Are you sure you want to refresh the CPV permissions for these tenants?", - multiPost: false, - }, - { - label: "Reset CPV Permissions", - type: "POST", - url: `/api/ExecCPVPermissions?&ResetSP=true`, - data: { TenantFilter: "customerId" }, - confirmText: - "Are you sure you want to reset the CPV permissions for these tenants? (This will delete the Service Principal and re-add it.)", - multiPost: false, - }, - { - label: "Remove Tenant", - type: "POST", - url: `/api/ExecRemoveTenant`, - data: { TenantID: "customerId" }, - confirmText: "Are you sure you want to remove this tenant?", - multiPost: false, - }, - ]; - - // Offcanvas details - const offCanvas = { - extendedInfoFields: [ - "displayName", - "defaultDomainName", - "Excluded", - "ExcludeDate", - "ExcludeUser", - ], - actions: actions, - }; - - // Columns for the table - const columns = [ - "displayName", // Tenant Name - "defaultDomainName", // Default Domain - "Excluded", // Excluded Status - "ExcludeDate", // Exclude Date - "ExcludeUser", // Exclude User - ]; - - return ( - <> - - - - - Force Refresh - - } - tenantInTitle={false} - apiUrl="/api/ExecExcludeTenant?ListAll=True" - actions={actions} - offCanvas={offCanvas} - simpleColumns={columns} - filters={[ - { - filterName: "Included tenants", - //true or false filters by yes/no - value: [{ id: "Excluded", value: "No" }], - type: "column", - }, - { - filterName: "Excluded tenants", - value: [{ id: "Excluded", value: "Yes" }], - type: "column", - }, - ]} - /> - - - ); + return ; }; Page.getLayout = (page) => ( diff --git a/src/pages/cipp/super-admin/cipp-roles/add.js b/src/pages/cipp/super-admin/cipp-roles/add.js new file mode 100644 index 000000000000..1734bd047245 --- /dev/null +++ b/src/pages/cipp/super-admin/cipp-roles/add.js @@ -0,0 +1,23 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippRoleAddEdit } from "/src/components/CippSettings/CippRoleAddEdit"; +import { CardContent, Stack, Alert } from "@mui/material"; + +const AddRolePage = () => { + return ( + + + + + Create a new custom role with specific permissions and settings. + + + + + + ); +}; + +AddRolePage.getLayout = (page) => {page}; + +export default AddRolePage; diff --git a/src/pages/cipp/super-admin/cipp-roles/edit.js b/src/pages/cipp/super-admin/cipp-roles/edit.js new file mode 100644 index 000000000000..85a4b2e0c431 --- /dev/null +++ b/src/pages/cipp/super-admin/cipp-roles/edit.js @@ -0,0 +1,28 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CippRoleAddEdit } from "/src/components/CippSettings/CippRoleAddEdit"; +import { CardContent, Stack, Alert } from "@mui/material"; + +const EditRolePage = () => { + const router = useRouter(); + const { role } = router.query; + + return ( + + + + + Editing an existing role will update the permissions for all users assigned to this + role. + + + + + + ); +}; + +EditRolePage.getLayout = (page) => {page}; + +export default EditRolePage; diff --git a/src/pages/cipp/super-admin/cipp-roles/index.js b/src/pages/cipp/super-admin/cipp-roles/index.js new file mode 100644 index 000000000000..91a7da35ad7c --- /dev/null +++ b/src/pages/cipp/super-admin/cipp-roles/index.js @@ -0,0 +1,31 @@ +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import tabOptions from "../tabOptions"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import CippRoles from "/src/components/CippSettings/CippRoles"; +import { CardContent, Stack, Typography } from "@mui/material"; + +const Page = () => { + return ( + + + + + CIPP roles can be used to restrict permissions for users with the 'editor' or 'readonly' + roles in CIPP. They can be limited to a subset of tenants and API permissions. To + restrict direct API access, create a role with the name 'CIPP-API'. + + + + + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/super-admin/custom-roles.js b/src/pages/cipp/super-admin/custom-roles.js deleted file mode 100644 index 76e95c16241b..000000000000 --- a/src/pages/cipp/super-admin/custom-roles.js +++ /dev/null @@ -1,36 +0,0 @@ -import { TabbedLayout } from "/src/layouts/TabbedLayout"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import tabOptions from "./tabOptions"; -import CippPageCard from "/src/components/CippCards/CippPageCard"; -import { CippCustomRoles } from "/src/components/CippSettings/CippCustomRoles"; -import { Alert, CardContent, Stack, Typography } from "@mui/material"; -import { WarningAmberOutlined } from "@mui/icons-material"; - -const Page = () => { - return ( - - - - - Custom roles can be used to restrict permissions for users with the 'editor' or - 'readonly' roles in CIPP. They can be limited to a subset of tenants and API - permissions. To restrict direct API access, create a role with the name 'CIPP-API'. - - }> - This functionality is in beta and should be treated as such. The custom role must be - added to the user in SWA in conjunction with the base role. (e.g. editor,mycustomrole) - - - - - - ); -}; - -Page.getLayout = (page) => ( - - {page} - -); - -export default Page; diff --git a/src/pages/cipp/super-admin/function-offloading.js b/src/pages/cipp/super-admin/function-offloading.js index 489785fa9274..8ac30a79831e 100644 --- a/src/pages/cipp/super-admin/function-offloading.js +++ b/src/pages/cipp/super-admin/function-offloading.js @@ -3,12 +3,14 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { useForm } from "react-hook-form"; -import { Typography } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Alert, Link } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { ApiGetCall } from "../../../api/ApiCall"; +import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; import { useEffect } from "react"; -import Link from "next/link"; +import NextLink from "next/link"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { TrashIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Function Offloading"; @@ -25,13 +27,37 @@ const Page = () => { queryKey: "execOffloadFunctions", }); + const deleteOffloadEntry = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["execOffloadFunctions"], + }); + + const handleDeleteOffloadEntry = (row) => { + const entity = { + RowKey: row.Name, + PartitionKey: "Version", + }; + + deleteOffloadEntry.mutate({ + url: "/api/ExecAzBobbyTables", + data: { + FunctionName: "Remove-AzDataTableEntity", + TableName: "Version", + Parameters: { + Entity: entity, + Force: true, + }, + }, + }); + }; + useEffect(() => { if (execOffloadFunctions.isSuccess) { formControl.reset({ OffloadFunctions: execOffloadFunctions.data?.OffloadFunctions, }); } - }, [execOffloadFunctions.isSuccess]); + }, [execOffloadFunctions.isSuccess, execOffloadFunctions.data]); return ( { queryKey={"execOffloadFunctions"} > - - - This mode enables offloading some of the more processor intensive functions to a - separate function app. This can be useful in environments where the CIPP server is under - heavy load. Please review{" "} - - our documentation - {" "} - for more information on how to configure this for your environment. - + + This mode enables offloading some of the more processor intensive functions to a separate + function app. This can be useful in environments where the CIPP server is under heavy + load. Please review{" "} + + our documentation + {" "} + for more information on how to configure this for your environment. + + If you are self-hosted, you must deploy the additional function app(s) to your CIPP + resource group and enable CI/CD or all background tasks will fail. + + + + , + url: "/api/ExecAzBobbyTables", + type: "POST", + customFunction: handleDeleteOffloadEntry, + confirmText: + "Are you sure you want to delete the offloaded function entry for [Name]? This does not delete the function app from Azure, this must be done first or it will register again.", + condition: (row) => row.Default !== true, + }, + ]} + /> - + {execOffloadFunctions.data?.Alerts?.length > 0 && ( + + {execOffloadFunctions.data?.Alerts.map((alert, index) => ( + + {alert} + + ))} + + )} + diff --git a/src/pages/cipp/super-admin/sam-app-permissions.js b/src/pages/cipp/super-admin/sam-app-permissions.js index 6161405e391a..f52b0a0711d5 100644 --- a/src/pages/cipp/super-admin/sam-app-permissions.js +++ b/src/pages/cipp/super-admin/sam-app-permissions.js @@ -42,6 +42,7 @@ const Page = () => { This functionality is in beta and should be treated as such. Removing permissions from the CIPP-SAM app is not advised. + {execSamAppPermissions.isLoading && } {execSamAppPermissions.isSuccess && ( { TenantMode: execPartnerMode.data?.TenantMode, }); } - }, [execPartnerMode.isSuccess]); + }, [execPartnerMode.isSuccess, execPartnerMode.data]); return ( { queryKey={["execPartnerMode", "TenantSelector"]} > - + The configuration settings below should only be modified by a super admin. Super admins can configure what tenant mode CIPP operates in. See{" "} @@ -72,7 +73,7 @@ const Page = () => { for more information on how to configure these modes and what they mean. - + { + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }, + }); + + return ( + { + return { + DisplayName: values.displayName, + hidefromGAL: values.hidefromGAL, + email: values.email, + FirstName: values.firstName, + LastName: values.lastName, + Title: values.jobTitle, + StreetAddress: values.streetAddress, + PostalCode: values.postalCode, + City: values.city, + State: values.state, + CountryOrRegion: values.country?.value || values.country, + Company: values.companyName, + mobilePhone: values.mobilePhone, + phone: values.businessPhone, + website: values.website, + mailTip: values.mailTip, + }; + }} + > + + + ); +}; + +AddContactTemplates.getLayout = (page) => {page}; + +export default AddContactTemplates; diff --git a/src/pages/email/administration/contacts-template/deploy.jsx b/src/pages/email/administration/contacts-template/deploy.jsx new file mode 100644 index 000000000000..6766b209d6ba --- /dev/null +++ b/src/pages/email/administration/contacts-template/deploy.jsx @@ -0,0 +1,65 @@ +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; + +const Page = () => { + const formControl = useForm({ + mode: "onChange", + defaultValues: { + selectedTenants: [], + TemplateList: [], + }, + }); + + return ( + + + + + + + + + {/* TemplateList */} + + option, + url: "/api/ListContactTemplates", + }} + placeholder="Select a template or enter PowerShell JSON manually" + /> + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/administration/contacts-template/edit.jsx b/src/pages/email/administration/contacts-template/edit.jsx new file mode 100644 index 000000000000..100eebe46731 --- /dev/null +++ b/src/pages/email/administration/contacts-template/edit.jsx @@ -0,0 +1,146 @@ +import { useEffect, useMemo, useCallback } from "react"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import ContactFormLayout from "/src/components/CippFormPages/CippAddEditContact"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import countryList from "/src/data/countryList.json"; +import { useRouter } from "next/router"; + +const countryLookup = new Map(countryList.map((country) => [country.Name, country.Code])); + +const EditContactTemplate = () => { + const router = useRouter(); + const { id } = router.query; + + const contactTemplateInfo = ApiGetCall({ + url: `/api/ListContactTemplates?id=${id}`, + queryKey: `ListContactTemplates-${id}`, + waiting: !!id, + }); + + const defaultFormValues = useMemo( + () => ({ + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }), + [] + ); + + const formControl = useForm({ + mode: "onChange", + defaultValues: defaultFormValues, + }); + + // Memoize processed contact data + const processedContactData = useMemo(() => { + if (!contactTemplateInfo.isSuccess || !contactTemplateInfo.data) { + return null; + } + + // Handle both single object (when fetching by ID) and array responses + const contact = Array.isArray(contactTemplateInfo.data) + ? contactTemplateInfo.data[0] + : contactTemplateInfo.data; + const address = contact.addresses?.[0] || {}; + const phones = contact.phones || []; + + // Use Map for O(1) phone lookup + const phoneMap = new Map(phones.map((p) => [p.type, p.number])); + + return { + ContactTemplateID: id || "", + displayName: contact.displayName || "", + firstName: contact.givenName || "", + lastName: contact.surname || "", + email: contact.email || "", + hidefromGAL: contact.hidefromGAL || false, + streetAddress: address.street || "", + postalCode: address.postalCode || "", + city: address.city || "", + state: address.state || "", + country: address.countryOrRegion ? countryLookup.get(address.countryOrRegion) || "" : "", + companyName: contact.companyName || "", + mobilePhone: phoneMap.get("mobile") || "", + businessPhone: phoneMap.get("business") || "", + jobTitle: contact.jobTitle || "", + website: contact.website || "", + mailTip: contact.mailTip || "", + }; + }, [contactTemplateInfo.isSuccess, contactTemplateInfo.data]); + + // Use callback to prevent unnecessary re-renders + const resetForm = useCallback(() => { + if (processedContactData) { + formControl.reset(processedContactData); + } + }, [processedContactData, formControl]); + + useEffect(() => { + resetForm(); + }, [resetForm]); + + // Memoize custom data formatter + const customDataFormatter = useCallback((values) => { + return { + ContactTemplateID: id, + DisplayName: values.displayName, + hidefromGAL: values.hidefromGAL, + email: values.email, + FirstName: values.firstName, + LastName: values.lastName, + Title: values.jobTitle, + StreetAddress: values.streetAddress, + PostalCode: values.postalCode, + City: values.city, + State: values.state, + CountryOrRegion: values.country?.value || values.country, + Company: values.companyName, + mobilePhone: values.mobilePhone, + phone: values.businessPhone, + website: values.website, + mailTip: values.mailTip, + }; + }); + + const contactTemplate = Array.isArray(contactTemplateInfo.data) + ? contactTemplateInfo.data[0] + : contactTemplateInfo.data; + + return ( + + {contactTemplateInfo.isLoading && } + {!contactTemplateInfo.isLoading && ( + + )} + + ); +}; + +EditContactTemplate.getLayout = (page) => {page}; + +export default EditContactTemplate; diff --git a/src/pages/email/administration/contacts-template/index.jsx b/src/pages/email/administration/contacts-template/index.jsx new file mode 100644 index 000000000000..6257f6aa9a5c --- /dev/null +++ b/src/pages/email/administration/contacts-template/index.jsx @@ -0,0 +1,114 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Button } from "@mui/material"; +import Link from "next/link"; +import { RocketLaunch } from "@mui/icons-material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub, Edit } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Contact Templates"; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveContactTemplates", + data: { + ID: "GUID", + }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + { + label: "Edit Contact Template", + link: "/email/administration/contacts-template/edit?id=[GUID]", + icon: , + color: "success", + target: "_self", + }, + ]; + const simpleColumns = ["name", "contactTemplateName", "GUID"]; + + return ( + + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/email/administration/contacts/add.jsx b/src/pages/email/administration/contacts/add.jsx index 00c8becbe01a..c8591f61acca 100644 --- a/src/pages/email/administration/contacts/add.jsx +++ b/src/pages/email/administration/contacts/add.jsx @@ -1,11 +1,10 @@ -import React from "react"; -import { Grid, Divider } from "@mui/material"; import { useForm } from "react-hook-form"; -import { useSelector } from "react-redux"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "../../../../hooks/use-settings"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; const AddContact = () => { const tenantDomain = useSettings().currentTenant; @@ -18,6 +17,17 @@ const AddContact = () => { lastName: "", email: "", hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", }, }); @@ -28,21 +38,32 @@ const AddContact = () => { title="Add Contact" backButtonTitle="Contacts Overview" postUrl="/api/AddContact" + resetForm={true} customDataformatter={(values) => { - // Add tenantDomain to the payload return { tenantID: tenantDomain, - firstName: values.firstName, - lastName: values.lastName, - displayName: values.displayName, - email: values.email, + DisplayName: values.displayName, hidefromGAL: values.hidefromGAL, + email: values.email, + FirstName: values.firstName, + LastName: values.lastName, + Title: values.jobTitle, + StreetAddress: values.streetAddress, + PostalCode: values.postalCode, + City: values.city, + State: values.state, + CountryOrRegion: values.country?.value || values.country, + Company: values.companyName, + mobilePhone: values.mobilePhone, + phone: values.businessPhone, + website: values.website, + mailTip: values.mailTip, }; }} > {/* Display Name */} - + { {/* First Name and Last Name */} - + { formControl={formControl} /> - + { {/* Email */} - + { {/* Hide from GAL */} - + [country.Name, country.Code])); + +const EditContact = () => { + const tenantDomain = useSettings().currentTenant; + const router = useRouter(); + const { id } = router.query; + + const contactInfo = ApiGetCall({ + url: `/api/ListContacts?tenantFilter=${tenantDomain}&id=${id}`, + queryKey: `ListContacts-${id}`, + waiting: !!id, + }); + + const defaultFormValues = useMemo( + () => ({ + displayName: "", + firstName: "", + lastName: "", + email: "", + hidefromGAL: false, + streetAddress: "", + postalCode: "", + city: "", + state: "", + country: "", + companyName: "", + mobilePhone: "", + businessPhone: "", + jobTitle: "", + website: "", + mailTip: "", + }), + [] + ); + + const formControl = useForm({ + mode: "onChange", + defaultValues: defaultFormValues, + }); + + // Memoize processed contact data + const processedContactData = useMemo(() => { + if (!contactInfo.isSuccess || !contactInfo.data) { + return null; + } + + const contact = contactInfo.data; + const address = contact.addresses?.[0] || {}; + const phones = contact.phones || []; + + // Use Map for O(1) phone lookup + const phoneMap = new Map(phones.map((p) => [p.type, p.number])); + + return { + displayName: contact.displayName || "", + firstName: contact.givenName || "", + lastName: contact.surname || "", + email: contact.mail || "", + hidefromGAL: contact.hidefromGAL || false, + streetAddress: address.street || "", + postalCode: address.postalCode || "", + city: address.city || "", + state: address.state || "", + country: address.countryOrRegion ? countryLookup.get(address.countryOrRegion) || "" : "", + companyName: contact.companyName || "", + mobilePhone: phoneMap.get("mobile") || "", + businessPhone: phoneMap.get("business") || "", + jobTitle: contact.jobTitle || "", + website: contact.website || "", + mailTip: contact.mailTip || "", + }; + }, [contactInfo.isSuccess, contactInfo.data]); + + // Use callback to prevent unnecessary re-renders + const resetForm = useCallback(() => { + if (processedContactData) { + formControl.reset(processedContactData); + } + }, [processedContactData, formControl]); + + useEffect(() => { + resetForm(); + }, [resetForm]); + + // Memoize custom data formatter + const customDataFormatter = useCallback( + (values) => { + const contact = Array.isArray(contactInfo.data) ? contactInfo.data[0] : contactInfo.data; + return { + tenantID: tenantDomain, + ContactID: contact?.id, + DisplayName: values.displayName, + hidefromGAL: values.hidefromGAL, + email: values.email, + FirstName: values.firstName, + LastName: values.lastName, + Title: values.jobTitle, + StreetAddress: values.streetAddress, + PostalCode: values.postalCode, + City: values.city, + State: values.state, + CountryOrRegion: values.country?.value || values.country, + Company: values.companyName, + mobilePhone: values.mobilePhone, + phone: values.businessPhone, + website: values.website, + mailTip: values.mailTip, + }; + }, + [tenantDomain, contactInfo.data] + ); + + const contact = Array.isArray(contactInfo.data) ? contactInfo.data[0] : contactInfo.data; + + return ( + + {contactInfo.isLoading && } + {!contactInfo.isLoading && ( + + {/* Display Name */} + + + + + {/* First Name and Last Name */} + + + + + + + + + + {/* Email */} + + + + + {/* Hide from GAL */} + + + + + + + {/* Company Information */} + + + + + + + + + + {/* Address Information */} + + + + + + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + + + + + {/* Phone Numbers */} + + + + + + + + )} + + ); +}; + +EditContact.getLayout = (page) => {page}; + +export default EditContact; diff --git a/src/pages/email/administration/contacts/index.js b/src/pages/email/administration/contacts/index.js index d32c9f8a2a6b..48e5f2a65453 100644 --- a/src/pages/email/administration/contacts/index.js +++ b/src/pages/email/administration/contacts/index.js @@ -1,34 +1,55 @@ +import { useMemo } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Edit } from "@mui/icons-material"; +import { Edit, PersonAdd } from "@mui/icons-material"; import { Button } from "@mui/material"; import Link from "next/link"; +import TrashIcon from "@heroicons/react/24/outline/TrashIcon"; const Page = () => { const pageTitle = "Contacts"; - - const actions = [ + const actions = useMemo(() => [ + { + label: "Edit Contact", + link: "/email/administration/contacts/edit?id=[Guid]", + multiPost: false, + postEntireRow: true, + icon: , + color: "warning", + condition: (row) => !row.IsDirSynced, + }, { label: "Remove Contact", type: "POST", url: "/api/RemoveContact", data: { - TenantFilter: "Tenant", - GUID: "id", + GUID: "Guid", + mail: "WindowsEmailAddress", }, - confirmText: "Are you sure you want to delete this contact?", + confirmText: + "Are you sure you want to delete this contact? Remember this will not work if the contact is AD Synced.", color: "danger", + icon: , + condition: (row) => !row.IsDirSynced, }, - { - label: "Edit Contact", - link: "/email/administration/edit-contact/[id]", - multiPost: false, - icon: , - color: "warning", - }, - ]; + ], []); + + const simpleColumns = useMemo(() => [ + "DisplayName", + "WindowsEmailAddress", + "Company", + "IsDirSynced" + ], []); - const simpleColumns = ["displayName", "mail", "companyName", "onPremisesSyncEnabled"]; + const cardButton = useMemo(() => ( + + ), []); return ( { apiUrl="/api/ListContacts" actions={actions} simpleColumns={simpleColumns} - cardButton={ - <> - - - } + cardButton={cardButton} /> ); }; diff --git a/src/pages/email/administration/mailbox-rules/index.js b/src/pages/email/administration/mailbox-rules/index.js index c80b749aad07..956d9df8aa38 100644 --- a/src/pages/email/administration/mailbox-rules/index.js +++ b/src/pages/email/administration/mailbox-rules/index.js @@ -1,17 +1,47 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { TrashIcon } from "@heroicons/react/24/outline"; import { getCippTranslation } from "../../../../utils/get-cipp-translation"; import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; +import { Block, PlayArrow, DeleteForever } from "@mui/icons-material"; const Page = () => { const pageTitle = "Mailbox Rules"; const actions = [ + { + label: "Enable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: "Identity", + userPrincipalName: "UserPrincipalName", + ruleName: "Name", + Enable: true, + }, + condition: (row) => !row.Enabled, + confirmText: "Are you sure you want to enable this mailbox rule?", + multiPost: false, + }, + { + label: "Disable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: "Identity", + userPrincipalName: "UserPrincipalName", + ruleName: "Name", + Disable: true, + }, + condition: (row) => row.Enabled, + confirmText: "Are you sure you want to disable this mailbox rule?", + multiPost: false, + }, { label: "Remove Mailbox Rule", - type: "GET", - icon: , + type: "POST", + icon: , url: "/api/ExecRemoveMailboxRule", data: { ruleId: "Identity", userPrincipalName: "UserPrincipalName", ruleName: "Name" }, confirmText: "Are you sure you want to remove this mailbox rule?", @@ -39,6 +69,7 @@ const Page = () => { title="Rule Details" propertyItems={properties} actionItems={actions} + data={data} /> ); }, diff --git a/src/pages/email/administration/mailboxes/addshared.jsx b/src/pages/email/administration/mailboxes/addshared.jsx index eab577bed6cf..7c1e00885b84 100644 --- a/src/pages/email/administration/mailboxes/addshared.jsx +++ b/src/pages/email/administration/mailboxes/addshared.jsx @@ -1,7 +1,6 @@ -import React from "react"; -import { Grid, Divider } from "@mui/material"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; -import { useSelector } from "react-redux"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -29,6 +28,7 @@ const AddContact = () => { title="Shared Mailbox" backButtonTitle="Mailbox Overview" postUrl="/api/AddSharedMailbox" + resetForm={true} customDataformatter={(values) => { return { tenantID: tenantDomain, @@ -39,7 +39,7 @@ const AddContact = () => { }} > - + { {/* Email */} - + { formControl={formControl} /> - + diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index 81726a1abd14..da5479b87fb4 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -2,141 +2,16 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import Link from "next/link"; import { Button } from "@mui/material"; - -import { - Archive, - MailOutline, - Person, - Room, - Visibility, - VisibilityOff, - PhonelinkLock, - Key, -} from "@mui/icons-material"; -import { TrashIcon, MagnifyingGlassIcon, PlayCircleIcon } from "@heroicons/react/24/outline"; +import { Add } from "@mui/icons-material"; +import CippExchangeActions from "../../../../components/CippComponents/CippExchangeActions"; const Page = () => { const pageTitle = "Mailboxes"; - // Define actions for mailboxes - const actions = [ - { - label: "Edit permissions", - link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", - color: "info", - icon: , - }, - { - label: "Research Compromised Account", - link: "/identity/administration/users/user/bec?userId=[ExternalDirectoryObjectId]", - color: "info", - icon: , - }, - { - label: "Send MFA Push", - type: "GET", - url: "/api/ExecSendPush", - data: { - UserEmail: "UPN", - }, - confirmText: "Are you sure you want to send an MFA request?", - icon: , - }, - { - label: "Convert to Shared Mailbox", - type: "GET", - icon: , - url: "/api/ExecConvertToSharedMailbox", - data: { - ID: "UPN", - }, - confirmText: "Are you sure you want to convert this mailbox to a shared mailbox?", - condition: (row) => row.recipientTypeDetails !== "SharedMailbox", - }, - { - label: "Convert to User Mailbox", - type: "GET", - url: "/api/ExecConvertToSharedMailbox", - icon: , - data: { - ID: "UPN", - ConvertToUser: true, - }, - confirmText: "Are you sure you want to convert this mailbox to a user mailbox?", - condition: (row) => row.recipientTypeDetails !== "UserMailbox", - }, - { - label: "Convert to Room Mailbox", - type: "GET", - url: "/api/ExecConvertToRoomMailbox", - icon: , - data: { - ID: "UPN", - }, - confirmText: "Are you sure you want to convert this mailbox to a room mailbox?", - condition: (row) => row.recipientTypeDetails !== "RoomMailbox", - }, - { - //tested - label: "Enable Online Archive", - type: "GET", - icon: , - url: "/api/ExecEnableArchive", - data: { ID: "UPN" }, - confirmText: "Are you sure you want to enable the online archive for this user?", - multiPost: false, - condition: (row) => row.ArchiveGuid === "00000000-0000-0000-0000-000000000000", - }, - { - label: "Hide from Global Address List", - type: "POST", - url: "/api/ExecHideFromGAL", - icon: , - data: { - ID: "UPN", - HidefromGAL: true, - }, - confirmText: - "Are you sure you want to hide this mailbox from the global address list? This will not work if the user is AD Synced.", - condition: (row) => row.HiddenFromAddressListsEnabled === false, - }, - { - label: "Unhide from Global Address List", - type: "POST", - url: "/api/ExecHideFromGAL", - icon: , - data: { - ID: "UPN", - }, - confirmText: - "Are you sure you want to unhide this mailbox from the global address list? This will not work if the user is AD Synced.", - condition: (row) => row.HiddenFromAddressListsEnabled === true, - }, - { - label: "Start Managed Folder Assistant", - type: "GET", - url: "/api/ExecStartManagedFolderAssistant", - icon: , - data: { - ID: "UPN", - }, - confirmText: "Are you sure you want to start the managed folder assistant for this user?", - }, - { - label: "Delete Mailbox", - type: "GET", - icon: , // Added - url: "/api/RemoveMailbox", - data: { ID: "UPN" }, - confirmText: "Are you sure you want to delete this mailbox?", - multiPost: false, - }, - ]; - // Define off-canvas details const offCanvas = { extendedInfoFields: ["displayName", "UPN", "AdditionalEmailAddresses", "recipientTypeDetails"], - actions: actions, + actions: CippExchangeActions(), }; const filterList = [ @@ -176,13 +51,17 @@ const Page = () => { - diff --git a/src/pages/email/administration/quarantine/index.js b/src/pages/email/administration/quarantine/index.js index e784d8888a0e..3009973001a0 100644 --- a/src/pages/email/administration/quarantine/index.js +++ b/src/pages/email/administration/quarantine/index.js @@ -1,8 +1,16 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { useEffect, useState } from "react"; -import { Dialog, DialogTitle, DialogContent, IconButton, Skeleton, Typography, CircularProgress } from "@mui/material"; -import { Block, Close, Done, DoneAll, Subject } from "@mui/icons-material"; +import { + Dialog, + DialogTitle, + DialogContent, + IconButton, + Skeleton, + Typography, + CircularProgress, +} from "@mui/material"; +import { Block, Close, Done, DoneAll } from "@mui/icons-material"; import { CippMessageViewer } from "/src/components/CippComponents/CippMessageViewer.jsx"; import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import { useSettings } from "/src/hooks/use-settings"; @@ -10,13 +18,14 @@ import { EyeIcon, DocumentTextIcon } from "@heroicons/react/24/outline"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; const simpleColumns = [ + "ReceivedTime", + "ReleaseStatus", + "Subject", "SenderAddress", "RecipientAddress", - "Subject", "Type", - "ReceivedTime", - "ReleaseStatus", "PolicyName", + "Tenant", ]; const detailColumns = ["Received", "Status", "SenderAddress", "RecipientAddress"]; const pageTitle = "Quarantine Management"; @@ -30,6 +39,7 @@ const Page = () => { const [traceDetails, setTraceDetails] = useState([]); const [traceMessageId, setTraceMessageId] = useState(null); const [messageSubject, setMessageSubject] = useState(null); + const [messageContentsWaiting, setMessageContentsWaiting] = useState(false); const getMessageContents = ApiGetCall({ url: "/api/ListMailQuarantineMessage", @@ -37,7 +47,7 @@ const Page = () => { tenantFilter: tenantFilter, Identity: messageId, }, - waiting: false, + waiting: messageContentsWaiting, queryKey: `ListMailQuarantineMessage-${messageId}`, }); @@ -52,7 +62,9 @@ const Page = () => { const viewMessage = (row) => { const id = row.Identity; setMessageId(id); - getMessageContents.waiting = true; + if (!messageContentsWaiting) { + setMessageContentsWaiting(true); + } getMessageContents.refetch(); setDialogOpen(true); }; @@ -76,7 +88,7 @@ const Page = () => { } else { setDialogContent(); } - }, [getMessageContents.isSuccess]); + }, [getMessageContents.isSuccess, getMessageContents.data]); const actions = [ { @@ -95,12 +107,14 @@ const Page = () => { label: "Release", type: "POST", url: "/api/ExecQuarantineManagement", + multiPost: true, data: { Identity: "Identity", - Type: "Release", + Type: "!Release", }, confirmText: "Are you sure you want to release this message?", icon: , + condition: (row) => row.ReleaseStatus !== "RELEASED", }, { label: "Deny", @@ -108,10 +122,11 @@ const Page = () => { url: "/api/ExecQuarantineManagement", data: { Identity: "Identity", - Type: "Deny", + Type: "!Deny", }, confirmText: "Are you sure you want to deny this message?", icon: , + condition: (row) => row.ReleaseStatus !== "DENIED", }, { label: "Release & Allow Sender", @@ -119,12 +134,13 @@ const Page = () => { url: "/api/ExecQuarantineManagement", data: { Identity: "Identity", - Type: "Release", + Type: "!Release", AllowSender: true, }, confirmText: "Are you sure you want to release this email and add the sender to the whitelist?", icon: , + condition: (row) => row.ReleaseStatus !== "RELEASED", }, ]; @@ -144,6 +160,11 @@ const Page = () => { value: [{ id: "ReleaseStatus", value: "RELEASED" }], type: "column", }, + { + filterName: "Requested", + value: [{ id: "ReleaseStatus", value: "REQUESTED" }], + type: "column", + }, ]; return ( @@ -151,6 +172,7 @@ const Page = () => { { const tenantDomain = useSettings().currentTenant; @@ -17,9 +19,146 @@ const AddTenantAllowBlockList = () => { listType: null, listMethod: null, NoExpiration: false, + RemoveAfter: false, }, }); + const noExpiration = useWatch({ control: formControl.control, name: "NoExpiration" }); + const removeAfter = useWatch({ control: formControl.control, name: "RemoveAfter" }); + const listMethod = useWatch({ control: formControl.control, name: "listMethod" }); + const listType = useWatch({ control: formControl.control, name: "listType" }); + + const isListMethodBlock = listMethod?.value === "Block"; + const isListTypeFileHash = listType?.value === "FileHash"; + const isListTypeSenderUrlOrFileHash = ["Sender", "Url", "FileHash"].includes(listType?.value); + const isNoExpirationCompatible = isListMethodBlock || + (listMethod?.value === "Allow" && (listType?.value === "Url" || listType?.value === "IP")); + + useEffect(() => { + if (noExpiration) { + formControl.setValue("RemoveAfter", false); + } + + if (removeAfter) { + formControl.setValue("NoExpiration", false); + } + + if (isListMethodBlock) { + formControl.setValue("RemoveAfter", false); + } + + if (listType && !isListTypeSenderUrlOrFileHash) { + formControl.setValue("RemoveAfter", false); + } + + if (isListTypeFileHash) { + formControl.setValue("listMethod", { label: "Block", value: "Block" }); + } + + if (listMethod || listType) { + if (!isNoExpirationCompatible && noExpiration) { + formControl.setValue("NoExpiration", false); + } + } + }, [ + noExpiration, + removeAfter, + isListMethodBlock, + listType, + isListTypeSenderUrlOrFileHash, + isListTypeFileHash, + isNoExpirationCompatible, + formControl + ]); + + const validateEntries = (value) => { + if (!value) return true; + + const entries = value.split(/[,;]/).map(e => e.trim()); + const currentListType = listType?.value; + + if (currentListType === "FileHash") { + for (const entry of entries) { + if (entry.length !== 64) + return "File hash entries must be exactly 64 characters"; + + const hashResult = getCippValidator(entry, "sha256"); + if (hashResult !== true) + return hashResult; + } + } else if (currentListType === "IP") { + for (const entry of entries) { + const ipv6Result = getCippValidator(entry, "ipv6"); + const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); + + if (ipv6Result !== true && ipv6CidrResult !== true) + return "Invalid IPv6 address format. Use colon-hexadecimal or CIDR notation"; + } + } else if (currentListType === "Url") { + for (const entry of entries) { + if (entry.length > 250) + return "URL entries must be 250 characters or less"; + + // For entries with wildcards, use the improved wildcard validators + if (entry.includes('*') || entry.includes('~')) { + // Try both wildcard validators + const wildcardUrlResult = getCippValidator(entry, "wildcardUrl"); + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardUrlResult !== true && wildcardDomainResult !== true) { + // If basic pattern check fails too, give a more specific message + if (!/^[a-zA-Z0-9\.\-\*\~\/]+$/.test(entry)) { + return "Invalid wildcard pattern. Use only letters, numbers, dots, hyphens, slashes, and wildcards (* or ~)"; + } + + // If it has basic valid characters but doesn't match our patterns + return "Invalid wildcard format. Common formats are *.domain.com or domain.*"; + } + continue; + } + + // For non-wildcard entries, use standard validators + const ipv4Result = getCippValidator(entry, "ip"); + const ipv4CidrResult = getCippValidator(entry, "ipv4cidr"); + const ipv6Result = getCippValidator(entry, "ipv6"); + const ipv6CidrResult = getCippValidator(entry, "ipv6cidr"); + const hostnameResult = getCippValidator(entry, "hostname"); + const urlResult = getCippValidator(entry, "url"); + + // If none of the validators pass + if (ipv4Result !== true && + ipv4CidrResult !== true && + ipv6Result !== true && + ipv6CidrResult !== true && + hostnameResult !== true && + urlResult !== true) { + return "Invalid URL format. Enter hostnames, IPv4, or IPv6 addresses"; + } + } + } else if (currentListType === "Sender") { + for (const entry of entries) { + // Check for wildcards first + if (entry.includes('*') || entry.includes('~')) { + const wildcardDomainResult = getCippValidator(entry, "wildcardDomain"); + + if (wildcardDomainResult !== true) { + return "Invalid sender wildcard pattern. Common format is *.domain.com"; + } + continue; + } + + // For non-wildcard entries, use senderEntry validator + const senderResult = getCippValidator(entry, "senderEntry"); + + if (senderResult !== true) { + return senderResult; + } + } + } + + return true; + }; + return ( { notes: values.notes, listMethod: values.listMethod?.value, NoExpiration: values.NoExpiration, + RemoveAfter: values.RemoveAfter }; }} > {/* Entries */} - + {/* Notes & List Type */} - + { formControl={formControl} /> - + {/* List Method */} - + {/* No Expiration */} - + + + + {/* Remove After */} + + diff --git a/src/pages/email/administration/tenant-allow-block-lists/index.js b/src/pages/email/administration/tenant-allow-block-lists/index.js index 97925a29f708..4770163895fa 100644 --- a/src/pages/email/administration/tenant-allow-block-lists/index.js +++ b/src/pages/email/administration/tenant-allow-block-lists/index.js @@ -2,6 +2,8 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import TrashIcon from "@heroicons/react/24/outline/TrashIcon"; +import { PlaylistAdd } from "@mui/icons-material"; const Page = () => { const pageTitle = "Tenant Allow/Block Lists"; @@ -12,25 +14,21 @@ const Page = () => { type: "POST", url: "/api/RemoveTenantAllowBlockList", data: { - TenantFilter: "Tenant", Entries: "Value", ListType: "ListType", }, - confirmText: "Are you sure you want to delete?", + confirmText: "Are you sure you want to delete this entry?", color: "danger", + icon: , }, ]; - const offCanvas = { - extendedInfoFields: ["Value", "Notes", "ExpirationDate"], - actions: actions, - }; - const simpleColumns = [ "Value", "ListType", "Action", "Notes", + "LastUsedDate", "LastModifiedDateTime", "ExpirationDate", ]; @@ -40,7 +38,6 @@ const Page = () => { title={pageTitle} apiUrl="/api/ListTenantAllowBlockList" actions={actions} - offCanvas={offCanvas} simpleColumns={simpleColumns} titleButton={{ label: "Add", @@ -48,7 +45,11 @@ const Page = () => { }} cardButton={ <> - diff --git a/src/pages/email/connectionfilter/deploy/index.js b/src/pages/email/connectionfilter/deploy/index.js deleted file mode 100644 index 92cbabc77ae2..000000000000 --- a/src/pages/email/connectionfilter/deploy/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Apply Spamfilter Template"; - - return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the apply spamfilter template section.

    -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/connectionfilter/list-templates/index.js b/src/pages/email/connectionfilter/list-templates/index.js deleted file mode 100644 index 4a104db53a18..000000000000 --- a/src/pages/email/connectionfilter/list-templates/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; - -const Page = () => { - const pageTitle = "Connection filter Templates"; - - const actions = [ - { - label: "Delete Template", - type: "POST", - url: "/api/RemoveConnectionfilterTemplate", - data: { ID: "GUID" }, - confirmText: "Do you want to delete the template?", - icon: , - color: "danger", - }, - ]; - - const offCanvas = { - extendedInfoFields: [ - "name", - "IsDefault", - "IPAllowList", - "IPBlockList", - "EnableSafeList", - "GUID", - ], - actions: actions, - }; - - const simpleColumns = [ - "name", - "IsDefault", - "IPAllowList", - "IPBlockList", - "EnableSafeList", - "GUID", - ]; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; -export default Page; diff --git a/src/pages/email/connectors/deploy-connector/index.js b/src/pages/email/connectors/deploy-connector/index.js deleted file mode 100644 index cddfb2691027..000000000000 --- a/src/pages/email/connectors/deploy-connector/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Deploy Connector Templates"; - - return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the deploy connector templates section.

    -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/connectors/list-connector-templates/index.js b/src/pages/email/connectors/list-connector-templates/index.js deleted file mode 100644 index 671c148f21e2..000000000000 --- a/src/pages/email/connectors/list-connector-templates/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; -import ConnectorTemplateDetails from "../../../../components/CippComponents/ConnectorTemplateDetails"; - -const Page = () => { - const pageTitle = "Exchange Connector Templates"; - - const actions = [ - { - label: "Delete Template", - type: "POST", - url: "/api/RemoveExConnectorTemplate", - data: { - ID: "GUID", - }, - confirmText: "Do you want to delete the template?", - icon: , - color: "danger", - }, - ]; - - const offCanvas = { - children: (data) => , - actions: actions, - size: "lg", - }; - - const simpleColumns = ["name", "cippconnectortype", "GUID"]; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; -export default Page; diff --git a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js index c72597ef6a25..dbd167e86105 100644 --- a/src/pages/email/reports/SharedMailboxEnabledAccount/index.js +++ b/src/pages/email/reports/SharedMailboxEnabledAccount/index.js @@ -1,14 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; - -/* - NOTE for Devs: - - The original component used a Redux selector (`useSelector`) for tenant data, - which is handled by `CippTablePage` in the refactored version, thus eliminating `useSelector`. - - The `ModalService` with `confirm` handling was originally used to confirm blocking sign-in. - The action here replaces it with a confirmation text as per current guidelines. - - Original button and `FontAwesomeIcon` (faBan) are not used since action confirmation is handled by CippTablePage. -*/ +import { Block } from "@mui/icons-material"; const Page = () => { return ( @@ -19,9 +11,11 @@ const Page = () => { { label: "Block Sign In", type: "POST", + icon: , url: "/api/ExecDisableUser", - data: { TenantFilter: "Tenant", ID: "id" }, - confirmText: "Are you sure you want to block this user from signing in?", + data: { ID: "id" }, + confirmText: "Are you sure you want to block the sign-in for this mailbox?", + condition: (row) => row.accountEnabled && !row.onPremisesSyncEnabled, }, ]} offCanvas={{ @@ -29,6 +23,7 @@ const Page = () => { "UserPrincipalName", "displayName", "accountEnabled", + "assignedLicenses", "onPremisesSyncEnabled", ], }} @@ -36,8 +31,15 @@ const Page = () => { "UserPrincipalName", "displayName", "accountEnabled", + "assignedLicenses", "onPremisesSyncEnabled", ]} + filters={[ + { + id: "accountEnabled", + value: "Yes" + } + ]} /> ); }; diff --git a/src/pages/email/reports/antiphishing-filters/index.js b/src/pages/email/reports/antiphishing-filters/index.js index 96a1080d9498..23cee4f3b5dd 100644 --- a/src/pages/email/reports/antiphishing-filters/index.js +++ b/src/pages/email/reports/antiphishing-filters/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check } from "@mui/icons-material"; const Page = () => { const pageTitle = "List of Anti-Phishing Filters"; @@ -9,24 +10,26 @@ const Page = () => { { label: "Enable Rule", type: "POST", + icon: , url: "/api/EditAntiPhishingFilter", data: { - State: "Enable", - TenantFilter: "TenantFilter", // TenantFilter used in API path as per original file - RuleName: "id", + State: "!Enable", + RuleName: "RuleName", }, confirmText: "Are you sure you want to enable this rule?", + condition: (row) => row.State === "Disabled", }, { label: "Disable Rule", type: "POST", + icon: , url: "/api/EditAntiPhishingFilter", data: { - State: "Disable", - TenantFilter: "TenantFilter", - RuleName: "id", + State: "!Disable", + RuleName: "RuleName", }, confirmText: "Are you sure you want to disable this rule?", + condition: (row) => row.State === "Enabled", }, // Uncomment the following block if Delete Rule is to be re-enabled in the future /* @@ -35,8 +38,7 @@ const Page = () => { type: "POST", url: "/api/RemoveAntiPhishingFilter", data: { - TenantFilter: "TenantFilter", - RuleName: "id", + RuleName: "RuleName", }, confirmText: "Are you sure you want to delete this rule?", }, diff --git a/src/pages/email/reports/global-address-list/index.js b/src/pages/email/reports/global-address-list/index.js index 5b0be1040072..314fb23f66f0 100644 --- a/src/pages/email/reports/global-address-list/index.js +++ b/src/pages/email/reports/global-address-list/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; const Page = () => { const actions = [ @@ -7,21 +8,25 @@ const Page = () => { label: "Unhide from Global Address List", type: "POST", url: "/api/ExecHideFromGAL", + icon: , data: { HideFromGAL: false, ID: "PrimarySmtpAddress", }, confirmText: "Are you sure you want to show this mailbox in the Global Address List?", + condition: (row) => row.HiddenFromAddressListsEnabled == true, }, { label: "Hide from Global Address List", type: "POST", url: "/api/ExecHideFromGAL", + icon: , data: { HideFromGAL: true, ID: "PrimarySmtpAddress", }, confirmText: "Are you sure you want to hide this mailbox from the Global Address List?", + condition: (row) => row.HiddenFromAddressListsEnabled == false, }, ]; diff --git a/src/pages/email/reports/mailbox-activity/index.js b/src/pages/email/reports/mailbox-activity/index.js new file mode 100644 index 000000000000..554f6337b717 --- /dev/null +++ b/src/pages/email/reports/mailbox-activity/index.js @@ -0,0 +1,152 @@ +import { useState } from "react"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { + Button, + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + SvgIcon, + Stack, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { ExpandMore, Sort } from "@mui/icons-material"; +import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { useForm } from "react-hook-form"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; + +const Page = () => { + const formControl = useForm({ + defaultValues: { + period: { value: "D30", label: "30 days" }, + }, + }); + + const [expanded, setExpanded] = useState(false); + const [selectedPeriod, setSelectedPeriod] = useState("D30"); + const [selectedPeriodLabel, setSelectedPeriodLabel] = useState("30 days"); + + const periodOptions = [ + { value: "D7", label: "7 days" }, + { value: "D30", label: "30 days" }, + { value: "D90", label: "90 days" }, + { value: "D180", label: "180 days" }, + ]; + + const onSubmit = (data) => { + const periodValue = + typeof data.period === "object" && data.period?.value ? data.period.value : data.period; + const periodLabel = + typeof data.period === "object" && data.period?.label ? data.period.label : data.period; + + setSelectedPeriod(periodValue); + setSelectedPeriodLabel(periodLabel); + setExpanded(false); + }; + + const clearFilters = () => { + formControl.reset({ + period: { value: "D30", label: "30 days" }, + }); + setSelectedPeriod("D30"); + setSelectedPeriodLabel("30 days"); + setExpanded(false); + }; + + const tableFilter = ( + setExpanded(!expanded)}> + }> + + + + + + Report Period + + (Period: {selectedPeriodLabel}) + + + + + +
    + + + + + + + + + + + + +
    +
    +
    + ); + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/reports/malware-filters/index.js b/src/pages/email/reports/malware-filters/index.js index 4d5ffe472d98..4e0c638e0f8a 100644 --- a/src/pages/email/reports/malware-filters/index.js +++ b/src/pages/email/reports/malware-filters/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check } from "@mui/icons-material"; const Page = () => { return ( @@ -10,24 +11,26 @@ const Page = () => { { label: "Enable Rule", type: "POST", + icon: , url: "/api/EditMalwareFilter", data: { - State: "Enable", - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + State: "!Enable", + RuleName: "RuleName", }, confirmText: "Are you sure you want to enable this rule?", + condition: (row) => row.State === "Disabled", }, { label: "Disable Rule", type: "POST", + icon: , url: "/api/EditMalwareFilter", data: { - State: "Disable", - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + State: "!Disable", + RuleName: "RuleName", }, confirmText: "Are you sure you want to disable this rule?", + condition: (row) => row.State === "Enabled", }, /* Uncomment and add additional actions if required by future specs { @@ -35,8 +38,7 @@ const Page = () => { type: "POST", url: "/api/RemoveMalwareFilter", data: { - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to delete this rule?", }, diff --git a/src/pages/email/reports/safeattachments-filters/index.js b/src/pages/email/reports/safeattachments-filters/index.js index b61ac47207bc..3821410fe6e0 100644 --- a/src/pages/email/reports/safeattachments-filters/index.js +++ b/src/pages/email/reports/safeattachments-filters/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check } from "@mui/icons-material"; const Page = () => { const pageTitle = "List of Safe Attachment Filters"; @@ -10,36 +11,37 @@ const Page = () => { { label: "Enable Rule", type: "POST", + icon: , url: "/api/EditSafeAttachmentsFilter", data: { - State: "Enable", - TenantFilter: "tenant.defaultDomainName", // TenantFilter uses default domain name in context - RuleName: "row.RuleName", + State: "!enable", + RuleName: "RuleName", }, confirmText: "Are you sure you want to enable this rule?", color: "info", + condition: (row) => row.State === "Disabled", }, { label: "Disable Rule", type: "POST", + icon: , url: "/api/EditSafeAttachmentsFilter", data: { State: "Disable", - TenantFilter: "tenant.defaultDomainName", // TenantFilter uses default domain name in context - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to disable this rule?", color: "info", + condition: (row) => row.State === "Enabled", }, // Commented out "Delete Rule" action from the original file as it was also commented in legacy code /* { label: "Delete Rule", - type: "POST", + type: "GET", url: "/api/RemoveSafeAttachmentsFilter", data: { - TenantFilter: "tenant.defaultDomainName", - RuleName: "row.RuleName", + RuleName: "RuleName", }, confirmText: "Are you sure you want to delete this rule?", color: "danger", diff --git a/src/pages/email/reports/safelinks-filters/index.js b/src/pages/email/reports/safelinks-filters/index.js deleted file mode 100644 index 7d2bf1ad5a37..000000000000 --- a/src/pages/email/reports/safelinks-filters/index.js +++ /dev/null @@ -1,83 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; - -/* Note: Tenant information is passed directly in apiData instead of using Redux (e.g., useSelector) */ -/* Original file included a "Delete Rule" action. If needed, add back by following other action formats. */ -/* Columns have been converted to simpleColumns matching the original column names exactly. */ -/* Removed custom formatters and FontAwesome imports, as table formatting is handled by CippTablePage */ - -const Page = () => { - return ( - - ); -}; - -Page.getLayout = (page) => {page}; -export default Page; diff --git a/src/pages/email/resources/management/equipment/add.jsx b/src/pages/email/resources/management/equipment/add.jsx new file mode 100644 index 000000000000..a186a19e0ba5 --- /dev/null +++ b/src/pages/email/resources/management/equipment/add.jsx @@ -0,0 +1,83 @@ +import React from "react"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormDomainSelector } from "/src/components/CippComponents/CippFormDomainSelector"; +import { useSettings } from "/src/hooks/use-settings"; + +const AddEquipmentMailbox = () => { + const tenantDomain = useSettings().currentTenant; + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + username: "", + domain: null, + location: "", + department: "", + company: "", + }, + }); + + return ( + { + const shippedValues = { + tenantID: tenantDomain, + domain: values.domain?.value, + displayName: values.displayName.trim(), + username: values.username.trim(), + userPrincipalName: values.username.trim() + "@" + (values.domain?.value || "").trim(), + }; + + return shippedValues; + }} + > + + {/* Display Name */} + + + + + + + {/* Username and Domain */} + + + + + + + + + ); +}; + +AddEquipmentMailbox.getLayout = (page) => {page}; + +export default AddEquipmentMailbox; diff --git a/src/pages/email/resources/management/equipment/edit.jsx b/src/pages/email/resources/management/equipment/edit.jsx new file mode 100644 index 000000000000..79b55a4a33af --- /dev/null +++ b/src/pages/email/resources/management/equipment/edit.jsx @@ -0,0 +1,447 @@ +import React, { useEffect } from "react"; +import { Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "/src/api/ApiCall"; +import countryList from "/src/data/countryList.json"; +import timezoneList from "/src/data/timezoneList.json"; + +// Work days options +const workDaysOptions = [ + { value: "Sunday", label: "Sunday" }, + { value: "Monday", label: "Monday" }, + { value: "Tuesday", label: "Tuesday" }, + { value: "Wednesday", label: "Wednesday" }, + { value: "Thursday", label: "Thursday" }, + { value: "Friday", label: "Friday" }, + { value: "Saturday", label: "Saturday" }, + { value: "WeekDay", label: "Weekdays (Monday-Friday)" }, + { value: "WeekendDay", label: "Weekend (Saturday-Sunday)" }, + { value: "AllDays", label: "All Days" }, +]; + +// Automation Processing Options +const automateProcessingOptions = [ + { value: "None", label: "None - No processing" }, + { value: "AutoUpdate", label: "AutoUpdate - Accept/Decline but not delete" }, + { value: "AutoAccept", label: "AutoAccept - Accept and delete" }, +]; + +const EditEquipmentMailbox = () => { + const router = useRouter(); + const { equipmentId } = router.query; + const tenantDomain = useSettings().currentTenant; + const formControl = useForm({ + mode: "onChange", + }); + + const equipmentInfo = ApiGetCall({ + url: `/api/ListEquipment?EquipmentId=${equipmentId}&tenantFilter=${tenantDomain}`, + queryKey: `Equipment-${equipmentId}`, + waiting: false, + }); + + useEffect(() => { + if (equipmentInfo.isSuccess && equipmentInfo.data?.[0]) { + const equipment = equipmentInfo.data[0]; + formControl.reset({ + // Core Properties + displayName: equipment.displayName, + hiddenFromAddressListsEnabled: equipment.hiddenFromAddressListsEnabled, + + // Equipment Details + department: equipment.department, + company: equipment.company, + + // Location Information + streetAddress: equipment.streetAddress, + city: equipment.city, + stateOrProvince: equipment.stateOrProvince, + postalCode: equipment.postalCode, + countryOrRegion: equipment.countryOrRegion + ? countryList.find((c) => c.Name === equipment.countryOrRegion)?.Code || "" + : "", + phone: equipment.phone, + tags: equipment.tags?.map((tag) => ({ label: tag, value: tag })) || [], + + // Booking Information + allowConflicts: equipment.allowConflicts, + allowRecurringMeetings: equipment.allowRecurringMeetings, + bookingWindowInDays: equipment.bookingWindowInDays, + maximumDurationInMinutes: equipment.maximumDurationInMinutes, + processExternalMeetingMessages: equipment.processExternalMeetingMessages, + forwardRequestsToDelegates: equipment.forwardRequestsToDelegates, + scheduleOnlyDuringWorkHours: equipment.scheduleOnlyDuringWorkHours, + automateProcessing: equipment.automateProcessing, + + // Calendar Configuration + workDays: + equipment.workDays?.split(",")?.map((day) => ({ + label: day.trim(), + value: day.trim(), + })) || [], + workHoursStartTime: equipment.workHoursStartTime, + workHoursEndTime: equipment.workHoursEndTime, + workingHoursTimeZone: equipment.workingHoursTimeZone + ? { + value: equipment.workingHoursTimeZone, + label: timezoneList.find((tz) => tz.standardTime === equipment.workingHoursTimeZone) + ? `${equipment.workingHoursTimeZone} - ${ + timezoneList.find((tz) => tz.standardTime === equipment.workingHoursTimeZone) + ?.timezone + }` + : equipment.workingHoursTimeZone, + } + : null, + }); + } + }, [equipmentInfo.isSuccess, equipmentInfo.data]); + + useEffect(() => { + if (equipmentId) { + equipmentInfo.refetch(); + } + }, [router.query, equipmentId, tenantDomain]); + + return ( + ({ + tenantID: tenantDomain, + equipmentId: equipmentId, + displayName: values.displayName?.trim(), + hiddenFromAddressListsEnabled: values.hiddenFromAddressListsEnabled, + + // Equipment Details + department: values.department?.trim(), + company: values.company?.trim(), + + // Location Information + streetAddress: values.streetAddress?.trim(), + city: values.city?.trim(), + stateOrProvince: values.stateOrProvince?.trim(), + postalCode: values.postalCode?.trim(), + countryOrRegion: values.countryOrRegion?.value || values.countryOrRegion || null, + phone: values.phone?.trim(), + tags: values.tags?.map((tag) => tag.value), + + // Booking Information + allowConflicts: values.allowConflicts, + allowRecurringMeetings: values.allowRecurringMeetings, + bookingWindowInDays: values.bookingWindowInDays, + maximumDurationInMinutes: values.maximumDurationInMinutes, + processExternalMeetingMessages: values.processExternalMeetingMessages, + forwardRequestsToDelegates: values.forwardRequestsToDelegates, + scheduleOnlyDuringWorkHours: values.scheduleOnlyDuringWorkHours, + automateProcessing: values.automateProcessing?.value || values.automateProcessing, + + // Calendar Configuration + workDays: values.workDays?.map((day) => day.value).join(","), + workHoursStartTime: values.workHoursStartTime, + workHoursEndTime: values.workHoursEndTime, + workingHoursTimeZone: values.workingHoursTimeZone?.value || values.workingHoursTimeZone, + })} + > + {equipmentInfo.isLoading && ( + + )} + {equipmentInfo.isSuccess && ( + + {/* Basic Information */} + + + Basic Information + + + + + + + + + + + + + + {/* Booking Information */} + + + Booking Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Working Hours */} + + + Working Hours + + + + + + + + + + + + + ({ + value: tz.standardTime, + label: `${tz.standardTime} - ${tz.timezone}`, + }))} + multiple={false} + creatable={false} + formControl={formControl} + /> + + + + + + + + + + + + + {/* Equipment & Location Details */} + + + Equipment & Location Details + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + + + + + + + + + )} + + ); +}; + +EditEquipmentMailbox.getLayout = (page) => {page}; + +export default EditEquipmentMailbox; diff --git a/src/pages/email/resources/management/equipment/index.js b/src/pages/email/resources/management/equipment/index.js new file mode 100644 index 000000000000..9a3f97e29cc5 --- /dev/null +++ b/src/pages/email/resources/management/equipment/index.js @@ -0,0 +1,83 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Button } from "@mui/material"; +import Link from "next/link"; +import { AddBusiness, Edit, Block, LockOpen, Key } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const pageTitle = "Equipment"; + + const actions = [ + { + label: "Edit Equipment", + link: `/email/resources/management/equipment/edit?equipmentId=[ExternalDirectoryObjectId]`, + icon: , + color: "info", + condition: (row) => !row.isDirSynced, + }, + { + label: "Edit permissions", + link: "/identity/administration/users/user/exchange?userId=[ExternalDirectoryObjectId]", + color: "info", + icon: , + }, + { + label: "Block Sign In", + type: "POST", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "ExternalDirectoryObjectId" }, + confirmText: "Are you sure you want to block the sign-in for this equipment mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + { + label: "Unblock Sign In", + type: "POST", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "ExternalDirectoryObjectId", Enable: true }, + confirmText: "Are you sure you want to unblock sign-in for this equipment mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + { + label: "Delete Equipment", + type: "POST", + icon: , + url: "/api/RemoveUser", + data: { ID: "ExternalDirectoryObjectId" }, + confirmText: "Are you sure you want to delete this equipment mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + ]; + + return ( + } + > + Add Equipment + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/resources/management/list-rooms/add.jsx b/src/pages/email/resources/management/list-rooms/add.jsx index b1a7aeeec5e6..b4ef65fd7317 100644 --- a/src/pages/email/resources/management/list-rooms/add.jsx +++ b/src/pages/email/resources/management/list-rooms/add.jsx @@ -1,5 +1,6 @@ import React from "react"; -import { Grid, Divider } from "@mui/material"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -44,7 +45,7 @@ const AddRoomMailbox = () => { > {/* Display Name */} - + { {/* Username and Domain */} - + { validators={{ required: "Username is required" }} /> - + { {/* Resource Capacity (Optional) */} - + { + const router = useRouter(); + const { roomId } = router.query; + const tenantDomain = useSettings().currentTenant; + const formControl = useForm({ + mode: "onChange", + }); + + const roomInfo = ApiGetCall({ + url: `/api/ListRooms?roomId=${roomId}&tenantFilter=${tenantDomain}`, + queryKey: `Room-${roomId}`, + waiting: false, + }); + + useEffect(() => { + if (roomInfo.isSuccess && roomInfo.data?.[0]) { + const room = roomInfo.data[0]; + formControl.reset({ + // Core Properties + displayName: room.displayName, + hiddenFromAddressListsEnabled: room.hiddenFromAddressListsEnabled, + + // Room Booking Settings + capacity: room.capacity, + + // Location Information + building: room.building, + floor: room.floor, + floorLabel: room.floorLabel, + street: room.street, + city: room.city, + state: room.state, + postalCode: room.postalCode, + countryOrRegion: room.countryOrRegion + ? countryList.find((c) => c.Name === room.countryOrRegion)?.Code || "" + : "", + + // Room Equipment + audioDeviceName: room.audioDeviceName, + videoDeviceName: room.videoDeviceName, + displayDeviceName: room.displayDeviceName, + + // Room Features + isWheelChairAccessible: room.isWheelChairAccessible, + phone: room.phone, + tags: room.tags?.map((tag) => ({ label: tag, value: tag })) || [], + + // Calendar Properties + AllowConflicts: room.AllowConflicts, + AllowRecurringMeetings: room.AllowRecurringMeetings, + BookingWindowInDays: room.BookingWindowInDays, + MaximumDurationInMinutes: room.MaximumDurationInMinutes, + ProcessExternalMeetingMessages: room.ProcessExternalMeetingMessages, + EnforceCapacity: room.EnforceCapacity, + ForwardRequestsToDelegates: room.ForwardRequestsToDelegates, + ScheduleOnlyDuringWorkHours: room.ScheduleOnlyDuringWorkHours, + AutomateProcessing: room.AutomateProcessing, + + // Calendar Configuration + WorkDays: + room.WorkDays?.split(",")?.map((day) => ({ + label: day.trim(), + value: day.trim(), + })) || [], + WorkHoursStartTime: room.WorkHoursStartTime, + WorkHoursEndTime: room.WorkHoursEndTime, + WorkingHoursTimeZone: room.WorkingHoursTimeZone + ? { + value: room.WorkingHoursTimeZone, + label: timezoneList.find((tz) => tz.standardTime === room.WorkingHoursTimeZone) + ? `${room.WorkingHoursTimeZone} - ${ + timezoneList.find((tz) => tz.standardTime === room.WorkingHoursTimeZone) + ?.timezone + }` + : room.WorkingHoursTimeZone, + } + : null, + }); + } + }, [roomInfo.isSuccess, roomInfo.data]); + + useEffect(() => { + if (roomId) { + roomInfo.refetch(); + } + }, [router.query, roomId, tenantDomain]); + + return ( + ({ + tenantID: tenantDomain, + roomId: roomId, + displayName: values.displayName?.trim(), + hiddenFromAddressListsEnabled: values.hiddenFromAddressListsEnabled, + + // Room Booking Settings + capacity: values.capacity, + + // Location Information + building: values.building?.trim(), + floor: values.floor, + floorLabel: values.floorLabel?.trim(), + street: values.street?.trim(), + city: values.city?.trim(), + state: values.state?.trim(), + postalCode: values.postalCode?.trim(), + countryOrRegion: values.countryOrRegion?.value || values.countryOrRegion || null, + + // Room Equipment + audioDeviceName: values.audioDeviceName?.trim(), + videoDeviceName: values.videoDeviceName?.trim(), + displayDeviceName: values.displayDeviceName?.trim(), + + // Room Features + isWheelChairAccessible: values.isWheelChairAccessible, + phone: values.phone?.trim(), + tags: values.tags?.map((tag) => tag.value), + + // Calendar Properties + AllowConflicts: values.AllowConflicts, + AllowRecurringMeetings: values.AllowRecurringMeetings, + BookingWindowInDays: values.BookingWindowInDays, + MaximumDurationInMinutes: values.MaximumDurationInMinutes, + ProcessExternalMeetingMessages: values.ProcessExternalMeetingMessages, + EnforceCapacity: values.EnforceCapacity, + ForwardRequestsToDelegates: values.ForwardRequestsToDelegates, + ScheduleOnlyDuringWorkHours: values.ScheduleOnlyDuringWorkHours, + AutomateProcessing: values.AutomateProcessing?.value || values.AutomateProcessing, + + // Calendar Configuration + WorkDays: values.WorkDays?.map((day) => day.value).join(","), + WorkHoursStartTime: values.WorkHoursStartTime, + WorkHoursEndTime: values.WorkHoursEndTime, + WorkingHoursTimeZone: values.WorkingHoursTimeZone?.value || values.WorkingHoursTimeZone, + })} + > + {roomInfo.isLoading && ( + + )} + {roomInfo.isSuccess && ( + + {/* Basic Information */} + + + Basic Information + + + + + + + + + + + {/* Booking Settings */} + + + Booking Settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Working Hours */} + + + Working Hours + + + + + + + + + + + + ({ + value: tz.standardTime, + label: `${tz.standardTime} - ${tz.timezone}`, + }))} + multiple={false} + creatable={false} + formControl={formControl} + /> + + + + + + + + + {/* Room Facilities */} + + + Room Facilities & Equipment + + + + + + + + + + + + + + + + + + + + + + + {/* Location Information */} + + + Location Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + + + )} + + ); +}; + +EditRoomMailbox.getLayout = (page) => {page}; + +export default EditRoomMailbox; diff --git a/src/pages/email/resources/management/list-rooms/index.js b/src/pages/email/resources/management/list-rooms/index.js index e81e9313ef7a..68c79ba360c2 100644 --- a/src/pages/email/resources/management/list-rooms/index.js +++ b/src/pages/email/resources/management/list-rooms/index.js @@ -2,17 +2,80 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import { AddHomeWork, Edit, Block, LockOpen, Key } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; const Page = () => { const pageTitle = "Rooms"; + const actions = [ + { + label: "Edit Room", + link: `/email/resources/management/list-rooms/edit?roomId=[id]`, + icon: , + color: "info", + condition: (row) => !row.isDirSynced, + }, + { + label: "Edit permissions", + link: "/identity/administration/users/user/exchange?userId=[id]", + color: "info", + icon: , + }, + { + label: "Block Sign In", + type: "POST", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "id" }, + confirmText: "Are you sure you want to block the sign-in for this room mailbox?", + multiPost: false, + condition: (row) => !row.accountDisabled && !row.isDirSynced, + }, + { + label: "Unblock Sign In", + type: "POST", + icon: , + url: "/api/ExecDisableUser", + data: { ID: "id", Enable: true }, + confirmText: "Are you sure you want to unblock sign-in for this room mailbox?", + multiPost: false, + condition: (row) => row.accountDisabled && !row.isDirSynced, + }, + { + label: "Delete Room", + type: "POST", + icon: , + url: "/api/RemoveUser", + data: { ID: "id" }, + confirmText: "Are you sure you want to delete this room mailbox?", + multiPost: false, + condition: (row) => !row.isDirSynced, + }, + ]; + return ( + } diff --git a/src/pages/email/resources/management/room-lists/add.jsx b/src/pages/email/resources/management/room-lists/add.jsx new file mode 100644 index 000000000000..606259d728e0 --- /dev/null +++ b/src/pages/email/resources/management/room-lists/add.jsx @@ -0,0 +1,50 @@ +import { Box } from "@mui/material"; +import CippFormPage from "../../../../../components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../../hooks/use-settings"; +import CippAddRoomListForm from "../../../../../components/CippFormPages/CippAddRoomListForm"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + const tenantDomain = userSettingsDefaults.currentTenant; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + displayName: "", + username: "", + primDomain: null, + }, + }); + + return ( + <> + { + return { + tenantFilter: tenantDomain, + displayName: values.displayName?.trim(), + username: values.username?.trim(), + primDomain: values.primDomain, + }; + }} + > + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/email/resources/management/room-lists/edit.jsx b/src/pages/email/resources/management/room-lists/edit.jsx new file mode 100644 index 000000000000..b1d77015f017 --- /dev/null +++ b/src/pages/email/resources/management/room-lists/edit.jsx @@ -0,0 +1,345 @@ +import { useEffect, useState } from "react"; +import { Box, Button, Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "../../../../../api/ApiCall"; +import { useSettings } from "../../../../../hooks/use-settings"; +import { CippDataTable } from "../../../../../components/CippTable/CippDataTable"; + +const EditRoomList = () => { + const router = useRouter(); + const { groupId } = router.query; + const [groupIdReady, setGroupIdReady] = useState(false); + const [showMembershipTable, setShowMembershipTable] = useState(false); + const [combinedData, setCombinedData] = useState([]); + const [originalAllowExternal, setOriginalAllowExternal] = useState(null); + const tenantFilter = useSettings().currentTenant; + + const groupInfo = ApiGetCall({ + url: `/api/ListRoomLists?groupID=${groupId}&tenantFilter=${tenantFilter}&members=true&owners=true`, + queryKey: `ListRoomLists-${groupId}`, + waiting: groupIdReady, + }); + + useEffect(() => { + if (groupId) { + setGroupIdReady(true); + groupInfo.refetch(); + } + }, [router.query, groupId, tenantFilter]); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: tenantFilter, + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + }, + }); + + useEffect(() => { + if (groupInfo.isSuccess) { + const group = groupInfo.data?.groupInfo; + if (group) { + // Create combined data for the table + const owners = Array.isArray(groupInfo.data?.owners) ? groupInfo.data.owners : []; + const members = Array.isArray(groupInfo.data?.members) ? groupInfo.data.members : []; + + const combinedData = [ + ...owners.map((o) => ({ + type: "Owner", + userPrincipalName: o.userPrincipalName, + displayName: o.displayName, + })), + ...members.map((m) => ({ + type: "Room", + userPrincipalName: m.PrimarySmtpAddress || m.userPrincipalName || m.mail, + displayName: m.DisplayName || m.displayName, + })), + ]; + setCombinedData(combinedData); + + // Store original allowExternal value for comparison + const allowExternalValue = groupInfo?.data?.allowExternal; + setOriginalAllowExternal(allowExternalValue); + + // Reset the form with all values + formControl.reset({ + tenantFilter: tenantFilter, + mail: group.PrimarySmtpAddress || group.mail, + mailNickname: group.Alias || group.mailNickname || "", + allowExternal: allowExternalValue, + displayName: group.DisplayName || group.displayName, + description: group.Description || group.description || "", + groupId: group.Guid || group.id, + groupType: "Room List", + // Initialize empty arrays for add/remove actions + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + }); + } + } + }, [groupInfo.isSuccess, router.query, groupInfo.isFetching]); + + return ( + <> + { + // Only include allowExternal if it has changed from the original value + const modifiedValues = { ...values }; + if (originalAllowExternal !== null && values.allowExternal === originalAllowExternal) { + delete modifiedValues.allowExternal; + } + return modifiedValues; + }} + titleButton={ + <> + + + } + > + {showMembershipTable ? ( + + + + ) : ( + + + + Room List Properties + + + + + + + + + + + + + + Add Members + + + + + `${room.displayName || "Unknown"} (${ + room.mail || room.userPrincipalName || "No email" + })`, + valueField: "mail", + addedField: { + roomType: "bookingType", + capacity: "capacity", + id: "id", + }, + queryKey: `rooms-${tenantFilter}`, + showRefresh: true, + dataFilter: (rooms) => { + // Get current member emails to filter out + const members = Array.isArray(groupInfo.data?.members) + ? groupInfo.data.members + : []; + const currentMemberEmails = members + .map((m) => m.mail || m.userPrincipalName) + .filter(Boolean); + + // Filter out rooms that are already members + // rooms here have been transformed to {label, value, addedFields} format + const filteredRooms = rooms.filter((room) => { + const roomEmail = room.value; // email is in the value field + const isAlreadyMember = currentMemberEmails.includes(roomEmail); + return !isAlreadyMember; + }); + + return filteredRooms; + }, + }} + /> + + + + + `${user.displayName || "Unknown"} (${ + user.userPrincipalName || user.mail || "No email" + })`, + valueField: "userPrincipalName", + addedField: { + id: "id", + }, + queryKey: `users-${tenantFilter}`, + showRefresh: true, + dataFilter: (users) => { + // Get current owner userPrincipalNames to filter out + const owners = Array.isArray(groupInfo.data?.owners) + ? groupInfo.data.owners + : []; + const currentOwnerEmails = owners + .map((o) => o.userPrincipalName) + .filter(Boolean); + + // Filter out users that are already owners + // users here have been transformed to {label, value, addedFields} format + const filteredUsers = users.filter((user) => { + const userEmail = user.value; // userPrincipalName is in the value field + const isAlreadyOwner = currentOwnerEmails.includes(userEmail); + return !isAlreadyOwner; + }); + + return filteredUsers; + }, + }} + /> + + + + + Remove Members + + + + ({ + label: `${m.DisplayName || m.displayName || "Unknown"} (${ + m.PrimarySmtpAddress || m.userPrincipalName || m.mail + })`, + value: m.PrimarySmtpAddress || m.userPrincipalName || m.mail, + addedFields: { id: m.ExternalDirectoryObjectId || m.id }, + })) + : [] + } + /> + + + + ({ + label: `${o.displayName} (${o.userPrincipalName})`, + value: o.userPrincipalName, + addedFields: { id: o.id }, + })) + : [] + } + /> + + + + + Room List Settings + + + + + + + + )} + + + ); +}; + +EditRoomList.getLayout = (page) => {page}; + +export default EditRoomList; diff --git a/src/pages/email/resources/management/room-lists/index.js b/src/pages/email/resources/management/room-lists/index.js index 11abfc1683cb..b29198a98e9a 100644 --- a/src/pages/email/resources/management/room-lists/index.js +++ b/src/pages/email/resources/management/room-lists/index.js @@ -1,38 +1,56 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Visibility, ListAlt, Edit } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { Button } from "@mui/material"; +import Link from "next/link"; const Page = () => { const pageTitle = "Room Lists"; - const apiUrl = "/api/ListRoomLists" + const apiUrl = "/api/ListRoomLists"; const actions = [ { - label: "View included Rooms", - link: `/email/resources/management/room-lists/list/view?roomAddress=[emailAddress]`, - color: "info", + label: "Edit Room List", + link: "/email/resources/management/room-lists/edit?groupId=[PrimarySmtpAddress]", + multiPost: false, + icon: , + color: "success", + }, + { + label: "Delete Room List", + type: "POST", + url: "/api/ExecGroupsDelete", + icon: , + data: { + id: "Guid", + displayName: "DisplayName", + GroupType: "!Distribution List", + }, + confirmText: "Are you sure you want to delete this room list?", + multiPost: false, }, ]; const offCanvas = { extendedInfoFields: [ - "id", - "emailAddress", - "displayName", - "phone", - "placeId", - "geoCoordinates", - "address.city", - "address.countryOrRegion", + "Guid", + "PrimarySmtpAddress", + "DisplayName", + "Phone", + "Identity", + "Notes", + "MailNickname", ], actions: actions, }; const simpleColumns = [ - "displayName", - "geoCoordinates", - "placeId", - "address.city", - "address.countryOrRegion", + "DisplayName", + "PrimarySmtpAddress", + "Identity", + "Phone", + "Notes", ]; return ( @@ -40,8 +58,20 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} actions={actions} + apiDataKey="Results" offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + <> + + + } /> ); }; diff --git a/src/pages/email/resources/management/room-lists/list/view.jsx b/src/pages/email/resources/management/room-lists/list/view.jsx deleted file mode 100644 index 64bd4a5f07ce..000000000000 --- a/src/pages/email/resources/management/room-lists/list/view.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useRouter } from "next/router"; -import { useSettings } from "/src/hooks/use-settings"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { roomAddress } = router.query; - const pageTitle = `Rooms included in ${roomAddress}`; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/spamfilter/deploy/index.js b/src/pages/email/spamfilter/deploy/index.js deleted file mode 100644 index 92cbabc77ae2..000000000000 --- a/src/pages/email/spamfilter/deploy/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Apply Spamfilter Template"; - - return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the apply spamfilter template section.

    -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/email/spamfilter/list-connectionfilter-templates/index.js b/src/pages/email/spamfilter/list-connectionfilter-templates/index.js new file mode 100644 index 000000000000..f5d660457154 --- /dev/null +++ b/src/pages/email/spamfilter/list-connectionfilter-templates/index.js @@ -0,0 +1,104 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Connection filter Templates"; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveConnectionfilterTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "name", + "IsDefault", + "IPAllowList", + "IPBlockList", + "EnableSafeList", + "GUID", + ], + actions: actions, + }; + + const simpleColumns = [ + "name", + "IsDefault", + "IPAllowList", + "IPBlockList", + "EnableSafeList", + "GUID", + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/email/connectionfilter/list-connectionfilter/add.jsx b/src/pages/email/spamfilter/list-connectionfilter/add.jsx similarity index 92% rename from src/pages/email/connectionfilter/list-connectionfilter/add.jsx rename to src/pages/email/spamfilter/list-connectionfilter/add.jsx index c2e2c9a5785f..a0c30550356d 100644 --- a/src/pages/email/connectionfilter/list-connectionfilter/add.jsx +++ b/src/pages/email/spamfilter/list-connectionfilter/add.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; -import { Grid, Divider } from "@mui/material"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -33,13 +34,14 @@ const AddPolicy = () => { postUrl="/api/AddConnectionFilter" > - + @@ -47,7 +49,7 @@ const AddPolicy = () => { {/* TemplateList */} - + { - + { @@ -8,13 +9,14 @@ const Page = () => { const actions = [ { - label: "Create template based on rule", + label: "Create template based on filter", type: "POST", url: "/api/AddConnectionfilterTemplate", dataFunction: (data) => { return { ...data }; }, - confirmText: "Are you sure you want to create a template based on this rule?", + icon: , + confirmText: "Are you sure you want to create a template based on this filter?", }, ]; @@ -32,13 +34,7 @@ const Page = () => { actions: actions, }; - const simpleColumns = [ - "Name", - "IsDefault", - "IPAllowList", - "IPBlockList", - "EnableSafeList", - ]; + const simpleColumns = ["Name", "IsDefault", "IPAllowList", "IPBlockList", "EnableSafeList"]; return ( { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/email/spamfilter/list-quarantine-policies/add.jsx b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx new file mode 100644 index 000000000000..69def81cf558 --- /dev/null +++ b/src/pages/email/spamfilter/list-quarantine-policies/add.jsx @@ -0,0 +1,144 @@ +import { useEffect } from "react"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useWatch } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; + +const AddPolicy = () => { + const formControl = useForm({ + mode: "onChange", + defaultValues: { + selectedTenants: [], + TemplateList: null, + PowerShellCommand: "", + }, + }); + + const templateListVal = useWatch({ control: formControl.control, name: "TemplateList" }); + + useEffect(() => { + if (templateListVal?.value) { + formControl.setValue("PowerShellCommand", JSON.stringify(templateListVal?.value)); + } + }, [templateListVal, formControl]); + + // Watch the value of QuarantineNotification + const quarantineNotification = useWatch({ + control: formControl.control, + name: "QuarantineNotification", + }); + + return ( + + + + + + + {/* */} + + {/* TemplateList, can be added later. But did not seem necessary with so few settings */} + {/* + option, + url: "/api/ListSpamFilterTemplates", + }} + placeholder="Select a template or enter PowerShell JSON manually" + /> + */} + + + + + + + + + + + + + + + + + + + + + ); +}; + +AddPolicy.getLayout = (page) => {page}; + +export default AddPolicy; diff --git a/src/pages/email/spamfilter/list-quarantine-policies/index.js b/src/pages/email/spamfilter/list-quarantine-policies/index.js new file mode 100644 index 000000000000..fa301cb631f4 --- /dev/null +++ b/src/pages/email/spamfilter/list-quarantine-policies/index.js @@ -0,0 +1,435 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import CippButtonCard from "/src/components/CippCards/CippButtonCard"; +import { CippInfoBar } from "/src/components/CippCards/CippInfoBar"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; +import { Alert, Typography, Stack, Tooltip, IconButton, SvgIcon, Button } from "@mui/material"; +import { Grid } from "@mui/system"; +import Link from "next/link"; +import { + AccessTime, + CorporateFare, + AlternateEmail, + Language, + Sync, + RocketLaunch, + Edit, + Delete, +} from "@mui/icons-material"; +import { useSettings } from "/src/hooks/use-settings"; +import { useDialog } from "/src/hooks/use-dialog"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Quarantine Policies"; + const { currentTenant } = useSettings(); + + const createDialog = useDialog(); + + // Use ApiGetCall directly as a hook + const GlobalQuarantinePolicy = ApiGetCall({ + url: "/api/ListQuarantinePolicy", + data: { tenantFilter: currentTenant, type: "GlobalQuarantinePolicy" }, + queryKey: `GlobalQuarantinePolicy-${currentTenant}`, + }); + + // Get the policy data regardless of array or object + const globalQuarantineData = Array.isArray(GlobalQuarantinePolicy.data) + ? GlobalQuarantinePolicy.data[0] + : GlobalQuarantinePolicy.data; + + const hasGlobalQuarantinePolicyData = !!globalQuarantineData; + + + if (hasGlobalQuarantinePolicyData) { + globalQuarantineData.EndUserSpamNotificationFrequency = + globalQuarantineData?.EndUserSpamNotificationFrequency === "P1D" + ? "Daily" + : globalQuarantineData?.EndUserSpamNotificationFrequency === "P7D" + ? "Weekly" + : globalQuarantineData?.EndUserSpamNotificationFrequency === "PT4H" + ? "4 hours" + : globalQuarantineData?.EndUserSpamNotificationFrequency + } + + const multiLanguagePropertyItems = hasGlobalQuarantinePolicyData + ? ( + Array.isArray(globalQuarantineData?.MultiLanguageSetting) && globalQuarantineData.MultiLanguageSetting.length > 0 + ? globalQuarantineData.MultiLanguageSetting.map((language, idx) => ({ + language: language == "Default" ? "English_USA" + : language == "English" ? "English_GB" + : language, + senderDisplayName: + globalQuarantineData.MultiLanguageSenderName[idx] && + globalQuarantineData.MultiLanguageSenderName[idx].trim() !== "" + ? globalQuarantineData.MultiLanguageSenderName[idx] + : "None", + subject: + globalQuarantineData.EsnCustomSubject[idx] && + globalQuarantineData.EsnCustomSubject[idx].trim() !== "" + ? globalQuarantineData.EsnCustomSubject[idx] + : "None", + disclaimer: + globalQuarantineData.MultiLanguageCustomDisclaimer[idx] && + globalQuarantineData.MultiLanguageCustomDisclaimer[idx].trim() !== "" + ? globalQuarantineData.MultiLanguageCustomDisclaimer[idx] + : "None", + })) + : [ + { + language: "None", + senderDisplayName: "None", + subject: "None", + disclaimer: "None", + }, + ] + ) + : []; + + const buttonCardActions = [ + <> + + + { + GlobalQuarantinePolicy.refetch(); + }} + > + + + + + + + ]; + + // Actions to perform (Edit,Delete Policy) + const actions = [ + { + label: "Edit Policy", + type: "POST", + url: "/api/EditQuarantinePolicy?type=QuarantinePolicy", + setDefaultValues: true, + fields: [ + { + type: "textField", + name: "Name", + label: "Policy Name", + disabled: true, + }, + { + type: "autoComplete", + name: "ReleaseActionPreference", + label: "Select release action preference", + multiple : false, + creatable : false, + options: [ + { label: "Release", value: "Release" }, + { label: "Request Release", value: "RequestRelease" }, + ], + }, + { + type: "switch", + name: "Delete", + label: "Delete", + }, + { + type: "switch", + name: "Preview", + label: "Preview", + }, + { + type: "switch", + name: "BlockSender", + label: "Block Sender", + }, + { + type: "switch", + name: "AllowSender", + label: "Allow Sender", + }, + { + type: "switch", + name: "QuarantineNotification", + label: "Quarantine Notification", + }, + { + type: "switch", + name: "IncludeMessagesFromBlockedSenderAddress", + label: "Include Messages From Blocked Sender Address", + }, + ], + data: { Identity: "Guid", Action: "!Edit" }, + confirmText: "Update Quarantine Policy '[Name]'? Policy Name cannot be changed.", + multiPost: false, + icon: , + color: "info", + condition: (row) => row.Guid != "00000000-0000-0000-0000-000000000000", + }, + { + label: "Delete Policy", + type: "POST", + icon: , + url: "/api/RemoveQuarantinePolicy", + data: { + Name: "Name", + Identity: "Guid", + }, + confirmText: ( + <> + + Are you sure you want to delete this policy? + + + Note: This will delete the Quarantine policy, even if it is currently in use.
    + Removing the Admin and User Access it applies to emails. +
    + + Confirm the Quarantine is not applied in any of the following policies: +
      +
    • Anti-phishing
    • +
    • Anti-spam
    • +
    • Anti-malware
    • +
    • Safe Attachments
    • +
    +
    + + ), + condition: (row) => row.Guid != "00000000-0000-0000-0000-000000000000", + }, + ]; + + // Off-canvas structure: displays extended details and includes actions (Enable/Disable Rule) + const offCanvas = { + extendedInfoFields: [ + "Id", // Policy Name/Id + "Name", // Policy Name + "EndUserQuarantinePermissions", + "Guid", + "Builtin", + "WhenCreated", // Creation Date + "WhenChanged", // Last Modified Date + ], + actions: actions, + }; + + const filterList = [ + { + filterName: "Custom Policies", + value: [{ id: "Builtin", value: "No" }], + type: "column", + }, + { + filterName: "Built-in Policies", + value: [{ id: "Builtin", value: "Yes" }], + type: "column", + }, + ]; + + + const customLanguageOffcanvas = + multiLanguagePropertyItems && multiLanguagePropertyItems.length > 0 + ? { + offcanvas: { + title: "Custom Language Settings", + propertyItems: multiLanguagePropertyItems.map((item, idx) => ({ + label: "", + value: ( + + + {item.language} + + } + cardSx={{ mb: 2 }} + > + + {Object.entries(item) + .filter(([key]) => key !== "language") + .map(([key, value]) => ( + + + {key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())} + + + {value} + + + ))} + + + ), + })), + } + } + : {}; + + // Simplified columns for the table + const simpleColumns = [ + "Name", + "ReleaseActionPreference", + "Delete", + "Preview", + "BlockSender", + "AllowSender", + "QuarantineNotification", + "IncludeMessagesFromBlockedSenderAddress", + "WhenCreated", + "WhenChanged", + ]; + + + + // Prepare data for CippInfoBar as a const to clean up the code + const infoBarData = [ + { + icon: , + data: globalQuarantineData?.EndUserSpamNotificationFrequency ?? "n/a", + name: "Notification Frequency", + }, + { + icon: , + data: hasGlobalQuarantinePolicyData + ? (globalQuarantineData?.OrganizationBrandingEnabled + ? "Enabled" + : "Disabled" + ) + : "n/a", + name: "Branding", + }, + { + icon: , + data: hasGlobalQuarantinePolicyData + ? (globalQuarantineData?.EndUserSpamNotificationCustomFromAddress + ? globalQuarantineData?.EndUserSpamNotificationCustomFromAddress + : "None") + : "n/a" , + name: "Custom Sender Address", + }, + { + icon: , + toolTip: "More Info", + data: hasGlobalQuarantinePolicyData + ? ( + multiLanguagePropertyItems.length > 0 + ? multiLanguagePropertyItems.map(item => item.language).join(", ") + : "None" + ) + : "n/a", + name: "Custom Language", + ...customLanguageOffcanvas, + }, + ]; + + return ( + <> + + + + + + + + + + + + + } + /> + + + ); +}; + +// Layout configuration: ensure page uses DashboardLayout +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/email/spamfilter/list-spamfilter/add.jsx b/src/pages/email/spamfilter/list-spamfilter/add.jsx index 893f45c7ee51..b25c936e8ecc 100644 --- a/src/pages/email/spamfilter/list-spamfilter/add.jsx +++ b/src/pages/email/spamfilter/list-spamfilter/add.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; -import { Grid, Divider } from "@mui/material"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -33,13 +34,14 @@ const AddPolicy = () => { postUrl="/api/AddSpamFilter" > - + @@ -47,7 +49,7 @@ const AddPolicy = () => { {/* TemplateList */} - + { - + { const pageTitle = "Spam Filters"; + const apiUrl = "/api/ListSpamfilter"; const actions = [ { label: "Create template based on rule", type: "POST", + icon: , url: "/api/AddSpamfilterTemplate", dataFunction: (data) => { return { ...data }; @@ -19,31 +24,33 @@ const Page = () => { { label: "Enable Rule", type: "POST", + icon: , url: "/api/EditSpamfilter", data: { - State: "enable", - TenantFilter: "Tenant", + State: "!enable", name: "Name", }, confirmText: "Are you sure you want to enable this rule?", + condition: (row) => row.ruleState === "Disabled", }, { label: "Disable Rule", type: "POST", + icon: , url: "/api/EditSpamfilter", data: { - State: "disable", - TenantFilter: "Tenant", + State: "!disable", name: "Name", }, confirmText: "Are you sure you want to disable this rule?", + condition: (row) => row.ruleState === "Enabled", }, { label: "Delete Rule", type: "POST", + icon: , url: "/api/RemoveSpamFilter", data: { - TenantFilter: "Tenant", name: "Name", }, confirmText: "Are you sure you want to delete this rule?", @@ -88,13 +95,17 @@ const Page = () => { return ( - diff --git a/src/pages/email/spamfilter/list-templates/index.js b/src/pages/email/spamfilter/list-templates/index.js index faa7f9a905a0..adcc2884e065 100644 --- a/src/pages/email/spamfilter/list-templates/index.js +++ b/src/pages/email/spamfilter/list-templates/index.js @@ -1,11 +1,62 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Spamfilter Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "POST", diff --git a/src/pages/email/tools/mailbox-restores/add.jsx b/src/pages/email/tools/mailbox-restores/add.jsx index edeb42b10f3f..75602521ec80 100644 --- a/src/pages/email/tools/mailbox-restores/add.jsx +++ b/src/pages/email/tools/mailbox-restores/add.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; @@ -91,7 +91,7 @@ const MailboxRestoreForm = () => { Restore Settings - + { /> {/* Target Mailbox */} - + { validators={{ validate: (value) => (value ? true : "Please select a target mailbox.") }} /> - + { Optional Settings - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { ]} /> - + { options={wellKnownFolders} /> - + { options={wellKnownFolders} /> - + { /> - + { ]} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { ]} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - + { @@ -12,10 +13,10 @@ const Page = () => { label: "Resume Restore Request", type: "POST", url: "/api/ExecMailboxRestore", + icon: , data: { - TenantFilter: "Tenant", Identity: "Identity", - Action: "Resume", + Action: "!Resume", }, confirmText: "Are you sure you want to resume this restore request?", color: "info", @@ -24,10 +25,10 @@ const Page = () => { label: "Suspend Restore Request", type: "POST", url: "/api/ExecMailboxRestore", + icon: , data: { - TenantFilter: "Tenant", Identity: "Identity", - Action: "Suspend", + Action: "!Suspend", }, confirmText: "Are you sure you want to suspend this restore request?", color: "warning", @@ -36,10 +37,10 @@ const Page = () => { label: "Remove Restore Request", type: "POST", url: "/api/ExecMailboxRestore", + icon: , data: { - TenantFilter: "Tenant", Identity: "Identity", - Action: "Remove", + Action: "!Remove", }, confirmText: "Are you sure you want to remove this restore request?", color: "danger", @@ -63,7 +64,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/email/tools/message-trace/index.js b/src/pages/email/tools/message-trace/index.js index 1cde2ba78cba..64414e82bb4c 100644 --- a/src/pages/email/tools/message-trace/index.js +++ b/src/pages/email/tools/message-trace/index.js @@ -5,7 +5,6 @@ import { DialogTitle, DialogContent, IconButton, - Container, Stack, Typography, CircularProgress, @@ -83,6 +82,12 @@ const Page = () => { }, icon: , }, + { + label: "View in Explorer", + noConfirm: true, + link: `https://security.microsoft.com/realtimereportsv3?tid=${tenantFilter}&dltarget=Explorer&dlstorage=Url&viewid=allemail&query-NetworkMessageId=[MessageTraceId]`, + icon: , + }, ]; const onSubmit = () => { @@ -150,7 +155,7 @@ const Page = () => { accordionExpanded={true} > - + { /> {formControl.watch("dateFilter") === "relative" && ( - + { )} {formControl.watch("dateFilter") === "startEnd" && ( <> - + { disabled={isMessageIdSet} /> - + { disabled={isMessageIdSet} /> - + { formControl={formControl} /> - + { disabled={isMessageIdSet} /> - + { disabled={isMessageIdSet} /> - + { {/* Submit and Clear Buttons */} - + diff --git a/src/pages/email/transport/list-connector-templates/index.js b/src/pages/email/transport/list-connector-templates/index.js new file mode 100644 index 000000000000..75723a53422d --- /dev/null +++ b/src/pages/email/transport/list-connector-templates/index.js @@ -0,0 +1,112 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Button } from "@mui/material"; +import Link from "next/link"; +import { RocketLaunch } from "@mui/icons-material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import ConnectorTemplateDetails from "../../../../components/CippComponents/ConnectorTemplateDetails"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Exchange Connector Templates"; + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveExConnectorTemplate", + data: { + ID: "GUID", + }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + children: (data) => , + actions: actions, + size: "lg", + }; + + const simpleColumns = ["name", "cippconnectortype", "GUID"]; + + return ( + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/email/connectors/list-connectors/add.jsx b/src/pages/email/transport/list-connectors/add.jsx similarity index 86% rename from src/pages/email/connectors/list-connectors/add.jsx rename to src/pages/email/transport/list-connectors/add.jsx index 5410d79c653d..acd4f0c0a50f 100644 --- a/src/pages/email/connectors/list-connectors/add.jsx +++ b/src/pages/email/transport/list-connectors/add.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; -import { Grid, Divider } from "@mui/material"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -27,19 +28,20 @@ const AddPolicy = () => { return ( - + @@ -47,7 +49,7 @@ const AddPolicy = () => { {/* TemplateList */} - + { formControl={formControl} multiple={false} api={{ - queryKey: `TemplateListTransport`, + queryKey: `TemplateListConnectors`, labelField: "name", valueField: (option) => option, url: "/api/ListExconnectorTemplates", @@ -66,7 +68,7 @@ const AddPolicy = () => { - + { @@ -11,6 +12,7 @@ const Page = () => { label: "Create template based on connector", type: "POST", url: "/api/AddExConnectorTemplate", + icon: , postEntireRow: true, confirmText: "Are you sure you want to create a template based on this connector?", color: "info", @@ -19,8 +21,10 @@ const Page = () => { label: "Enable Connector", type: "POST", url: "/api/EditExConnector", + icon: , + condition: (row) => !row.Enabled, data: { - State: "Enable", + State: "!Enable", GUID: "Guid", Type: "cippconnectortype", }, @@ -31,8 +35,10 @@ const Page = () => { label: "Disable Connector", type: "POST", url: "/api/EditExConnector", + icon: , + condition: (row) => row.Enabled, data: { - State: "Disable", + State: "!Disable", GUID: "Guid", Type: "cippconnectortype", }, @@ -43,6 +49,7 @@ const Page = () => { label: "Delete Connector", type: "POST", url: "/api/RemoveExConnector", + icon: , data: { GUID: "Guid", Type: "cippconnectortype", @@ -77,13 +84,13 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} - titleButton={{ - label: "Deploy Connector", - href: "/email/connectors/deploy-connector", - }} cardButton={ <> - diff --git a/src/pages/email/transport/list-rules/add.jsx b/src/pages/email/transport/list-rules/add.jsx index 10b44ad400c7..e98931c56eda 100644 --- a/src/pages/email/transport/list-rules/add.jsx +++ b/src/pages/email/transport/list-rules/add.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; -import { Grid, Divider } from "@mui/material"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -33,13 +34,14 @@ const AddPolicy = () => { postUrl="/api/AddTransportRule" > - + @@ -47,7 +49,7 @@ const AddPolicy = () => { {/* TemplateList */} - + { - + { actions: actions, }; - const simpleColumns = ["Name", "State", "Mode", "RuleErrorAction", "WhenChanged", "Comments"]; + const simpleColumns = [ + "Name", + "State", + "Mode", + "RuleErrorAction", + "WhenChanged", + "Comments", + "Tenant", + ]; return ( { ]} cardButton={ <> - @@ -97,5 +110,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/email/transport/list-templates/index.js b/src/pages/email/transport/list-templates/index.js index 1bfff7428f93..ea143610103f 100644 --- a/src/pages/email/transport/list-templates/index.js +++ b/src/pages/email/transport/list-templates/index.js @@ -1,13 +1,64 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { TrashIcon } from "@heroicons/react/24/outline"; import { Button } from "@mui/material"; +import { RocketLaunch, GitHub } from "@mui/icons-material"; import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Transport Rule Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", type: "POST", @@ -35,7 +86,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/MEM/add-policy/index.js b/src/pages/endpoint/MEM/add-policy/index.js index 158d85c81539..b0ff95a5c89e 100644 --- a/src/pages/endpoint/MEM/add-policy/index.js +++ b/src/pages/endpoint/MEM/add-policy/index.js @@ -10,7 +10,7 @@ const Page = () => { title: "Step 1", description: "Tenant Selection", component: CippTenantStep, - componentProps: { type: "multiple", valueField: "customerId" }, + componentProps: { type: "multiple" }, }, { title: "Step 2", diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js new file mode 100644 index 000000000000..283259e5dec3 --- /dev/null +++ b/src/pages/endpoint/MEM/devices/index.js @@ -0,0 +1,355 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useSettings } from "/src/hooks/use-settings"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { + Sync, + RestartAlt, + LocationOn, + Password, + PasswordOutlined, + Key, + Edit, + Security, + FindInPage, + Shield, + Archive, + AutoMode, + Recycling, + ManageAccounts, +} from "@mui/icons-material"; + +const Page = () => { + const pageTitle = "Devices"; + const tenantFilter = useSettings().currentTenant; + + const actions = [ + { + label: "View in Intune", + link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, + 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 this device", + }, + { + 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 this device?", + }, + { + label: "Reboot Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "rebootNow", + }, + confirmText: "Are you sure you want to reboot this device?", + }, + { + label: "Locate Device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "locateDevice", + }, + confirmText: "Are you sure you want to locate this device?", + }, + { + label: "Retrieve LAPs password", + type: "POST", + icon: , + url: "/api/ExecGetLocalAdminPassword", + data: { + GUID: "azureADDeviceId", + }, + confirmText: "Are you sure you want to retrieve the local admin password?", + }, + { + label: "Rotate Local Admin Password", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "RotateLocalAdminPassword", + }, + confirmText: "Are you sure you want to rotate the password for this device?", + }, + { + label: "Retrieve Bitlocker Keys", + type: "POST", + icon: , + url: "/api/ExecGetRecoveryKey", + data: { + GUID: "azureADDeviceId", + }, + confirmText: "Are you sure you want to retrieve the Bitlocker keys?", + }, + { + 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 this device?", + }, + { + 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 this device?", + }, + { + 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 this device?", + }, + { + label: "Generate logs and ship to MEM", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "CreateDeviceLogCollectionRequest", + }, + confirmText: "Are you sure you want to generate logs and ship these to MEM?", + }, + { + label: "Fresh Start (Remove user data)", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + }, + confirmText: "Are you sure you want to Fresh Start this device?", + }, + { + label: "Fresh Start (Do not remove user data)", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: true, + }, + confirmText: "Are you sure you want to Fresh Start this device?", + }, + { + label: "Wipe Device, keep enrollment data", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "cleanWindowsDevice", + keepUserData: false, + keepEnrollmentData: true, + }, + confirmText: "Are you sure you want to wipe this device, 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, + }, + confirmText: "Are you sure you want to wipe this device, 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, + }, + confirmText: + "Are you sure you want to wipe this device? 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, + }, + confirmText: + "Are you sure you want to wipe this device? 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", + }, + confirmText: "Are you sure you want to Autopilot Reset this device?", + }, + { + label: "Delete device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "delete", + }, + confirmText: "Are you sure you want to retire this device?", + }, + { + label: "Retire device", + type: "POST", + icon: , + url: "/api/ExecDeviceAction", + data: { + GUID: "id", + Action: "retire", + }, + confirmText: "Are you sure you want to retire this device?", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["deviceName", "userPrincipalName"], + actions: actions, + }; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/endpoint/MEM/list-appprotection-policies/index.js b/src/pages/endpoint/MEM/list-appprotection-policies/index.js index 0e889eaee6a7..b637aa950730 100644 --- a/src/pages/endpoint/MEM/list-appprotection-policies/index.js +++ b/src/pages/endpoint/MEM/list-appprotection-policies/index.js @@ -1,10 +1,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Book } from "@mui/icons-material"; +import { Book, RocketLaunch } from "@mui/icons-material"; import { TrashIcon } from "@heroicons/react/24/outline"; +import { PermissionButton } from "/src/utils/permissions.js"; +import Link from "next/link"; const Page = () => { const pageTitle = "App Protection & Configuration Policies"; + const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const actions = [ { @@ -58,6 +61,16 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + } + > + Deploy Policy + + } /> ); }; diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js index 51eb0354d5f9..a2bb8eb989d1 100644 --- a/src/pages/endpoint/MEM/list-compliance-policies/index.js +++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js @@ -1,19 +1,22 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Book, LaptopChromebook } from "@mui/icons-material"; +import { Book, LaptopChromebook, RocketLaunch } from "@mui/icons-material"; import { GlobeAltIcon, TrashIcon, UserIcon } from "@heroicons/react/24/outline"; +import { PermissionButton } from "/src/utils/permissions.js"; +import Link from "next/link"; const Page = () => { const pageTitle = "Intune Compliance Policies"; + const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const actions = [ { label: "Create template based on policy", - type: "GET", + type: "POST", url: "/api/AddIntuneTemplate", data: { ID: "id", - URLName: "URLName", + ODataType: "@odata.type", }, confirmText: "Are you sure you want to create a template based on this policy?", icon: , @@ -21,7 +24,7 @@ const Page = () => { }, { label: "Assign to All Users", - type: "GET", + type: "POST", url: "/api/ExecAssignPolicy", data: { AssignTo: "allLicensedUsers", @@ -34,7 +37,7 @@ const Page = () => { }, { label: "Assign to All Devices", - type: "GET", + type: "POST", url: "/api/ExecAssignPolicy", data: { AssignTo: "AllDevices", @@ -47,7 +50,7 @@ const Page = () => { }, { label: "Assign Globally (All Users / All Devices)", - type: "GET", + type: "POST", url: "/api/ExecAssignPolicy", data: { AssignTo: "AllDevicesAndUsers", @@ -60,7 +63,7 @@ const Page = () => { }, { label: "Delete Policy", - type: "GET", + type: "POST", url: "/api/RemovePolicy", data: { ID: "id", @@ -99,6 +102,16 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + } + > + Deploy Policy + + } /> ); }; diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js index 299881df53fe..a59547b8cbf7 100644 --- a/src/pages/endpoint/MEM/list-policies/index.js +++ b/src/pages/endpoint/MEM/list-policies/index.js @@ -1,15 +1,18 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Book, LaptopChromebook } from "@mui/icons-material"; +import { Book, LaptopChromebook, RocketLaunch } from "@mui/icons-material"; import { GlobeAltIcon, TrashIcon, UserIcon } from "@heroicons/react/24/outline"; +import { PermissionButton } from "/src/utils/permissions.js"; +import Link from "next/link"; const Page = () => { const pageTitle = "Configuration Policies"; + const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const actions = [ { label: "Create template based on policy", - type: "GET", + type: "POST", url: "/api/AddIntuneTemplate", data: { ID: "id", @@ -60,7 +63,7 @@ const Page = () => { }, { label: "Delete Policy", - type: "GET", + type: "POST", url: "/api/RemovePolicy", data: { ID: "id", @@ -98,6 +101,16 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + cardButton={ + } + > + Deploy Policy + + } /> ); }; diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx new file mode 100644 index 000000000000..d55737b0f3a0 --- /dev/null +++ b/src/pages/endpoint/MEM/list-scripts/index.jsx @@ -0,0 +1,281 @@ +import { Layout as DashboardLayout } from "/src/layouts/index"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage"; +import { TrashIcon, PencilIcon } from "@heroicons/react/24/outline"; +import { showToast } from "/src/store/toasts"; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + IconButton, + CircularProgress, + DialogActions, +} from "@mui/material"; +import { CippCodeBlock } from "/src/components/CippComponents/CippCodeBlock"; +import { useState, useEffect, useMemo } from "react"; +import { useDispatch } from "react-redux"; +import { Close, Save } from "@mui/icons-material"; +import { useSettings } from "../../../../hooks/use-settings"; +import { Stack } from "@mui/system"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; + +const Page = () => { + const pageTitle = "Scripts"; + const [codeOpen, setCodeOpen] = useState(false); + const [codeContent, setCodeContent] = useState(""); + const [scriptId, setScriptId] = useState(null); + const [saveScript, setSaveScript] = useState(false); + const [codeContentChanged, setCodeContentChanged] = useState(false); + const [warnOpen, setWarnOpen] = useState(false); + const [currentScript, setCurrentScript] = useState(null); + + const dispatch = useDispatch(); + + const language = useMemo(() => { + return currentScript?.scriptType?.toLowerCase() === ("macos" || "linux") ? "shell" : "powershell"; + }, [currentScript?.scriptType]); + + + const tenantFilter = useSettings().currentTenant; + const { + isLoading: scriptIsLoading, + isRefetching: scriptIsFetching, + refetch: scriptRefetch, + data, + } = useQuery({ + queryKey: ["script", { scriptId }], + queryFn: async () => { + const response = await fetch( + `/api/EditIntuneScript?TenantFilter=${tenantFilter}&ScriptId=${scriptId}` + ); + return response.json(); + }, + refetchOnWindowFocus: false, + enabled: false, + }); + + // Refetch the script on scriptId change + useEffect(() => { + if (scriptId) { + scriptRefetch().then(({ data }) => { + setCurrentScript(data); + const scriptBytes = Buffer.from(data.scriptContent, "base64"); + setCodeContent(scriptBytes.toString("ascii")); + }); + } + }, [scriptId, scriptRefetch]); + + const handleScriptEdit = async (row, action) => { + setScriptId(row.id); + setCodeOpen(!codeOpen); + }; + + const codeChange = (newValue, evt) => { + setCodeContent(newValue); + setCodeContentChanged(true); + }; + + const codeClosed = () => { + if (codeContentChanged) { + setWarnOpen(!warnOpen); + } else { + setCodeOpen(!codeOpen); + setCodeContentChanged(false); + setScriptId(null); + setCodeContent(""); + } + }; + + const { refetch: saveScriptRefetch, isFetching: isSaving } = useQuery({ + queryKey: ["saveScript"], + queryFn: async () => { + const scriptBytes = Buffer.from(codeContent, "ascii"); + const { + runAs32Bit, + id, + displayName, + description, + scriptContent, + runAsAccount, + fileName, + roleScopeTagIds, + scriptType, + } = currentScript; + const patchData = { + TenantFilter: tenantFilter, + ScriptId: id, + ScriptType: scriptType, + IntuneScript: JSON.stringify({ + runAs32Bit, + id, + displayName, + description, + scriptContent: scriptBytes.toString("base64"), // Convert to base64 + runAsAccount, + fileName, + roleScopeTagIds, + }), + }; + + const response = await fetch("/api/EditIntuneScript", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patchData), + }); + + if (!response.ok) { + dispatch( + showToast({ + title: "Script Save Error", + message: "Your Intune script could not be saved.", + type: "error", + }) + ); + } + + return response.json(); + }, + enabled: false, + refetchOnWindowFocus: false, + }); + + const queryClient = useQueryClient(); + + const saveCode = async () => { + const { data } = await saveScriptRefetch(); + setCodeContentChanged(false); + setCodeOpen(!codeOpen); + dispatch( + showToast({ + title: "Script Saved", + message: "Your Intune script has been saved successfully.", + type: "update", + }) + ); + }; + + const actions = [ + { + label: "Edit Script", + icon: , + color: "primary", + noConfirm: true, + customFunction: handleScriptEdit, + }, + { + label: "Delete Script", + type: "POST", + url: "/api/RemoveIntuneScript", + data: { + ID: "id", + displayName: "displayName", + ScriptType: "scriptType", + }, + confirmText: "Are you sure you want to delete this script?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "scriptType", + "id", + "fileName", + "displayName", + "description", + "lastModifiedDateTime", + "runAsAccount", + "createdDateTime", + "runAs32Bit", + "executionFrequency", + "enforceSignatureCheck", + ], + actions: actions, + }; + + const simpleColumns = [ + "scriptType", + "displayName", + "description", + "runAsAccount", + "lastModifiedDateTime", + ]; + + return ( + <> + + + + + Script Content + {!isSaving && ( + + + + )} + {!isSaving && ( + + + + )} + {isSaving && ( + + )} + + + {(scriptIsFetching || scriptIsLoading) && } + {!scriptIsFetching && !scriptIsLoading && ( + + )} + + + + Confirmation + + Changes detected, are you sure you want to close? + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/endpoint/MEM/list-templates/edit.jsx b/src/pages/endpoint/MEM/list-templates/edit.jsx new file mode 100644 index 000000000000..71eb1bc6c00c --- /dev/null +++ b/src/pages/endpoint/MEM/list-templates/edit.jsx @@ -0,0 +1,127 @@ +import { Alert, Box } from "@mui/material"; +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 { ApiGetCall } from "/src/api/ApiCall"; +import CippTemplateFieldRenderer from "/src/components/CippComponents/CippTemplateFieldRenderer"; + +const EditIntuneTemplate = () => { + const router = useRouter(); + const { id } = router.query; + const formControl = useForm({ mode: "onChange" }); + + const templateQuery = ApiGetCall({ + url: `/api/ListIntuneTemplates?id=${id}`, + queryKey: `IntuneTemplate-${id}`, + enabled: !!id, + }); + + const templateData = Array.isArray(templateQuery.data) + ? templateQuery.data.find((t) => t.id === id) + : templateQuery.data; + + // Custom data formatter to convert autoComplete objects to values + const customDataFormatter = (values) => { + // Recursively extract values from autoComplete objects and fix @odata issues + const extractValues = (obj) => { + if (!obj) return obj; + + // If this is an autoComplete object with label/value, return just the value + if ( + obj && + typeof obj === "object" && + obj.hasOwnProperty("value") && + obj.hasOwnProperty("label") + ) { + return obj.value; + } + + // If it's an array, process each item + if (Array.isArray(obj)) { + return obj.map((item) => extractValues(item)); + } + + // If it's an object, process each property + if (typeof obj === "object") { + const result = {}; + Object.keys(obj).forEach((key) => { + const value = extractValues(obj[key]); + + // Handle @odata objects created by React Hook Form's dot notation interpretation + if (key.endsWith("@odata") && value && typeof value === "object") { + // Convert @odata objects back to dot notation properties + Object.keys(value).forEach((odataKey) => { + // Always try to restore the original @odata property, regardless of form value + const baseKey = key.replace("@odata", ""); + const originalKey = `${baseKey}@odata.${odataKey}`; + const originalValue = getOriginalValueByPath(templateData, originalKey); + if (originalValue !== undefined) { + result[originalKey] = originalValue; + } + }); + } else { + result[key] = value; + } + }); + return result; + } + + // For primitive values, return as-is + return obj; + }; + + // Helper function to get original value by dot-notation path + const getOriginalValueByPath = (obj, path) => { + const keys = path.split("."); + let current = obj; + for (const key of keys) { + if (current && typeof current === "object" && key in current) { + current = current[key]; + } else { + return undefined; + } + } + return current; + }; + + // Extract values from the entire form data and include id + const processedValues = extractValues(values); + + return { + id, + ...processedValues, + }; + }; + + return ( + + + {templateQuery.isLoading ? ( + + ) : templateQuery.isError || !templateData ? ( + Error loading template or template not found. + ) : ( + + )} + + + ); +}; + +EditIntuneTemplate.getLayout = (page) => {page}; + +export default EditIntuneTemplate; diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index e61d3930aedc..0bc90907c397 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -1,12 +1,25 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { EyeIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { Edit, GitHub } from "@mui/icons-material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { ApiGetCall } from "/src/api/ApiCall"; const Page = () => { const pageTitle = "Available Endpoint Manager Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); const actions = [ + { + label: "Edit Template", + link: `/endpoint/MEM/list-templates/edit?id=[GUID]`, + icon: , + color: "info", + }, { label: "Edit Template Name and Description", type: "POST", @@ -30,9 +43,53 @@ const Page = () => { icon: , color: "info", }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", - type: "GET", + type: "POST", url: "/api/RemoveIntuneTemplate", data: { ID: "GUID" }, confirmText: "Do you want to delete the template?", diff --git a/src/pages/endpoint/applications/list/add.jsx b/src/pages/endpoint/applications/list/add.jsx index 7bceb2e69857..1c431696526c 100644 --- a/src/pages/endpoint/applications/list/add.jsx +++ b/src/pages/endpoint/applications/list/add.jsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; -import { Grid, Divider, Button } from "@mui/material"; +import { Divider, Button, Alert } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -49,11 +50,11 @@ const ApplicationDeploymentForm = () => { }; const ChocosearchResults = ApiPostCall({ - urlfromData: true, + urlFromData: true, }); const winGetSearchResults = ApiPostCall({ - urlfromData: true, + urlFromData: true, }); const searchApp = (searchText, type) => { @@ -92,7 +93,7 @@ const ApplicationDeploymentForm = () => { }} > - + { validators={{ required: "Please select an application type" }} /> - - - {/* Tenant Selector */} - + - - + + + - + - + + + This is a community contribution and is not covered under a vendor sponsorship. Please + join our Discord community for assistance with this MSP App. + + + { compareType="is" compareValue="datto" > - + { /> {selectedTenants?.map((tenant, index) => ( - - {console.log(tenant)} + { compareValue="syncro" > {selectedTenants?.map((tenant, index) => ( - + { {/* Similar blocks for other rmmname values */} - {/* For "immy" */} - - {selectedTenants?.map((tenant, index) => ( - - - - ))} - - {/* For "huntress" */} { compareType="is" compareValue="huntress" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { compareType="is" compareValue="automate" > - + { /> {selectedTenants?.map((tenant, index) => ( - + { ))} {selectedTenants?.map((tenant, index) => ( - + { compareValue="cwcommand" > {selectedTenants?.map((tenant, index) => ( - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { compareType="is" compareValue="StoreApp" > - + { formControl={formControl} /> - + - + ({ - value: item, - label: `${item.applicationName} - ${item.packagename}`, - }))} + options={ + winGetSearchResults.data?.data + ? winGetSearchResults.data?.data?.map((item) => ({ + value: item, + label: `${item.applicationName} - ${item.packagename}`, + })) + : [] + } multiple={false} formControl={formControl} isFetching={winGetSearchResults.isLoading} /> - + { validators={{ required: "Package Identifier is required" }} /> - + { validators={{ required: "Application Name is required" }} /> - + { {/* Install Options */} - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { compareType="is" compareValue="chocolateyApp" > - + { formControl={formControl} /> - + - + ({ - value: item, - label: `${item.applicationName} - ${item.packagename}`, - })) + ChocosearchResults.isSuccess && ChocosearchResults.data?.data + ? ChocosearchResults.data?.data?.Results?.map((item) => ({ + value: item, + label: `${item.applicationName} - ${item.packagename}`, + })) + : [] } multiple={false} formControl={formControl} @@ -505,7 +510,7 @@ const ApplicationDeploymentForm = () => { /> - + { validators={{ required: "Package Name is required" }} /> - + { validators={{ required: "Application Name is required" }} /> - + { formControl={formControl} /> - + { {/* Install Options */} - + { {/* Assign To Options */} - + { compareType="is" compareValue="customGroup" > - + { > {/* Office App Fields */} - + { formControl={formControl} /> - + { validators={{ required: "Please select an update channel" }} /> - + ({ value: tag, - label: language, + label: `${language} (${tag})`, }))} multiple={true} formControl={formControl} validators={{ required: "Please select at least one language" }} /> - + { formControl={formControl} /> - + { defaultValue={true} /> - + { defaultValue={true} /> - + { {/* Assign To Options */} - + { type: "POST", url: "/api/ExecAssignApp", data: { - AssignTo: "AllUsers", - TenantFilter: "Tenant", + AssignTo: "!AllUsers", ID: "id", }, confirmText: "Are you sure you want to assign this app to all users?", @@ -27,8 +26,7 @@ const Page = () => { type: "POST", url: "/api/ExecAssignApp", data: { - AssignTo: "AllDevices", - TenantFilter: "Tenant", + AssignTo: "!AllDevices", ID: "id", }, confirmText: "Are you sure you want to assign this app to all devices?", @@ -40,8 +38,7 @@ const Page = () => { type: "POST", url: "/api/ExecAssignApp", data: { - AssignTo: "Both", - TenantFilter: "Tenant", + AssignTo: "!Both", ID: "id", }, confirmText: "Are you sure you want to assign this app to all users and devices?", @@ -53,7 +50,6 @@ const Page = () => { type: "POST", url: "/api/RemoveApp", data: { - TenantFilter: "Tenant", ID: "id", }, confirmText: "Are you sure you want to delete this application?", @@ -95,7 +91,7 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/applications/queue/index.js b/src/pages/endpoint/applications/queue/index.js index 0f30341cefcb..5a0dd0f3dc98 100644 --- a/src/pages/endpoint/applications/queue/index.js +++ b/src/pages/endpoint/applications/queue/index.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { CheckmarkIcon } from "react-hot-toast"; import { Button } from "@mui/material"; +import { PlayArrow, Add } from "@mui/icons-material"; import Link from "next/link"; import { ApiPostCall } from "../../../../api/ApiCall"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; @@ -40,9 +40,11 @@ const Page = () => { tenantInTitle={false} cardButton={ <> - + - diff --git a/src/pages/endpoint/autopilot/add-device/index.js b/src/pages/endpoint/autopilot/add-device/index.js index 8b8d45513a1e..13578993b90c 100644 --- a/src/pages/endpoint/autopilot/add-device/index.js +++ b/src/pages/endpoint/autopilot/add-device/index.js @@ -3,7 +3,7 @@ import { CippWizardConfirmation } from "/src/components/CippWizard/CippWizardCon import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; import { CippTenantStep } from "/src/components/CippWizard/CippTenantStep.jsx"; import { useSettings } from "../../../../hooks/use-settings"; -import { CippWizardCSVImport } from "../../../../components/CippWizard/CippWizardCSVImport"; +import { CippWizardAutopilotImport } from "../../../../components/CippWizard/CippWizardAutopilotImport"; import { CippWizardBulkOptions } from "../../../../components/CippWizard/CippWizardBulkOptions"; import { CippWizardAutopilotOptions } from "../../../../components/CippWizard/CippWizardAutopilotOptions"; @@ -21,18 +21,37 @@ const Page = () => { { title: "Step 2", description: "Device Import", - component: CippWizardCSVImport, + component: CippWizardAutopilotImport, componentProps: { name: "autopilotData", - manualFields: true, - fields: ["SerialNumber", "oemManufacturerName", "modelName", "productKey", "hardwareHash"], - nameToCSVMapping: { - SerialNumber: "Device serial number", - oemManufacturerName: "Manufacturer name", - modelName: "Device Model", - productKey: "Windows product ID", - hardwareHash: "Hardware hash", - }, + fields: [ + { + friendlyName: "Serialnumber", + propertyName: "SerialNumber", + alternativePropertyNames: ["Device Serial Number"] + }, + { + friendlyName: "Manufacturer", + propertyName: "oemManufacturerName", + alternativePropertyNames: ["Manufacturer name"] + }, + { + friendlyName: "Model", + propertyName: "modelName", + alternativePropertyNames: ["Device model"] + }, + { + friendlyName: "Product ID", + propertyName: "productKey", + alternativePropertyNames: ["Windows Product ID"] + }, + { + friendlyName: "Hardware hash", + propertyName: "hardwareHash", + alternativePropertyNames: ["Hardware Hash"] + } + ], + fileName: "autopilot-template" }, }, { diff --git a/src/pages/endpoint/autopilot/add-status-page/index.js b/src/pages/endpoint/autopilot/add-status-page/index.js index 2ee87af41a8e..3057f7e365d8 100644 --- a/src/pages/endpoint/autopilot/add-status-page/index.js +++ b/src/pages/endpoint/autopilot/add-status-page/index.js @@ -1,14 +1,121 @@ - +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; const Page = () => { - const pageTitle = "Add Status Page"; + const formControl = useForm({ + mode: "onChange", + defaultValues: { + TimeOutInMinutes: "", + ErrorMessage: "", + ShowProgress: false, + EnableLog: false, + OBEEOnly: false, + blockDevice: false, + Allowretry: false, + AllowReset: false, + AllowFail: false, + }, + }); return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the add status page section.

    -
    + + + {/* Tenant Selector */} + + + + + + + {/* Form Fields */} + + + + + + + + + {/* Switches */} + + + + + + + + + + + ); }; diff --git a/src/pages/endpoint/autopilot/list-devices/index.js b/src/pages/endpoint/autopilot/list-devices/index.js index b738be0ba6af..cd5d6244aa74 100644 --- a/src/pages/endpoint/autopilot/list-devices/index.js +++ b/src/pages/endpoint/autopilot/list-devices/index.js @@ -1,14 +1,19 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; import { Button } from "@mui/material"; +import { PersonAdd, Delete, Sync, Add, Edit, Sell } from "@mui/icons-material"; +import { useDialog } from "../../../../hooks/use-dialog"; import Link from "next/link"; const Page = () => { const pageTitle = "Autopilot Devices"; + const createDialog = useDialog(); const actions = [ { label: "Assign device", + icon: , type: "POST", url: "/api/ExecAssignAPDevice", data: { @@ -36,8 +41,72 @@ const Page = () => { ], color: "info", }, + { + label: "Rename Device", + icon: , + type: "POST", + url: "/api/ExecRenameAPDevice", + data: { + deviceId: "id", + serialNumber: "serialNumber", + }, + confirmText: "Enter the new display name for the device.", + fields: [ + { + type: "textField", + name: "displayName", + label: "New Display Name", + required: true, + validate: (value) => { + if (!value) { + return "Display name is required."; + } + if (value.length > 15) { + return "Display name must be 15 characters or less."; + } + if (/\s/.test(value)) { + return "Display name cannot contain spaces."; + } + if (!/^[a-zA-Z0-9-]+$/.test(value)) { + return "Display name can only contain letters, numbers, and hyphens."; + } + if (/^[0-9]+$/.test(value)) { + return "Display name cannot contain only numbers."; + } + return true; // Indicates validation passed + }, + }, + ], + color: "secondary", + }, + { + label: "Edit Group Tag", + icon: , + type: "POST", + url: "/api/ExecSetAPDeviceGroupTag", + data: { + deviceId: "id", + serialNumber: "serialNumber", + }, + confirmText: "Enter the new group tag for the device.", + fields: [ + { + type: "textField", + name: "groupTag", + label: "Group Tag", + validate: (value) => { + if (value && value.length > 128) { + return "Group tag cannot exceed 128 characters."; + } + return true; // Validation passed + }, + }, + ], + color: "secondary", + }, { label: "Delete Device", + icon: , type: "POST", url: "/api/RemoveAPDevice", data: { ID: "id" }, @@ -67,20 +136,36 @@ const Page = () => { ]; return ( - - - - } - /> + <> + + + + + } + /> + + ); }; diff --git a/src/pages/endpoint/autopilot/list-profiles/add.jsx b/src/pages/endpoint/autopilot/list-profiles/add.jsx index 23bdaaaffd26..6f86da5c0d63 100644 --- a/src/pages/endpoint/autopilot/list-profiles/add.jsx +++ b/src/pages/endpoint/autopilot/list-profiles/add.jsx @@ -1,6 +1,6 @@ -import React from "react"; -import { Grid, Divider } from "@mui/material"; -import { useForm, useWatch } from "react-hook-form"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -38,13 +38,14 @@ const AutopilotProfileForm = () => { > {/* Tenant Selector */} - + @@ -52,7 +53,7 @@ const AutopilotProfileForm = () => { {/* Form Fields */} - + { /> - + ({ + options={languageList.map(({ language, tag, "Geographic area": geographicArea }) => ({ value: tag, - label: language, + label: `${language} - ${geographicArea}`, // Format as "language - geographic area" for display }))} formControl={formControl} multiple={false} /> - + { /> - + { {/* Switches */} - + { @@ -32,7 +32,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/autopilot/list-status-pages/add.jsx b/src/pages/endpoint/autopilot/list-status-pages/add.jsx deleted file mode 100644 index 328283d07829..000000000000 --- a/src/pages/endpoint/autopilot/list-status-pages/add.jsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from "react"; -import { Grid, Divider } from "@mui/material"; -import { useForm, useWatch } from "react-hook-form"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import CippFormPage from "/src/components/CippFormPages/CippFormPage"; -import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; - -const AutopilotStatusPageForm = () => { - const formControl = useForm({ - mode: "onChange", - defaultValues: { - TimeOutInMinutes: "", - ErrorMessage: "", - ShowProgress: false, - EnableLog: false, - OBEEOnly: false, - blockDevice: false, - Allowretry: false, - AllowReset: false, - AllowFail: false, - }, - }); - - return ( - - - {/* Tenant Selector */} - - - - - - - {/* Form Fields */} - - - - - - - - - {/* Switches */} - - - - - - - - - - - - ); -}; - -AutopilotStatusPageForm.getLayout = (page) => {page}; - -export default AutopilotStatusPageForm; diff --git a/src/pages/endpoint/autopilot/list-status-pages/index.js b/src/pages/endpoint/autopilot/list-status-pages/index.js index 939685bd69ad..fc1525f4cbbb 100644 --- a/src/pages/endpoint/autopilot/list-status-pages/index.js +++ b/src/pages/endpoint/autopilot/list-status-pages/index.js @@ -2,6 +2,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import Link from "next/link"; +import { PostAdd } from "@mui/icons-material"; const Page = () => { const pageTitle = "Autopilot Status Pages"; @@ -25,7 +26,11 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/endpoint/reports/analyticsdevicescore/index.js b/src/pages/endpoint/reports/analyticsdevicescore/index.js index 16702d203621..5660e0fbf612 100644 --- a/src/pages/endpoint/reports/analyticsdevicescore/index.js +++ b/src/pages/endpoint/reports/analyticsdevicescore/index.js @@ -10,7 +10,7 @@ const Page = () => { // Actions from the source file const actions = [ { - label: "View in InTune", + label: "View in Intune", link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, color: "info", icon: , diff --git a/src/pages/endpoint/reports/autopilot-deployment/index.js b/src/pages/endpoint/reports/autopilot-deployment/index.js new file mode 100644 index 000000000000..28db44474ea8 --- /dev/null +++ b/src/pages/endpoint/reports/autopilot-deployment/index.js @@ -0,0 +1,112 @@ +import { EyeIcon, DocumentTextIcon } from "@heroicons/react/24/outline"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useSettings } from "/src/hooks/use-settings"; +import { CheckCircle, Error, Warning, Refresh } from "@mui/icons-material"; + +const Page = () => { + const pageTitle = "Autopilot Deployments"; + const tenantFilter = useSettings().currentTenant; + + // Actions for viewing device in Intune and deployment details + const actions = [ + { + label: "View Device in Intune", + link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[deviceId]`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, + { + label: "View Deployment Details", + link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_DeviceSettings/DeploymentOverviewMenuBlade/~/autopilotDeployment/deploymentProfileId/[windowsAutopilotDeploymentProfileDisplayName]`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, + ]; + + // Extended info fields for the off-canvas panel + const offCanvas = { + extendedInfoFields: [ + "id", + "deviceId", + "userId", + "eventDateTime", + "deviceRegisteredDateTime", + "enrollmentStartDateTime", + "enrollmentType", + "deviceSerialNumber", + "managedDeviceName", + "userPrincipalName", + "windowsAutopilotDeploymentProfileDisplayName", + "enrollmentState", + "windows10EnrollmentCompletionPageConfigurationDisplayName", + "deploymentState", + "deviceSetupStatus", + "accountSetupStatus", + "osVersion", + "deploymentDuration", + "deploymentTotalDuration", + "deviceSetupDuration", + "accountSetupDuration", + "deploymentStartDateTime", + "deploymentEndDateTime", + "enrollmentFailureDetails", + ], + actions: actions, + }; + + // Columns to be displayed in the table (most important first) + const simpleColumns = [ + "managedDeviceName", + "eventDateTime", + "deviceSerialNumber", + "userPrincipalName", + "deploymentState", + "enrollmentState", + "enrollmentType", + "deploymentTotalDuration", + "windowsAutopilotDeploymentProfileDisplayName", + "enrollmentFailureDetails", + ]; + + // Predefined filters for common deployment scenarios + const filterList = [ + { + filterName: "Failed Deployments", + value: [{ id: "deploymentState", value: "failed" }], + type: "column", + }, + { + filterName: "Successful Deployments", + value: [{ id: "deploymentState", value: "success" }], + type: "column", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/endpoint/reports/devices/index.js b/src/pages/endpoint/reports/devices/index.js deleted file mode 100644 index 4ce9ebcbc46a..000000000000 --- a/src/pages/endpoint/reports/devices/index.js +++ /dev/null @@ -1,143 +0,0 @@ -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { useSettings } from "/src/hooks/use-settings"; -import { EyeIcon } from "@heroicons/react/24/outline"; - -const Page = () => { - const pageTitle = "Devices"; - const tenantFilter = useSettings().currentTenant; - - const actions = [ - { - label: "Sync Device", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "syncDevice", - }, - confirmText: "Are you sure you want to sync this device?", - }, - { - label: "Reboot Device", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "rebootNow", - }, - confirmText: "Are you sure you want to reboot this device?", - }, - { - label: "Locate Device", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "locateDevice", - }, - confirmText: "Are you sure you want to locate this device?", - }, - { - label: "Retrieve LAPs password", - type: "POST", - url: "/api/ExecGetLocalAdminPassword", - data: { - GUID: "azureADDeviceId", - }, - confirmText: "Are you sure you want to retrieve the local admin password?", - }, - { - label: "Rotate Local Admin Password", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "RotateLocalAdminPassword", - }, - confirmText: "Are you sure you want to rotate the password for this device?", - }, - { - label: "Retrieve Bitlocker Keys", - type: "POST", - url: "/api/ExecGetRecoveryKey", - data: { - GUID: "azureADDeviceId", - }, - confirmText: "Are you sure you want to retrieve the Bitlocker keys?", - }, - { - label: "Windows Defender Full Scan", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "WindowsDefenderScan", - quickScan: false, - }, - confirmText: "Are you sure you want to perform a full scan on this device?", - }, - { - label: "Windows Defender Quick Scan", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "WindowsDefenderScan", - quickScan: true, - }, - confirmText: "Are you sure you want to perform a quick scan on this device?", - }, - { - label: "Update Windows Defender", - type: "POST", - url: "/api/ExecDeviceAction", - data: { - GUID: "id", - Action: "windowsDefenderUpdateSignatures", - }, - confirmText: - "Are you sure you want to update the Windows Defender signatures for this device?", - }, - { - label: "View in InTune", - link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, - color: "info", - icon: , - target: "_blank", - multiPost: false, - external: true, - }, - ]; - - const offCanvas = { - extendedInfoFields: ["deviceName", "userPrincipalName"], - actions: actions, - }; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/endpoint/reports/workfromanywhere/index.js b/src/pages/endpoint/reports/workfromanywhere/index.js new file mode 100644 index 000000000000..cd963abfbe72 --- /dev/null +++ b/src/pages/endpoint/reports/workfromanywhere/index.js @@ -0,0 +1,87 @@ +import { EyeIcon } from "@heroicons/react/24/outline"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useSettings } from "/src/hooks/use-settings"; + +const Page = () => { + const pageTitle = "Work from anywhere Report"; + const tenantFilter = useSettings().currentTenant; + + // Actions from the source file + const actions = [ + { + label: "View in Intune", + link: `https://intune.microsoft.com/${tenantFilter}/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/overview/mdmDeviceId/[id]`, + color: "info", + icon: , + target: "_blank", + multiPost: false, + external: true, + }, + ]; + + // OffCanvas details based on the source file + const offCanvas = { + extendedInfoFields: [ + "id", + "deviceName", + "serialNumber", + "model", + "manufacturer", + "ownership", + "upgradeEligibility", + ], + actions: actions, + }; + + // Columns to be displayed in the table + const simpleColumns = [ + "deviceName", + "serialNumber", + "model", + "manufacturer", + "ownership", + "managedBy", + "osVersion", + "upgradeEligibility", + "ramCheckFailed", + "storageCheckFailed", + "processorCoreCountCheckFailed", + "processorSpeedCheckFailed", + "tpmCheckFailed", + "secureBootCheckFailed", + "processorFamilyCheckFailed", + "processor64BitCheckFailed", + "osCheckFailed", + ]; + + // Predefined filters to be applied to the table + const filterList = [ + { + filterName: "Upgrade not eligible", + value: [{ id: "upgradeEligibility", value: "notCapable" }], + type: "column", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/fullPageLoading.js b/src/pages/fullPageLoading.js index 12b1d3f961f9..f4d4e0ed5aec 100644 --- a/src/pages/fullPageLoading.js +++ b/src/pages/fullPageLoading.js @@ -1,7 +1,7 @@ -import { Grid } from "@mui/material"; +import "@mui/material"; import { Layout as DashboardLayout } from "../layouts/index.js"; import Head from "next/head.js"; -import { Box, Container, Stack } from "@mui/system"; +import { Box, Container, Stack, Grid } from "@mui/system"; const FullPageLoading = () => { return ( @@ -17,7 +17,7 @@ const FullPageLoading = () => { - + Loading... #Todo: Make pretty, make it obey user settings for theme. diff --git a/src/pages/identity/administration/deleted-items/index.js b/src/pages/identity/administration/deleted-items/index.js index ab568cfb92d9..66d27c8bfd83 100644 --- a/src/pages/identity/administration/deleted-items/index.js +++ b/src/pages/identity/administration/deleted-items/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { RestoreFromTrash, Warning } from "@mui/icons-material"; const Page = () => { const pageTitle = "Deleted Items"; @@ -7,10 +8,21 @@ const Page = () => { const actions = [ { label: "Restore Object", - type: "GET", + type: "POST", + icon: , url: "/api/ExecRestoreDeleted", - data: { TenantFilter: "Tenant", ID: "id" }, - confirmText: "Are you sure you want to restore this user?", + data: { ID: "id", userPrincipalName: "userPrincipalName", displayName: "displayName" }, + confirmText: "Are you sure you want to restore this object?", + multiPost: false, + }, + { + label: "Permanently Delete Object", + type: "POST", + icon: , + url: "/api/RemoveDeletedObject", + data: { ID: "id", userPrincipalName: "userPrincipalName", displayName: "displayName" }, + confirmText: + "Are you sure you want to permanently delete this object? This action cannot be undone.", multiPost: false, }, ]; diff --git a/src/pages/identity/administration/devices/index.js b/src/pages/identity/administration/devices/index.js index 456329c33755..9d0f56c18643 100644 --- a/src/pages/identity/administration/devices/index.js +++ b/src/pages/identity/administration/devices/index.js @@ -1,7 +1,7 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. import { useSettings } from "/src/hooks/use-settings"; -import { EyeIcon } from "@heroicons/react/24/outline"; +import { Visibility, CheckCircleOutline, Block, VpnKey, DeleteForever } from "@mui/icons-material"; const Page = () => { const pageTitle = "Devices"; @@ -12,7 +12,7 @@ const Page = () => { label: "View in Entra", link: `https://entra.microsoft.com/${tenantFilter}/#view/Microsoft_AAD_Devices/DeviceDetailsMenuBlade/~/Properties/objectId/[id]/deviceId/`, color: "info", - icon: , + icon: , target: "_blank", multiPost: false, external: true, @@ -27,6 +27,8 @@ const Page = () => { }, confirmText: "Are you sure you want to enable this device?", multiPost: false, + condition: (row) => !row.accountEnabled, + icon: , }, { label: "Disable Device", @@ -38,16 +40,19 @@ const Page = () => { }, confirmText: "Are you sure you want to disable this device?", multiPost: false, + condition: (row) => row.accountEnabled, + icon: , }, { label: "Retrieve BitLocker Keys", - type: "GET", + type: "POST", url: "/api/ExecGetRecoveryKey", data: { - GUID: "id", + GUID: "deviceId", }, confirmText: "Are you sure you want to retrieve the BitLocker keys?", multiPost: false, + icon: , }, { label: "Delete Device", @@ -59,6 +64,7 @@ const Page = () => { }, confirmText: "Are you sure you want to delete this device?", multiPost: false, + icon: , }, ]; @@ -72,11 +78,12 @@ const Page = () => { $count: true, }} apiDataKey="Results" + queryKey={`EntraDevices-${tenantFilter}`} actions={actions} simpleColumns={[ "displayName", "accountEnabled", - "recipientType", + "trustType", "enrollmentType", "manufacturer", "model", diff --git a/src/pages/identity/administration/group-templates/add.jsx b/src/pages/identity/administration/group-templates/add.jsx index 808fa0b81985..fe5b597fe926 100644 --- a/src/pages/identity/administration/group-templates/add.jsx +++ b/src/pages/identity/administration/group-templates/add.jsx @@ -17,6 +17,7 @@ const Page = () => { return ( <> { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { id } = router.query; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Fetch template data + const { data: template, isFetching } = ApiGetCall({ + url: `/api/ListGroupTemplates?id=${id}`, + queryKey: `GroupTemplate-${id}`, + waiting: !!id, + }); + + // Map groupType values to valid radio options + const mapGroupType = (type) => { + // Map of group types to the corresponding option value + const groupTypeMap = { + // Standard mappings + azurerole: "azurerole", + generic: "generic", + m365: "m365", + dynamic: "dynamic", + dynamicdistribution: "dynamicdistribution", + distribution: "distribution", + security: "security", + + // Additional mappings from possible backend values + Unified: "m365", + Security: "generic", + Distribution: "distribution", + "Mail-enabled security": "security", + "Mail Enabled Security": "security", + "Azure Role Group": "azurerole", + "Azure Active Directory Role Group": "azurerole", + "Security Group": "generic", + "Microsoft 365 Group": "m365", + "Microsoft 365 (Unified)": "m365", + "Dynamic Group": "dynamic", + DynamicMembership: "dynamic", + "Dynamic Distribution Group": "dynamicdistribution", + DynamicDistribution: "dynamicdistribution", + "Distribution List": "distribution", + }; + + // Return just the value for the radio group, not the label/value pair + return groupTypeMap[type] || "generic"; // Default to generic if no mapping exists + }; + + // Set form values when template data is loaded + useEffect(() => { + if (template) { + const templateData = template[0]; + + // Make sure we have the necessary data before proceeding + if (templateData) { + formControl.reset({ + ...templateData, + groupType: mapGroupType(templateData.groupType), + tenantFilter: userSettingsDefaults.currentTenant, + }); + } + } + }, [template, formControl, userSettingsDefaults.currentTenant]); + + return ( + <> + + {/* Add debugging output to check what values are set */} +
    {JSON.stringify(formControl.watch(), null, 2)}
    + + + + +
    + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/group-templates/index.js b/src/pages/identity/administration/group-templates/index.js index 52bb163880a8..b7dc1b5aa2a3 100644 --- a/src/pages/identity/administration/group-templates/index.js +++ b/src/pages/identity/administration/group-templates/index.js @@ -1,17 +1,76 @@ import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { AddBox, RocketLaunch, Delete, GitHub, Edit } from "@mui/icons-material"; import Link from "next/link"; -import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; +import { getCippTranslation } from "../../../../utils/get-cipp-translation"; +import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; const Page = () => { const pageTitle = "Group Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); const actions = [ + { + label: "Edit Template", + icon: , + link: "/identity/administration/group-templates/edit?id=[GUID]", + }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", - type: "GET", + type: "POST", url: "/api/RemoveGroupTemplate", + icon: , data: { ID: "GUID", }, @@ -21,26 +80,50 @@ const Page = () => { ]; const offCanvas = { - children: (row) => , + children: (data) => { + const keys = Object.keys(data).filter( + (key) => !key.includes("@odata") && !key.includes("@data") + ); + const properties = []; + keys.forEach((key) => { + if (data[key] && data[key].length > 0) { + properties.push({ + label: getCippTranslation(key), + value: getCippFormatting(data[key], key), + }); + } + }); + return ( + + ); + }, }; return ( - - } offCanvas={offCanvas} - simpleColumns={["Displayname", "Description", "groupType", "GUID"]} + simpleColumns={["displayName", "description", "groupType", "GUID"]} /> ); }; diff --git a/src/pages/identity/administration/groups/add.jsx b/src/pages/identity/administration/groups/add.jsx index c6b10b5240f9..3eddb73d6ba0 100644 --- a/src/pages/identity/administration/groups/add.jsx +++ b/src/pages/identity/administration/groups/add.jsx @@ -3,6 +3,7 @@ import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm } from "react-hook-form"; import { useSettings } from "../../../../hooks/use-settings"; +import { useEffect } from "react"; import CippAddGroupForm from "../../../../components/CippFormPages/CippAddGroupForm"; const Page = () => { @@ -10,11 +11,15 @@ const Page = () => { const formControl = useForm({ mode: "onChange", - defaultValues: { - tenantFilter: userSettingsDefaults.currentTenant, - }, }); + useEffect(() => { + formControl.setValue( + "tenantFilter", + userSettingsDefaults?.currentTenant || "" + ); + }, [userSettingsDefaults, formControl]); + return ( <> { title="Groups" backButtonTitle="Group Overview" postUrl="/api/AddGroup" + resetForm={true} > diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index 65df31bfa199..a052a7f28b30 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from "react"; -import { Grid, Divider, Typography } from "@mui/material"; -import { useForm, useWatch } from "react-hook-form"; +import { useEffect, useState } from "react"; +import { Box, Button, Divider, Typography, Alert } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; @@ -13,16 +14,22 @@ import { CippDataTable } from "../../../../components/CippTable/CippDataTable"; const EditGroup = () => { const router = useRouter(); - const { groupId } = router.query; + const { groupId, groupType } = router.query; + const [groupIdReady, setGroupIdReady] = useState(false); + const [showMembershipTable, setShowMembershipTable] = useState(false); + const [combinedData, setCombinedData] = useState([]); + const [initialValues, setInitialValues] = useState({}); const tenantFilter = useSettings().currentTenant; + const groupInfo = ApiGetCall({ - url: `/api/ListGroups?groupID=${groupId}&tenantFilter=${tenantFilter}&members=true&owners=true`, + url: `/api/ListGroups?groupID=${groupId}&tenantFilter=${tenantFilter}&members=true&owners=true&groupType=${groupType}`, queryKey: `ListGroups-${groupId}`, - waiting: false, + waiting: groupIdReady, }); - const [combinedData, setCombinedData] = useState([]); + useEffect(() => { if (groupId) { + setGroupIdReady(true); groupInfo.refetch(); } }, [router.query, groupId, tenantFilter]); @@ -30,15 +37,21 @@ const EditGroup = () => { const formControl = useForm({ mode: "onChange", defaultValues: { - tenantId: tenantFilter, + tenantFilter: tenantFilter, + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + AddContact: [], + RemoveContact: [], }, }); - const groupType = useWatch({ control: formControl.control, name: "groupType" }); useEffect(() => { if (groupInfo.isSuccess) { const group = groupInfo.data?.groupInfo; if (group) { + // Create combined data for the table const combinedData = [ ...(groupInfo.data?.owners?.map((o) => ({ type: "Owner", @@ -46,19 +59,24 @@ const EditGroup = () => { displayName: o.displayName, })) || []), ...(groupInfo.data?.members?.map((m) => ({ - type: "Member", - userPrincipalName: m.userPrincipalName, + type: m?.["@odata.type"] === "#microsoft.graph.orgContact" ? "Contact" : "Member", + userPrincipalName: m.userPrincipalName ?? m.mail, displayName: m.displayName, })) || []), ]; setCombinedData(combinedData); - formControl.reset({ - tenantId: tenantFilter, + // Create initial values object + const formValues = { + tenantFilter: tenantFilter, mail: group.mail, - allowExternal: group.allowExternal, - sendCopies: group.sendCopies, - groupName: group.displayName, + mailNickname: group.mailNickname || "", + allowExternal: groupInfo?.data?.allowExternal, + sendCopies: groupInfo?.data?.sendCopies, + hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, + displayName: group.displayName, + description: group.description || "", + membershipRules: group.membershipRule || "", groupId: group.id, groupType: (() => { if (group.groupTypes?.includes("Unified")) { @@ -80,132 +98,340 @@ const EditGroup = () => { } return null; })(), + securityEnabled: group.securityEnabled, + // Initialize empty arrays for add/remove actions + AddMember: [], + RemoveMember: [], + AddOwner: [], + RemoveOwner: [], + AddContact: [], + RemoveContact: [], + }; + + // Store initial values for comparison + setInitialValues({ + allowExternal: groupInfo?.data?.allowExternal, + sendCopies: groupInfo?.data?.sendCopies, + hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, + securityEnabled: group.securityEnabled, }); + + // Reset the form with all values + formControl.reset(formValues); } } }, [groupInfo.isSuccess, router.query, groupInfo.isFetching]); + // Custom data formatter to only send changed values + const customDataFormatter = (formData) => { + const cleanedData = { ...formData }; + + // Properties that should only be sent if they've changed from initial values + const changeDetectionProperties = [ + "allowExternal", + "sendCopies", + "hideFromOutlookClients", + "securityEnabled", + ]; + + changeDetectionProperties.forEach((property) => { + if (formData[property] === initialValues[property]) { + delete cleanedData[property]; + } + }); + + return cleanedData; + }; + return ( - - - - Add - - - - + <> + + + + } + > + {groupInfo.isSuccess && groupInfo.data?.groupInfo?.onPremisesSyncEnabled && ( + + This group is synced from on-premises Active Directory. Changes should be made in the + on-premises environment instead. + + )} + {showMembershipTable ? ( + + + + ) : ( + + + + Group Properties + + + + + + + + + + - {/* AddOwners */} - - `${option.displayName} (${option.userPrincipalName})`} - valueField="userPrincipalName" - /> - - - - - - - Remove - - ({ - label: `${m.displayName} (${m.userPrincipalName})`, - value: m.userPrincipalName, - }))} - name="RemoveMember" - label="Remove Member" - multiple={true} - /> - + {groupInfo.data?.groupInfo?.groupTypes?.includes("DynamicMembership") && ( + + + + )} - {/* RemoveOwners */} - - ({ - label: `${o.displayName} (${o.userPrincipalName})`, - value: o.userPrincipalName, - }))} - formControl={formControl} - name="RemoveOwner" - label="Remove Owner" - multiple={true} - /> - - - - + + + Add Members + + + + + !groupInfo.data?.members?.some((m) => m.id === option.value) + } + /> + - - {(groupType === "Microsoft 365" || groupType === "Distribution List") && ( - + + + !groupInfo.data?.owners?.some((o) => o.id === option.value) + } + /> + + + + + !groupInfo.data?.members + ?.filter((m) => m?.["@odata.type"] === "#microsoft.graph.orgContact") + ?.some((c) => c.id === option.value) + } + /> + + + + + Remove Members + + + m?.["@odata.type"] !== "#microsoft.graph.orgContact") + ?.map((m) => ({ + label: `${m.displayName} (${m.userPrincipalName})`, + value: m.id, + addedFields: { + userPrincipalName: m.userPrincipalName, + displayName: m.displayName, + id: m.id, + }, + })) || [] + } /> - )} - {groupType === "Microsoft 365" && ( - + ({ + label: `${o.displayName} (${o.userPrincipalName})`, + value: o.id, + addedFields: { + userPrincipalName: o.userPrincipalName, + displayName: o.displayName, + id: o.id, + }, + })) || [] + } /> - )} - - - - - - - + + + m?.["@odata.type"] === "#microsoft.graph.orgContact") + ?.map((m) => ({ + label: `${m.displayName} (${m.mail})`, + value: m.mail, + addedFields: { id: m.id }, + })) || [] + } + /> + + + + + Group Settings + + {(groupType === "Microsoft 365" || groupType === "Distribution List") && ( + + + + )} + + {groupType === "Microsoft 365" && ( + + + + )} + + {groupType === "Microsoft 365" && ( + + + + )} + {groupType === "Microsoft 365" && ( + + + + )} + + + )} + + ); }; diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index 1fb57be03d1e..1851119434a5 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -2,8 +2,16 @@ import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import Link from "next/link"; -import { EyeIcon, LockClosedIcon, LockOpenIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { LockOpen, Visibility, VisibilityOff } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { + Visibility, + VisibilityOff, + GroupAdd, + Edit, + LockOpen, + Lock, + GroupSharp, +} from "@mui/icons-material"; const Page = () => { const pageTitle = "Groups"; @@ -11,18 +19,17 @@ const Page = () => { { //tested label: "Edit Group", - link: "/identity/administration/groups/edit?groupId=[id]", + link: "/identity/administration/groups/edit?groupId=[id]&groupType=[calculatedGroupType]", multiPost: false, - icon: , + icon: , color: "success", }, { label: "Hide from Global Address List", - type: "GET", + type: "POST", url: "/api/ExecGroupsHideFromGAL", icon: , data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", HidefromGAL: true, @@ -33,13 +40,13 @@ const Page = () => { }, { label: "Unhide from Global Address List", - type: "GET", + type: "POST", url: "/api/ExecGroupsHideFromGAL", icon: , data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", + HidefromGAL: false, }, confirmText: "Are you sure you want to unhide this mailbox from the global address list? Remember this will not work if the group is AD Synched.", @@ -47,11 +54,10 @@ const Page = () => { }, { label: "Only allow messages from people inside the organisation", - type: "GET", + type: "POST", url: "/api/ExecGroupsDeliveryManagement", - icon: , + icon: , data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", OnlyAllowInternal: true, @@ -62,21 +68,37 @@ const Page = () => { }, { label: "Allow messages from people inside and outside the organisation", - type: "GET", - icon: , + type: "POST", + icon: , url: "/api/ExecGroupsDeliveryManagement", data: { - TenantFilter: "TenantFilter", ID: "mail", GroupType: "calculatedGroupType", + 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: "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: "Delete Group", - type: "GET", + type: "POST", url: "/api/ExecGroupsDelete", icon: , data: { @@ -106,22 +128,12 @@ const Page = () => { title={pageTitle} cardButton={ <> - } - apiUrl="/api/ListGraphRequest" - apiData={{ - Endpoint: "groups", - $select: - "id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName,assignedLicenses", - $count: true, - $orderby: "displayName", - $top: 999, - manualPagination: true, - }} - apiDataKey="Results" + apiUrl="/api/ListGroups" actions={actions} offCanvas={offCanvas} simpleColumns={[ diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 0efe13f00e24..fd3f8e5ddfe0 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -1,4 +1,5 @@ -import { Box, Divider, Grid } from "@mui/material"; +import { Box, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; @@ -21,15 +22,17 @@ const Page = () => { > - + - + { { label: "New User", value: "create" }, { label: "Existing User", value: "select" }, ]} + required={true} /> @@ -50,41 +54,53 @@ const Page = () => { compareType="is" compareValue="create" > - + - + - + - + { + if (!option?.value) { + return "Domain is required"; + } + return true; + }, + }} /> - + @@ -94,36 +110,59 @@ const Page = () => { compareType="is" compareValue="select" > - - + + - + { + if (!value) { + return "Start date is required"; + } + return true; + }, + }} /> - + { + const startDate = formControl.getValues("startDate"); + if (!value) { + return "End date is required"; + } + if (new Date(value) < new Date(startDate)) { + return "End date must be after start date"; + } + return true; + }, + }} /> - + { name="adminRoles" options={gdaproles.map((role) => ({ label: role.Name, value: role.ObjectId }))} formControl={formControl} + required={true} + validators={{ + validate: (options) => { + if (!options?.length) { + return "At least one role is required"; + } + return true; + }, + }} /> - + { formControl={formControl} /> - + { { label: "Remove Roles", value: "RemoveRoles" }, ]} formControl={formControl} + required={true} + validators={{ + validate: (option) => { + if (!option?.value) { + return "Expiration action is required"; + } + return true; + }, + }} /> - + { @@ -8,7 +9,7 @@ const Page = () => { - @@ -21,6 +22,6 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/identity/administration/offboarding-wizard/index.js b/src/pages/identity/administration/offboarding-wizard/index.js index 5e6dddaf1e6a..62b290dec440 100644 --- a/src/pages/identity/administration/offboarding-wizard/index.js +++ b/src/pages/identity/administration/offboarding-wizard/index.js @@ -30,6 +30,7 @@ const Page = () => { api: { url: "/api/ListGraphRequest", dataKey: "Results", + queryKey: "Users - {tenant}", data: { Endpoint: "users", manualPagination: true, diff --git a/src/pages/identity/administration/risky-users/index.js b/src/pages/identity/administration/risky-users/index.js index 759117480d8b..9fe9b7004690 100644 --- a/src/pages/identity/administration/risky-users/index.js +++ b/src/pages/identity/administration/risky-users/index.js @@ -1,7 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Clear } from "@mui/icons-material"; -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { Clear, Search } from "@mui/icons-material"; const Page = () => { const pageTitle = "Risky Users"; @@ -9,7 +8,7 @@ const Page = () => { const actions = [ { label: "Dismiss Risk", - type: "GET", + type: "POST", icon: , url: "/api/ExecDismissRiskyUser", data: { userId: "id", userDisplayName: "userDisplayName" }, @@ -19,7 +18,7 @@ const Page = () => { { label: "Research Compromised Account", type: "GET", - icon: , + icon: , link: "/identity/administration/users/user/bec?userId=[id]", confirmText: "Are you sure you want to research this compromised account?", multiPost: false, diff --git a/src/pages/identity/administration/users/add.jsx b/src/pages/identity/administration/users/add.jsx index 5d15605280e7..2d06521fafa0 100644 --- a/src/pages/identity/administration/users/add.jsx +++ b/src/pages/identity/administration/users/add.jsx @@ -11,7 +11,7 @@ const Page = () => { const userSettingsDefaults = useSettings(); const formControl = useForm({ - mode: "onChange", + mode: "onBlur", defaultValues: { tenantFilter: userSettingsDefaults.currentTenant, usageLocation: userSettingsDefaults.usageLocation, @@ -36,6 +36,7 @@ const Page = () => { newFields.usageLocation = { label: usageLocation, value: usageLocation }; } newFields.tenantFilter = userSettingsDefaults.currentTenant; + formControl.reset(newFields); } }, [formValues]); @@ -55,7 +56,7 @@ const Page = () => { label="Copy properties from another user" multiple={false} select={ - "id,userPrincipalName,displayName,givenName,surname,mailNickname,jobTitle,department,streetAddress,postalCode,companyName,mobilePhone,businessPhones,usageLocation" + "id,userPrincipalName,displayName,givenName,surname,mailNickname,jobTitle,department,streetAddress,city,state,postalCode,companyName,mobilePhone,businessPhones,usageLocation,office" } addedField={{ groupType: "calculatedGroupType", @@ -68,11 +69,14 @@ const Page = () => { jobTitle: "jobTitle", department: "department", streetAddress: "streetAddress", + city: "city", + state: "state", postalCode: "postalCode", companyName: "companyName", mobilePhone: "mobilePhone", businessPhones: "businessPhones", usageLocation: "usageLocation", + office: "office", }} /> diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js index c03a62bb56cd..69a290b92615 100644 --- a/src/pages/identity/administration/users/index.js +++ b/src/pages/identity/administration/users/index.js @@ -1,17 +1,38 @@ import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { Button } from "@mui/material"; +import { Send, GroupAdd, PersonAdd } from "@mui/icons-material"; import Link from "next/link"; import { useSettings } from "/src/hooks/use-settings.js"; +import { PermissionButton } from "../../../../utils/permissions"; import { CippUserActions } from "/src/components/CippComponents/CippUserActions.jsx"; const Page = () => { const pageTitle = "Users"; const tenant = useSettings().currentTenant; + const cardButtonPermissions = ["Identity.User.ReadWrite"]; + + const filters = [ + { + filterName: "Account Enabled", + value: [{ id: "accountEnabled", value: "Yes" }], + type: "column", + }, + { + filterName: "Account Disabled", + value: [{ id: "accountEnabled", value: "No" }], + type: "column", + }, + { + filterName: "Guest Accounts", + value: [{ id: "userType", value: "Guest" }], + type: "column", + }, + ]; const offCanvas = { extendedInfoFields: [ "createdDateTime", // Created Date (UTC) + "id", // Unique ID "userPrincipalName", // UPN "givenName", // Given Name "surname", // Surname @@ -23,7 +44,7 @@ const Page = () => { "city", // City "department", // Department "onPremisesLastSyncDateTime", // OnPrem Last Sync - "id", // Unique ID + "onPremisesDistinguishedName", // OnPrem DN "otherMails", // Alternate Email Addresses ], actions: CippUserActions(), @@ -35,22 +56,37 @@ const Page = () => { apiUrl="/api/ListGraphRequest" cardButton={ <> - - - + } apiData={{ Endpoint: "users", manualPagination: true, $select: - "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,onPremisesDistinguishedName,officeLocation,onPremisesLastSyncDateTime,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled", + "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", $count: true, $orderby: "displayName", $top: 999, @@ -67,24 +103,7 @@ const Page = () => { "proxyAddresses", "assignedLicenses", ]} - filters={[ - { - filterName: "Account Enabled", - //true or false filters by yes/no - value: [{ id: "accountEnabled", value: "Yes" }], - type: "column", - }, - { - filterName: "Account Disabled", - value: [{ id: "accountEnabled", value: "No" }], - type: "column", - }, - { - filterName: "Guest Accounts", - value: [{ id: "userType", value: "Guest" }], - type: "column", - }, - ]} + filters={filters} /> ); }; diff --git a/src/pages/identity/administration/users/invite.jsx b/src/pages/identity/administration/users/invite.jsx index cd755cfd9909..cf6cabf8d965 100644 --- a/src/pages/identity/administration/users/invite.jsx +++ b/src/pages/identity/administration/users/invite.jsx @@ -1,4 +1,5 @@ -import { Box, Grid } from "@mui/material"; +import { Box } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm } from "react-hook-form"; @@ -19,12 +20,12 @@ const Page = () => { - + diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx new file mode 100644 index 000000000000..8ea240386296 --- /dev/null +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -0,0 +1,611 @@ +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; +import { + Stack, + Typography, + Card, + CardContent, + Chip, + Box, + TextField, + Checkbox, + Button, + Switch, + FormControlLabel, + Autocomplete, +} from "@mui/material"; +import { CippWizardStepButtons } from "/src/components/CippWizard/CippWizardStepButtons"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { Delete } from "@mui/icons-material"; + +// User properties that can be patched +const PATCHABLE_PROPERTIES = [ + { + property: "city", + label: "City", + type: "string", + }, + { + property: "companyName", + label: "Company Name", + type: "string", + }, + { + property: "country", + label: "Country", + type: "string", + }, + { + property: "department", + label: "Department", + type: "string", + }, + { + property: "employeeType", + label: "Employee Type", + type: "string", + }, + { + property: "jobTitle", + label: "Job Title", + type: "string", + }, + { + property: "officeLocation", + label: "Office Location", + type: "string", + }, + { + property: "postalCode", + label: "Postal Code", + type: "string", + }, + { + property: "preferredDataLocation", + label: "Preferred Data Location", + type: "string", + }, + { + property: "preferredLanguage", + label: "Preferred Language", + type: "string", + }, + { + property: "showInAddressList", + label: "Show in Address List", + type: "boolean", + }, + { + property: "state", + label: "State/Province", + type: "string", + }, + { + property: "streetAddress", + label: "Street Address", + type: "string", + }, + { + property: "usageLocation", + label: "Usage Location", + type: "string", + }, +]; + +// Step 1: Display users to be updated +const UsersDisplayStep = (props) => { + const { onNextStep, onPreviousStep, formControl, currentStep, users, onUsersChange } = props; + + const handleRemoveUser = (userToRemove) => { + const updatedUsers = users.filter((user) => user.id !== userToRemove.id); + onUsersChange(updatedUsers); + }; + + // Clean user data without circular references + const tableData = + users?.map((user) => ({ + id: user.id, + displayName: user.displayName, + userPrincipalName: user.userPrincipalName, + jobTitle: user.jobTitle, + department: user.department, + // Only include serializable properties + })) || []; + + const columns = ["displayName", "userPrincipalName", "jobTitle", "department"]; + + // Define actions separately to avoid circular references + const rowActions = [ + { + label: "Remove from List", + icon: , + color: "error", + customFunction: (user) => handleRemoveUser(user), + noConfirm: true, + }, + ]; + + return ( + + + Users to be updated + + The following users will be updated with the properties you select in the next step. You + can remove users from this list if needed. + + + + {users && users.length > 0 ? ( + + ) : ( + + + + No users selected. Please go back and select users from the main table. + + + + )} + + 0 ? onNextStep : undefined} + formControl={formControl} + noNextButton={!users || users.length === 0} + /> + + ); +}; + +// Step 2: Property selection and input +const PropertySelectionStep = (props) => { + const { onNextStep, onPreviousStep, formControl, currentStep } = props; + const [selectedProperties, setSelectedProperties] = useState([]); + + // Register form fields + formControl.register("selectedProperties", { required: true }); + formControl.register("propertyValues", { required: false }); + + const handlePropertyValueChange = (property, value) => { + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = { ...currentValues, [property]: value }; + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + }; + + const renderPropertyInput = (propertyName) => { + const property = PATCHABLE_PROPERTIES.find((p) => p.property === propertyName); + const currentValue = formControl.getValues("propertyValues")?.[propertyName]; + + if (property?.type === "boolean") { + return ( + handlePropertyValueChange(propertyName, e.target.checked)} + /> + } + label={property.label} + key={propertyName} + /> + ); + } + + // Default to text input for string types with consistent styling + return ( + handlePropertyValueChange(propertyName, e.target.value)} + placeholder={`Enter new value for ${property?.label || propertyName}`} + variant="filled" + size="small" + slotProps={{ + inputLabel: { + shrink: true, + sx: { transition: "none" }, + }, + input: { + notched: true, + sx: { + transition: "none", + "& .MuiOutlinedInput-notchedOutline": { + transition: "none", + }, + }, + }, + }} + /> + ); + }; + + return ( + + + Select Properties to update + + Choose which user properties you want to modify and provide the new values. + + + + selectedProperties.includes(prop.property))} + onChange={(event, newValue) => { + // Check if "Select All" was clicked + const selectAllOption = newValue.find((option) => option.isSelectAll); + + if (selectAllOption) { + // If Select All is in the new value, select all properties + const allSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; + const newProperties = allSelected ? [] : PATCHABLE_PROPERTIES.map((p) => p.property); + setSelectedProperties(newProperties); + formControl.setValue("selectedProperties", newProperties); + + // Reset property values when selection changes + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = {}; + newProperties.forEach((prop) => { + if (currentValues[prop]) { + newValues[prop] = currentValues[prop]; + } + }); + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + } else { + // Normal property selection + const newProperties = newValue + .filter((prop) => !prop.isSelectAll) + .map((prop) => prop.property); + setSelectedProperties(newProperties); + formControl.setValue("selectedProperties", newProperties); + + // Reset property values when selection changes + const currentValues = formControl.getValues("propertyValues") || {}; + const newValues = {}; + newProperties.forEach((prop) => { + if (currentValues[prop]) { + newValues[prop] = currentValues[prop]; + } + }); + formControl.setValue("propertyValues", newValues); + formControl.trigger(); + } + }} + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.property === value.property} + size="small" + renderOption={(props, option, { selected }) => { + const isAllSelected = selectedProperties.length === PATCHABLE_PROPERTIES.length; + const isIndeterminate = + selectedProperties.length > 0 && + selectedProperties.length < PATCHABLE_PROPERTIES.length; + + if (option.isSelectAll) { + return ( +
  • + + {option.label} +
  • + ); + } + + return ( +
  • + + {option.label} +
  • + ); + }} + renderInput={(params) => ( + + )} + renderTags={(value, getTagProps) => + value + .filter((option) => !option.isSelectAll) + .map((option, index) => ( + + )) + } + /> + + {selectedProperties.length > 0 && ( + + + + Properties to update + + {selectedProperties.map(renderPropertyInput)} + + + )} + + +
    + ); +}; + +// Step 3: Confirmation +const ConfirmationStep = (props) => { + const { lastStep, formControl, onPreviousStep, currentStep, users } = props; + const formValues = formControl.getValues(); + const { selectedProperties = [], propertyValues = {} } = formValues; + + // Create API call handler for bulk patch + const patchUsersApi = ApiPostCall({ + relatedQueryKeys: ["ListUsers"], + }); + + const handleSubmit = () => { + // Validate that we still have users to patch + if (!users || users.length === 0) { + console.error("No users to patch"); + return; + } + + // Create bulk request data + const patchData = users.map((user) => { + const userData = { + id: user.id, + tenantFilter: user.Tenant || user.tenantFilter, + }; + selectedProperties.forEach((propName) => { + if (propertyValues[propName] !== undefined && propertyValues[propName] !== "") { + userData[propName] = propertyValues[propName]; + } + }); + return userData; + }); + + // Submit to API + patchUsersApi.mutate({ + url: "/api/PatchUser", + data: patchData, + }); + }; + + // Clean user data for table display + const tableData = + users?.map((user) => ({ + id: user.id, + displayName: user.displayName, + userPrincipalName: user.userPrincipalName, + jobTitle: user.jobTitle, + department: user.department, + })) || []; + + const columns = ["displayName", "userPrincipalName", "jobTitle", "department"]; + + return ( + + + Confirm User Updates + + Review the users that will be updated with {selectedProperties.length} selected{" "} + {selectedProperties.length === 1 ? "property" : "properties"}, then click Submit to apply + the changes. + + + + {/* Properties to be updated */} + {selectedProperties.length > 0 && ( + + + + Properties to Update + + + {selectedProperties.map((propName) => { + const property = PATCHABLE_PROPERTIES.find((p) => p.property === propName); + const value = propertyValues[propName]; + const displayValue = + property?.type === "boolean" ? (value ? "Yes" : "No") : value || "Not set"; + + return ( + + + {property?.label || propName}: + + + {displayValue} + + + ); + })} + + + + )} + + {users && users.length > 0 ? ( + + ) : ( + + + + No users to update. Please go back and select users. + + + + )} + + + + + {currentStep > 0 && ( + + )} + + + + ); +}; + +const Page = () => { + const router = useRouter(); + const [users, setUsers] = useState([]); + + // Get users from URL parameters or session storage + useEffect(() => { + try { + if (router.query.users) { + const parsedUsers = JSON.parse(decodeURIComponent(router.query.users)); + setUsers(Array.isArray(parsedUsers) ? parsedUsers : [parsedUsers]); + } else { + // Fallback to session storage + const storedUsers = sessionStorage.getItem("patchWizardUsers"); + if (storedUsers) { + const parsedUsers = JSON.parse(storedUsers); + setUsers(Array.isArray(parsedUsers) ? parsedUsers : [parsedUsers]); + // Clear session storage after use + sessionStorage.removeItem("patchWizardUsers"); + } + } + } catch (error) { + console.error("Error parsing users data:", error); + setUsers([]); + } + }, [router.query.users]); + + const steps = [ + { + title: "Step 1", + description: "Review Users", + component: UsersDisplayStep, + componentProps: { + users: users, + onUsersChange: setUsers, + }, + }, + { + title: "Step 2", + description: "Select Properties", + component: PropertySelectionStep, + }, + { + title: "Step 3", + description: "Confirmation", + component: ConfirmationStep, + componentProps: { + users: users, + }, + }, + ]; + + const initialState = { + selectedProperties: [], + propertyValues: {}, + }; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/users/user/bec.jsx b/src/pages/identity/administration/users/user/bec.jsx index 88619b41fc13..8b3bd33da2b5 100644 --- a/src/pages/identity/administration/users/user/bec.jsx +++ b/src/pages/identity/administration/users/user/bec.jsx @@ -1,17 +1,16 @@ -import React, { use, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; -import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { CheckCircle, Download, Mail } from "@mui/icons-material"; +import { CheckCircle, Download, Mail, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import ReactTimeAgo from "react-time-ago"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { Box, Stack } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import CippRemediationCard from "../../../../../components/CippCards/CippRemediationCard"; import CippButtonCard from "../../../../../components/CippCards/CippButtonCard"; import { SvgIcon, Typography, CircularProgress, Button } from "@mui/material"; @@ -58,7 +57,11 @@ const Page = () => { // Fetch BEC Check result using GUID const becPollingCall = ApiGetCall({ - url: `/api/execBECCheck?GUID=${becInitialCall.data?.GUID}`, + url: `/api/execBECCheck`, + data: { + GUID: becInitialCall.data?.GUID, + tenantFilter: userSettingsDefaults.currentTenant, + }, queryKey: `execBECCheck-polling-${becInitialCall.data?.GUID}`, waiting: false, }); @@ -111,7 +114,7 @@ const Page = () => { const getUserMessage = () => { if (!becPollingCall.data) return null; if (becPollingCall.data.NewUsers && becPollingCall.data.NewUsers.length > 0) { - return "Suspicious new users have been found in the last 14 days. Please review the list below and take action as needed."; + return "New users have been found in the last 14 days. Please review the list below and take action as needed."; } return "No new users found."; }; @@ -126,7 +129,7 @@ const Page = () => { if (hasPotentialBreach) { return "Potential Breach found."; } - return "Suspicious new applications have been found. Please review the list below and take action as needed."; + return "New applications have been found. Please review the list below and take action as needed."; } return "No new applications found."; }; @@ -137,39 +140,52 @@ const Page = () => { becPollingCall.data.MailboxPermissionChanges && becPollingCall.data.MailboxPermissionChanges.length > 0 ) { - return "Suspicious mailbox permission changes have been found."; + return "Mailbox permission changes have been found."; } return "No mailbox permission changes found."; }; + const subtitle = userRequest.isSuccess + ? [ + { + icon: , + text: , + }, + { + icon: , + text: , + }, + { + icon: , + text: ( + <> + Created: + + ), + }, + { + icon: , + text: ( + + ), + }, + ] + : []; + return ( , - text: ( - - ), - }, - { - icon: , - text: ( - <> - Created:{" "} - - - ), - }, - ] - : [] - } + subtitle={subtitle} isFetching={userRequest.isFetching} > {/* Loading State: Show only Remediation Card and Check 1 with Loading Skeleton */} @@ -182,7 +198,7 @@ const Page = () => { > {/* Remediation Card */} - + { /> {/* Check 1 Card with Loading */} - + { > {/* Remediation Card */} - + { /> {/* All Steps */} - + { becPollingCall.data.NewRules.length > 0 && ( - {becPollingCall.data.NewRules.map((rule) => ( - + {becPollingCall.data.NewRules.map((rule, index) => ( + ))} @@ -336,8 +352,9 @@ const Page = () => { becPollingCall.data.NewUsers.length > 0 && ( - {becPollingCall.data.NewUsers.map((user) => ( + {becPollingCall.data.NewUsers.map((user, index) => ( @@ -379,8 +396,9 @@ const Page = () => { becPollingCall.data.AddedApps.length > 0 && ( - {becPollingCall.data.AddedApps.map((app) => ( + {becPollingCall.data.AddedApps.map((app, index) => ( @@ -422,8 +440,9 @@ const Page = () => { becPollingCall.data.MailboxPermissionChanges.length > 0 && ( - {becPollingCall.data.MailboxPermissionChanges.map((permission) => ( + {becPollingCall.data.MailboxPermissionChanges.map((permission, index) => ( @@ -465,8 +484,9 @@ const Page = () => { becPollingCall.data.MFADevices.length > 0 && ( - {becPollingCall.data.MFADevices.map((permission) => ( + {becPollingCall.data.MFADevices.map((permission, index) => ( @@ -507,8 +527,9 @@ const Page = () => { becPollingCall.data.ChangedPasswords.length > 0 && ( - {becPollingCall.data.ChangedPasswords.map((permission) => ( + {becPollingCall.data.ChangedPasswords.map((permission, index) => ( diff --git a/src/pages/identity/administration/users/user/conditional-access.jsx b/src/pages/identity/administration/users/user/conditional-access.jsx index 6bda3f8534c2..a879193b52e1 100644 --- a/src/pages/identity/administration/users/user/conditional-access.jsx +++ b/src/pages/identity/administration/users/user/conditional-access.jsx @@ -4,13 +4,13 @@ import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Mail, Forward } from "@mui/icons-material"; +import { Mail, Fingerprint, Launch } from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import ReactTimeAgo from "react-time-ago"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; -import { Box, Stack, Typography, Button, CircularProgress } from "@mui/material"; -import Grid from "@mui/material/Grid"; +import { Box, Stack, Typography, Button } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import countryList from "/src/data/countryList"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; @@ -41,6 +41,10 @@ const Page = () => { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( @@ -49,6 +53,21 @@ const Page = () => { ), }, + { + icon: , + text: ( + + ), + }, ] : []; @@ -89,7 +108,7 @@ const Page = () => { > {/* Form Section */} - + { ]} formControl={formControl} /> - + - + { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( @@ -339,10 +343,10 @@ const Page = () => { }} > - + - + Latest Logon { const userSettingsDefaults = useSettings(); const router = useRouter(); @@ -25,7 +27,7 @@ const Page = () => { }); const formControl = useForm({ - mode: "onChange", + mode: "onBlur", defaultValues: { tenantFilter: userSettingsDefaults.currentTenant, }, @@ -34,8 +36,16 @@ const Page = () => { useEffect(() => { if (userRequest.isSuccess) { const user = userRequest.data?.[0]; + //if we have userSettingsDefaults.userAttributes set, grab the .label from each userSsettingsDefaults, then set defaultAttributes.${label}.value to user.${label} + let defaultAttributes = {}; + if (userSettingsDefaults.userAttributes) { + userSettingsDefaults.userAttributes.forEach((attribute) => { + defaultAttributes[attribute.label] = { Value: user?.[attribute.label] }; + }); + } formControl.reset({ ...user, + defaultAttributes: defaultAttributes, tenantFilter: userSettingsDefaults.currentTenant, licenses: user.assignedLicenses.map((license) => ({ label: getCippLicenseTranslation([license]), @@ -55,14 +65,33 @@ const Page = () => { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( <> - Created + Created: ), }, + { + icon: , + text: ( + + ), + }, ] : []; @@ -73,6 +102,11 @@ const Page = () => { subtitle={subtitle} isFetching={userRequest.isLoading} > + {userRequest.isSuccess && userRequest.data?.[0]?.onPremisesSyncEnabled && ( + + This user is synced from on-premises Active Directory. Changes should be made in the on-premises environment instead. + + )} { > {userRequest.isLoading && } {userRequest.isSuccess && ( - + + + )} diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 29082e3b6fb8..d4f6014ecb4d 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -4,29 +4,52 @@ import { useRouter } from "next/router"; import { ApiGetCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Check, Error, Mail } from "@mui/icons-material"; +import { + Check, + Error, + Mail, + Fingerprint, + Launch, + Delete, + Star, + CalendarToday, + AlternateEmail, + PersonAdd, + Block, + PlayArrow, +} from "@mui/icons-material"; import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; import tabOptions from "./tabOptions"; import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { Box, Stack } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; import { CippExchangeInfoCard } from "../../../../../components/CippCards/CippExchangeInfoCard"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import CippExchangeSettingsForm from "../../../../../components/CippFormPages/CippExchangeSettingsForm"; import { useForm } from "react-hook-form"; import { Alert, Button, Collapse, CircularProgress, Typography } from "@mui/material"; import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; -import { TrashIcon } from "@heroicons/react/24/outline"; import { CippPropertyListCard } from "../../../../../components/CippCards/CippPropertyListCard"; import { getCippTranslation } from "../../../../../utils/get-cipp-translation"; import { getCippFormatting } from "../../../../../utils/get-cipp-formatting"; +import CippExchangeActions from "../../../../../components/CippComponents/CippExchangeActions"; +import { CippApiDialog } from "../../../../../components/CippComponents/CippApiDialog"; +import { useDialog } from "../../../../../hooks/use-dialog"; +import CippAliasDialog from "../../../../../components/CippComponents/CippAliasDialog"; +import CippMailboxPermissionsDialog from "../../../../../components/CippComponents/CippMailboxPermissionsDialog"; +import CippCalendarPermissionsDialog from "../../../../../components/CippComponents/CippCalendarPermissionsDialog"; const Page = () => { const userSettingsDefaults = useSettings(); const [waiting, setWaiting] = useState(false); const [showDetails, setShowDetails] = useState(false); + const [actionData, setActionData] = useState({ ready: false }); + const createDialog = useDialog(); + const aliasDialog = useDialog(); + const permissionsDialog = useDialog(); + const calendarPermissionsDialog = useDialog(); const router = useRouter(); const { userId } = router.query; @@ -42,11 +65,22 @@ const Page = () => { waiting: waiting, }); const userRequest = ApiGetCall({ - url: `/api/ListUserMailboxDetails?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, + url: `/api/ListUserMailboxDetails?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}&userMail=${graphUserRequest.data?.[0]?.userPrincipalName}`, queryKey: `Mailbox-${userId}`, waiting: waiting, }); + const usersList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `users`, + tenantFilter: userSettingsDefaults.currentTenant, + $select: "id,displayName,userPrincipalName,mail", + $top: 999, + }, + queryKey: `UserNames-${userSettingsDefaults.currentTenant}`, + }); + const oooRequest = ApiGetCall({ url: `/api/ListOoO?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, queryKey: `ooo-${userId}`, @@ -65,6 +99,156 @@ const Page = () => { waiting: waiting, }); + const groupsList = ApiGetCall({ + url: "/api/ListGraphRequest", + data: { + Endpoint: `groups`, + tenantFilter: userSettingsDefaults.currentTenant, + $filter: "securityEnabled eq true and mailEnabled eq true", + $select: "id,displayName,mail,description", + $top: 999, + }, + queryKey: `MailEnabledSecurityGroups-${userSettingsDefaults.currentTenant}`, + }); + + const getPermissionInfo = (userIdentifier, groupsList) => { + // Handle undefined/null cases first + if (!userIdentifier) { + return { + type: "Unknown", + displayName: "Unknown User", + }; + } + + // Handle special built-in cases + if (userIdentifier === "Default" || userIdentifier === "Anonymous") { + return { + type: "System", + displayName: userIdentifier, + }; + } + + // Check if it's a group - handle Exchange's different naming patterns + const matchingGroup = groupsList?.data?.Results?.find((group) => { + // Ensure group properties exist before comparison + if (!group) return false; + + return ( + // Exact match on mail address + (group.mail && group.mail === userIdentifier) || + // Exact match on display name + (group.displayName && group.displayName === userIdentifier) || + // Partial match - permission identifier starts with group display name (handles timestamps) + (group.displayName && userIdentifier.startsWith(group.displayName)) + ); + }); + + if (matchingGroup) { + return { + type: "Group", + displayName: matchingGroup.displayName, // Use clean name from Graph API + }; + } + + // If not a system entity or group, assume it's a user + return { + type: "User", + displayName: userIdentifier, // Keep original for users + }; + }; + + // Define API configurations for the dialogs + const aliasApiConfig = { + type: "POST", + url: "/api/EditUserAliases", + relatedQueryKeys: `ListUsers-${userId}`, + confirmText: "Add the specified proxy addresses to this user?", + customDataformatter: (row, action, formData) => { + return { + id: userId, + tenantFilter: userSettingsDefaults.currentTenant, + AddedAliases: formData?.AddedAliases?.join(",") || "", + userPrincipalName: graphUserRequest?.data?.[0]?.userPrincipalName, + }; + }, + }; + + const permissionsApiConfig = { + type: "POST", + url: "/api/ExecModifyMBPerms", + relatedQueryKeys: `Mailbox-${userId}`, + confirmText: "Add the specified permissions to this mailbox?", + customDataformatter: (row, action, data) => { + const permissions = []; + const { permissions: permissionValues } = data; + const autoMap = permissionValues.AutoMap === undefined ? true : permissionValues.AutoMap; + + // Build permissions array based on form values + if (permissionValues?.AddFullAccess) { + permissions.push({ + UserID: permissionValues.AddFullAccess, + PermissionLevel: "FullAccess", + Modification: "Add", + AutoMap: autoMap, + }); + } + if (permissionValues?.AddSendAs) { + permissions.push({ + UserID: permissionValues.AddSendAs, + PermissionLevel: "SendAs", + Modification: "Add", + }); + } + if (permissionValues?.AddSendOnBehalf) { + permissions.push({ + UserID: permissionValues.AddSendOnBehalf, + PermissionLevel: "SendOnBehalf", + Modification: "Add", + }); + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }; + }, + }; + + const calendarPermissionsApiConfig = { + type: "POST", + url: "/api/ExecModifyCalPerms", + relatedQueryKeys: `CalendarPermissions-${userId}`, + confirmText: "Add the specified permissions to this calendar?", + customDataformatter: (row, action, data) => { + if (!data.UserToGetPermissions || !data.Permissions) return null; + + // Build permission object dynamically + const permission = { + UserID: data.UserToGetPermissions, + PermissionLevel: data.Permissions, + FolderName: calPermissions.data?.[0]?.FolderName ?? "Calendar", + Modification: "Add", + }; + + if (data.CanViewPrivateItems) { + permission.CanViewPrivateItems = true; + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [permission], + }; + }, + }; + + useEffect(() => { + if (permissionsDialog.open) { + usersList.refetch(); + } + }, [permissionsDialog.open]); + useEffect(() => { if (oooRequest.isSuccess) { formControl.setValue("ooo.ExternalMessage", oooRequest.data?.ExternalMessage); @@ -91,8 +275,118 @@ const Page = () => { } }, [userId]); + useEffect(() => { + if (userRequest.isSuccess && userRequest.data?.[0]) { + const currentSettings = userRequest.data[0]; + const forwardingAddress = currentSettings.ForwardingAddress; + const forwardingSmtpAddress = currentSettings.MailboxActionsData?.ForwardingSmtpAddress; + const forwardAndDeliver = currentSettings.ForwardAndDeliver; + + let forwardingType = "disabled"; + let cleanAddress = ""; + + if (forwardingSmtpAddress) { + // External forwarding + forwardingType = "ExternalAddress"; + cleanAddress = forwardingSmtpAddress; + } else if (forwardingAddress) { + // Internal forwarding + forwardingType = "internalAddress"; + cleanAddress = forwardingAddress; + } + + // Set form values + formControl.setValue("forwarding.forwardOption", forwardingType); + formControl.setValue("forwarding.KeepCopy", forwardAndDeliver === true); + + if (forwardingType === "internalAddress") { + formControl.setValue("forwarding.ForwardInternal", cleanAddress); + formControl.setValue("forwarding.ForwardExternal", ""); + } else if (forwardingType === "ExternalAddress") { + formControl.setValue("forwarding.ForwardExternal", cleanAddress); + formControl.setValue("forwarding.ForwardInternal", ""); + } else { + formControl.setValue("forwarding.ForwardInternal", ""); + formControl.setValue("forwarding.ForwardExternal", ""); + } + } + }, [userRequest.isSuccess, userRequest.dataUpdatedAt, formControl]); + const title = graphUserRequest.isSuccess ? graphUserRequest.data?.[0]?.displayName : "Loading..."; + // Create options array for mailbox permissions (no system users) + const mailboxPermissionOptions = useMemo(() => { + const options = []; + + // Add users + if (usersList?.data?.Results) { + usersList.data.Results.forEach((user) => { + options.push({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + type: "user", + }); + }); + } + + // Add mail-enabled security groups + if (groupsList?.data?.Results) { + groupsList.data.Results.forEach((group) => { + options.push({ + value: group.mail, + label: `${group.displayName} (${group.mail})`, + type: "group", + }); + }); + } + + // Sort alphabetically by label + return options.sort((a, b) => a.label.localeCompare(b.label)); + }, [usersList?.data?.Results, groupsList?.data?.Results]); + + // Create options array for calendar permissions (includes system users) + const calendarPermissionOptions = useMemo(() => { + const options = []; + + // Add special system users for calendar permissions + options.push({ + value: "Default", + label: "Default", + type: "system", + }); + + // Add users + if (usersList?.data?.Results) { + usersList.data.Results.forEach((user) => { + options.push({ + value: user.userPrincipalName, + label: `${user.displayName} (${user.userPrincipalName})`, + type: "user", + }); + }); + } + + // Add mail-enabled security groups + if (groupsList?.data?.Results) { + groupsList.data.Results.forEach((group) => { + options.push({ + value: group.mail, + label: `${group.displayName} (${group.mail})`, + type: "group", + }); + }); + } + + // Sort alphabetically by label, but keep system users at the top + return options.sort((a, b) => { + if (a.type === "system" && b.type !== "system") return -1; + if (b.type === "system" && a.type !== "system") return 1; + return a.label.localeCompare(b.label); + }); + }, [usersList?.data?.Results, groupsList?.data?.Results]); + + const isUserGroupLoading = usersList.isFetching || groupsList.isFetching; + const subtitle = graphUserRequest.isSuccess ? [ { @@ -101,19 +395,84 @@ const Page = () => { ), }, + { + icon: , + text: , + }, { icon: , text: ( <> - Created + Created: ), }, + { + icon: , + text: ( + + ), + }, ] : []; const data = userRequest.data?.[0]; + const mailboxPermissionActions = [ + { + label: "Remove Permission", + type: "POST", + icon: , + url: "/api/ExecModifyMBPerms", + customDataformatter: (row, action, formData) => { + var permissions = []; + if (Array.isArray(row)) { + row.forEach((item) => { + // Safely extract original user identifier + const originalUser = item?._raw?.User || item?.User; + if (originalUser) { + // Only add if we have a valid user + permissions.push({ + UserID: originalUser, // Use original identifier for API calls + PermissionLevel: item?.AccessRights || "Unknown", + Modification: "Remove", + }); + } + }); + } else { + // Safely extract original user identifier + const originalUser = row?._raw?.User || row?.User; + if (originalUser) { + // Only add if we have a valid user + permissions.push({ + UserID: originalUser, // Use original identifier for API calls + PermissionLevel: row?.AccessRights || "Unknown", + Modification: "Remove", + }); + } + } + + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }; + }, + confirmText: "Are you sure you want to remove this permission?", + multiPost: false, + relatedQueryKeys: `Mailbox-${userId}`, + }, + ]; + const permissions = [ { id: 1, @@ -126,21 +485,73 @@ const Page = () => { ), }, - text: "Current mailbox permissions", + text: "Mailbox Permissions", subtext: userRequest.data?.[0]?.Permissions?.length !== 0 - ? "Other users have access to this mailbox" - : "No other users have access to this mailbox", + ? "Other users or groups have access to this mailbox" + : "No other users or groups have access to this mailbox", statusColor: "green.main", - //map each of the permissions to a label/value pair, where the label is the user's name and the value is the permission level - propertyItems: - userRequest.data?.[0]?.Permissions?.map((permission) => ({ - label: permission.User, - value: permission.AccessRights, - })) || [], + cardLabelBoxActions: ( + + ), + table: { + title: "Mailbox Permissions", + hideTitle: true, + data: + userRequest.data?.[0]?.Permissions?.map((permission) => { + const userIdentifier = permission?.User; + const permissionInfo = getPermissionInfo(permission.User, groupsList); + return { + User: permissionInfo.displayName, // Show clean name + AccessRights: permission.AccessRights, + Type: permissionInfo.type, + _raw: permission, + }; + }) || [], + refreshFunction: () => userRequest.refetch(), + isFetching: userRequest.isFetching, + simpleColumns: ["User", "AccessRights", "Type"], + actions: mailboxPermissionActions, + offCanvas: { + children: (data) => { + const originalUser = data?._raw?.User || data?.User; + const permissionInfo = getPermissionInfo(originalUser, groupsList); + return ( + + ); + }, + }, + }, }, ]; + // Replace your existing calCard array with this simple version: const calCard = [ { id: 1, @@ -153,30 +564,183 @@ const Page = () => { ), }, - text: "Current Calendar permissions", - subtext: calPermissions.data?.length - ? "Other users have access to this users calendar" - : "No other users have access to this users calendar", + text: "Calendar permissions", + subtext: + calPermissions.data?.length !== 0 + ? "Other users or groups have access to this calendar" + : "No other users or groups have access to this calendar", statusColor: "green.main", - //map each of the permissions to a label/value pair, where the label is the user's name and the value is the permission level - propertyItems: - calPermissions.data?.map((permission) => ({ - label: `${permission.User} - ${permission.FolderName}`, - value: permission.AccessRights.join(", "), - })) || [], + cardLabelBoxActions: ( + + ), + table: { + title: "Calendar Permissions", + hideTitle: true, + data: + calPermissions.data?.map((permission) => { + const userIdentifier = permission?.User; + const permissionInfo = getPermissionInfo(permission.User, groupsList); + return { + User: permissionInfo.displayName, + AccessRights: permission?.AccessRights?.join(", ") || "Unknown", + FolderName: permission?.FolderName || "Unknown", + Type: permissionInfo.type, + _raw: permission, + }; + }) || [], + refreshFunction: () => calPermissions.refetch(), + isFetching: calPermissions.isFetching, + simpleColumns: ["User", "AccessRights", "FolderName", "Type"], + actions: [ + { + label: "Remove Permission", + type: "POST", + icon: , + url: "/api/ExecModifyCalPerms", + customDataformatter: (row, action, formData) => { + var permissions = []; + if (Array.isArray(row)) { + row.forEach((item) => { + const originalUser = item._raw ? item._raw.User : item.User; + permissions.push({ + UserID: originalUser, // Use original identifier for API calls + PermissionLevel: item.AccessRights, + FolderName: item.FolderName, + Modification: "Remove", + }); + }); + } else { + const originalUser = row._raw ? row._raw.User : row.User; + permissions.push({ + UserID: originalUser, // Use original identifier for API calls + PermissionLevel: row.AccessRights, + FolderName: row.FolderName, + Modification: "Remove", + }); + } + return { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: permissions, + }; + }, + confirmText: "Are you sure you want to remove this calendar permission?", + multiPost: false, + relatedQueryKeys: `CalendarPermissions-${userId}`, + condition: (row) => row.User !== "Default" && row.User !== "Anonymous", + }, + ], + offCanvas: { + children: (data) => { + const originalUser = data._raw ? data._raw.User : data.User; + const permissionInfo = getPermissionInfo(originalUser, groupsList); + return ( + , + url: "/api/ExecModifyCalPerms", + data: { + userID: graphUserRequest.data?.[0]?.userPrincipalName, + tenantFilter: userSettingsDefaults.currentTenant, + permissions: [ + { + UserID: originalUser, // Use original identifier for API calls + PermissionLevel: data.AccessRights, + FolderName: data.FolderName, + Modification: "Remove", + }, + ], + }, + confirmText: "Are you sure you want to remove this calendar permission?", + multiPost: false, + relatedQueryKeys: `CalendarPermissions-${userId}`, + }, + ]} + /> + ); + }, + }, + }, }, ]; const mailboxRuleActions = [ + { + label: "Enable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + customDataformatter: (row, action, formData) => { + return { + ruleId: row?.Identity, + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + ruleName: row?.Name, + Enable: true, + }; + }, + condition: (row) => row && !row.Enabled, + confirmText: "Are you sure you want to enable this mailbox rule?", + multiPost: false, + }, + { + label: "Disable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + customDataformatter: (row, action, formData) => { + return { + ruleId: row?.Identity, + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + ruleName: row?.Name, + Disable: true, + }; + }, + condition: (row) => row && row.Enabled, + confirmText: "Are you sure you want to disable this mailbox rule?", + multiPost: false, + }, { label: "Remove Mailbox Rule", - type: "GET", - icon: , + type: "POST", + icon: , url: "/api/ExecRemoveMailboxRule", - data: { - ruleId: "Identity", - ruleName: "Name", - userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + customDataformatter: (row, action, formData) => { + return { + ruleId: row?.Identity, + ruleName: row?.Name, + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + }; }, confirmText: "Are you sure you want to remove this mailbox rule?", multiPost: false, @@ -228,7 +792,149 @@ const Page = () => { cardSx={{ p: 0, m: -2 }} title="Rule Details" propertyItems={properties} - actionItems={mailboxRuleActions} + actionItems={[ + { + label: "Enable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: data?.Identity, + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + ruleName: data?.Name, + Enable: true, + }, + confirmText: "Are you sure you want to enable this mailbox rule?", + multiPost: false, + }, + { + label: "Disable Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecSetMailboxRule", + data: { + ruleId: data?.Identity, + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + ruleName: data?.Name, + Disable: true, + }, + confirmText: "Are you sure you want to disable this mailbox rule?", + multiPost: false, + }, + { + label: "Remove Mailbox Rule", + type: "POST", + icon: , + url: "/api/ExecRemoveMailboxRule", + data: { + ruleId: data?.Identity, + ruleName: data?.Name, + userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, + }, + confirmText: "Are you sure you want to remove this mailbox rule?", + multiPost: false, + relatedQueryKeys: `MailboxRules-${userId}`, + }, + ]} + /> + ); + }, + }, + }, + }, + ]; + + const proxyAddressActions = [ + { + label: "Make Primary", + type: "POST", + icon: , + url: "/api/EditUserAliases", + data: { + id: userId, + tenantFilter: userSettingsDefaults.currentTenant, + MakePrimary: "Address", + }, + confirmText: "Are you sure you want to make this the primary proxy address?", + multiPost: false, + relatedQueryKeys: `ListUsers-${userId}`, + condition: (row) => row && row.Type !== "Primary", + }, + { + label: "Remove Proxy Address", + type: "POST", + icon: , + url: "/api/EditUserAliases", + data: { + id: userId, + tenantFilter: userSettingsDefaults.currentTenant, + RemovedAliases: "Address", + }, + confirmText: "Are you sure you want to remove this proxy address?", + multiPost: false, + relatedQueryKeys: `ListUsers-${userId}`, + condition: (row) => row && row.Type !== "Primary", + }, + ]; + + const proxyAddressesCard = [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: graphUserRequest.isFetching ? ( + + ) : graphUserRequest.data?.[0]?.proxyAddresses?.length > 1 ? ( + + ) : ( + + ), + }, + text: "Proxy Addresses", + subtext: + graphUserRequest.data?.[0]?.proxyAddresses?.length > 1 + ? "Proxy addresses are configured for this user" + : "No proxy addresses configured for this user", + statusColor: "green.main", + cardLabelBoxActions: ( + + ), + table: { + title: "Proxy Addresses", + hideTitle: true, + data: + graphUserRequest.data?.[0]?.proxyAddresses?.map((address) => ({ + Address: address, + Type: address.startsWith("SMTP:") ? "Primary" : "Alias", + })) || [], + refreshFunction: () => graphUserRequest.refetch(), + isFetching: graphUserRequest.isFetching, + simpleColumns: ["Address", "Type"], + actions: proxyAddressActions, + offCanvas: { + children: (data) => { + return ( + ); }, @@ -242,6 +948,8 @@ const Page = () => { tabOptions={tabOptions} title={title} subtitle={subtitle} + actions={CippExchangeActions()} + actionsData={userRequest.data?.[0]?.MailboxActionsData} isFetching={graphUserRequest.isLoading} > @@ -251,11 +959,12 @@ const Page = () => { sx={{ flexGrow: 1, py: 4, + mr: 2, }} > {userRequest?.data?.[0]?.Mailbox?.[0]?.error && ( - + @@ -283,25 +992,35 @@ const Page = () => { "Microsoft.Exchange.Configuration.Tasks.ManagementObjectNotFoundException" ) && ( <> - - + + userRequest.refetch()} + /> - + + { currentSettings={userRequest.data?.[0]} isFetching={userRequest.isFetching} formControl={formControl} + oooRequest={oooRequest} /> @@ -317,6 +1037,55 @@ const Page = () => { )} + {actionData.ready && ( + + )} + + {({ formHook }) => } + + + + {({ formHook }) => ( + + )} + + + + {({ formHook }) => ( + + )} + ); }; diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 984371102e4f..a49b54631195 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -1,15 +1,15 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useSettings } from "/src/hooks/use-settings"; import { useRouter } from "next/router"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; import CippFormSkeleton from "/src/components/CippFormPages/CippFormSkeleton"; import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { AdminPanelSettings, Check, Group, Mail } from "@mui/icons-material"; +import { AdminPanelSettings, Check, Group, Mail, Fingerprint, Launch } 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/material/Grid2"; +import { Grid } from "@mui/system"; import { CippUserInfoCard } from "../../../../../components/CippCards/CippUserInfoCard"; import { SvgIcon, Typography } from "@mui/material"; import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; @@ -24,6 +24,7 @@ const CippMap = dynamic(() => import("/src/components/CippComponents/CippMap"), import { Button, Dialog, DialogTitle, DialogContent, IconButton } from "@mui/material"; import { Close } from "@mui/icons-material"; import { CippPropertyList } from "../../../../../components/CippComponents/CippPropertyList"; +import { CippCodeBlock } from "../../../../../components/CippComponents/CippCodeBlock"; const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { return ( @@ -80,38 +81,52 @@ const Page = () => { }, [userId]); const userRequest = ApiGetCall({ - url: `/api/ListUsers?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, + url: `/api/ListUsers?UserId=${userId}&tenantFilter=${router.query.tenantFilter ?? userSettingsDefaults.currentTenant}`, queryKey: `ListUsers-${userId}`, waiting: waiting, }); - const userMemberOf = ApiGetCall({ - url: "/api/ListGraphRequest", - data: { - Endpoint: `/users/${userId}/memberOf`, - tenantFilter: userSettingsDefaults.currentTenant, - $top: 99, - }, - queryKey: `UserMemberOf-${userId}`, + const userBulkRequest = ApiPostCall({ + urlFromData: true, }); - const MFARequest = ApiGetCall({ - url: "/api/ListGraphRequest", - data: { - Endpoint: `/users/${userId}/authentication/methods`, - tenantFilter: userSettingsDefaults.currentTenant, - noPagination: true, - $top: 99, - }, - queryKey: `MFA-${userId}`, - waiting: waiting, - }); + useEffect(() => { + if (userId && userSettingsDefaults.currentTenant && !userBulkRequest.isSuccess) { + 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", + }, + ], + tenantFilter: userSettingsDefaults.currentTenant, + noPaginateIds: ["signInLogs"], + }, + }); + } + }, [userId, userSettingsDefaults.currentTenant, userBulkRequest.isSuccess]); - const signInLogs = ApiGetCall({ - url: `/api/ListUserSigninLogs?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}&top=1`, - queryKey: `ListSignIns-${userId}`, - waiting: waiting, - }); + 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 signInLogs = signInLogsData?.body?.value || []; + const userMemberOf = userMemberOfData?.body?.value || []; + const mfaDevices = mfaDevicesData?.body?.value || []; // Set the title and subtitle for the layout const title = userRequest.isSuccess ? <>{userRequest.data?.[0]?.displayName} : "Loading..."; @@ -122,14 +137,33 @@ const Page = () => { icon: , text: , }, + { + icon: , + text: , + }, { icon: , text: ( <> - Created + Created: ), }, + { + icon: , + text: ( + + ), + }, ] : []; @@ -140,8 +174,8 @@ const Page = () => { let conditionalAccessPoliciesItems = []; let mfaDevicesItems = []; - if (signInLogs.isSuccess && signInLogs.data && signInLogs.data.length > 0) { - const signInData = signInLogs.data[0]; + if (signInLogs.length > 0) { + const signInData = signInLogs[0]; signInLogItem = { id: 1, @@ -197,7 +231,7 @@ const Page = () => { <> Location - + { ]} /> - + { }, ]; } - } else if (signInLogs.isError) { + } else if (signInLogsData?.status !== 200) { signInLogItem = { id: 1, cardLabelBox: "!", text: "Error loading sign-in logs. Do you have a P1 license?", - subtext: signInLogs.error.message, + subtext: signInLogsData?.error?.message || "Unknown error", statusColor: "error.main", statusText: "Error", propertyItems: [], @@ -328,13 +362,13 @@ const Page = () => { id: 1, cardLabelBox: "!", text: "Error loading conditional access policies. Do you have a P1 license?", - subtext: signInLogs.error.message, + subtext: signInLogsData?.error?.message || "Unknown error", statusColor: "error.main", statusText: "Error", propertyItems: [], }, ]; - } else if (signInLogs.isSuccess && (!signInLogs.data || signInLogs.data.length === 0)) { + } else if (signInLogs.length === 0) { signInLogItem = { id: 1, cardLabelBox: "-", @@ -343,7 +377,21 @@ const Page = () => { "There are no sign-in logs for this user, or you do not have a P1 license to detect this data.", statusColor: "warning.main", statusText: "No Data", - propertyItems: [], + propertyItems: [ + { + label: "Error", + value: signInLogsData?.error?.message || "Unknown error", + }, + { + label: "Inner Error", + value: ( + + ), + }, + ], }; conditionalAccessPoliciesItems = [ @@ -361,16 +409,14 @@ const Page = () => { } // Prepare MFA devices items - if (MFARequest.isSuccess && MFARequest.data) { - const mfaResults = MFARequest.data.Results || []; - + if (mfaDevices.length > 0) { // Exclude password authentication method - const mfaDevices = mfaResults.filter( + const mfaDevicesFiltered = mfaDevices.filter( (method) => method["@odata.type"] !== "#microsoft.graph.passwordAuthenticationMethod" ); - if (mfaDevices.length > 0) { - mfaDevicesItems = mfaDevices.map((device, index) => ({ + if (mfaDevicesFiltered.length > 0) { + mfaDevicesItems = mfaDevicesFiltered.map((device, index) => ({ id: index, cardLabelBox: { cardLabelBoxHeader: , @@ -414,20 +460,37 @@ const Page = () => { }, ]; } - } else if (MFARequest.isError) { + } else if (mfaDevicesData?.status !== 200) { // Error fetching MFA devices mfaDevicesItems = [ { id: 1, cardLabelBox: "!", text: "Error loading MFA devices", - subtext: MFARequest.error.message, + subtext: `Status code: ${mfaDevicesData?.status}`, statusColor: "error.main", statusText: "Error", - propertyItems: [], + propertyItems: [ + { + label: "Error", + value: mfaDevicesData?.body?.error?.message || "Unknown Error", + }, + { + label: "Inner Error", + value: ( + + ), + }, + ], }, ]; - } else if (MFARequest.isSuccess && (!MFARequest.data || !MFARequest.data.Results)) { + } else if (mfaDevices.length === 0) { // No MFA devices data available mfaDevicesItems = [ { @@ -442,7 +505,7 @@ const Page = () => { ]; } - const groupMembershipItems = userMemberOf.isSuccess + const groupMembershipItems = userMemberOf ? [ { id: 1, @@ -461,7 +524,7 @@ const Page = () => { link: "/identity/administration/groups/edit?groupId=[id]", }, ], - data: userMemberOf?.data?.Results.filter( + data: userMemberOf?.filter( (item) => item?.["@odata.type"] === "#microsoft.graph.group" ), simpleColumns: ["displayName", "groupTypes", "securityEnabled", "mailEnabled"], @@ -470,7 +533,7 @@ const Page = () => { ] : []; - const roleMembershipItems = userMemberOf.isSuccess + const roleMembershipItems = userMemberOf ? [ { id: 1, @@ -482,7 +545,7 @@ const Page = () => { table: { title: "Admin Roles", hideTitle: true, - data: userMemberOf?.data?.Results.filter( + data: userMemberOf?.filter( (item) => item?.["@odata.type"] === "#microsoft.graph.directoryRole" ), simpleColumns: ["displayName", "description"], @@ -509,41 +572,41 @@ const Page = () => { }} > - + - + Latest Logon Applied Conditional Access Policies 0 ? true : false} /> Multi-Factor Authentication Devices 0 ? true : false} /> Memberships 0 ? true : false} /> 0 ? true : false} /> diff --git a/src/pages/identity/reports/azure-ad-connect-report/index.js b/src/pages/identity/reports/azure-ad-connect-report/index.js index 529ce0333b4e..97594a13d6ac 100644 --- a/src/pages/identity/reports/azure-ad-connect-report/index.js +++ b/src/pages/identity/reports/azure-ad-connect-report/index.js @@ -9,9 +9,6 @@ const simpleColumns = [ ]; const apiUrl = "/api/ListAzureADConnectStatus"; -const actions = []; // No actions specified in the original code - -const offCanvas = null; // No off-canvas details provided const Page = () => { return ( @@ -21,8 +18,6 @@ const Page = () => { apiData={{ DataToReturn: "AzureADObjectsInError", }} - actions={actions} - offCanvas={offCanvas} simpleColumns={simpleColumns} /> ); diff --git a/src/pages/identity/reports/inactive-users-report/index.js b/src/pages/identity/reports/inactive-users-report/index.js index 8764ec1236c1..b3bd18ea373a 100644 --- a/src/pages/identity/reports/inactive-users-report/index.js +++ b/src/pages/identity/reports/inactive-users-report/index.js @@ -23,7 +23,7 @@ const Page = () => { }, { label: "Block Sign In", - type: "GET", + type: "POST", icon: , url: "/api/ExecDisableUser", data: { ID: "azureAdUserId" }, @@ -32,7 +32,7 @@ const Page = () => { }, { label: "Delete User", - type: "GET", + type: "POST", icon: , url: "/api/RemoveUser", data: { ID: "azureAdUserId" }, diff --git a/src/pages/identity/reports/mfa-report/index.js b/src/pages/identity/reports/mfa-report/index.js index fd18f1bdef0c..206e8337e2ef 100644 --- a/src/pages/identity/reports/mfa-report/index.js +++ b/src/pages/identity/reports/mfa-report/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { LockPerson } from "@mui/icons-material"; const Page = () => { const pageTitle = "MFA Report"; @@ -15,19 +16,71 @@ const Page = () => { "MFAMethods", "CAPolicies", ]; + const filters = [ + { + filterName: "Enabled, licensed users", + value: [ + { id: "AccountEnabled", value: "Yes" }, + { id: "isLicensed", value: "Yes" }, + ], + type: "column", + }, + { + filterName: "Enabled, licensed users missing MFA", + value: [ + { id: "AccountEnabled", value: "Yes" }, + { id: "isLicensed", value: "Yes" }, + { id: "MFARegistration", value: "No" }, + ], + type: "column", + }, + { + filterName: "No MFA methods registered", + value: [{ id: "MFARegistration", value: "No" }], + type: "column", + }, + { + filterName: "MFA methods registered", + value: [{ id: "MFARegistration", value: "Yes" }], + type: "column", + }, + ]; - /* Filters not supported in the current structure, need dev attention for integration. - filterlist: [ - { filterName: 'Enabled users', filter: '"accountEnabled":true' }, - { filterName: 'Non-guest users', filter: 'Complex: UPN notlike #EXT#' }, - { filterName: 'Licensed users', filter: 'Complex: IsLicensed eq true' }, - { filterName: 'Enabled, licensed non-guest users missing MFA', filter: 'Complex: UPN notlike #EXT#; IsLicensed eq true; accountEnabled eq true; MFARegistration ne true' }, - { filterName: 'No MFA methods registered', filter: 'Complex: MFARegistration ne true' }, - { filterName: 'MFA methods registered', filter: 'Complex: MFARegistration eq true' }, - ], - */ + const actions = [ + { + label: "Set Per-User MFA", + type: "POST", + icon: , + url: "/api/ExecPerUserMFA", + data: { userId: "UPN" }, + fields: [ + { + type: "autoComplete", + name: "State", + label: "State", + options: [ + { label: "Enforced", value: "Enforced" }, + { label: "Enabled", value: "Enabled" }, + { label: "Disabled", value: "Disabled" }, + ], + multiple: false, + creatable: false, + }, + ], + confirmText: "Are you sure you want to set per-user MFA for these users?", + multiPost: false, + }, + ]; - return ; + return ( + + ); }; Page.getLayout = (page) => {page}; diff --git a/src/pages/identity/reports/risk-detections/index.js b/src/pages/identity/reports/risk-detections/index.js index f6fe5e6b07da..3c54fc260e31 100644 --- a/src/pages/identity/reports/risk-detections/index.js +++ b/src/pages/identity/reports/risk-detections/index.js @@ -23,8 +23,7 @@ const Page = () => { "userDisplayName", "userPrincipalName", "detectedDateTime", - "location.city", - "location.countryOrRegion", + "location", "ipAddress", "riskLevel", "riskState", @@ -39,8 +38,7 @@ const Page = () => { const simpleColumns = [ "detectedDateTime", "userPrincipalName", - "location.city", - "location.countryOrRegion", + "location", "ipAddress", "riskState", "riskDetail", diff --git a/src/pages/identity/reports/signin-report/index.js b/src/pages/identity/reports/signin-report/index.js index 6304b8fa0da6..38aada551af9 100644 --- a/src/pages/identity/reports/signin-report/index.js +++ b/src/pages/identity/reports/signin-report/index.js @@ -1,14 +1,13 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { useState } from "react"; -import { Button, Grid, TextField, Switch, FormControlLabel } from "@mui/material"; +import { Button, TextField, Switch, FormControlLabel } from "@mui/material"; +import { Grid } from "@mui/system"; import CippButtonCard from "/src/components/CippCards/CippButtonCard"; const Page = () => { const pageTitle = "Sign Ins Report"; const apiUrl = "/api/ListSignIns"; - const actions = []; - const offCanvas = null; const simpleColumns = [ "createdDateTime", "userPrincipalName", @@ -16,6 +15,7 @@ const Page = () => { "authenticationRequirement", "errorCode", "additionalDetails", + "ipAddress", "locationcipp", ]; @@ -43,7 +43,7 @@ const Page = () => { const tableFilter = ( - + { fullWidth /> - + { fullWidth /> - + { /> {filterValues.failedLogonsOnly && ( - + { /> )} - + @@ -102,8 +102,6 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} apiData={appliedFilters} - actions={actions} - offCanvas={offCanvas} simpleColumns={simpleColumns} queryKey={`ListSignIns-${JSON.stringify(appliedFilters)}`} /> diff --git a/src/pages/index.js b/src/pages/index.js index 0c60b06fe5a2..ef5e04907619 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,6 +1,7 @@ import Head from "next/head"; import { useEffect, useState } from "react"; -import { Box, Container, Grid, Button } from "@mui/material"; +import { Box, Container, Button, Card, CardContent, Tooltip } from "@mui/material"; +import { Grid } from "@mui/system"; import { CippInfoBar } from "../components/CippCards/CippInfoBar"; import { CippChartCard } from "../components/CippCards/CippChartCard"; import { CippPropertyListCard } from "../components/CippCards/CippPropertyListCard"; @@ -12,10 +13,13 @@ import { BulkActionsMenu } from "../components/bulk-actions-menu.js"; import { CippUniversalSearch } from "../components/CippCards/CippUniversalSearch.jsx"; import { ApiGetCall } from "../api/ApiCall.jsx"; import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClipboard.jsx"; +import { ExecutiveReportButton } from "../components/ExecutiveReportButton.js"; +import { CippStandardsDialog } from "../components/CippCards/CippStandardsDialog.jsx"; const Page = () => { const { currentTenant } = useSettings(); const [domainVisible, setDomainVisible] = useState(false); + const [standardsDialogOpen, setStandardsDialogOpen] = useState(false); const organization = ApiGetCall({ url: "/api/ListOrg", @@ -80,7 +84,12 @@ const Page = () => { name: "Default Domain", data: ( <> - + domain.isDefault === true)?.name + } + type="chip" + /> ), }, @@ -158,6 +167,13 @@ const Page = () => { const [PortalMenuItems, setPortalMenuItems] = useState([]); + const formatStorageSize = (sizeInMB) => { + if (sizeInMB >= 1024) { + return `${(sizeInMB / 1024).toFixed(2)}GB`; + } + return `${sizeInMB}MB`; + }; + useEffect(() => { if (currentTenantInfo.isSuccess) { const tenantLookup = currentTenantInfo.data?.find( @@ -167,6 +183,7 @@ const Page = () => { label: portal.label, target: "_blank", link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), + icon: portal.icon, })); setPortalMenuItems(menuItems); } @@ -180,20 +197,40 @@ const Page = () => { - - - - - + + + + + + + {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} + + + + - + - + { /> - - + + + setStandardsDialogOpen(true)} + /> + - + { Number(sharepoint.data?.GeoUsedStorageMB) || 0, ]} labels={[ - `Free (${ + `Free (${formatStorageSize( sharepoint.data?.TenantStorageMB - sharepoint.data?.GeoUsedStorageMB - }MB)`, - `Used (${Number(sharepoint.data?.GeoUsedStorageMB)}MB)`, + )})`, + `Used (${formatStorageSize(sharepoint.data?.GeoUsedStorageMB)})`, ]} /> {/* Converted Domain Names to Property List */} - + { /> - + { /> - + { + + setStandardsDialogOpen(false)} + standardsData={standards.data} + currentTenant={currentTenant} + /> ); }; diff --git a/src/pages/license.js b/src/pages/license.js new file mode 100644 index 000000000000..442aede25829 --- /dev/null +++ b/src/pages/license.js @@ -0,0 +1,714 @@ +import { Container } from "@mui/system"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { Link } from "@mui/material"; + +const Page = () => { + const pageTitle = "License"; + + return ( + +

    GNU AFFERO GENERAL PUBLIC LICENSE

    +

    Version 3, 19 November 2007

    + +

    + Copyright © 2007 Free Software Foundation, Inc. < + https://fsf.org/> +
    + Everyone is permitted to copy and distribute verbatim copies of this license document, but + changing it is not allowed. +

    + +

    Preamble

    + +

    + The GNU Affero General Public License is a free, copyleft license for software and other + kinds of works, specifically designed to ensure cooperation with the community in the case + of network server software. +

    + +

    + The licenses for most software and other practical works are designed to take away your + freedom to share and change the works. By contrast, our General Public Licenses are intended + to guarantee your freedom to share and change all versions of a program--to make sure it + remains free software for all its users. +

    + +

    + When we speak of free software, we are referring to freedom, not price. Our General Public + Licenses are designed to make sure that you have the freedom to distribute copies of free + software (and charge for them if you wish), that you receive source code or can get it if + you want it, that you can change the software or use pieces of it in new free programs, and + that you know you can do these things. +

    + +

    + Developers that use our General Public Licenses protect your rights with two steps: (1) + assert copyright on the software, and (2) offer you this License which gives you legal + permission to copy, distribute and/or modify the software. +

    + +

    + A secondary benefit of defending all users' freedom is that improvements made in + alternate versions of the program, if they receive widespread use, become available for + other developers to incorporate. Many developers of free software are heartened and + encouraged by the resulting cooperation. However, in the case of software used on network + servers, this result may fail to come about. The GNU General Public License permits making a + modified version and letting the public access it on a server without ever releasing its + source code to the public. +

    + +

    + The GNU Affero General Public License is designed specifically to ensure that, in such + cases, the modified source code becomes available to the community. It requires the operator + of a network server to provide the source code of the modified version running there to the + users of that server. Therefore, public use of a modified version, on a publicly accessible + server, gives the public access to the source code of the modified version. +

    + +

    + An older license, called the Affero General Public License and published by Affero, was + designed to accomplish similar goals. This is a different license, not a version of the + Affero GPL, but Affero has released a new version of the Affero GPL which permits + relicensing under this license. +

    + +

    The precise terms and conditions for copying, distribution and modification follow.

    + +

    TERMS AND CONDITIONS

    + +

    0. Definitions.

    + +

    "This License" refers to version 3 of the GNU Affero General Public License.

    + +

    + "Copyright" also means copyright-like laws that apply to other kinds of works, + such as semiconductor masks. +

    + +

    + "The Program" refers to any copyrightable work licensed under this License. Each + licensee is addressed as "you". "Licensees" and "recipients" + may be individuals or organizations. +

    + +

    + To "modify" a work means to copy from or adapt all or part of the work in a + fashion requiring copyright permission, other than the making of an exact copy. The + resulting work is called a "modified version" of the earlier work or a work + "based on" the earlier work. +

    + +

    + A "covered work" means either the unmodified Program or a work based on the + Program. +

    + +

    + To "propagate" a work means to do anything with it that, without permission, would + make you directly or secondarily liable for infringement under applicable copyright law, + except executing it on a computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the public, and in some + countries other activities as well. +

    + +

    + To "convey" a work means any kind of propagation that enables other parties to + make or receive copies. Mere interaction with a user through a computer network, with no + transfer of a copy, is not conveying. +

    + +

    + An interactive user interface displays "Appropriate Legal Notices" to the extent + that it includes a convenient and prominently visible feature that (1) displays an + appropriate copyright notice, and (2) tells the user that there is no warranty for the work + (except to the extent that warranties are provided), that licensees may convey the work + under this License, and how to view a copy of this License. If the interface presents a list + of user commands or options, such as a menu, a prominent item in the list meets this + criterion. +

    + +

    1. Source Code.

    + +

    + The "source code" for a work means the preferred form of the work for making + modifications to it. "Object code" means any non-source form of a work. +

    + +

    + A "Standard Interface" means an interface that either is an official standard + defined by a recognized standards body, or, in the case of interfaces specified for a + particular programming language, one that is widely used among developers working in that + language. +

    + +

    + The "System Libraries" of an executable work include anything, other than the work + as a whole, that (a) is included in the normal form of packaging a Major Component, but + which is not part of that Major Component, and (b) serves only to enable use of the work + with that Major Component, or to implement a Standard Interface for which an implementation + is available to the public in source code form. A "Major Component", in this + context, means a major essential component (kernel, window system, and so on) of the + specific operating system (if any) on which the executable work runs, or a compiler used to + produce the work, or an object code interpreter used to run it. +

    + +

    + The "Corresponding Source" for a work in object code form means all the source + code needed to generate, install, and (for an executable work) run the object code and to + modify the work, including scripts to control those activities. However, it does not include + the work's System Libraries, or general-purpose tools or generally available free + programs which are used unmodified in performing those activities but which are not part of + the work. For example, Corresponding Source includes interface definition files associated + with source files for the work, and the source code for shared libraries and dynamically + linked subprograms that the work is specifically designed to require, such as by intimate + data communication or control flow between those subprograms and other parts of the work. +

    + +

    + The Corresponding Source need not include anything that users can regenerate automatically + from other parts of the Corresponding Source. +

    + +

    The Corresponding Source for a work in source code form is that same work.

    + +

    2. Basic Permissions.

    + +

    + All rights granted under this License are granted for the term of copyright on the Program, + and are irrevocable provided the stated conditions are met. This License explicitly affirms + your unlimited permission to run the unmodified Program. The output from running a covered + work is covered by this License only if the output, given its content, constitutes a covered + work. This License acknowledges your rights of fair use or other equivalent, as provided by + copyright law. +

    + +

    + You may make, run and propagate covered works that you do not convey, without conditions so + long as your license otherwise remains in force. You may convey covered works to others for + the sole purpose of having them make modifications exclusively for you, or provide you with + facilities for running those works, provided that you comply with the terms of this License + in conveying all material for which you do not control copyright. Those thus making or + running the covered works for you must do so exclusively on your behalf, under your + direction and control, on terms that prohibit them from making any copies of your + copyrighted material outside their relationship with you. +

    + +

    + Conveying under any other circumstances is permitted solely under the conditions stated + below. Sublicensing is not allowed; section 10 makes it unnecessary. +

    + +

    3. Protecting Users' Legal Rights From Anti-Circumvention Law.

    + +

    + No covered work shall be deemed part of an effective technological measure under any + applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted + on 20 December 1996, or similar laws prohibiting or restricting circumvention of such + measures. +

    + +

    + When you convey a covered work, you waive any legal power to forbid circumvention of + technological measures to the extent such circumvention is effected by exercising rights + under this License with respect to the covered work, and you disclaim any intention to limit + operation or modification of the work as a means of enforcing, against the work's + users, your or third parties' legal rights to forbid circumvention of technological + measures. +

    + +

    4. Conveying Verbatim Copies.

    + +

    + You may convey verbatim copies of the Program's source code as you receive it, in any + medium, provided that you conspicuously and appropriately publish on each copy an + appropriate copyright notice; keep intact all notices stating that this License and any + non-permissive terms added in accord with section 7 apply to the code; keep intact all + notices of the absence of any warranty; and give all recipients a copy of this License along + with the Program. +

    + +

    + You may charge any price or no price for each copy that you convey, and you may offer + support or warranty protection for a fee. +

    + +

    5. Conveying Modified Source Versions.

    + +

    + You may convey a work based on the Program, or the modifications to produce it from the + Program, in the form of source code under the terms of section 4, provided that you also + meet all of these conditions: +

    + +
      +
    • + a) The work must carry prominent notices stating that you modified it, and giving a + relevant date. +
    • + +
    • + b) The work must carry prominent notices stating that it is released under this License + and any conditions added under section 7. This requirement modifies the requirement in + section 4 to "keep intact all notices". +
    • + +
    • + c) You must license the entire work, as a whole, under this License to anyone who comes + into possession of a copy. This License will therefore apply, along with any applicable + section 7 additional terms, to the whole of the work, and all its parts, regardless of how + they are packaged. This License gives no permission to license the work in any other way, + but it does not invalidate such permission if you have separately received it. +
    • + +
    • + d) If the work has interactive user interfaces, each must display Appropriate Legal + Notices; however, if the Program has interactive interfaces that do not display + Appropriate Legal Notices, your work need not make them do so. +
    • +
    + +

    + A compilation of a covered work with other separate and independent works, which are not by + their nature extensions of the covered work, and which are not combined with it such as to + form a larger program, in or on a volume of a storage or distribution medium, is called an + "aggregate" if the compilation and its resulting copyright are not used to limit + the access or legal rights of the compilation's users beyond what the individual works + permit. Inclusion of a covered work in an aggregate does not cause this License to apply to + the other parts of the aggregate. +

    + +

    6. Conveying Non-Source Forms.

    + +

    + You may convey a covered work in object code form under the terms of sections 4 and 5, + provided that you also convey the machine-readable Corresponding Source under the terms of + this License, in one of these ways: +

    + +
      +
    • + a) Convey the object code in, or embodied in, a physical product (including a physical + distribution medium), accompanied by the Corresponding Source fixed on a durable physical + medium customarily used for software interchange. +
    • + +
    • + b) Convey the object code in, or embodied in, a physical product (including a physical + distribution medium), accompanied by a written offer, valid for at least three years and + valid for as long as you offer spare parts or customer support for that product model, to + give anyone who possesses the object code either (1) a copy of the Corresponding Source + for all the software in the product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no more than your reasonable + cost of physically performing this conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. +
    • + +
    • + c) Convey individual copies of the object code with a copy of the written offer to provide + the Corresponding Source. This alternative is allowed only occasionally and + noncommercially, and only if you received the object code with such an offer, in accord + with subsection 6b. +
    • + +
    • + d) Convey the object code by offering access from a designated place (gratis or for a + charge), and offer equivalent access to the Corresponding Source in the same way through + the same place at no further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to copy the object code is a + network server, the Corresponding Source may be on a different server (operated by you or + a third party) that supports equivalent copying facilities, provided you maintain clear + directions next to the object code saying where to find the Corresponding Source. + Regardless of what server hosts the Corresponding Source, you remain obligated to ensure + that it is available for as long as needed to satisfy these requirements. +
    • + +
    • + e) Convey the object code using peer-to-peer transmission, provided you inform other peers + where the object code and Corresponding Source of the work are being offered to the + general public at no charge under subsection 6d. +
    • +
    + +

    + A separable portion of the object code, whose source code is excluded from the Corresponding + Source as a System Library, need not be included in conveying the object code work. +

    + +

    + A "User Product" is either (1) a "consumer product", which means any + tangible personal property which is normally used for personal, family, or household + purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining + whether a product is a consumer product, doubtful cases shall be resolved in favor of + coverage. For a particular product received by a particular user, "normally used" + refers to a typical or common use of that class of product, regardless of the status of the + particular user or of the way in which the particular user actually uses, or expects or is + expected to use, the product. A product is a consumer product regardless of whether the + product has substantial commercial, industrial or non-consumer uses, unless such uses + represent the only significant mode of use of the product. +

    + +

    + "Installation Information" for a User Product means any methods, procedures, + authorization keys, or other information required to install and execute modified versions + of a covered work in that User Product from a modified version of its Corresponding Source. + The information must suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because modification has been made. +

    + +

    + If you convey an object code work under this section in, or with, or specifically for use + in, a User Product, and the conveying occurs as part of a transaction in which the right of + possession and use of the User Product is transferred to the recipient in perpetuity or for + a fixed term (regardless of how the transaction is characterized), the Corresponding Source + conveyed under this section must be accompanied by the Installation Information. But this + requirement does not apply if neither you nor any third party retains the ability to install + modified object code on the User Product (for example, the work has been installed in ROM). +

    + +

    + The requirement to provide Installation Information does not include a requirement to + continue to provide support service, warranty, or updates for a work that has been modified + or installed by the recipient, or for the User Product in which it has been modified or + installed. Access to a network may be denied when the modification itself materially and + adversely affects the operation of the network or violates the rules and protocols for + communication across the network. +

    + +

    + Corresponding Source conveyed, and Installation Information provided, in accord with this + section must be in a format that is publicly documented (and with an implementation + available to the public in source code form), and must require no special password or key + for unpacking, reading or copying. +

    + +

    7. Additional Terms.

    + +

    + "Additional permissions" are terms that supplement the terms of this License by + making exceptions from one or more of its conditions. Additional permissions that are + applicable to the entire Program shall be treated as though they were included in this + License, to the extent that they are valid under applicable law. If additional permissions + apply only to part of the Program, that part may be used separately under those permissions, + but the entire Program remains governed by this License without regard to the additional + permissions. +

    + +

    + When you convey a copy of a covered work, you may at your option remove any additional + permissions from that copy, or from any part of it. (Additional permissions may be written + to require their own removal in certain cases when you modify the work.) You may place + additional permissions on material, added by you to a covered work, for which you have or + can give appropriate copyright permission. +

    + +

    + Notwithstanding any other provision of this License, for material you add to a covered work, + you may (if authorized by the copyright holders of that material) supplement the terms of + this License with terms: +

    + +
      +
    • + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 + and 16 of this License; or +
    • + +
    • + b) Requiring preservation of specified reasonable legal notices or author attributions in + that material or in the Appropriate Legal Notices displayed by works containing it; or +
    • + +
    • + c) Prohibiting misrepresentation of the origin of that material, or requiring that + modified versions of such material be marked in reasonable ways as different from the + original version; or +
    • + +
    • + d) Limiting the use for publicity purposes of names of licensors or authors of the + material; or +
    • + +
    • + e) Declining to grant rights under trademark law for use of some trade names, trademarks, + or service marks; or +
    • + +
    • + f) Requiring indemnification of licensors and authors of that material by anyone who + conveys the material (or modified versions of it) with contractual assumptions of + liability to the recipient, for any liability that these contractual assumptions directly + impose on those licensors and authors. +
    • +
    + +

    + All other non-permissive additional terms are considered "further restrictions" + within the meaning of section 10. If the Program as you received it, or any part of it, + contains a notice stating that it is governed by this License along with a term that is a + further restriction, you may remove that term. If a license document contains a further + restriction but permits relicensing or conveying under this License, you may add to a + covered work material governed by the terms of that license document, provided that the + further restriction does not survive such relicensing or conveying. +

    + +

    + If you add terms to a covered work in accord with this section, you must place, in the + relevant source files, a statement of the additional terms that apply to those files, or a + notice indicating where to find the applicable terms. +

    + +

    + Additional terms, permissive or non-permissive, may be stated in the form of a separately + written license, or stated as exceptions; the above requirements apply either way. +

    + +

    8. Termination.

    + +

    + You may not propagate or modify a covered work except as expressly provided under this + License. Any attempt otherwise to propagate or modify it is void, and will automatically + terminate your rights under this License (including any patent licenses granted under the + third paragraph of section 11). +

    + +

    + However, if you cease all violation of this License, then your license from a particular + copyright holder is reinstated (a) provisionally, unless and until the copyright holder + explicitly and finally terminates your license, and (b) permanently, if the copyright holder + fails to notify you of the violation by some reasonable means prior to 60 days after the + cessation. +

    + +

    + Moreover, your license from a particular copyright holder is reinstated permanently if the + copyright holder notifies you of the violation by some reasonable means, this is the first + time you have received notice of violation of this License (for any work) from that + copyright holder, and you cure the violation prior to 30 days after your receipt of the + notice. +

    + +

    + Termination of your rights under this section does not terminate the licenses of parties who + have received copies or rights from you under this License. If your rights have been + terminated and not permanently reinstated, you do not qualify to receive new licenses for + the same material under section 10. +

    + +

    9. Acceptance Not Required for Having Copies.

    + +

    + You are not required to accept this License in order to receive or run a copy of the + Program. Ancillary propagation of a covered work occurring solely as a consequence of using + peer-to-peer transmission to receive a copy likewise does not require acceptance. However, + nothing other than this License grants you permission to propagate or modify any covered + work. These actions infringe copyright if you do not accept this License. Therefore, by + modifying or propagating a covered work, you indicate your acceptance of this License to do + so. +

    + +

    10. Automatic Licensing of Downstream Recipients.

    + +

    + Each time you convey a covered work, the recipient automatically receives a license from the + original licensors, to run, modify and propagate that work, subject to this License. You are + not responsible for enforcing compliance by third parties with this License. +

    + +

    + An "entity transaction" is a transaction transferring control of an organization, + or substantially all assets of one, or subdividing an organization, or merging + organizations. If propagation of a covered work results from an entity transaction, each + party to that transaction who receives a copy of the work also receives whatever licenses to + the work the party's predecessor in interest had or could give under the previous + paragraph, plus a right to possession of the Corresponding Source of the work from the + predecessor in interest, if the predecessor has it or can get it with reasonable efforts. +

    + +

    + You may not impose any further restrictions on the exercise of the rights granted or + affirmed under this License. For example, you may not impose a license fee, royalty, or + other charge for exercise of rights granted under this License, and you may not initiate + litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent + claim is infringed by making, using, selling, offering for sale, or importing the Program or + any portion of it. +

    + +

    11. Patents.

    + +

    + A "contributor" is a copyright holder who authorizes use under this License of the + Program or a work on which the Program is based. The work thus licensed is called the + contributor's "contributor version". +

    + +

    + A contributor's "essential patent claims" are all patent claims owned or + controlled by the contributor, whether already acquired or hereafter acquired, that would be + infringed by some manner, permitted by this License, of making, using, or selling its + contributor version, but do not include claims that would be infringed only as a consequence + of further modification of the contributor version. For purposes of this definition, + "control" includes the right to grant patent sublicenses in a manner consistent + with the requirements of this License. +

    + +

    + Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under + the contributor's essential patent claims, to make, use, sell, offer for sale, import + and otherwise run, modify and propagate the contents of its contributor version. +

    + +

    + In the following three paragraphs, a "patent license" is any express agreement or + commitment, however denominated, not to enforce a patent (such as an express permission to + practice a patent or covenant not to sue for patent infringement). To "grant" such + a patent license to a party means to make such an agreement or commitment not to enforce a + patent against the party. +

    + +

    + If you convey a covered work, knowingly relying on a patent license, and the Corresponding + Source of the work is not available for anyone to copy, free of charge and under the terms + of this License, through a publicly available network server or other readily accessible + means, then you must either (1) cause the Corresponding Source to be so available, or (2) + arrange to deprive yourself of the benefit of the patent license for this particular work, + or (3) arrange, in a manner consistent with the requirements of this License, to extend the + patent license to downstream recipients. "Knowingly relying" means you have actual + knowledge that, but for the patent license, your conveying the covered work in a country, or + your recipient's use of the covered work in a country, would infringe one or more + identifiable patents in that country that you have reason to believe are valid. +

    + +

    + If, pursuant to or in connection with a single transaction or arrangement, you convey, or + propagate by procuring conveyance of, a covered work, and grant a patent license to some of + the parties receiving the covered work authorizing them to use, propagate, modify or convey + a specific copy of the covered work, then the patent license you grant is automatically + extended to all recipients of the covered work and works based on it. +

    + +

    + A patent license is "discriminatory" if it does not include within the scope of + its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or + more of the rights that are specifically granted under this License. You may not convey a + covered work if you are a party to an arrangement with a third party that is in the business + of distributing software, under which you make payment to the third party based on the + extent of your activity of conveying the work, and under which the third party grants, to + any of the parties who would receive the covered work from you, a discriminatory patent + license (a) in connection with copies of the covered work conveyed by you (or copies made + from those copies), or (b) primarily for and in connection with specific products or + compilations that contain the covered work, unless you entered into that arrangement, or + that patent license was granted, prior to 28 March 2007. +

    + +

    + Nothing in this License shall be construed as excluding or limiting any implied license or + other defenses to infringement that may otherwise be available to you under applicable + patent law. +

    + +

    12. No Surrender of Others' Freedom.

    + +

    + If conditions are imposed on you (whether by court order, agreement or otherwise) that + contradict the conditions of this License, they do not excuse you from the conditions of + this License. If you cannot convey a covered work so as to satisfy simultaneously your + obligations under this License and any other pertinent obligations, then as a consequence + you may not convey it at all. For example, if you agree to terms that obligate you to + collect a royalty for further conveying from those to whom you convey the Program, the only + way you could satisfy both those terms and this License would be to refrain entirely from + conveying the Program. +

    + +

    + 13. Remote Network Interaction; Use with the GNU General Public License. +

    + +

    + Notwithstanding any other provision of this License, if you modify the Program, your + modified version must prominently offer all users interacting with it remotely through a + computer network (if your version supports such interaction) an opportunity to receive the + Corresponding Source of your version by providing access to the Corresponding Source from a + network server at no charge, through some standard or customary means of facilitating + copying of software. This Corresponding Source shall include the Corresponding Source for + any work covered by version 3 of the GNU General Public License that is incorporated + pursuant to the following paragraph. +

    + +

    + Notwithstanding any other provision of this License, you have permission to link or combine + any covered work with a work licensed under version 3 of the GNU General Public License into + a single combined work, and to convey the resulting work. The terms of this License will + continue to apply to the part which is the covered work, but the work with which it is + combined will remain governed by version 3 of the GNU General Public License. +

    + +

    14. Revised Versions of this License.

    + +

    + The Free Software Foundation may publish revised and/or new versions of the GNU Affero + General Public License from time to time. Such new versions will be similar in spirit to the + present version, but may differ in detail to address new problems or concerns. +

    + +

    + Each version is given a distinguishing version number. If the Program specifies that a + certain numbered version of the GNU Affero General Public License "or any later + version" applies to it, you have the option of following the terms and conditions + either of that numbered version or of any later version published by the Free Software + Foundation. If the Program does not specify a version number of the GNU Affero General + Public License, you may choose any version ever published by the Free Software Foundation. +

    + +

    + If the Program specifies that a proxy can decide which future versions of the GNU Affero + General Public License can be used, that proxy's public statement of acceptance of a + version permanently authorizes you to choose that version for the Program. +

    + +

    + Later license versions may give you additional or different permissions. However, no + additional obligations are imposed on any author or copyright holder as a result of your + choosing to follow a later version. +

    + +

    15. Disclaimer of Warranty.

    + +

    + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM + "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. + SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR + OR CORRECTION. +

    + +

    16. Limitation of Liability.

    + +

    + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT + HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE + LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL + DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO + LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES + OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR + OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +

    + +

    17. Interpretation of Sections 15 and 16.

    + +

    + If the disclaimer of warranty and limitation of liability provided above cannot be given + local legal effect according to their terms, reviewing courts shall apply local law that + most closely approximates an absolute waiver of all civil liability in connection with the + Program, unless a warranty or assumption of liability accompanies a copy of the Program in + return for a fee. +

    +
    + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/loading.js b/src/pages/loading.js new file mode 100644 index 000000000000..63e65e3c81eb --- /dev/null +++ b/src/pages/loading.js @@ -0,0 +1,70 @@ +import { Box, Container, Stack } from "@mui/material"; +import { Grid } from "@mui/system"; +import Head from "next/head"; +import { CippImageCard } from "../components/CippCards/CippImageCard"; + +import { ApiGetCall } from "../api/ApiCall"; +import { useState, useEffect } from "react"; + +const Page = () => { + const [loadingText, setLoadingText] = useState("Please wait while we log you in..."); + const orgData = ApiGetCall({ + url: "/api/me", + queryKey: "authmecipp", + }); + + const [loadingImage, setLoadingImage] = useState( + "/assets/illustrations/undraw_analysis_dq08.svg" + ); + + useEffect(() => { + const timer = setTimeout(() => { + if (!orgData.isSuccess) { + setLoadingText( + "The function app may be experiencing a cold start currently, this can take a little longer than usual..." + ); + setLoadingImage("/assets/illustrations/undraw-into-the-night-nd84.svg"); + } + }, 20000); // 20 seconds + + return () => clearTimeout(timer); + }, [orgData.isSuccess]); + + return ( + <> + + Loading + + + + + + + + + + + + + + ); +}; + +export default Page; diff --git a/src/pages/onboarding.js b/src/pages/onboarding.js index ffe3018ddd58..ee61bbe28261 100644 --- a/src/pages/onboarding.js +++ b/src/pages/onboarding.js @@ -24,8 +24,7 @@ const Page = () => { value: "CreateApp", }, { - description: - "I would like to refresh my token or replace the user I've used for my previous token.", + description: "I would like to refresh my token or replace the account I've used.", icon: , label: "Refresh Tokens for existing application ", value: "UpdateTokens", diff --git a/src/pages/onboardingv2.js b/src/pages/onboardingv2.js new file mode 100644 index 000000000000..ee7f73445035 --- /dev/null +++ b/src/pages/onboardingv2.js @@ -0,0 +1,120 @@ +import { Layout as DashboardLayout } from "../layouts/index.js"; +import { CippWizardConfirmation } from "../components/CippWizard/CippWizardConfirmation.jsx"; +import { CippDeploymentStep } from "../components/CippWizard/CIPPDeploymentStep.jsx"; +import CippWizardPage from "../components/CippWizard/CippWizardPage.jsx"; +import { CippWizardOptionsList } from "../components/CippWizard/CippWizardOptionsList.jsx"; +import { CippSAMDeploy } from "../components/CippWizard/CippSAMDeploy.jsx"; +import { CippTenantModeDeploy } from "../components/CippWizard/CippTenantModeDeploy.jsx"; +import { CippBaselinesStep } from "../components/CippWizard/CippBaselinesStep.jsx"; +import { CippNotificationsStep } from "../components/CippWizard/CippNotificationsStep.jsx"; +import { CippAlertsStep } from "../components/CippWizard/CippAlertsStep.jsx"; +import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const steps = [ + { + description: "Onboarding", + component: CippWizardOptionsList, + componentProps: { + title: "Select your setup method", + subtext: `This wizard will guide you through setting up CIPPs access to your client tenants. If this is your first time setting up CIPP you will want to choose the option "Create application for me and connect to my tenants",`, + valuesKey: "SyncTool", + options: [ + { + description: + "Choose this option if this is your first setup, or if you'd like to redo the previous setup.", + icon: , + label: "First Setup", + value: "FirstSetup", + }, + { + description: + "Choose this option if you would like to add a tenant to your environment.", + icon: , + label: "Add a tenant", + value: "AddTenant", + }, + { + description: + "Choose this option if you want to setup which application registration is used to connect to your tenants.", + icon: , + label: "Create a new application registration for me and connect to my tenants", + value: "CreateApp", + }, + { + description: "I would like to refresh my token or replace the account I've used.", + icon: , + label: "Refresh Tokens for existing application registration", + value: "UpdateTokens", + }, + { + description: + "I have an existing application and would like to manually enter my token, or update them. This is only recommended for advanced users.", + icon: , + label: "Manually enter credentials", + value: "Manual", + }, + ], + }, + }, + { + description: "Application", + component: CippSAMDeploy, + showStepWhen: (values) => + values?.selectedOption === "CreateApp" || values?.selectedOption === "FirstSetup", + }, + { + description: "Tenants", + component: CippTenantModeDeploy, + showStepWhen: (values) => + values?.selectedOption === "CreateApp" || + values?.selectedOption === "FirstSetup" || + values?.selectedOption === "AddTenant", + }, + { + description: "Baselines", + component: CippBaselinesStep, + showStepWhen: (values) => values?.selectedOption === "FirstSetup", + }, + { + description: "Notifications", + component: CippNotificationsStep, + showStepWhen: (values) => values?.selectedOption === "FirstSetup", + }, + { + description: "Next Steps", + component: CippAlertsStep, + showStepWhen: (values) => values?.selectedOption === "FirstSetup", + }, + { + description: "Refresh Tokens", + component: CippDeploymentStep, + showStepWhen: (values) => values?.selectedOption === "UpdateTokens", + }, + { + description: "Manually enter credentials", + component: CippDeploymentStep, + showStepWhen: (values) => values?.selectedOption === "Manual", + }, + { + description: "Confirmation", + component: CippWizardConfirmation, + //confirm and finish button, perform tasks, launch checks etc. + }, + ]; + + return ( + <> + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/security/defender/deployment/index.js b/src/pages/security/defender/deployment/index.js index 8a3007ec7a99..c429c3107e45 100644 --- a/src/pages/security/defender/deployment/index.js +++ b/src/pages/security/defender/deployment/index.js @@ -1,11 +1,12 @@ -import React from "react"; -import { Grid, Typography, Divider } from "@mui/material"; -import { useForm, useWatch } from "react-hook-form"; +import { Typography, Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; +import { CippFormInputArray } from "/src/components/CippComponents/CippFormInputArray"; const DeployDefenderForm = () => { const formControl = useForm({ @@ -25,13 +26,14 @@ const DeployDefenderForm = () => { - + @@ -39,7 +41,7 @@ const DeployDefenderForm = () => { {/* Defender Setup Section */} - + { compareType="is" compareValue={true} > - + Defender Setup Defender and MEM Reporting - + - + { formControl={formControl} /> - + { {/* Defender Defaults Policy Section */} - + { compareType="is" compareValue={true} > - + Defender Defaults Policy Select Defender policies to deploy - + - + { formControl={formControl} /> - + { {/* Assign to Group */} - + Assign to Group { { label: "Assign to all users and devices", value: "AllDevicesAndUsers" }, ]} formControl={formControl} + validators={{ required: "Assignment must be selected" }} row /> @@ -296,8 +299,79 @@ const DeployDefenderForm = () => { + {/* Exclusion Policy Section */} + + + + + + + Exclusion Policy + Configure Defender Exclusions + + + + + + + + + + + + Assign to Group + + + + + + {/* ASR Section */} - + { compareType="is" compareValue={true} > - + ASR Rules Set Attack Surface Reduction Rules + - + - + + { name="ASR.WMIPersistence" formControl={formControl} /> + { name="ASR.BlockOfficeApps" formControl={formControl} /> + - + { name="ASR.blockJSVB" formControl={formControl} /> + { {/* Assign to Group */} - + Assign to Group { { label: "Assign to all users and devices", value: "AllDevicesAndUsers" }, ]} formControl={formControl} + validators={{ required: "Assignment must be selected" }} row /> diff --git a/src/pages/security/incidents/list-alerts/index.js b/src/pages/security/incidents/list-alerts/index.js index 09e1411d9d3b..9379da71347d 100644 --- a/src/pages/security/incidents/list-alerts/index.js +++ b/src/pages/security/incidents/list-alerts/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Assignment, Done } from "@mui/icons-material"; const Page = () => { const pageTitle = "Alerts List"; @@ -9,26 +10,26 @@ const Page = () => { { label: "Set status to in progress", type: "POST", + icon: , url: "/api/ExecSetSecurityAlert", data: { - TenantFilter: "Tenant", - GUID: "id", - Status: "inProgress", - Vendor: "vendorInformation.vendor", - Provider: "vendorInformation.provider", + GUID: "Id", + Status: "!inProgress", + Vendor: "RawResult.vendorInformation.vendor", + Provider: "RawResult.vendorInformation.provider", }, confirmText: "Are you sure you want to set the status to in progress?", }, { label: "Set status to resolved", type: "POST", + icon: , url: "/api/ExecSetSecurityAlert", data: { - TenantFilter: "Tenant", - GUID: "id", - Status: "resolved", - Vendor: "vendorInformation.vendor", - Provider: "vendorInformation.provider", + GUID: "Id", + Status: "!resolved", + Vendor: "RawResult.vendorInformation.vendor", + Provider: "RawResult.vendorInformation.provider", }, confirmText: "Are you sure you want to set the status to resolved?", }, @@ -37,9 +38,9 @@ const Page = () => { // Define off-canvas details const offCanvas = { extendedInfoFields: [ - "eventDateTime", // Created on - "title", // Title - "category", // Category + "EventDateTime", // Created on + "Title", // Title + "Category", // Category "Status", // Status "Severity", // Severity "Tenant", // Tenant @@ -50,17 +51,19 @@ const Page = () => { // Simplified columns for the table const simpleColumns = [ - "eventDateTime", // Created Date (Local) - "Tenant", // Tenant - "title", // Title - "Severity", // Severity + "EventDateTime", // Created Date (Local) "Status", // Status + "Title", // Title + "Severity", // Severity + "Category", // Category + "Tenant", // Tenant ]; return ( { const pageTitle = "Incidents List"; @@ -9,47 +10,46 @@ const Page = () => { { label: "Assign to self", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", - GUID: "id", - Assigned: "currentUserId", + GUID: "Id", }, confirmText: "Are you sure you want to assign this incident to yourself?", }, { label: "Set status to active", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", - GUID: "id", - Status: "active", - Assigned: "currentAssignedUser", + GUID: "Id", + Status: "!active", + Assigned: "AssignedTo", }, confirmText: "Are you sure you want to set the status to active?", }, { label: "Set status to in progress", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", - GUID: "id", - Status: "inProgress", - Assigned: "currentAssignedUser", + GUID: "Id", + Status: "!inProgress", + Assigned: "AssignedTo", }, confirmText: "Are you sure you want to set the status to in progress?", }, { label: "Set status to resolved", type: "POST", + icon: , url: "/api/ExecSetSecurityIncident", data: { - TenantFilter: "Tenant", - GUID: "id", - Status: "resolved", - Assigned: "currentAssignedUser", + GUID: "Id", + Status: "!resolved", + Assigned: "AssignedTo", }, confirmText: "Are you sure you want to set the status to resolved?", }, @@ -76,13 +76,22 @@ const Page = () => { }; // Simplified columns for the table - const simpleColumns = ["Created", "Tenant", "Id", "DisplayName", "Status", "Severity", "Tags"]; + const simpleColumns = [ + "Created", + "Tenant", + "Id", + "DisplayName", + "Status", + "Severity", + "Tags", + "IncidentUrl", + ]; return ( { ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/security/safelinks/safelinks-template/add.jsx b/src/pages/security/safelinks/safelinks-template/add.jsx new file mode 100644 index 000000000000..3e9998b0a63e --- /dev/null +++ b/src/pages/security/safelinks/safelinks-template/add.jsx @@ -0,0 +1,64 @@ +import { useEffect } from "react"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useWatch } from "react-hook-form"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; + +const DeploySafeLinksPolicyTemplate = () => { + const formControl = useForm({ + mode: "onChange", + defaultValues: { + selectedTenants: [], + TemplateList: [], + }, + }); + + return ( + + + + + + + + option, + url: "/api/ListSafeLinksPolicyTemplates", + }} + placeholder="Select a template" + validators={{ required: "A template must be selected" }} + /> + + + + ); +}; + +DeploySafeLinksPolicyTemplate.getLayout = (page) => {page}; +export default DeploySafeLinksPolicyTemplate; diff --git a/src/pages/security/safelinks/safelinks-template/create.jsx b/src/pages/security/safelinks/safelinks-template/create.jsx new file mode 100644 index 000000000000..e87962890042 --- /dev/null +++ b/src/pages/security/safelinks/safelinks-template/create.jsx @@ -0,0 +1,52 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Watch policy name to pass to rule form + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'createTemplate'); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks-template/edit.jsx b/src/pages/security/safelinks/safelinks-template/edit.jsx new file mode 100644 index 000000000000..4945a158aa58 --- /dev/null +++ b/src/pages/security/safelinks/safelinks-template/edit.jsx @@ -0,0 +1,76 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { useEffect } from "react"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const router = useRouter(); + const { ID } = router.query; + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Watch policy name to pass to rule form + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Get existing template data + const templateData = ApiGetCall({ + url: `/api/ListSafeLinksPolicyTemplateDetails?ID=${ID}`, + queryKey: `SafeLinksTemplate-${ID}`, + enabled: !!ID, + }); + + // Populate forms with existing data when available + useEffect(() => { + if (templateData.isSuccess && templateData.data?.Results) { + const template = templateData.data.Results; + + // Use utility to populate form + safeLinksDataUtils.populateFormData(formControl, template, userSettingsDefaults, 'template'); + } + }, [templateData.isSuccess, templateData.data, ID, formControl, userSettingsDefaults]); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'template', { ID }); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks-template/index.jsx b/src/pages/security/safelinks/safelinks-template/index.jsx new file mode 100644 index 000000000000..87388958ff66 --- /dev/null +++ b/src/pages/security/safelinks/safelinks-template/index.jsx @@ -0,0 +1,121 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { Button } from "@mui/material"; +import { RocketLaunch, GitHub, Edit, Add } from "@mui/icons-material"; +import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Safe Links Policy Templates"; + + // Check if GitHub integration is enabled + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + label: "Edit Template", + link: "/security/safelinks/safelinks-template/edit?ID=[GUID]", + icon: , + color: "success", + target: "_self", + }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveSafeLinksPolicyTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["TemplateName", "TemplateDescription", "GUID"], + actions: actions, + }; + + const simpleColumns = ["TemplateName", "TemplateDescription", "GUID"]; + + return ( + + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks/add.jsx b/src/pages/security/safelinks/safelinks/add.jsx new file mode 100644 index 000000000000..0153bdb1dcac --- /dev/null +++ b/src/pages/security/safelinks/safelinks/add.jsx @@ -0,0 +1,52 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; + +const Page = () => { + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + }, + }); + + // Watch policy name to pass to rule form + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'add'); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks/edit.jsx b/src/pages/security/safelinks/safelinks/edit.jsx new file mode 100644 index 000000000000..c0ed200b0b26 --- /dev/null +++ b/src/pages/security/safelinks/safelinks/edit.jsx @@ -0,0 +1,88 @@ +import { Box } from "@mui/material"; +import CippFormPage from "/src/components/CippFormPages/CippFormPage"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm, useWatch } from "react-hook-form"; +import { useSettings } from "/src/hooks/use-settings"; +import { useEffect } from "react"; +import { SafeLinksForm, safeLinksDataUtils } from "/src/components/CippFormPages/CippSafeLinksPolicyRuleForm"; +import { useRouter } from "next/router"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const router = useRouter(); + const { PolicyName, RuleName } = router.query; + const userSettingsDefaults = useSettings(); + + // Main form for policy configuration + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + tenantFilter: userSettingsDefaults.currentTenant, + PolicyName: PolicyName, + }, + }); + + // Watch policy name for rule synchronization + const watchPolicyName = useWatch({ control: formControl.control, name: "PolicyName" }); + + // Get existing policy and rule data + const policyData = ApiGetCall({ + url: `/api/ListSafeLinksPolicyDetails?PolicyName=${PolicyName}&RuleName=${RuleName}&tenantFilter=${userSettingsDefaults.currentTenant}`, + queryKey: `SafeLinksPolicy-${PolicyName}`, + enabled: !!PolicyName, + }); + + // Populate forms with existing data when available + useEffect(() => { + if (policyData.isSuccess && policyData.data?.Results) { + const results = policyData.data.Results; + const policy = results.Policy || {}; + const rule = results.Rule || {}; + + // Combine policy and rule data + const combinedData = { + ...policy, + ...rule, + RuleName: rule.RuleName || RuleName, + SafeLinksPolicy: policy.PolicyName || PolicyName, + State: rule.State, + }; + + // Use utility to populate form + safeLinksDataUtils.populateFormData(formControl, combinedData, userSettingsDefaults, 'edit'); + } + }, [policyData.isSuccess, policyData.data, PolicyName, RuleName, formControl, userSettingsDefaults]); + + // Use the utility to create the data formatter + const customDataFormatter = safeLinksDataUtils.createDataFormatter(formControl, 'edit'); + + return ( + <> + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; \ No newline at end of file diff --git a/src/pages/security/safelinks/safelinks/index.jsx b/src/pages/security/safelinks/safelinks/index.jsx new file mode 100644 index 000000000000..70ac3bcf51c0 --- /dev/null +++ b/src/pages/security/safelinks/safelinks/index.jsx @@ -0,0 +1,169 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Block, Check, LowPriority, Edit, DeleteForever, Policy, Book } from "@mui/icons-material"; +import { Button } from "@mui/material"; +import Link from "next/link"; + +const Page = () => { + const pageTitle = "Safe Links Policies"; + const apiUrl = "/api/ListSafeLinksPolicy"; + + const filterList = [ + { + filterName: "Enabled Rules", + value: [{ id: "State", value: "Enabled" }], + type: "column", + }, + { + filterName: "Disabled Rules", + value: [{ id: "State", value: "Disabled" }], + type: "column", + } + ]; + + const actions = [ + { + label: "Edit Safe Links Policy", + link: "/security/safelinks/safelinks/edit?PolicyName=[PolicyName]&RuleName=[RuleName]&tenantFilter=[tenantFilter]", + icon: , + color: "success", + target: "_self", + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy") && row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Enable Rule", + type: "POST", + icon: , + url: "/api/EditSafeLinksPolicy", + data: { + PolicyName: "PolicyName", + Name: "PolicyName", + Enabled: true + }, + confirmText: "Are you sure you want to enable this rule?", + color: "info", + condition: (row) => row.State === "Disabled" && !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Disable Rule", + type: "POST", + icon: , + url: "/api/EditSafeLinksPolicy", + data: { + PolicyName: "PolicyName", + Name: "PolicyName", + Enabled: false + }, + confirmText: "Are you sure you want to disable this rule?", + color: "info", + condition: (row) => row.State === "Enabled" && !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Set Priority", + type: "POST", + icon: , + url: "/api/EditSafeLinksPolicy", + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + data: { + PolicyName: "PolicyName", + Name: "PolicyName" + }, + confirmText: "What would you like to set the priority to?", + color: "info", + hideBulk: true, + fields: [ + { + type: "number", + name: "Priority", + label: "Priority", + placeholder: "Enter a number", + validators: { + required: "Priority is required", + min: { + value: 0, + message: "Priority must be at least 0 and no more than -1 of the lowest priority", + }, + }, + }, + ], + }, + { + label: "Create template based on policy", + type: "POST", + url: "/api/AddSafeLinksPolicyTemplate", + postEntireRow: true, + confirmText: "Are you sure you want to create a template based on this policy?", + icon: , + hideBulk: true, + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + }, + { + label: "Delete Rule", + type: "GET", + icon: , + url: "/api/ExecDeleteSafeLinksPolicy", + data: { + RuleName: "RuleName", + PolicyName: "PolicyName", + }, + confirmText: "Are you sure you want to delete this policy and rule?", + color: "danger", + condition: (row) => !row.IsBuiltInProtection && !row.PolicyName.startsWith("Standard Preset Security Policy") && !row.PolicyName.startsWith("Strict Preset Security Policy")&& row.PolicyName !== "Built-In Protection Policy", + } + ]; + + // Define columns for the table + const simpleColumns = [ + "PolicyName", + "ConfigurationStatus", + "IsValid", + "State", + "Priority", + "Description", + "RecipientDomainIs", + "SentTo", + "SentToMemberOf", + "ExceptIfSentTo", + "ExceptIfSentToMemberOf", + "ExceptIfRecipientDomainIs", + "DoNotRewriteUrls", + "EnableSafeLinksForEmail", + "EnableSafeLinksForTeams", + "EnableSafeLinksForOffice", + "TrackClicks", + "ScanUrls", + "EnableForInternalSenders", + "DeliverMessageAfterScan", + "AllowClickThrough", + "DisableUrlRewrite", + "EnableOrganizationBranding", + "WhenCreated", + "WhenChanged", + ]; + + const offCanvas = { + extendedInfoFields: ["RuleName", "ConfigurationStatus", "IsValid", "PolicyName", "State", "WhenCreated", "WhenChanged"], + actions: actions, + }; + + return ( + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; \ No newline at end of file diff --git a/src/pages/teams-share/onedrive/index.js b/src/pages/teams-share/onedrive/index.js index c14fd268acb2..9ec970fbda1e 100644 --- a/src/pages/teams-share/onedrive/index.js +++ b/src/pages/teams-share/onedrive/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { PersonAdd, PersonRemove } from "@mui/icons-material"; const Page = () => { const pageTitle = "OneDrive"; @@ -7,6 +8,7 @@ const Page = () => { const actions = [ { label: "Add permissions to OneDrive", + icon: , type: "POST", url: "/api/ExecSharePointPerms", data: { @@ -23,19 +25,28 @@ const Page = () => { multiple: false, creatable: false, api: { - url: "/api/listUsers", - labelField: (onedriveAccessUser) => - `${onedriveAccessUser.displayName} (${onedriveAccessUser.userPrincipalName})`, + 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: "userPrincipalName", addedField: { - displayName: "displayName", + id: "id", }, + showRefresh: true, }, }, ], }, { label: "Remove permissions from OneDrive", + icon: , type: "POST", url: "/api/ExecSharePointPerms", data: { @@ -65,17 +76,11 @@ const Page = () => { }, ]; - const offCanvas = { - extendedInfoFields: ["UPN"], - actions: actions, - }; - return ( { backButtonTitle="Back to Sites" > - + - + { required /> - + { }} /> - + { }} /> - + { const tenantFilter = useSettings().currentTenant; const fields = [ - "SiteName", + "siteName", "siteDescription", "siteOwner", - "TemplateName", + "templateName", "siteDesign", "sensitivityLabel", ]; diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index d88d75533e92..38baf8abb4c2 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -1,6 +1,14 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { + Add, + AddToPhotos, + PersonAdd, + PersonRemove, + AdminPanelSettings, + NoAccounts, +} from "@mui/icons-material"; import Link from "next/link"; const Page = () => { @@ -10,6 +18,7 @@ const Page = () => { { label: "Add Member", type: "POST", + icon: , url: "/api/ExecSetSharePointMember", data: { groupId: "ownerPrincipalName", @@ -26,12 +35,21 @@ const Page = () => { multiple: false, creatable: false, api: { - url: "/api/listUsers", + 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: "userPrincipalName", addedField: { id: "id", }, + showRefresh: true, }, }, ], @@ -40,6 +58,7 @@ const Page = () => { { label: "Remove Member", type: "POST", + icon: , url: "/api/ExecSetSharePointMember", data: { groupId: "ownerPrincipalName", @@ -56,12 +75,21 @@ const Page = () => { multiple: false, creatable: false, api: { - url: "/api/listUsers", + 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: "userPrincipalName", addedField: { id: "id", }, + showRefresh: true, }, }, ], @@ -70,6 +98,7 @@ const Page = () => { { label: "Add Site Admin", type: "POST", + icon: , url: "/api/ExecSharePointPerms", data: { UPN: "ownerPrincipalName", @@ -85,12 +114,21 @@ const Page = () => { multiple: false, creatable: false, api: { - url: "/api/listUsers", + 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: "userPrincipalName", addedField: { id: "id", }, + showRefresh: true, }, }, ], @@ -99,6 +137,7 @@ const Page = () => { { label: "Remove Site Admin", type: "POST", + icon: , url: "/api/ExecSharePointPerms", data: { UPN: "ownerPrincipalName", @@ -114,12 +153,21 @@ const Page = () => { multiple: false, creatable: false, api: { - url: "/api/listUsers", + 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: "userPrincipalName", - addedFields: { + addedField: { id: "id", }, + showRefresh: true, }, }, ], @@ -151,10 +199,14 @@ const Page = () => { ]} cardButton={ <> - - diff --git a/src/pages/teams-share/teams/business-voice/index.js b/src/pages/teams-share/teams/business-voice/index.js index 725a2a8a5331..6cef4fd00045 100644 --- a/src/pages/teams-share/teams/business-voice/index.js +++ b/src/pages/teams-share/teams/business-voice/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { PersonAdd, PersonRemove, LocationOn } from "@mui/icons-material"; const Page = () => { const pageTitle = "Teams Business Voice"; @@ -9,6 +10,7 @@ const Page = () => { { label: "Assign User", type: "POST", + icon: , url: "/api/ExecTeamsVoicePhoneNumberAssignment", data: { PhoneNumber: "TelephoneNumber", @@ -34,6 +36,7 @@ const Page = () => { { label: "Unassign User", type: "POST", + icon: , url: "/api/ExecRemoveTeamsVoicePhoneNumberAssignment", data: { PhoneNumber: "TelephoneNumber", @@ -45,6 +48,7 @@ const Page = () => { { label: "Set Emergency Location", type: "POST", + icon: , url: "/api/ExecTeamsVoicePhoneNumberAssignment", data: { PhoneNumber: "TelephoneNumber", diff --git a/src/pages/teams-share/teams/list-team/add.jsx b/src/pages/teams-share/teams/list-team/add.jsx index 6fe593adc697..23cdb4df916e 100644 --- a/src/pages/teams-share/teams/list-team/add.jsx +++ b/src/pages/teams-share/teams/list-team/add.jsx @@ -1,5 +1,6 @@ import React from "react"; -import { Grid, Divider } from "@mui/material"; +import { Divider } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -27,6 +28,7 @@ const TeamsAddTeamForm = () => { title="Add Team" backButtonTitle="Teams Overview" postUrl="/api/AddTeam" + resetForm={true} customDataformatter={(values) => { const shippedValues = { tenantID: tenantDomain, @@ -40,7 +42,7 @@ const TeamsAddTeamForm = () => { > {/* Display Name */} - + { {/* Description */} - + { - + { {/* Visibility */} - + { const pageTitle = "Teams"; @@ -12,6 +14,20 @@ const Page = () => { link: "/identity/administration/groups/edit?groupId=[id]", multiPost: false, color: "warning", + icon: , + }, + { + label: "Delete Team", + type: "POST", + url: "/api/ExecGroupsDelete", + icon: , + data: { + ID: "id", + GroupType: "!Microsoft 365", + DisplayName: "displayName", + }, + confirmText: "Are you sure you want to delete this team?", + multiPost: false, }, ]; @@ -23,7 +39,7 @@ const Page = () => { simpleColumns={["displayName", "description", "visibility", "mailNickname", "id"]} cardButton={ <> - diff --git a/src/pages/tenant/administration/add-subscription/index.jsx b/src/pages/tenant/administration/add-subscription/index.jsx index 5d0d64a6dbbc..474313f5879b 100644 --- a/src/pages/tenant/administration/add-subscription/index.jsx +++ b/src/pages/tenant/administration/add-subscription/index.jsx @@ -1,11 +1,13 @@ -import React from "react"; -import { Box, Divider, Grid } from "@mui/material"; +import { Box } from "@mui/material"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; -import { darken, lighten, styled } from "@mui/system"; +import { Grid, darken, lighten, styled } from "@mui/system"; +import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; +import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; +import { getCippTranslation } from "../../../../utils/get-cipp-translation"; const Page = () => { const userSettingsDefaults = useSettings(); @@ -19,6 +21,12 @@ const Page = () => { iagree: false, }, }); + + const selectedSku = useWatch({ + control: formControl.control, + name: "sku", + }); + const GroupHeader = styled("div")(({ theme }) => ({ position: "sticky", top: "-8px", @@ -46,7 +54,7 @@ const Page = () => { {/* Conditional Access Policy Selector */} - + { url: "/api/ListCSPsku", labelField: (option) => `${option?.name[0]?.value} (${option?.sku})`, valueField: "sku", + + addedField: { + billingCycle: "billingCycle", + commitmentTerm: "commitmentTerm", + description: "description", + }, }} multiple={false} formControl={formControl} + required={true} + validators={{ + validate: (option) => { + return option?.value ? true : "This field is required."; + }, + }} + sortOptions={true} /> - + - + {selectedSku?.value && ( + + + + )} + { const apiRequest = ApiPostCall({ - relatedQueryKeys: "ListAlertsQueue", + relatedQueryKeys: ["ListAlertsQueue", "ListCurrentAlerts"], }); const router = useRouter(); const [editAlert, setAlertEdit] = useState(false); @@ -44,6 +46,7 @@ const AlertWizard = () => { const existingAlert = ApiGetCall({ url: "/api/ListAlertsQueue", relatedQueryKeys: "ListAlertsQueue", + queryKey: "ListCurrentAlerts", }); const [recurrenceOptions, setRecurrenceOptions] = useState([ { value: "30m", label: "Every 30 minutes" }, @@ -60,7 +63,7 @@ const AlertWizard = () => { { label: "Email", value: "Email" }, { label: "PSA", value: "PSA" }, ]; - const actionstoTake = [ + const actionsToTake = [ //{ value: 'cippcommand', label: 'Execute a CIPP Command' }, { value: "becremediate", label: "Execute a BEC Remediate" }, { value: "disableuser", label: "Disable the user in the log entry" }, @@ -80,22 +83,84 @@ const AlertWizard = () => { const alert = existingAlert?.data?.find((alert) => alert.RowKey === router.query.id); if (alert?.LogType === "Scripted") { setAlertType("script"); - formControl.setValue("tenantFilter", { - value: alert.RawAlert.Tenant, - label: alert.RawAlert.Tenant, - }); + + // Create formatted excluded tenants array if it exists + const excludedTenantsFormatted = Array.isArray(alert.excludedTenants) + ? alert.excludedTenants.map((tenant) => ({ value: tenant, label: tenant })) + : []; + + // Format the command object const usedCommand = alertList?.find( (cmd) => cmd.name === alert.RawAlert.Command.replace("Get-CIPPAlert", "") ); - formControl.setValue("command", { value: usedCommand, label: usedCommand.label }); - formControl.setValue( - "recurrence", - recurrenceOptions?.find((opt) => opt.value === alert.RawAlert.Recurrence) + + // Format recurrence option + const recurrenceOption = recurrenceOptions?.find( + (opt) => opt.value === alert.RawAlert.Recurrence ); + + // Format post execution values const postExecutionValue = postExecutionOptions.filter((opt) => alert.RawAlert.PostExecution.split(",").includes(opt.value) ); - formControl.setValue("postExecution", postExecutionValue); + + // Create tenant filter object - handle both regular tenants and tenant groups + let tenantFilterForForm; + if (alert.RawAlert.TenantGroup) { + try { + const tenantGroupObject = JSON.parse(alert.RawAlert.TenantGroup); + tenantFilterForForm = { + value: tenantGroupObject.value, + label: tenantGroupObject.label, + type: "Group", + addedFields: tenantGroupObject, + }; + } catch (error) { + console.error("Error parsing tenant group:", error); + // Fall back to regular tenant + tenantFilterForForm = { + value: alert.RawAlert.Tenant, + label: alert.RawAlert.Tenant, + type: "Tenant", + }; + } + } else { + tenantFilterForForm = { + value: alert.RawAlert.Tenant, + label: alert.RawAlert.Tenant, + type: "Tenant", + }; + } + + // Create the reset object with all the form values + const resetObject = { + tenantFilter: tenantFilterForForm, + excludedTenants: excludedTenantsFormatted, + command: { value: usedCommand, label: usedCommand.label }, + recurrence: recurrenceOption, + postExecution: postExecutionValue, + }; + + // Parse Parameters field if it exists and is a string + if (usedCommand?.requiresInput && alert.RawAlert.Parameters) { + try { + // Check if Parameters is a string that needs parsing + const params = + typeof alert.RawAlert.Parameters === "string" + ? JSON.parse(alert.RawAlert.Parameters) + : alert.RawAlert.Parameters; + + // Set the input value if it exists + if (params.InputValue) { + resetObject[usedCommand.inputName] = params.InputValue; + } + } catch (error) { + console.error("Error parsing parameters:", error); + } + } + + // Reset the form with all values at once + formControl.reset(resetObject, { keepDirty: false }); } if (alert?.PartitionKey === "Webhookv2") { setAlertType("audit"); @@ -109,13 +174,53 @@ const AlertWizard = () => { })) ); - formControl.reset({ + // Format conditions properly for form + const formattedConditions = alert.RawAlert.Conditions.map((condition) => { + const formattedCondition = { + Property: condition.Property, + Operator: condition.Operator, + }; + + // Handle Input based on Property type + if (condition.Property.value === "String") { + // For String type, we need to set both the nested value and the direct value + formattedCondition.Input = { + value: condition.Input.value, + }; + } else { + // For List type, use the full Input object + formattedCondition.Input = condition.Input; + } + + return formattedCondition; + }); + + const resetData = { RowKey: router.query.clone ? undefined : router.query.id ? router.query.id : undefined, tenantFilter: alert.RawAlert.Tenants, + excludedTenants: alert.excludedTenants?.filter((tenant) => tenant !== null) || [], Actions: alert.RawAlert.Actions, - conditions: alert.RawAlert.Conditions, + conditions: formattedConditions, logbook: foundLogbook, - }); + }; + + formControl.reset(resetData); + + // After reset, manually set the Input values to ensure they're properly registered + setTimeout(() => { + formattedConditions.forEach((condition, index) => { + if (condition.Property.value === "String") { + // For String properties, set the nested value path + formControl.setValue(`conditions.${index}.Input.value`, condition.Input.value); + } else { + // For List properties, set the direct Input value + formControl.setValue(`conditions.${index}.Input`, condition.Input); + } + }); + + // Trigger validation to ensure all fields are properly registered + formControl.trigger(); + }, 100); } } }, [existingAlert.isSuccess, router, editAlert]); @@ -148,9 +253,13 @@ const AlertWizard = () => { recommendedOption.label += " (Recommended)"; } setRecurrenceOptions(updatedRecurrenceOptions); - formControl.setValue("recurrence", recommendedOption); + + // Only set the recommended recurrence if we're NOT editing an existing alert + if (!editAlert) { + formControl.setValue("recurrence", recommendedOption); + } } - }, [commandValue]); + }, [commandValue, editAlert]); useEffect(() => { // Logic to handle template-based form updates when a preset is selected @@ -201,7 +310,7 @@ const AlertWizard = () => { }; const handleAuditSubmit = (values) => { - values.conditions = values.conditions.filter((condition) => condition.Property); + values.conditions = values.conditions.filter((condition) => condition?.Property); apiRequest.mutate({ url: "/api/AddAlert", data: values }); }; @@ -217,8 +326,9 @@ const AlertWizard = () => { const postObject = { RowKey: router.query.clone ? undefined : router.query.id ? router.query.id : undefined, - tenantFilter: values.tenantFilter?.value, - Name: `${values.tenantFilter.value}: ${values.command.label}`, + tenantFilter: values.tenantFilter, + excludedTenants: values.excludedTenants, + Name: `${values.tenantFilter?.label || values.tenantFilter?.value}: ${values.command.label}`, Command: { value: `Get-CIPPAlert${values.command.value.name}` }, Parameters: getInputParams(), ScheduledTime: Math.floor(new Date().getTime() / 1000) + 60, @@ -264,7 +374,7 @@ const AlertWizard = () => {
    - + setAlertType("audit")}> @@ -276,7 +386,7 @@ const AlertWizard = () => { - + setAlertType("script")}> @@ -291,24 +401,48 @@ const AlertWizard = () => { {/* Audit Log Form */} {alertType === "audit" && ( - - + +
    - - + + - - Select the tenants you want to include in this Alert. - - + + + + + + + + + + - + { sx={{ mb: 3 }} > - + { /> - + { /> + + + {addedEvent.map((event) => ( - + { formControl={formControl} label="Select property" options={getAuditLogSchema(logbookWatcher?.value)} + creatable={true} + onCreateOption={(option) => { + const propertyName = option.label || option; + + // Return the option with String type for immediate use + const newOption = { + label: propertyName, + value: "String", // Always set to String for custom properties + }; + + return newOption; + }} /> - + { ]} /> - + { field={`conditions.${event.id}.Property`} formControl={formControl} compareType="contains" - compareValue={"List:"} + compareValue="List:" > { /> - - handleAddCondition()}> - - - - - handleRemoveCondition(event.id)} - > - - + + + handleRemoveCondition(event.id)} + > + + + ))} - + { required: { value: true, message: "This field is required" }, }} formControl={formControl} - multiple - options={actionstoTake} + multiple={true} + creatable={false} + options={actionsToTake} /> - + @@ -459,28 +617,50 @@ const AlertWizard = () => { {/* Scripted CIPP Alert Form */} {alertType === "script" && ( - - + + - + - - Select the tenants you want to include in this Alert. - - + + + + + + + + + + - + { } > - + { }))} /> - + { options={recurrenceOptions} // Use the state-managed recurrenceOptions here /> - + {commandValue?.value?.requiresInput && ( { /> )} - + { required: { value: true, message: "This field is required" }, }} formControl={formControl} - multiple + multiple={true} + creatable={false} options={postExecutionOptions} /> - + diff --git a/src/pages/tenant/administration/alert-configuration/index.js b/src/pages/tenant/administration/alert-configuration/index.js index 6b68555d0e66..8040f018d37f 100644 --- a/src/pages/tenant/administration/alert-configuration/index.js +++ b/src/pages/tenant/administration/alert-configuration/index.js @@ -1,59 +1,76 @@ -import { Button } from "@mui/material"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. -import Link from "next/link"; -import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete } from "@mui/icons-material"; - -const Page = () => { - const pageTitle = "Alerts"; - const actions = [ - { - label: "Edit Alert", - link: "/tenant/administration/alert-configuration/alert?id=[RowKey]", - icon: , - color: "success", - target: "_self", - }, - { - label: "Clone & Edit Alert", - link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true", - icon: , - color: "success", - target: "_self", - }, - { - label: "Delete Alert", - type: "GET", - url: "/api/RemoveQueuedAlert", - data: { - ID: "RowKey", - EventType: "EventType", - }, - icon: , - relatedQueryKeys: "ListAlertsQueue", - confirmText: "Are you sure you want to delete this Alert?", - multiPost: false, - }, - ]; - - return ( - - Add Alert - - } - actions={actions} - simpleColumns={["Tenants", "EventType", "Conditions", "RepeatsEvery", "Actions"]} - queryKey="ListAlertsQueue" - /> - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +import { Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import Link from "next/link"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { CopyAll, Delete, NotificationAdd } from "@mui/icons-material"; + +const Page = () => { + const pageTitle = "Alerts"; + const actions = [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + condition: (row) => row?.EventType === "Scheduled Task", + }, + { + label: "Edit Alert", + link: "/tenant/administration/alert-configuration/alert?id=[RowKey]", + icon: , + color: "success", + target: "_self", + }, + { + label: "Clone & Edit Alert", + link: "/tenant/administration/alert-configuration/alert?id=[RowKey]&clone=true", + icon: , + color: "success", + target: "_self", + }, + { + label: "Delete Alert", + type: "POST", + url: "/api/RemoveQueuedAlert", + data: { + ID: "RowKey", + EventType: "EventType", + }, + icon: , + relatedQueryKeys: "ListAlertsQueue", + confirmText: "Are you sure you want to delete this Alert?", + multiPost: false, + }, + ]; + + return ( + } + > + Add Alert + + } + actions={actions} + simpleColumns={[ + "Tenants", + "EventType", + "Conditions", + "RepeatsEvery", + "Actions", + "excludedTenants", + ]} + queryKey="ListAlertsQueue" + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/app-consent-requests/index.js b/src/pages/tenant/administration/app-consent-requests/index.js index 3bc6c53b58d6..5aafe8c75958 100644 --- a/src/pages/tenant/administration/app-consent-requests/index.js +++ b/src/pages/tenant/administration/app-consent-requests/index.js @@ -1,29 +1,22 @@ -import { useState, useEffect, use } from "react"; +import { useState } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { - Grid, Button, Accordion, AccordionSummary, AccordionDetails, Typography, + SvgIcon, + Stack, } from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { Grid } from "@mui/system"; +import { Visibility, CheckCircle, ExpandMore, Security } from "@mui/icons-material"; +import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; -const simpleColumns = [ - "Tenant", - "CippStatus", - "appDisplayName", - "requestUser", - "requestReason", - "requestStatus", - "requestDate", -]; - const apiUrl = "/api/ListAppConsentRequests"; const pageTitle = "App Consent Requests"; @@ -31,38 +24,138 @@ const Page = () => { const tenantFilter = useSettings().currentTenant; const formControl = useForm({ defaultValues: { - requestStatus: "All", + requestStatus: "InProgress", }, }); - const [expanded, setExpanded] = useState(false); // Accordion state - const [filterParams, setFilterParams] = useState({}); // Dynamic filter params + const [expanded, setExpanded] = useState(true); // Accordion state - start expanded since we have a default filter + const [filterEnabled, setFilterEnabled] = useState(true); // State for filter toggle - start with filter enabled + const [requestStatus, setRequestStatus] = useState("InProgress"); // State for request status filter - default to InProgress + const [requestStatusLabel, setRequestStatusLabel] = useState("Pending"); // State for displaying filter label - default label const onSubmit = (data) => { - // Handle filter application logic - const { requestStatus } = data; - const filters = {}; + // Handle the case where requestStatus could be an object {label, value} or a string + const statusValue = + typeof data.requestStatus === "object" && data.requestStatus?.value + ? data.requestStatus.value + : data.requestStatus; + const statusLabel = + typeof data.requestStatus === "object" && data.requestStatus?.label + ? data.requestStatus.label + : data.requestStatus; + + // Check if any filter is applied + const hasFilter = statusValue !== "All"; + setFilterEnabled(hasFilter); - if (requestStatus !== "All") { - filters.requestStatus = requestStatus; - } + // Set request status filter if not "All" + setRequestStatus(hasFilter ? statusValue : null); + setRequestStatusLabel(hasFilter ? statusLabel : null); - setFilterParams(filters); + // Close the accordion after applying filters + setExpanded(false); + }; + + const clearFilters = () => { + formControl.reset({ + requestStatus: "All", + }); + setFilterEnabled(false); + setRequestStatus(null); + setRequestStatusLabel(null); + setExpanded(false); // Close the accordion when clearing filters + }; + + const actions = [ + { + label: "Review in Entra", + link: `https://entra.microsoft.com/${tenantFilter}/#view/Microsoft_AAD_IAM/StartboardApplicationsMenuBlade/~/AccessRequests`, + color: "info", + icon: , + target: "_blank", + external: true, + }, + { + label: "Approve in Entra", + link: "[consentUrl]", + color: "info", + icon: , + target: "_blank", + external: true, + }, + ]; + + const simpleColumns = [ + "requestUser", // Requester + "appDisplayName", // Application Name + "appId", // Application ID + "requestReason", // Reason + "requestStatus", // Status + "reviewedBy", // Reviewed by + "reviewedJustification", // Reviewed Reason + "consentUrl", // Consent URL + ]; + + const filters = [ + { + filterName: "Pending requests", + value: [{ id: "requestStatus", value: "InProgress" }], + type: "column", + }, + { + filterName: "Expired requests", + value: [{ id: "requestStatus", value: "Expired" }], + type: "column", + }, + { + filterName: "Completed requests", + value: [{ id: "requestStatus", value: "Completed" }], + type: "column", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "requestUser", // Requester + "appDisplayName", // Application Name + "appId", // Application ID + "requestReason", // Reason + "requestStatus", // Status + "reviewedBy", // Reviewed by + "reviewedJustification", // Reviewed Reason + "consentUrl", // Consent URL + ], + actions: actions, }; return ( setExpanded(!expanded)}> - }> - Filters + }> + + + + + + App Consent Request Filters + {filterEnabled ? ( + + ({requestStatusLabel && <>Status: {requestStatusLabel}}) + + ) : ( + + (No filters applied) + + )} + + {/* Request Status Filter */} - + { /> - {/* Submit Button */} - - + {/* Action Buttons */} + + + + + @@ -92,45 +208,14 @@ const Page = () => { title={pageTitle} apiUrl={apiUrl} simpleColumns={simpleColumns} - filters={[ - // Filter for showing only pending requests - { - filterName: "Pending requests", - value: [{ id: "requestStatus", value: "InProgress" }], - type: "column", - }, - ]} - queryKey={`AppConsentRequests-${JSON.stringify(filterParams)}`} + filters={filters} + queryKey={`AppConsentRequests-${requestStatus}-${filterEnabled}-${tenantFilter}`} apiData={{ - ...filterParams, - }} - offCanvas={{ - extendedInfoFields: [ - "requestUser", // Requester - "appDisplayName", // Application Name - "appId", // Application ID - "requestReason", // Reason - "requestStatus", // Status - "reviewedBy", // Reviewed by - "reviewedJustification", // Reviewed Reason - ], + RequestStatus: requestStatus, // Pass request status filter from state + Filter: filterEnabled, // Pass filter toggle state }} - actions={[ - { - label: "Review in Entra", - link: `https://entra.microsoft.com/${tenantFilter}/#view/Microsoft_AAD_IAM/StartboardApplicationsMenuBlade/~/AccessRequests`, - color: "info", - target: "_blank", - external: true, - }, - { - label: "Approve in Entra", - link: "[consentUrl]", - color: "info", - target: "_blank", - external: true, - }, - ]} + offCanvas={offCanvas} + actions={actions} /> ); }; diff --git a/src/pages/tenant/administration/applications/app-registrations.js b/src/pages/tenant/administration/applications/app-registrations.js new file mode 100644 index 000000000000..c311e1b56420 --- /dev/null +++ b/src/pages/tenant/administration/applications/app-registrations.js @@ -0,0 +1,220 @@ +// this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83 +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent.jsx"; +import { CertificateCredentialRemovalForm } from "/src/components/CippComponents/CertificateCredentialRemovalForm.jsx"; +import CippPermissionPreview from "/src/components/CippComponents/CippPermissionPreview.jsx"; +import { Launch, Delete, Edit, Key, Security, Block, CheckCircle, Save } from "@mui/icons-material"; +import { usePermissions } from "/src/hooks/use-permissions.js"; +import tabOptions from "./tabOptions"; + +const Page = () => { + const pageTitle = "App Registrations"; + const apiUrl = "/api/ListGraphRequest"; + + const { checkPermissions } = usePermissions(); + const canWriteApplication = checkPermissions(["Tenant.Application.ReadWrite"]); + + const actions = [ + { + icon: , + label: "View App Registration", + link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/[appId]`, + color: "info", + target: "_blank", + multiPost: false, + external: true, + }, + { + icon: , + label: "View API Permissions", + link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/[appId]`, + color: "info", + target: "_blank", + multiPost: false, + external: true, + }, + { + icon: , + label: "Remove Password Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "applications", + Action: "RemovePassword", + }, + children: ({ formHook, row }) => { + return ( + ({ + label: `${cred.displayName || "Unnamed"} (Expiration: ${new Date( + cred.endDateTime + ).toLocaleDateString()})`, + value: cred.keyId, + })) || [] + } + /> + ); + }, + confirmText: "Are you sure you want to remove the selected password credentials?", + condition: (row) => canWriteApplication && row?.passwordCredentials?.length > 0, + }, + { + icon: , + label: "Remove Certificate Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "applications", + Action: "RemoveKey", + }, + children: ({ formHook, row }) => { + return ; + }, + confirmText: "Are you sure you want to remove the selected certificate credentials?", + condition: (row) => canWriteApplication && row?.keyCredentials?.length > 0, + }, + { + icon: , + label: "Create Template from App Registration", + type: "POST", + color: "success", + multiPost: false, + url: "/api/ExecAppApprovalTemplate", + fields: [ + { + label: "Template Name", + name: "TemplateName", + type: "textField", + placeholder: "Enter a name for the template", + required: true, + validators: { + required: { value: true, message: "Template name is required" }, + }, + }, + ], + customDataformatter: (row, action, formData) => { + const propertiesToRemove = [ + "appId", + "id", + "createdDateTime", + "deletedDateTime", + "publisherDomain", + "servicePrincipalLockConfiguration", + "identifierUris", + "applicationIdUris", + "Tenant", + "CippStatus", + ]; + + const cleanManifest = { ...row }; + propertiesToRemove.forEach((prop) => { + delete cleanManifest[prop]; + }); + + return { + Action: "Save", + TemplateName: formData.TemplateName, + AppType: "ApplicationManifest", + AppName: row.displayName || row.appId, + ApplicationManifest: cleanManifest, + }; + }, + confirmText: "Are you sure you want to create a template from this app registration?", + condition: (row) => canWriteApplication && row.signInAudience === "AzureADMyOrg", + }, + { + icon: , + label: "Delete App Registration", + type: "POST", + color: "error", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "applications", + Action: "Delete", + }, + confirmText: + "Are you sure you want to delete this application registration? This action cannot be undone.", + condition: () => canWriteApplication, + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "displayName", + "id", + "appId", + "createdDateTime", + "signInAudience", + "disabledByMicrosoftStatus", + "replyUrls", + "passwordCredentials", + "keyCredentials", + ], + actions: actions, + children: (row) => { + return ( + + ); + }, + }; + + const simpleColumns = [ + "displayName", + "appId", + "createdDateTime", + "signInAudience", + "web.redirectUris", + "publisherDomain", + "passwordCredentials", + "keyCredentials", + ]; + + const apiParams = { + Endpoint: "applications", + $count: true, + $top: 999, + }; + + return ( + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/applications/enterprise-apps.js b/src/pages/tenant/administration/applications/enterprise-apps.js new file mode 100644 index 000000000000..9c7cabac6408 --- /dev/null +++ b/src/pages/tenant/administration/applications/enterprise-apps.js @@ -0,0 +1,191 @@ +// this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83 +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent.jsx"; +import { CertificateCredentialRemovalForm } from "/src/components/CippComponents/CertificateCredentialRemovalForm.jsx"; +import { Launch, Delete, Edit, Key, Security, Block, CheckCircle } from "@mui/icons-material"; +import { usePermissions } from "/src/hooks/use-permissions.js"; +import tabOptions from "./tabOptions"; + +const Page = () => { + const pageTitle = "Enterprise Applications"; + const apiUrl = "/api/ListGraphRequest"; + + const { checkPermissions } = usePermissions(); + const canWriteApplication = checkPermissions(["Tenant.Application.ReadWrite"]); + + const actions = [ + { + icon: , + label: "View Application", + link: `https://entra.microsoft.com/[Tenant]/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Overview/objectId/[id]/appId/[appId]`, + color: "info", + target: "_blank", + multiPost: false, + external: true, + }, + { + icon: , + label: "Remove Password Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "RemovePassword", + }, + children: ({ formHook, row }) => { + return ( + ({ + label: `${cred.displayName || "Unnamed"} (Expiration: ${new Date( + cred.endDateTime + ).toLocaleDateString()})`, + value: cred.keyId, + })) || [] + } + /> + ); + }, + confirmText: "Are you sure you want to remove the selected password credentials?", + condition: (row) => canWriteApplication && row?.passwordCredentials?.length > 0, + }, + { + icon: , + label: "Remove Certificate Credentials", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "RemoveKey", + }, + children: ({ formHook, row }) => { + return ; + }, + confirmText: "Are you sure you want to remove the selected certificate credentials?", + condition: (row) => canWriteApplication && row?.keyCredentials?.length > 0, + }, + { + icon: , + label: "Disable Service Principal", + type: "POST", + color: "warning", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "Update", + Payload: { + accountEnabled: false, + }, + }, + confirmText: + "Are you sure you want to disable this service principal? Users will not be able to sign in to this application.", + condition: (row) => canWriteApplication && row?.accountEnabled === true, + }, + { + icon: , + label: "Enable Service Principal", + type: "POST", + color: "success", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "Update", + Payload: { + accountEnabled: true, + }, + }, + confirmText: "Are you sure you want to enable this service principal?", + condition: (row) => canWriteApplication && row?.accountEnabled === false, + }, + { + icon: , + label: "Delete Service Principal", + type: "POST", + color: "error", + multiPost: false, + url: "/api/ExecApplication", + data: { + Id: "id", + Type: "servicePrincipals", + Action: "Delete", + }, + confirmText: + "Are you sure you want to delete this service principal? This will remove the application from this tenant but will not affect the app registration.", + condition: () => canWriteApplication, + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "displayName", + "createdDateTime", + "accountEnabled", + "publisherName", + "replyUrls", + "appOwnerOrganizationId", + "tags", + "passwordCredentials", + "keyCredentials", + ], + actions: actions, + }; + + const simpleColumns = [ + "info.logoUrl", + "displayName", + "appId", + "accountEnabled", + "createdDateTime", + "publisherName", + "homepage", + "passwordCredentials", + "keyCredentials", + ]; + + const apiParams = { + Endpoint: "servicePrincipals", + $select: + "id,appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags,passwordCredentials,keyCredentials", + $count: true, + $top: 999, + }; + + return ( + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/applications/permission-sets/add.js b/src/pages/tenant/administration/applications/permission-sets/add.js new file mode 100644 index 000000000000..6f27122c5562 --- /dev/null +++ b/src/pages/tenant/administration/applications/permission-sets/add.js @@ -0,0 +1,227 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippAppPermissionBuilder from "/src/components/CippComponents/CippAppPermissionBuilder"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { Alert, CardContent, Stack, Typography, Button, Box } from "@mui/material"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; +import { useEffect, useState } from "react"; +import { CopyAll } from "@mui/icons-material"; + +const Page = () => { + const router = useRouter(); + const { template, copy, name } = router.query; + const pageTitle = copy ? "Copy Permission Set" : "Add Permission Set"; + + const [initialPermissions, setInitialPermissions] = useState(null); + const [availableTemplates, setAvailableTemplates] = useState([]); + // Add refetch key for refreshing data after save + const [refetchKey, setRefetchKey] = useState(0); + + const formControl = useForm({ + mode: "onBlur", + }); + + // Get the specified template if template ID is provided + const { data: templateData, isFetching: templateFetching } = ApiGetCall({ + url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null, + queryKey: template ? ["execAppPermissionTemplateDetails", template, refetchKey] : null, + enabled: !!template, + }); + + // Get all available templates for importing + const { data: allTemplates, isLoading: templatesLoading } = ApiGetCall({ + url: "/api/ExecAppPermissionTemplate", + queryKey: "execAppPermissionTemplateList", + }); + + useEffect(() => { + if (allTemplates && allTemplates.length > 0) { + setAvailableTemplates(allTemplates); + } + }, [allTemplates]); + + useEffect(() => { + // Initialize with empty structure for new templates + if (!template && !copy && !initialPermissions) { + setInitialPermissions({ + Permissions: {}, + TemplateName: "New Permission Set", + }); + formControl.setValue("templateName", "New Permission Set"); + } else if (templateData && copy && !initialPermissions) { + // When copying, we want to load the permissions but assign a new ID + if (templateData[0]) { + const copyName = `Copy of ${templateData[0].TemplateName}`; + setInitialPermissions({ + Permissions: templateData[0].Permissions, + TemplateName: copyName, + }); + formControl.setValue("templateName", copyName); + } + } else if (templateData && !initialPermissions) { + // For editing, keep the same ID + if (templateData[0]) { + setInitialPermissions({ + TemplateId: templateData[0].TemplateId, + Permissions: templateData[0].Permissions, + TemplateName: templateData[0].TemplateName, + }); + formControl.setValue("templateName", templateData[0].TemplateName); + } + } + }, [templateData, copy, template]); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ExecAppPermissionTemplate", "execAppPermissionTemplate"], + }); + + const handleUpdatePermissions = (data) => { + let payload = { + ...data, + }; + + if (copy) { + // For copy, ensure we're not sending the original ID + delete payload.TemplateId; + } else if (template && !copy) { + // For editing, include the template ID + payload.TemplateId = template; + } + + // Use the current value from the text field + payload.TemplateName = formControl.getValues("templateName"); + + updatePermissions.mutate( + { + url: "/api/ExecAppPermissionTemplate?Action=Save", + data: payload, + queryKey: "execAppPermissionTemplate", + }, + { + onSuccess: (data) => { + // Instead of navigating away, stay on the page and refresh + if (copy || !template) { + // If we're copying or creating new, update the URL to edit mode with the new template ID + const newTemplateId = data.data[0].Metadata.TemplateId; + router.push( + { + pathname: "/tenant/administration/applications/permission-sets/edit", + query: { + template: newTemplateId, + name: payload.TemplateName, + }, + }, + undefined, + { shallow: true } + ); + } else { + // Otherwise just refresh the current data + setRefetchKey((prev) => prev + 1); + } + }, + } + ); + }; + + const handleImportTemplate = () => { + const importTemplate = formControl.getValues("importTemplate"); + if (!importTemplate) return; + + const selectedTemplate = availableTemplates.find((t) => t.TemplateId === importTemplate.value); + if (selectedTemplate) { + setInitialPermissions({ + Permissions: selectedTemplate.Permissions, + TemplateName: `Import of ${selectedTemplate.TemplateName}`, + }); + formControl.setValue("templateName", `Import of ${selectedTemplate.TemplateName}`); + } + }; + + return ( + + + + {(!templateFetching || !template) && ( + <> + + {copy + ? "Create a copy of an existing permission set with your own modifications." + : template + ? "Edit the permissions in this permission set." + : "Create a new permission set to define a collection of application permissions."} + + + Permission sets allow you to define collections of permissions that can be applied + to applications consistently. + + + + + {!template && !copy && ( + + + ({ + label: template.TemplateName, + value: template.TemplateId, + }))} + isFetching={templatesLoading} + multiple={false} + /> + + + + )} + + {initialPermissions && ( + <> + + Choose the permissions you want to assign to this permission set. Microsoft + Graph is the default Service Principal added and you can choose to add + additional Service Principals as needed. Note that some Service Principals do + not have any published permissions to choose from. + + + + )} + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/permission-sets/edit.js b/src/pages/tenant/administration/applications/permission-sets/edit.js new file mode 100644 index 000000000000..e14272fb7b76 --- /dev/null +++ b/src/pages/tenant/administration/applications/permission-sets/edit.js @@ -0,0 +1,163 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippAppPermissionBuilder from "/src/components/CippComponents/CippAppPermissionBuilder"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { Alert, CardContent, Skeleton, Stack, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { Button } from "@mui/material"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; + +const Page = () => { + const router = useRouter(); + const { template, name } = router.query; + + const [initialPermissions, setInitialPermissions] = useState(null); + + const formControl = useForm({ + mode: "onBlur", + }); + + // Add a key to force refetch when we save + const [refetchKey, setRefetchKey] = useState(0); + + const { + data: templateData, + isFetching, + refetch, + } = ApiGetCall({ + url: template ? `/api/ExecAppPermissionTemplate?TemplateId=${template}` : null, + queryKey: template ? ["execAppPermissionTemplate", template, refetchKey] : null, + enabled: !!template, + }); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ExecAppPermissionTemplate", "execAppPermissionTemplate"], + }); + + useEffect(() => { + if (templateData && templateData[0]) { + setInitialPermissions({ + TemplateId: templateData[0].TemplateId, + Permissions: templateData[0].Permissions, + TemplateName: templateData[0].TemplateName, + }); + formControl.setValue("templateName", templateData[0].TemplateName, { + shouldValidate: true, + shouldDirty: false, + }); + } + }, [templateData]); + + const handleUpdatePermissions = (data) => { + let payload = { + ...data, + TemplateId: template, + }; + + // Use the current value from the text field + payload.TemplateName = formControl.getValues("templateName"); + + updatePermissions.mutate( + { + url: "/api/ExecAppPermissionTemplate?Action=Save", + data: payload, + queryKey: "execAppPermissionTemplate", + }, + { + onSuccess: () => { + // Refresh the data by incrementing the refetch key + setRefetchKey((prev) => prev + 1); + + // Explicitly refetch the data + refetch(); + }, + } + ); + }; + + // Instead of redirecting, we'll display an error message + if (!template) { + return ( + + + + The requested permission set does not exist or was not specified. Please select a valid + permission set from the list. + + + + + ); + } + + return ( + + + + {isFetching && ( + + )} + {!isFetching && initialPermissions && ( + <> + + Modify the permissions in this permission set. Any changes will affect all + applications using this permission set. + + + Permission sets allow you to define collections of permissions that can be applied + to applications consistently. + + + + + + Choose the permissions you want to assign to this permission set. Microsoft Graph is + the default Service Principal added and you can choose to add additional Service + Principals as needed. Note that some Service Principals do not have any published + permissions to choose from. + + + + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/permission-sets/index.js b/src/pages/tenant/administration/applications/permission-sets/index.js new file mode 100644 index 000000000000..41b02f844fc4 --- /dev/null +++ b/src/pages/tenant/administration/applications/permission-sets/index.js @@ -0,0 +1,75 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Edit, Delete, ContentCopy, Add } from "@mui/icons-material"; +import tabOptions from "../tabOptions"; +import { Button } from "@mui/material"; +import Link from "next/link"; + +const Page = () => { + const pageTitle = "Permission Sets"; + const apiUrl = "/api/ExecAppPermissionTemplate"; + + const actions = [ + { + icon: , + label: "Edit Permission Set", + color: "warning", + link: "/tenant/administration/applications/permission-sets/edit?template=[TemplateId]&name=[TemplateName]", + }, + { + icon: , + label: "Copy Permission Set", + color: "info", + link: "/tenant/administration/applications/permission-sets/add?template=[TemplateId]©=true&name=[TemplateName]", + }, + { + icon: , + label: "Delete Permission Set", + color: "danger", + url: apiUrl, + data: { + Action: "Delete", + TemplateId: "TemplateId", + }, + type: "POST", + confirmText: "Are you sure you want to delete [TemplateName]?", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["TemplateName", "Permissions", "UpdatedBy", "Timestamp"], + actions: actions, + }; + + const simpleColumns = ["TemplateName", "Permissions", "UpdatedBy", "Timestamp"]; + + return ( + } + > + Add Permission Set + + } + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/applications/tabOptions.json b/src/pages/tenant/administration/applications/tabOptions.json new file mode 100644 index 000000000000..804316b7d5b8 --- /dev/null +++ b/src/pages/tenant/administration/applications/tabOptions.json @@ -0,0 +1,18 @@ +[ + { + "label": "Enterprise Apps", + "path": "/tenant/administration/applications/enterprise-apps" + }, + { + "label": "App Registrations", + "path": "/tenant/administration/applications/app-registrations" + }, + { + "label": "Permission Sets", + "path": "/tenant/administration/applications/permission-sets" + }, + { + "label": "Templates", + "path": "/tenant/administration/applications/templates" + } +] \ No newline at end of file diff --git a/src/pages/tenant/administration/applications/templates/add.js b/src/pages/tenant/administration/applications/templates/add.js new file mode 100644 index 000000000000..e927db0acdcf --- /dev/null +++ b/src/pages/tenant/administration/applications/templates/add.js @@ -0,0 +1,96 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { CardContent } from "@mui/material"; +import { useState } from "react"; +import AppApprovalTemplateForm from "/src/components/CippComponents/AppApprovalTemplateForm"; + +const Page = () => { + const router = useRouter(); + const { template, copy, name } = router.query; + const pageTitle = copy ? "Copy App Approval Template" : "Add App Approval Template"; + + // Add refetch key for refreshing data after save + const [refetchKey, setRefetchKey] = useState(0); + + const formControl = useForm({ + mode: "onBlur", + }); + + // Get the specified template if template ID is provided + const { data: templateData, isLoading: templateLoading } = ApiGetCall({ + url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null, + queryKey: template ? ["ExecAppApprovalTemplate", template, refetchKey] : null, + waiting: !!template, + }); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ListAppApprovalTemplates", "ExecAppApprovalTemplate"], + }); + + const handleSubmit = (payload) => { + updatePermissions.mutate( + { + url: "/api/ExecAppApprovalTemplate?Action=Save", + data: payload, + queryKey: "ExecAppApprovalTemplate", + }, + { + onSuccess: (data) => { + // If we're adding or copying, redirect to edit page with the new ID + if (!template || copy) { + // Check if data exists and has the expected structure + const newTemplateId = data?.[0]?.TemplateId; + + if (newTemplateId) { + router.push( + { + pathname: "/tenant/administration/applications/templates/edit", + query: { + template: newTemplateId, + name: payload.TemplateName, + }, + }, + undefined, + { shallow: true } + ); + } else { + // Handle the case where TemplateId is missing + console.error("Missing TemplateId in response:", data); + // Just refresh the data as fallback + setRefetchKey((prev) => prev + 1); + } + } else { + // Just refresh the data if we're editing + setRefetchKey((prev) => prev + 1); + } + }, + } + ); + }; + + return ( + + + + + + ); +}; + +// Changed from TabbedLayout to just DashboardLayout +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/templates/edit.js b/src/pages/tenant/administration/applications/templates/edit.js new file mode 100644 index 000000000000..807717c763cf --- /dev/null +++ b/src/pages/tenant/administration/applications/templates/edit.js @@ -0,0 +1,104 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useForm } from "react-hook-form"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import CippPageCard from "/src/components/CippCards/CippPageCard"; +import { Alert, Button, CardContent } from "@mui/material"; +import { useState } from "react"; +import Link from "next/link"; +import AppApprovalTemplateForm from "/src/components/CippComponents/AppApprovalTemplateForm"; + +const Page = () => { + const router = useRouter(); + const { template, name } = router.query; + + // Add a key to force refetch when we save + const [refetchKey, setRefetchKey] = useState(0); + + const formControl = useForm({ + mode: "onBlur", + }); + + const { data: templateData, isLoading } = ApiGetCall({ + url: template ? `/api/ExecAppApprovalTemplate?Action=Get&TemplateId=${template}` : null, + queryKey: template ? ["ExecAppApprovalTemplateList", template, refetchKey] : null, + enabled: !!template, + }); + + const updatePermissions = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ListAppApprovalTemplates", "ExecAppApprovalTemplate"], + }); + + const handleSubmit = (payload) => { + // Ensure we're passing the TemplateId in the payload for updates + const updatedPayload = { + ...payload, + TemplateId: template, + Action: "Save", + }; + + updatePermissions.mutate( + { + url: "/api/ExecAppApprovalTemplate?Action=Save", + data: updatedPayload, + queryKey: "ExecAppApprovalTemplate", + }, + { + onSuccess: (data) => { + // Check if we received a valid response + const newTemplateId = data?.[0]?.TemplateId || template; + + // Refresh the data + setRefetchKey((prev) => prev + 1); + }, + } + ); + }; + + // Show error if template doesn't exist + if (!template) { + return ( + + + + The requested app approval template does not exist or was not specified. Please select a + valid template from the list. + + + + + ); + } + + return ( + + + + + + ); +}; + +// Changed from TabbedLayout to just DashboardLayout +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/applications/templates/index.js b/src/pages/tenant/administration/applications/templates/index.js new file mode 100644 index 000000000000..980245b47aba --- /dev/null +++ b/src/pages/tenant/administration/applications/templates/index.js @@ -0,0 +1,241 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import CippPermissionPreview from "/src/components/CippComponents/CippPermissionPreview.jsx"; +import { Edit, Delete, ContentCopy, Add, GitHub } from "@mui/icons-material"; +import tabOptions from "../tabOptions"; +import { Button } from "@mui/material"; +import Link from "next/link"; +import { ApiGetCall } from "/src/api/ApiCall"; + +const Page = () => { + const pageTitle = "Templates"; + const apiUrl = "/api/ListAppApprovalTemplates"; + + // Fetch GitHub integration status + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + icon: , + label: "Edit Template", + color: "warning", + link: "/tenant/administration/applications/templates/edit?template=[TemplateId]&name=[TemplateName]", + }, + { + icon: , + label: "Copy Template", + color: "info", + link: "/tenant/administration/applications/templates/add?template=[TemplateId]©=true&name=[TemplateName]", + }, + { + icon: , + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + data: { + Action: "UploadTemplate", + GUID: "TemplateId", + TemplateType: "AppApproval", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + icon: , + label: "Delete Template", + color: "danger", + url: "/api/ExecAppApprovalTemplate", + data: { + Action: "Delete", + TemplateId: "TemplateId", + }, + type: "POST", + confirmText: "Are you sure you want to delete [TemplateName]?", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["TemplateName", "AppType", "AppId", "AppName", "UpdatedBy", "Timestamp"], + actions: actions, + children: (row) => { + // Default to EnterpriseApp for backward compatibility with older templates + const appType = row.AppType || "EnterpriseApp"; + + // Determine the title based on app type + let title = "Permission Preview"; + if (appType === "GalleryTemplate") { + title = "Gallery Template Info"; + } else if (appType === "ApplicationManifest") { + title = "Application Manifest"; + } + + return ( + + ); + }, + }; + + const columns = [ + { + name: "TemplateName", + label: "Template Name", + sortable: true, + }, + { + name: "AppType", + label: "Type", + sortable: true, + formatter: (row) => { + // Default to EnterpriseApp for backward compatibility with older templates + const appType = row.AppType || "EnterpriseApp"; + if (appType === "GalleryTemplate") { + return "Gallery Template"; + } else if (appType === "ApplicationManifest") { + return "Application Manifest"; + } else { + return "Enterprise App"; + } + }, + }, + { + name: "AppId", + label: "App ID", + sortable: true, + }, + { + name: "AppName", + label: "App Name", + sortable: true, + }, + { + name: "PermissionSetName", + label: "Permission Set", + sortable: true, + formatter: (row) => { + // Default to EnterpriseApp for backward compatibility with older templates + const appType = row.AppType || "EnterpriseApp"; + if (appType === "GalleryTemplate") { + return "Auto-Consent"; + } else if (appType === "ApplicationManifest") { + return "Manifest-Defined"; + } else { + return row.PermissionSetName || "-"; + } + }, + }, + { + name: "UpdatedBy", + label: "Updated By", + sortable: true, + }, + { + name: "Timestamp", + label: "Last Updated", + sortable: true, + }, + ]; + + const simpleColumns = [ + "TemplateName", + "AppType", + "AppId", + "AppName", + "PermissionSetName", + "UpdatedBy", + "Timestamp", + ]; + + return ( + } + > + Add App Approval Template + + } + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/audit-logs/index.js b/src/pages/tenant/administration/audit-logs/index.js index 70939b35cf04..9a329abfb73a 100644 --- a/src/pages/tenant/administration/audit-logs/index.js +++ b/src/pages/tenant/administration/audit-logs/index.js @@ -1,19 +1,27 @@ import { useState } from "react"; +import { useRouter } from "next/router"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button, Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material"; +import { + Box, + Button, + Accordion, + AccordionSummary, + AccordionDetails, + Typography, +} from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { useForm } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { EyeIcon } from "@heroicons/react/24/outline"; import { Grid } from "@mui/system"; +import tabOptions from "./tabOptions.json"; -const simpleColumns = ["Timestamp", "Tenant", "Title", "Actions"]; - -const apiUrl = "/api/ListAuditLogs"; -const pageTitle = "Audit Logs"; - -const actions = [ +// Saved Logs Configuration +const savedLogsColumns = ["Timestamp", "Tenant", "Title", "Actions"]; +const savedLogsApiUrl = "/api/ListAuditLogs"; +const savedLogsActions = [ { label: "View Log", link: "/tenant/administration/audit-logs/log?id=[LogId]", @@ -23,22 +31,23 @@ const actions = [ ]; const Page = () => { + const router = useRouter(); + const formControl = useForm({ mode: "onChange", defaultValues: { dateFilter: "relative", - Time: 1, + Time: 7, Interval: { label: "Days", value: "d" }, }, }); - const [expanded, setExpanded] = useState(false); // Accordion state - const [relativeTime, setRelativeTime] = useState("1d"); // Relative time filter - const [startDate, setStartDate] = useState(null); // Start date filter - const [endDate, setEndDate] = useState(null); // End date filter + const [expanded, setExpanded] = useState(false); + const [relativeTime, setRelativeTime] = useState("7d"); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); const onSubmit = (data) => { - // Handle filter application logic if (data.dateFilter === "relative") { setRelativeTime(`${data.Time}${data.Interval.value}`); setStartDate(null); @@ -50,113 +59,121 @@ const Page = () => { } }; - return ( - setExpanded(!expanded)}> - }> - Search Options - - -
    - - {/* Date Filter Type */} - - - + // API parameters for saved logs + const apiParams = { + RelativeTime: relativeTime ? relativeTime : "7d", + ...(startDate && { StartDate: startDate }), + ...(endDate && { EndDate: endDate }), + }; - {/* Relative Time Filter */} - {formControl.watch("dateFilter") === "relative" && ( - <> - - - - - - - - - - - - )} + const searchFilter = ( + setExpanded(!expanded)}> + }> + Search Options + + + + + {/* Date Filter Type */} + + + - {/* Start and End Date Filters */} - {formControl.watch("dateFilter") === "startEnd" && ( - <> - + {/* Relative Time Filter */} + {formControl.watch("dateFilter") === "relative" && ( + <> + + + - + - - )} + + + + )} - {/* Submit Button */} - - + {/* Start and End Date Filters */} + {formControl.watch("dateFilter") === "startEnd" && ( + <> + + + + + - - - - - } - title={pageTitle} - apiUrl={apiUrl} + + )} + + {/* Submit Button */} + + + +
    + +
    +
    + ); + + return ( + ); }; /* Comment to Developer: + - This page displays saved audit logs with date filtering options. - The filter options are implemented within an Accordion for a collapsible UI. - DateFilter types are supported as 'Relative' and 'Start/End'. - Relative time is calculated based on Time and Interval inputs. @@ -164,6 +181,10 @@ const Page = () => { - Filters are dynamically applied to the table query. */ -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; diff --git a/src/pages/tenant/administration/audit-logs/log.js b/src/pages/tenant/administration/audit-logs/log.js index a86ef5332c00..d240889c26e7 100644 --- a/src/pages/tenant/administration/audit-logs/log.js +++ b/src/pages/tenant/administration/audit-logs/log.js @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { ApiGetCall, ApiPostCall } from "/src/api/ApiCall"; +import { ApiGetCall } from "/src/api/ApiCall"; import { Box, Typography, @@ -61,7 +61,7 @@ const Page = () => { if (logRequest.isSuccess) { var data = logRequest?.data?.Results?.[0]; - if (data && data?.Data.ActionUrl.includes("identity/administration/ViewBec")) { + if (data && data?.Data?.ActionUrl?.includes("identity/administration/ViewBec")) { data.Data.ActionUrl = data.Data.ActionUrl.replace( "identity/administration/ViewBec", "identity/administration/users/user/bec" @@ -88,7 +88,12 @@ const Page = () => { { label: "Tenant", value: data.Tenant }, { label: "User", - value: data?.Data?.RawData?.UserKey ?? data?.Data?.RawData?.AuditRecord?.userId ?? "N/A", + value: + data?.Data?.RawData?.CIPPUserKey ?? + data?.Data?.RawData?.AuditRecord?.CIPPuserId ?? + data?.Data?.RawData?.AuditRecord?.UserKey ?? + data?.Data?.RawData?.userId ?? + "N/A", }, { label: "IP Address", value: data?.Data?.IP }, { @@ -152,7 +157,7 @@ const Page = () => { {logData.Title} - + { {lookupIp && ( - + @@ -186,7 +191,7 @@ const Page = () => { )} - + { + const router = useRouter(); + const [searchId, setSearchId] = useState(null); + const [searchName, setSearchName] = useState(null); + const [isReady, setIsReady] = useState(false); + const processLogsDialog = useDialog(); + + useEffect(() => { + if (router.isReady) { + setSearchId(router.query.id || router.query.searchId); + setSearchName(router.query.name ? decodeURIComponent(router.query.name) : null); + setIsReady(true); + } + }, [router.isReady, router.query.id, router.query.searchId, router.query.name]); + + if (!isReady) { + return
    Loading...
    ; + } + + if (!searchId) { + return
    Search ID is required
    ; + } + + const pageTitle = searchName ? `${searchName}` : `Search Results - ${searchId}`; + + const handleBackClick = () => { + router.push("/tenant/administration/audit-logs/searches"); + }; + + // Process Logs API configuration + const processLogsApi = { + type: "POST", + url: "/api/ExecAuditLogSearch", + confirmText: + "Process these logs? Note: This will only alert on logs that match your Alert Configuration rules.", + relatedQueryKeys: ["AuditLogSearches"], + allowResubmit: true, + data: { + Action: "ProcessLogs", + SearchId: searchId, + }, + }; + + // Define offcanvas configuration with larger size for audit log details + const offcanvas = { + title: "Audit Log Details", + size: "xl", // Make the offcanvas extra large + children: (row) => , + }; + + return ( + <> + + + +
    + } + apiUrl="/api/ListAuditLogSearches" + apiDataKey="Results" + simpleColumns={searchResultsColumns} + queryKey={`AuditLogSearchResults-${searchId}`} + apiData={{ + Type: "SearchResults", + SearchId: searchId, + }} + offCanvas={offcanvas} + actions={[]} + /> + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/audit-logs/searches.js b/src/pages/tenant/administration/audit-logs/searches.js new file mode 100644 index 000000000000..3c5a92dcfbde --- /dev/null +++ b/src/pages/tenant/administration/audit-logs/searches.js @@ -0,0 +1,508 @@ +import { useState, useEffect } from "react"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog.jsx"; +import { Button, Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { useForm } from "react-hook-form"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { Grid } from "@mui/system"; +import { Add, ManageSearch } from "@mui/icons-material"; +import { useDialog } from "/src/hooks/use-dialog"; +import tabOptions from "./tabOptions.json"; +import { useSettings } from "/src/hooks/use-settings"; + +const simpleColumns = ["displayName", "status", "filterStartDateTime", "filterEndDateTime"]; + +const apiUrl = "/api/ListAuditLogSearches?Type=Searches"; +const pageTitle = "Log Searches"; + +const actions = [ + { + label: "View Results", + link: "/tenant/administration/audit-logs/search-results?id=[id]&name=[displayName]", + color: "primary", + icon: , + }, + { + label: "Process Logs", + url: "/api/ExecAuditLogSearch", + confirmText: + "Process these logs? Note: This will only alert on logs that match your Alert Configuration rules.", + type: "POST", + data: { + Action: "ProcessLogs", + SearchId: "id", + }, + icon: , + }, +]; + +const Page = () => { + const createSearchDialog = useDialog(); + const currentTenant = useSettings().currentTenant; + + const filterControl = useForm({ + mode: "onChange", + defaultValues: { + StatusFilter: { label: "All", value: "" }, + DateFilter: { label: "All Time", value: "" }, + }, + }); + + const [expanded, setExpanded] = useState(false); + const [apiUrlWithFilters, setApiUrlWithFilters] = useState(apiUrl); + + // Watch for filter changes and update API URL + const statusFilter = filterControl.watch("StatusFilter"); + const dateFilter = filterControl.watch("DateFilter"); + + useEffect(() => { + const params = new URLSearchParams(); + params.set("Type", "Searches"); // Always set Type=Searches for this page + + if (statusFilter?.value) { + params.set("Status", statusFilter.value); + } + + if (dateFilter?.value) { + params.set("Days", dateFilter.value); + } + + setApiUrlWithFilters(`/api/ListAuditLogSearches?${params.toString()}`); + }, [statusFilter, dateFilter]); + + // Create Search Dialog Configuration + const createSearchFields = [ + { + type: "textField", + name: "DisplayName", + label: "Search Name", + }, + { + type: "autoComplete", + name: "TenantFilter", + label: "Tenant", + multiple: false, + creatable: false, + api: { + url: "/api/ListTenants?AllTenantSelector=false", + labelField: (option) => `${option.displayName} (${option.defaultDomainName})`, + valueField: "defaultDomainName", + queryKey: "ListTenants-FormnotAllTenants", + excludeTenantFilter: true, + }, + validators: { validate: (value) => !!value?.value || "Please select a tenant" }, + required: true, + }, + { + type: "datePicker", + name: "StartTime", + label: "Start Date & Time", + dateTimeType: "datetime-local", + validators: { required: "Start time is required" }, + required: true, + }, + { + type: "datePicker", + name: "EndTime", + label: "End Date & Time", + dateTimeType: "datetime-local", + validators: { required: "End time is required" }, + required: true, + }, + { + type: "autoComplete", + name: "ServiceFilters", + label: "Services", + multiple: true, + creatable: false, + options: [ + { label: "Azure Active Directory", value: "AzureActiveDirectory" }, + { label: "Dynamics 365", value: "CRM" }, + { label: "Exchange Online", value: "Exchange" }, + { label: "Microsoft Flow", value: "MicrosoftFlow" }, + { label: "Microsoft Teams", value: "MicrosoftTeams" }, + { label: "OneDrive for Business", value: "OneDrive" }, + { label: "Power BI", value: "PowerBI" }, + { label: "Security & Compliance", value: "ThreatIntelligence" }, + { label: "SharePoint Online", value: "SharePoint" }, + { label: "Yammer", value: "Yammer" }, + ], + validators: { + validate: (values) => values?.length > 0 || "Please select at least one service", + }, + }, + { + type: "autoComplete", + name: "RecordTypeFilters", + label: "Record Types", + multiple: true, + creatable: false, + options: [ + { label: "Azure Active Directory", value: "azureActiveDirectory" }, + { label: "Azure AD Account Logon", value: "azureActiveDirectoryAccountLogon" }, + { label: "Azure AD STS Logon", value: "azureActiveDirectoryStsLogon" }, + { label: "Compliance DLP Endpoint", value: "complianceDLPEndpoint" }, + { label: "Compliance DLP Exchange", value: "complianceDLPExchange" }, + { label: "Compliance DLP SharePoint", value: "complianceDLPSharePoint" }, + { label: "Data Governance", value: "dataGovernance" }, + { label: "Exchange Admin", value: "exchangeAdmin" }, + { label: "Exchange Item", value: "exchangeItem" }, + { label: "Exchange Item Group", value: "exchangeItemGroup" }, + { label: "Information Worker Protection", value: "informationWorkerProtection" }, + { label: "Label Content Explorer", value: "labelContentExplorer" }, + { label: "Microsoft Flow", value: "microsoftFlow" }, + { label: "Microsoft Forms", value: "microsoftForms" }, + { label: "Microsoft Stream", value: "microsoftStream" }, + { label: "Microsoft Teams", value: "microsoftTeams" }, + { label: "Microsoft Teams Admin", value: "microsoftTeamsAdmin" }, + { label: "Microsoft Teams Analytics", value: "microsoftTeamsAnalytics" }, + { label: "Microsoft Teams Device", value: "microsoftTeamsDevice" }, + { label: "Microsoft Teams Shifts", value: "microsoftTeamsShifts" }, + { label: "MIP Label", value: "mipLabel" }, + { label: "OneDrive", value: "oneDrive" }, + { label: "Power Apps App", value: "powerAppsApp" }, + { label: "Power Apps Plan", value: "powerAppsPlan" }, + { label: "Power BI Audit", value: "powerBIAudit" }, + { label: "Power BI DLP", value: "powerBIDlp" }, + { label: "Security & Compliance Alerts", value: "securityComplianceAlerts" }, + { label: "Security & Compliance Insights", value: "securityComplianceInsights" }, + { label: "Security & Compliance RBAC", value: "securityComplianceRBAC" }, + { label: "SharePoint", value: "sharePoint" }, + { label: "SharePoint File Operation", value: "sharePointFileOperation" }, + { label: "SharePoint List Operation", value: "sharePointListOperation" }, + { label: "SharePoint Sharing Operation", value: "sharePointSharingOperation" }, + { label: "Threat Intelligence", value: "threatIntelligence" }, + { label: "Threat Intelligence ATP Content", value: "threatIntelligenceAtpContent" }, + { label: "Threat Intelligence URL", value: "threatIntelligenceUrl" }, + { label: "Workplace Analytics", value: "workplaceAnalytics" }, + ], + }, + { + type: "autoComplete", + name: "KeywordFilter", + label: "Keywords", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter keywords to search for", + options: [], + }, + { + type: "autoComplete", + name: "OperationsFilters", + label: "Operations", + multiple: true, + creatable: true, + placeholder: "Enter or select operations", + options: [ + // Authentication & User Operations + { label: "User Logged In", value: "UserLoggedIn" }, + { label: "Mailbox Login", value: "mailboxlogin" }, + + // User Management Operations + { label: "Add User", value: "add user." }, + { label: "Update User", value: "update user." }, + { label: "Delete User", value: "delete user." }, + { label: "Reset User Password", value: "reset user password." }, + { label: "Change User Password", value: "change user password." }, + { label: "Change User License", value: "change user license." }, + + // Group Management Operations + { label: "Add Group", value: "add group." }, + { label: "Update Group", value: "update group." }, + { label: "Delete Group", value: "delete group." }, + { label: "Add Member to Group", value: "add member to group." }, + { label: "Remove Member from Group", value: "remove member from group." }, + + // Mailbox Operations + { label: "New Mailbox", value: "New-Mailbox" }, + { label: "Set Mailbox", value: "Set-Mailbox" }, + { label: "Add Mailbox Permission", value: "add-mailboxpermission" }, + { label: "Remove Mailbox Permission", value: "remove-mailboxpermission" }, + { label: "Mail Items Accessed", value: "mailitemsaccessed" }, + + // Email Operations + { label: "Send Message", value: "send" }, + { label: "Send As", value: "sendas" }, + { label: "Send On Behalf", value: "sendonbehalf" }, + { label: "Create Item", value: "create" }, + { label: "Update Message", value: "update" }, + { label: "Copy Messages", value: "copy" }, + { label: "Move Messages", value: "move" }, + { label: "Move to Deleted Items", value: "movetodeleteditems" }, + { label: "Soft Delete", value: "softdelete" }, + { label: "Hard Delete", value: "harddelete" }, + + // Inbox Rules + { label: "New Inbox Rule", value: "new-inboxrule" }, + { label: "Set Inbox Rule", value: "set-inboxrule" }, + { label: "Update Inbox Rules", value: "updateinboxrules" }, + + // Folder Operations + { label: "Add Folder Permissions", value: "addfolderpermissions" }, + { label: "Remove Folder Permissions", value: "removefolderpermissions" }, + { label: "Update Folder Permissions", value: "updatefolderpermissions" }, + { label: "Update Calendar Delegation", value: "updatecalendardelegation" }, + + // SharePoint/OneDrive Operations (Common ones) + { label: "File Accessed", value: "FileAccessed" }, + { label: "File Modified", value: "FileModified" }, + { label: "File Deleted", value: "FileDeleted" }, + { label: "File Downloaded", value: "FileDownloaded" }, + { label: "File Uploaded", value: "FileUploaded" }, + { label: "Sharing Set", value: "SharingSet" }, + { label: "Anonymous Link Created", value: "AnonymousLinkCreated" }, + + // Role and Permission Operations + { label: "Add Member to Role", value: "add member to role." }, + { label: "Remove Member from Role", value: "remove member from role." }, + { label: "Add Service Principal", value: "add service principal." }, + { label: "Remove Service Principal", value: "remove service principal." }, + + // Company and Domain Operations + { label: "Add Domain to Company", value: "add domain to company." }, + { label: "Remove Domain from Company", value: "remove domain from company." }, + { label: "Verify Domain", value: "verify domain." }, + { label: "Set Company Information", value: "set company information." }, + + // Security Operations + { label: "Disable Strong Authentication", value: "Disable Strong Authentication." }, + { label: "Apply Record Label", value: "applyrecordlabel" }, + { label: "Update STS Refresh Token", value: "Update StsRefreshTokenValidFrom Timestamp." }, + ], + }, + { + type: "autoComplete", + name: "UserPrincipalNameFilters", + label: "User Principal Names", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter user principal names", + options: [], + }, + { + type: "autoComplete", + name: "IPAddressFilters", + label: "IP Addresses", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter IP addresses", + options: [], + }, + { + type: "autoComplete", + name: "ObjectIdFilters", + label: "Object IDs", + multiple: true, + creatable: true, + freeSolo: true, + placeholder: "Enter object IDs", + options: [], + }, + { + type: "autoComplete", + name: "AdministrativeUnitFilters", + label: "Administrative Units", + multiple: true, + creatable: true, + placeholder: "Enter administrative units", + api: { + url: "/api/ListGraphRequest", + queryKey: "AdministrativeUnits", + data: { + Endpoint: "directoryObjects/microsoft.graph.administrativeUnit", + $select: "id,displayName", + }, + dataKey: "Results", + labelField: "displayName", + valueField: "id", + addedField: { + id: "id", + displayName: "displayName", + }, + showRefresh: true, + }, + }, + { + type: "switch", + name: "ProcessLogs", + label: "Process Logs for Alerts", + helperText: "Enable to store this search for alert processing", + }, + ]; + + const createSearchApi = { + type: "POST", + url: "/api/ExecAuditLogSearch", + confirmText: + "Create this audit log search? This may take several minutes to hours to complete.", + relatedQueryKeys: ["AuditLogSearches"], + allowResubmit: true, + customDataformatter: (row, action, data) => { + const formattedData = { ...data }; + console.log("Formatted Data:", formattedData); + // Extract value from TenantFilter autocomplete object + if (formattedData.TenantFilter?.value) { + formattedData.TenantFilter = formattedData.TenantFilter.value; + } + + // Handle KeywordFilter - extract values from array and join with spaces + if (Array.isArray(formattedData.KeywordFilter)) { + const keywords = formattedData.KeywordFilter.map((item) => + typeof item === "object" ? item.value : item + ).filter(Boolean); + formattedData.KeywordFilter = keywords.join(" "); + } + + // Extract values from RecordTypeFilters array + if (Array.isArray(formattedData.RecordTypeFilters)) { + formattedData.RecordTypeFilters = formattedData.RecordTypeFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from ServiceFilters array + if (Array.isArray(formattedData.ServiceFilters)) { + formattedData.ServiceFilters = formattedData.ServiceFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from OperationsFilters array + if (Array.isArray(formattedData.OperationsFilters)) { + formattedData.OperationsFilters = formattedData.OperationsFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from UserPrincipalNameFilters array + if (Array.isArray(formattedData.UserPrincipalNameFilters)) { + formattedData.UserPrincipalNameFilters = formattedData.UserPrincipalNameFilters.map( + (item) => (typeof item === "object" ? item.value : item) + ); + } + + // Extract values from IPAddressFilters array + if (Array.isArray(formattedData.IPAddressFilters)) { + formattedData.IPAddressFilters = formattedData.IPAddressFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from ObjectIdFilters array + if (Array.isArray(formattedData.ObjectIdFilters)) { + formattedData.ObjectIdFilters = formattedData.ObjectIdFilters.map((item) => + typeof item === "object" ? item.value : item + ); + } + + // Extract values from AdministrativeUnitFilters array + if (Array.isArray(formattedData.AdministrativeUnitFilters)) { + formattedData.AdministrativeUnitFilters = formattedData.AdministrativeUnitFilters.map( + (item) => (typeof item === "object" ? item.value : item) + ); + } + + // Remove empty arrays to avoid sending unnecessary data + Object.keys(formattedData).forEach((key) => { + if (Array.isArray(formattedData[key]) && formattedData[key].length === 0) { + delete formattedData[key]; + } + if ( + formattedData[key] === "" || + formattedData[key] === null || + formattedData[key] === undefined + ) { + delete formattedData[key]; + } + }); + + return formattedData; + }, + }; + + return ( + <> + setExpanded(!expanded)}> + }> + Filter Search List + + + + {/* Status Filter */} + + + + + {/* Date Range Filter */} + + + + + + + } + title={pageTitle} + apiUrl={apiUrlWithFilters} + apiDataKey="Results" + simpleColumns={simpleColumns} + queryKey={`AuditLogSearches-${filterControl.getValues().StatusFilter?.value || "All"}-${ + filterControl.getValues().DateFilter?.value || "AllTime" + }-${currentTenant}`} + actions={actions} + cardButton={ + + } + /> + + + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/audit-logs/tabOptions.json b/src/pages/tenant/administration/audit-logs/tabOptions.json new file mode 100644 index 000000000000..b7f764d5821f --- /dev/null +++ b/src/pages/tenant/administration/audit-logs/tabOptions.json @@ -0,0 +1,10 @@ +[ + { + "label": "Saved Logs", + "path": "/tenant/administration/audit-logs" + }, + { + "label": "Log Searches", + "path": "/tenant/administration/audit-logs/searches" + } +] \ No newline at end of file diff --git a/src/pages/tenant/administration/authentication-methods/index.js b/src/pages/tenant/administration/authentication-methods/index.js index 7da41b844058..d86eccb0c3e6 100644 --- a/src/pages/tenant/administration/authentication-methods/index.js +++ b/src/pages/tenant/administration/authentication-methods/index.js @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Check, Block } from "@mui/icons-material"; const Page = () => { const pageTitle = "Auth Methods"; @@ -12,16 +13,18 @@ const Page = () => { { label: "Enable Policy", type: "POST", + icon: , url: "/api/SetAuthMethod", - data: { state: "enabled", id: "id" }, + data: { state: "!enabled", id: "id" }, confirmText: "Are you sure you want to enable this policy?", multiPost: false, }, { label: "Disable Policy", type: "POST", + icon: , url: "/api/SetAuthMethod", - data: { state: "disabled", id: "id" }, + data: { state: "!disabled", id: "id" }, confirmText: "Are you sure you want to disable this policy?", multiPost: false, }, diff --git a/src/pages/tenant/administration/enterprise-apps/index.js b/src/pages/tenant/administration/enterprise-apps/index.js deleted file mode 100644 index 58fa6b2ddf5f..000000000000 --- a/src/pages/tenant/administration/enterprise-apps/index.js +++ /dev/null @@ -1,54 +0,0 @@ -// this page is going to need some love for accounting for filters: https://github.com/KelvinTegelaar/CIPP/blob/main/src/views/tenant/administration/ListEnterpriseApps.jsx#L83 -import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; - -const Page = () => { - const pageTitle = "Enterprise Applications"; - const apiUrl = "/api/ListGraphRequest"; - const actions = []; - - const offCanvas = { - extendedInfoFields: [ - "displayName", - "createdDateTime", - "publisherName", - "replyUrls", - "appOwnerOrganizationId", - "tags", - ], - actions: actions, - }; - - const simpleColumns = [ - "info.logoUrl", - "displayName", - "appId", - "createdDateTime", - "publisherName", - "homepage", - ]; - - const apiParams = { - Endpoint: "servicePrincipals", - $select: - "appId,displayName,createdDateTime,accountEnabled,homepage,publisherName,signInAudience,replyUrls,verifiedPublisher,info,api,appOwnerOrganizationId,tags", - $count: true, - $top: 999, - }; - - return ( - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/tenant/administration/securescore/index.js b/src/pages/tenant/administration/securescore/index.js index 432b4946e82f..f6169e00005b 100644 --- a/src/pages/tenant/administration/securescore/index.js +++ b/src/pages/tenant/administration/securescore/index.js @@ -3,12 +3,12 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; import { useSecureScore } from "../../../../hooks/use-securescore"; import { CippInfoBar } from "../../../../components/CippCards/CippInfoBar"; -import { Box, Button, Chip, Container, Grid, Typography } from "@mui/material"; +import { Box, Button, Chip, Container, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import { CheckCircleIcon, GlobeAltIcon } from "@heroicons/react/24/outline"; import { Map, Score } from "@mui/icons-material"; import { CippChartCard } from "../../../../components/CippCards/CippChartCard"; import TimeAgo from "javascript-time-ago"; -import en from "javascript-time-ago/locale/en.json"; import CippButtonCard from "../../../../components/CippCards/CippButtonCard"; import DOMPurify from "dompurify"; import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; @@ -17,6 +17,7 @@ import { useState } from "react"; import { CippTableDialog } from "../../../../components/CippComponents/CippTableDialog"; import { CippImageCard } from "../../../../components/CippCards/CippImageCard"; import { useSettings } from "../../../../hooks/use-settings"; +import { useRouter } from "next/navigation"; const Page = () => { const currentTenant = useSettings().currentTenant; @@ -25,9 +26,16 @@ const Page = () => { const [actionData, setActionData] = useState({ data: {}, action: {}, ready: false }); const [updatesData, setUpdatesData] = useState({ data: {}, ready: false }); const cippTableDialog = useDialog(); - - TimeAgo.addLocale(en); + const router = useRouter(); const timeAgo = new TimeAgo("en-US"); + + const openRemediation = (url) => { + if (url.startsWith("https")) { + window.open(url, "_blank"); + } else { + router.push(url); + } + }; return ( { > {currentTenant === "AllTenants" && ( - + { )} {currentTenant !== "AllTenants" && ( <> - + { ]} /> - + { {currentTenant !== "AllTenants" && secureScore.isSuccess && secureScore.translatedData.controlScores.map((secureScoreControl) => ( - + { CardButton={ <> - + {secureScoreControl.controlStateUpdates?.length > 0 && ( + } + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/administration/tenants/index.js b/src/pages/tenant/administration/tenants/index.js index 1fba7f387944..0b85618ece44 100644 --- a/src/pages/tenant/administration/tenants/index.js +++ b/src/pages/tenant/administration/tenants/index.js @@ -1,33 +1,11 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { ApiGetCall } from "../../../../api/ApiCall"; -import { useEffect } from "react"; +import { Edit } from "@mui/icons-material"; +import tabOptions from "./tabOptions"; const Page = () => { - //this page is special and requires us to craft the columns and DashboardLayout const pageTitle = "Tenants"; - const tenantData = ApiGetCall({ - url: "/api/listTenants", - queryKey: "ListTenants", - }); - - useEffect(() => { - if (tenantData.isSuccess) { - tenantData.data.forEach((tenant) => { - Object.assign(tenant, { - portal_m365: `https://admin.microsoft.com/Partner/BeginClientSession.aspx?CTID=${tenant.customerId}&CSDEST=o365admincenter`, - portal_exchange: `https://admin.exchange.microsoft.com/?landingpage=homepage&form=mac_sidebar&delegatedOrg=${tenant.defaultDomainName}`, - portal_entra: `https://entra.microsoft.com/${tenant.defaultDomainName}`, - portal_teams: `https://admin.teams.microsoft.com/?delegatedOrg=${tenant.defaultDomainName}`, - portal_azure: `https://portal.azure.com/${tenant.defaultDomainName}`, - portal_intune: `https://intune.microsoft.com/${tenant.defaultDomainName}`, - portal_security: `https://security.microsoft.com/?tid=${tenant.customerId}`, - portal_compliance: `https://purview.microsoft.com/?tid=${tenant.customerId}`, - portal_sharepoint: `https://admin.microsoft.com/Partner/beginclientsession.aspx?CTID=${tenant.customerId}&CSDEST=SharePoint`, - }); - }); - } - }, [tenantData.isSuccess]); const simpleColumns = [ "displayName", @@ -41,17 +19,35 @@ const Page = () => { "portal_security", "portal_compliance", ]; + + const actions = [ + { + label: "Edit Tenant", + link: "/tenant/administration/tenants/edit?id=[customerId]", + icon: , + }, + ]; + return ( ); }; -// Adding the layout for Dashboard -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; diff --git a/src/pages/tenant/administration/tenants/tabOptions.json b/src/pages/tenant/administration/tenants/tabOptions.json new file mode 100644 index 000000000000..66f0af8440a1 --- /dev/null +++ b/src/pages/tenant/administration/tenants/tabOptions.json @@ -0,0 +1,10 @@ +[ + { + "label": "Tenants", + "path": "/tenant/administration/tenants" + }, + { + "label": "Groups", + "path": "/tenant/administration/tenants/groups" + } +] diff --git a/src/pages/tenant/backup/backup-wizard/add.jsx b/src/pages/tenant/backup/backup-wizard/add.jsx index 2d2f4276c681..cbceda81daf6 100644 --- a/src/pages/tenant/backup/backup-wizard/add.jsx +++ b/src/pages/tenant/backup/backup-wizard/add.jsx @@ -1,10 +1,13 @@ import React from "react"; -import { Grid, Typography } from "@mui/material"; +import { Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; +import { omit } from "lodash"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; +import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; const CreateBackup = () => { const userSettingsDefaults = useSettings(); @@ -24,13 +27,12 @@ const CreateBackup = () => { antiphishing: true, CippWebhookAlerts: true, CippScriptedAlerts: true, - CippStandards: true, }, }); return ( { const tenantFilter = values.tenantFilter || tenantDomain; const shippedValues = { TenantFilter: tenantFilter, - Name: `CIPP Backup ${tenantFilter}`, + Name: `CIPP Backup - ${tenantFilter}`, Command: { value: `New-CIPPBackup` }, - Parameters: { backupType: "Scheduled", ScheduledBackupValues: { ...values } }, + Parameters: { backupType: "Scheduled", ScheduledBackupValues: { ...omit(values, ['tenantFilter']) } }, ScheduledTime: unixTime, Recurrence: { value: "1d" }, }; return shippedValues; }} backButtonTitle="Backup Tasks" + allowResubmit={true} > Backups are stored in CIPP's storage and can be restored using the CIPP Restore Backup Wizard. Backups run daily or on demand by clicking the backup now button. - - - + + - + Identity - + { formControl={formControl} /> - + - + Conditional Access - + { formControl={formControl} /> - {/* Optional: Add an empty Grid item to balance the layout */} - + {/* Optional: Add an empty Grid to balance the layout */} + - + Intune - + { formControl={formControl} /> - + { formControl={formControl} /> - + { formControl={formControl} /> - {/* Add an empty Grid item to fill the second column */} - + {/* Add an empty Grid to fill the second column */} + - + Email Security - + { formControl={formControl} /> - + { /> - + CIPP - + { formControl={formControl} /> - + { formControl={formControl} /> - - - - {/* Add an empty Grid item to fill the second column */} - + {/* Add an empty Grid to fill the second column */} + ); diff --git a/src/pages/tenant/backup/backup-wizard/index.js b/src/pages/tenant/backup/backup-wizard/index.js index a5a5b04b6d62..f7b9e0b6b452 100644 --- a/src/pages/tenant/backup/backup-wizard/index.js +++ b/src/pages/tenant/backup/backup-wizard/index.js @@ -1,6 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Restore, Backup } from "@mui/icons-material"; import Link from "next/link"; const Page = () => { @@ -10,10 +11,14 @@ const Page = () => { apiUrl="/api/ListScheduledItems" cardButton={ <> - - @@ -23,7 +28,13 @@ const Page = () => { showHidden: true, Type: "New-CIPPBackup", }} - simpleColumns={["Tenant", "TaskState", "ExecutedTime"]} + simpleColumns={[ + "Tenant", + "Name", + "Parameters.ScheduledBackupValues", + "TaskState", + "ExecutedTime", + ]} actions={[ { label: "Delete Task", @@ -34,7 +45,7 @@ const Page = () => { }, ]} offCanvas={{ - extendedInfoFields: ["RowKey", "TaskState", "ExecutedTime"], + extendedInfoFields: ["Name", "Tenant", "TaskState", "ExecutedTime"], actions: [ { label: "Delete Task", diff --git a/src/pages/tenant/backup/backup-wizard/restore.jsx b/src/pages/tenant/backup/backup-wizard/restore.jsx index 2a332140b139..7f4a6414346a 100644 --- a/src/pages/tenant/backup/backup-wizard/restore.jsx +++ b/src/pages/tenant/backup/backup-wizard/restore.jsx @@ -1,11 +1,13 @@ -import React, { useState, useEffect } from "react"; -import { Alert, Grid, Typography } from "@mui/material"; -import { useForm, useWatch } from "react-hook-form"; +import { useState, useEffect } from "react"; +import { Alert, Divider, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; +import { Chip, Stack } from "@mui/material"; const RestoreBackupForm = () => { const userSettingsDefaults = useSettings(); @@ -85,7 +87,6 @@ const RestoreBackupForm = () => { antiphishing: values.antiphishing, CippWebhookAlerts: values.CippWebhookAlerts, CippScriptedAlerts: values.CippScriptedAlerts, - CippStandards: values.CippStandards, overwrite: values.overwrite, }, }, @@ -95,6 +96,7 @@ const RestoreBackupForm = () => { Email: values.email, PSA: values.psa, }, + DisallowDuplicateName: true, }; return shippedValues; }} @@ -104,36 +106,42 @@ const RestoreBackupForm = () => { Use this form to restore a backup for a tenant. Please select the tenant, backup, and restore options. - + {/* Backup Selector */} - + `${option.RowKey}`, - valueField: "RowKey", + labelField: (option) => { + const match = option.BackupName.match(/.*_(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/); + return match ? `${match[1]} @ ${match[2]}:${match[3]}` : option.BackupName; + }, + valueField: "BackupName", data: { Type: "Scheduled", - TenantFilter: tenantFilter, + NameOnly: true, }, }} formControl={formControl} + required={true} + validators={{ + validate: (value) => !!value || "Please select a backup", + }} /> {/* Restore Settings */} - + Restore Settings {/* Identity */} - + Identity { {/* Conditional Access */} - + Conditional Access { {/* Intune */} - + Intune { {/* Email Security */} - + Email Security { {/* CIPP */} - + CIPP { name="CippScriptedAlerts" formControl={formControl} /> - {/* Overwrite Existing Entries */} - + { compareType="is" compareValue={true} > - + Warning: Overwriting existing entries will remove the current settings and replace them with the backup settings. If you have selected to restore @@ -245,10 +247,10 @@ const RestoreBackupForm = () => { {/* Send Results To */} - + Send Restore results to: - + { formControl={formControl} /> - + - + - + + + {/* Review and Confirm */} - - Review and Confirm - + + Review and Confirm + Please review the selected options before submitting. - + Selected Tenant: - {tenantFilter} + + {tenantFilter} + - + Selected Backup: - + {formControl.watch("backup")?.label || "None selected"} - + Overwrite Existing Configuration: - {formControl.watch("overwrite") ? "Yes" : "No"} + + {formControl.watch("overwrite") ? "Yes" : "No"} + - + + Send Results To: - - {formControl.watch("webhook") && "Webhook "} - {formControl.watch("email") && "E-mail "} - {formControl.watch("psa") && "PSA "} - {!formControl.watch("webhook") && - !formControl.watch("email") && - !formControl.watch("psa") && - "None"} + + + {formControl.watch("webhook") && } + {formControl.watch("email") && } + {formControl.watch("psa") && } + {!formControl.watch("webhook") && + !formControl.watch("email") && + !formControl.watch("psa") && } + diff --git a/src/pages/tenant/conditional/deploy-vacation/add.jsx b/src/pages/tenant/conditional/deploy-vacation/add.jsx index 5e64bb392ea3..b4fa24353a71 100644 --- a/src/pages/tenant/conditional/deploy-vacation/add.jsx +++ b/src/pages/tenant/conditional/deploy-vacation/add.jsx @@ -1,24 +1,25 @@ import React from "react"; -import { Box, Divider, Grid, Typography } from "@mui/material"; +import { Box, Divider, Stack, Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; -import { useSettings } from "/src/hooks/use-settings"; import { CippFormUserSelector } from "/src/components/CippComponents/CippFormUserSelector"; +import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; const Page = () => { - const userSettingsDefaults = useSettings(); - const tenantDomain = userSettingsDefaults?.currentTenant; - const formControl = useForm({ mode: "onChange", defaultValues: { - tenantFilter: tenantDomain, vacation: true, }, }); + // Watch the selected tenant to update dependent fields + const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + const tenantDomain = selectedTenant?.value || selectedTenant; + return ( <> { postUrl="/api/ExecCAExclusion" customDataformatter={(values) => { const shippedValues = { - tenantFilter: tenantDomain, - UserId: values.UserId?.value, + tenantFilter: values.tenantFilter?.value || values.tenantFilter, + Users: values.Users, PolicyId: values.PolicyId?.value, StartDate: values.startDate, EndDate: values.endDate, @@ -39,66 +40,124 @@ const Page = () => { return shippedValues; }} > - + Vacation mode adds scheduled tasks to add and remove users from Conditional Access (CA) exclusions for a specific period of time. Select the CA policy and the date range. - - + + + + + {/* User Selector */} - + {/* Conditional Access Policy Selector */} - + `${option.displayName}`, - valueField: "id", - }} + api={ + tenantDomain + ? { + queryKey: `ListConditionalAccessPolicies-${tenantDomain}`, + url: "/api/ListConditionalAccessPolicies", + data: { tenantFilter: tenantDomain }, + labelField: (option) => `${option.displayName}`, + valueField: "id", + } + : null + } multiple={false} formControl={formControl} + validators={{ + validate: (option) => { + if (!option?.value) { + return "Picking a policy is required"; + } + return true; + }, + }} + required={true} + disabled={!tenantDomain} /> {/* Start Date Picker */} - + { + if (!value) { + return "Start date is required"; + } + return true; + }, + }} /> {/* End Date Picker */} - + { + const startDate = formControl.getValues("startDate"); + if (!value) { + return "End date is required"; + } + if (startDate && value && new Date(value * 1000) < new Date(startDate * 1000)) { + return "End date must be after start date"; + } + return true; + }, + }} /> - +
    ); diff --git a/src/pages/tenant/conditional/deploy-vacation/index.js b/src/pages/tenant/conditional/deploy-vacation/index.js index ddddd13809c2..a4a95667cfee 100644 --- a/src/pages/tenant/conditional/deploy-vacation/index.js +++ b/src/pages/tenant/conditional/deploy-vacation/index.js @@ -1,14 +1,35 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; import { Button } from "@mui/material"; +import { EventAvailable } from "@mui/icons-material"; import Link from "next/link"; +import { Delete } from "@mui/icons-material"; +import { EyeIcon } from "@heroicons/react/24/outline"; const Page = () => { + const actions = [ + { + label: "View Task Details", + link: "/cipp/scheduler/task?id=[RowKey]", + icon: , + }, + { + label: "Cancel Vacation Mode", + type: "POST", + url: "/api/RemoveScheduledItem", + data: { ID: "RowKey" }, + confirmText: + "Are you sure you want to cancel this vacation mode entry? This might mean the user will remain in vacation mode permanently.", + icon: , + multiPost: false, + }, + ]; + return ( - @@ -17,14 +38,30 @@ const Page = () => { apiUrl="/api/ListScheduledItems?Type=Set-CIPPCAExclusion" queryKey="VacationMode" tenantInTitle={false} + actions={actions} simpleColumns={[ + "Tenant", "Name", "TaskState", "ScheduledTime", + "ExecutedTime", "Parameters.ExclusionType", + "Parameters.Users", "Parameters.UserName", - "Parameters.PolicyId", ]} + offCanvas={{ + extendedInfoFields: [ + "Name", + "TaskState", + "ScheduledTime", + "Parameters.Users", + "Parameters.UserName", + "Parameters.PolicyId", + "Tenant", + "ExecutedTime", + ], + actions: actions, + }} /> ); }; diff --git a/src/pages/tenant/conditional/list-named-locations/add.jsx b/src/pages/tenant/conditional/list-named-locations/add.jsx index 58691f1a29cb..906b869ed9d1 100644 --- a/src/pages/tenant/conditional/list-named-locations/add.jsx +++ b/src/pages/tenant/conditional/list-named-locations/add.jsx @@ -1,5 +1,6 @@ import React from "react"; -import { Grid, Typography } from "@mui/material"; +import { Typography } from "@mui/material"; +import { Grid } from "@mui/system"; import { useForm } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; @@ -31,17 +32,19 @@ const DeployNamedLocationForm = () => { - + - + { /> - + { compareType="is" compareValue="IPLocation" > - + { validators={{ required: "IPs are required" }} /> - + { compareType="is" compareValue="Countries" > - + { validators={{ required: "At least one country must be selected" }} /> - + { const pageTitle = "Named Locations"; const actions = [ + { + label: "Rename named location", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!rename", + }, + fields: [{ type: "textField", name: "input", label: "New Name" }], + confirmText: "Enter the new name for this named location.", + }, + { + label: "Mark as Trusted", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!setTrusted", + }, + confirmText: "Are you sure you want to mark this IP location as trusted?", + condition: (row) => + row["@odata.type"] == "#microsoft.graph.ipNamedLocation" && !row.isTrusted, + }, + { + label: "Mark as Untrusted", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!setUntrusted", + }, + confirmText: "Are you sure you want to mark this IP location as untrusted?", + condition: (row) => row["@odata.type"] == "#microsoft.graph.ipNamedLocation" && row.isTrusted, + }, { label: "Add location to named location", - type: "GET", + type: "POST", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", - change: "addLocation", + change: "!addLocation", }, fields: [{ type: "textField", name: "input", label: "Country Code" }], confirmText: "Enter a two-letter country code, e.g., US.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation", }, { label: "Remove location from named location", type: "POST", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", - change: "removeLocation", + change: "!removeLocation", }, fields: [{ type: "textField", name: "input", label: "Country Code" }], confirmText: "Enter a two-letter country code, e.g., US.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.countryNamedLocation", }, { label: "Add IP to named location", type: "POST", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", - change: "addIp", + change: "!addIp", }, fields: [{ type: "textField", name: "input", label: "IP" }], confirmText: "Enter an IP in CIDR format, e.g., 1.1.1.1/32.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.ipNamedLocation", }, { label: "Remove IP from named location", type: "POST", url: "/api/ExecNamedLocation", + icon: , data: { namedLocationId: "id", - change: "removeIp", + change: "!removeIp", }, fields: [{ type: "textField", name: "input", label: "IP" }], - confirmText: "Enter an IP in CIDR format, e.g., 1.1.1.1/32.", + condition: (row) => row["@odata.type"] == "#microsoft.graph.ipNamedLocation", + }, + { + label: "Delete named location", + type: "POST", + url: "/api/ExecNamedLocation", + icon: , + data: { + namedLocationId: "id", + change: "!delete", + }, + confirmText: + "Are you sure you want to delete this named location? This action cannot be undone.", + color: "error", }, ]; - const offCanvas = { - extendedInfoFields: ["displayName", "type", "rangeOrLocation"], - actions: actions, - }; - return ( - @@ -75,7 +135,7 @@ const Page = () => { simpleColumns={[ "displayName", "includeUnknownCountriesAndRegions", - "type", + "isTrusted", "rangeOrLocation", "modifiedDateTime", ]} diff --git a/src/pages/tenant/conditional/list-policies/index.js b/src/pages/tenant/conditional/list-policies/index.js index 35f77a73f286..0feb27282419 100644 --- a/src/pages/tenant/conditional/list-policies/index.js +++ b/src/pages/tenant/conditional/list-policies/index.js @@ -5,9 +5,11 @@ import { Check as CheckIcon, Delete as DeleteIcon, MenuBook as MenuBookIcon, + AddModerator as AddModeratorIcon, Visibility as VisibilityIcon, + Edit as EditIcon, } from "@mui/icons-material"; -import { Button } from "@mui/material"; +import { Box, Button } from "@mui/material"; import Link from "next/link"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; @@ -23,48 +25,58 @@ const Page = () => { type: "POST", url: "/api/AddCATemplate", dataFunction: (data) => { + if (Array.isArray(data)) { + return data.map((item) => JSON.parse(item.rawjson)); + } return JSON.parse(data.rawjson); }, + hideBulk: true, confirmText: "Are you sure you want to create a template based on this policy?", icon: , color: "info", }, { label: "Enable policy", - type: "GET", - url: "/api/EditCAPolicy?State=Enabled", + type: "POST", + url: "/api/EditCAPolicy", data: { GUID: "id", + State: "!Enabled", }, confirmText: "Are you sure you want to enable this policy?", + condition: (row) => row.state !== "enabled", icon: , color: "info", }, { label: "Disable policy", - type: "GET", - url: "/api/EditCAPolicy?State=Disabled", + type: "POST", + url: "/api/EditCAPolicy", data: { GUID: "id", + State: "!Disabled", }, confirmText: "Are you sure you want to disable this policy?", + condition: (row) => row.state !== "disabled", icon: , color: "info", }, { label: "Set policy to report only", - type: "GET", - url: "/api/EditCAPolicy?State=enabledForReportingButNotEnforced", + type: "POST", + url: "/api/EditCAPolicy", data: { GUID: "id", + State: "!enabledForReportingButNotEnforced", }, confirmText: "Are you sure you want to set this policy to report only?", + condition: (row) => row.state !== "enabledForReportingButNotEnforced", icon: , color: "info", }, { label: "Delete policy", - type: "GET", + type: "POST", url: "/api/RemoveCAPolicy", data: { GUID: "id", @@ -73,11 +85,52 @@ const Page = () => { icon: , color: "danger", }, + { + label: "Change Display Name", + type: "POST", + url: "/api/EditCAPolicy", + data: { + GUID: "id", + }, + confirmText: "Are you sure you want to change the display name of this policy?", + icon: , + color: "info", + hideBulk: true, + fields: [ + { + type: "textField", + name: "newDisplayName", + label: "New Display Name", + required: true, + validate: (value) => { + if (!value) { + return "Display name is required."; + } + return true; + }, + }, + ], + }, + { + label: "Add service provider exception to policy", + type: "POST", + url: "/api/ExecCAServiceExclusion", + data: { + GUID: "id", + }, + confirmText: "Are you sure you want to add the service provider exception to this policy?", + icon: , + color: "warning", + }, ]; // Off-canvas configuration const offCanvas = { - children: (row) => , + children: (row) => ( + + + + ), size: "xl", }; @@ -105,7 +158,11 @@ const Page = () => { - diff --git a/src/pages/tenant/conditional/list-template/edit.jsx b/src/pages/tenant/conditional/list-template/edit.jsx new file mode 100644 index 000000000000..0639fb829417 --- /dev/null +++ b/src/pages/tenant/conditional/list-template/edit.jsx @@ -0,0 +1,142 @@ +import React, { useEffect, useState } from "react"; +import { Alert, Box, Typography } from "@mui/material"; +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 { ApiGetCall } from "/src/api/ApiCall"; +import CippTemplateFieldRenderer from "/src/components/CippComponents/CippTemplateFieldRenderer"; + +const EditCATemplate = () => { + const router = useRouter(); + const { GUID } = router.query; + const [templateData, setTemplateData] = useState(null); + const [originalData, setOriginalData] = useState(null); + + const formControl = useForm({ mode: "onChange" }); + + // Fetch the template data + const templateQuery = ApiGetCall({ + url: `/api/ListCATemplates?GUID=${GUID}`, + queryKey: `CATemplate-${GUID}`, + enabled: !!GUID, + }); + + useEffect(() => { + if (templateQuery.isSuccess && templateQuery.data) { + // Find the template with matching GUID + const template = Array.isArray(templateQuery.data) + ? templateQuery.data.find((t) => t.GUID === GUID) + : templateQuery.data; + + if (template) { + setTemplateData(template); + setOriginalData(template); + } + } + }, [templateQuery.isSuccess, templateQuery.data, GUID]); + + // Custom data formatter to convert autoComplete objects to values + const customDataFormatter = (values) => { + // Recursively extract values from autoComplete objects and fix @odata issues + const extractValues = (obj) => { + if (!obj) return obj; + + // If this is an autoComplete object with label/value, return just the value + if ( + obj && + typeof obj === "object" && + obj.hasOwnProperty("value") && + obj.hasOwnProperty("label") + ) { + return obj.value; + } + + // If it's an array, process each item + if (Array.isArray(obj)) { + return obj.map((item) => extractValues(item)); + } + + // If it's an object, process each property + if (typeof obj === "object") { + const result = {}; + Object.keys(obj).forEach((key) => { + const value = extractValues(obj[key]); + + // Handle @odata objects created by React Hook Form's dot notation interpretation + if (key.endsWith("@odata") && value && typeof value === "object") { + // Convert @odata objects back to dot notation properties + Object.keys(value).forEach((odataKey) => { + // Always try to restore the original @odata property, regardless of form value + const baseKey = key.replace("@odata", ""); + const originalKey = `${baseKey}@odata.${odataKey}`; + const originalValue = getOriginalValueByPath(originalData, originalKey); + if (originalValue !== undefined) { + result[originalKey] = originalValue; + } + }); + } else { + result[key] = value; + } + }); + return result; + } + + // For primitive values, return as-is + return obj; + }; + + // Helper function to get original value by dot-notation path + const getOriginalValueByPath = (obj, path) => { + const keys = path.split("."); + let current = obj; + for (const key of keys) { + if (current && typeof current === "object" && key in current) { + current = current[key]; + } else { + return undefined; + } + } + return current; + }; + + // Extract values from the entire form data and include GUID + const processedValues = extractValues(values); + + return { + GUID, + ...processedValues, + }; + }; + + return ( + + + {templateQuery.isLoading ? ( + + ) : templateQuery.isError || !templateData ? ( + Error loading template or template not found. + ) : ( + + )} + + + ); +}; + +EditCATemplate.getLayout = (page) => {page}; + +export default EditCATemplate; diff --git a/src/pages/tenant/conditional/list-template/index.js b/src/pages/tenant/conditional/list-template/index.js index 41a8010d6b50..e53684b10849 100644 --- a/src/pages/tenant/conditional/list-template/index.js +++ b/src/pages/tenant/conditional/list-template/index.js @@ -2,15 +2,74 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { Delete, GitHub, Edit } from "@mui/icons-material"; +import { ApiGetCall } from "/src/api/ApiCall"; +import Link from "next/link"; const Page = () => { const pageTitle = "Available Conditional Access Templates"; - + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); const actions = [ + { + label: "Edit Template", + link: "/tenant/conditional/list-template/edit?GUID=[GUID]", + icon: , + color: "info", + }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", - type: "GET", + type: "POST", url: "/api/RemoveCATemplate", + icon: , data: { ID: "GUID" }, confirmText: "Do you want to delete the template?", multiPost: false, diff --git a/src/pages/tenant/gdap-management/index.js b/src/pages/tenant/gdap-management/index.js index 53a4c348a0e4..372c3422e0b6 100644 --- a/src/pages/tenant/gdap-management/index.js +++ b/src/pages/tenant/gdap-management/index.js @@ -1,22 +1,19 @@ import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; -import { Box, Container } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Container } from "@mui/system"; +import { Grid } from "@mui/system"; import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; import { ApiPostCall, ApiGetCallWithPagination } from "../../../api/ApiCall"; import { Add, AdminPanelSettings, - Group, HourglassBottom, Layers, SupervisorAccount, } from "@mui/icons-material"; import CippPermissionCheck from "../../../components/CippSettings/CippPermissionCheck"; -import { Alert, Button, Step, StepLabel, Stepper, SvgIcon, Typography } from "@mui/material"; -import { PlusIcon } from "@heroicons/react/24/outline"; -import { CippApiResults } from "../../../components/CippComponents/CippApiResults"; +import { Button } from "@mui/material"; import { useEffect, useState } from "react"; import CippButtonCard from "../../../components/CippCards/CippButtonCard"; import { WizardSteps } from "/src/components/CippWizard/wizard-steps"; @@ -62,7 +59,7 @@ const Page = () => { // check templates for CIPP Defaults if ( roleTemplates?.data?.pages?.[0].Results?.length > 0 && - roleTemplates?.data?.pages?.[0].Results?.find((t) => t.TemplateId === "CIPP Defaults") + roleTemplates?.data?.pages?.[0].Results?.find((t) => t?.TemplateId === "CIPP Defaults") ) { promptCreateDefaults = false; } @@ -129,7 +126,7 @@ const Page = () => { icon: , data: roleTemplates.data?.pages - ?.map((page) => page?.Results.length) + ?.map((page) => page?.Results?.length) .reduce((a, b) => a + b, 0) ?? 0, name: "Role Templates", }, diff --git a/src/pages/tenant/gdap-management/invites/add.js b/src/pages/tenant/gdap-management/invites/add.js index 22d72810f0c8..ba659a92cffb 100644 --- a/src/pages/tenant/gdap-management/invites/add.js +++ b/src/pages/tenant/gdap-management/invites/add.js @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import CippPageCard from "../../../../components/CippCards/CippPageCard"; import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; import { CippDataTable } from "../../../../components/CippTable/CippDataTable"; @@ -40,12 +40,12 @@ const Page = () => { const createCippDefaults = ApiPostCall({ urlFromData: true, - relatedQueryKeys: ["ListGDAPRoleTemplatesAutocomplete"], + relatedQueryKeys: ["ListGDAPRoleTemplatesAutocomplete", "ListGDAPRoleTemplates"], }); const templateList = ApiGetCall({ url: "/api/ExecGDAPRoleTemplate", - queryKey: "ListGDAPRoleTemplatesAutocomplete", + queryKey: "ListGDAPRoleTemplates", }); const selectedTemplate = useWatch({ control: formControl.control, name: "roleMappings" }); diff --git a/src/pages/tenant/gdap-management/invites/index.js b/src/pages/tenant/gdap-management/invites/index.js index 7cf52a876857..99dd16053bf1 100644 --- a/src/pages/tenant/gdap-management/invites/index.js +++ b/src/pages/tenant/gdap-management/invites/index.js @@ -3,25 +3,44 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Add } from "@mui/icons-material"; import Link from "next/link"; +import { TrashIcon } from "@heroicons/react/24/outline"; const pageTitle = "GDAP Invites"; const simpleColumns = ["Timestamp", "RowKey", "InviteUrl", "OnboardingUrl", "RoleMappings"]; const apiUrl = "/api/ListGDAPInvite"; +const actions = [ + { + label: "Delete Invite", + url: "/api/ExecGDAPInvite", + type: "POST", + icon: , + confirmText: + "Are you sure you want to delete this invite? This only removes the entry from the database, GDAP relationships cannot be terminated once they are in approval pending status.", + data: { + Action: "Delete", + InviteId: "RowKey", + }, + }, +]; + const Page = () => { return ( + } title={pageTitle} apiUrl={apiUrl} simpleColumns={simpleColumns} + actions={actions} tenantInTitle={false} queryKey="ListGDAPInvite" + maxHeightOffset="460px" /> ); }; diff --git a/src/pages/tenant/gdap-management/offboarding.js b/src/pages/tenant/gdap-management/offboarding.js index 1fb872a6d2da..bba83bba45aa 100644 --- a/src/pages/tenant/gdap-management/offboarding.js +++ b/src/pages/tenant/gdap-management/offboarding.js @@ -7,7 +7,7 @@ import { CippFormComponent } from "/src/components/CippComponents/CippFormCompon import vendorTenantList from "/src/data/vendorTenantList"; import { Box, Grid, Stack } from "@mui/system"; import { Alert, Divider, Typography } from "@mui/material"; -import { ApiGetCall } from "/src/api/ApiCall"; +import { ApiGetCall, ApiGetCallWithPagination } from "/src/api/ApiCall"; import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; import { ShieldCheckIcon } from "@heroicons/react/24/outline"; import { Apps, Description, Widgets } from "@mui/icons-material"; @@ -53,14 +53,14 @@ const Page = () => { data: { Endpoint: "servicePrincipals", TenantFilter: tenantId?.value, - $filter: `appOwnerOrganizationId eq %tenantid%`, + $filter: `appOwnerOrganizationId eq %partnertenantid%`, $select: "id,displayName,appId,appOwnerOrganizationId", $count: true, }, queryKey: "ListMSPApps-" + tenantId?.value, }); - const vendorApps = ApiGetCall({ + const vendorApps = ApiGetCallWithPagination({ url: "/api/ListGraphRequest", data: { Endpoint: "servicePrincipals", @@ -81,6 +81,7 @@ const Page = () => { hideBackButton={true} hidePageType={true} postUrl="/api/ExecOffboardTenant" + resetForm={true} > @@ -136,6 +137,15 @@ const Page = () => { (relationship) => relationship?.customer?.tenantId === tenantId.value )?.length ?? 0, icon: , + offcanvas: { + title: "GDAP Relationships", + propertyItems: gdapRelationships.data?.Results + ?.filter((relationship) => relationship?.customer?.tenantId === tenantId.value) + ?.map((relationship) => ({ + label: `Relationship: ${relationship?.displayName}`, + value: `Id: ${relationship?.id}`, + })), + }, }, { name: "CSP Contract", @@ -151,11 +161,27 @@ const Page = () => { name: "MSP Applications", data: mspApps.data?.Results?.length ?? 0, icon: , + offcanvas: { + title: "MSP Applications", + propertyItems: mspApps.data?.Results?.map((app) => ({ + label: app?.displayName, + value: app?.appId, + })), + }, }, { name: "Vendor Applications", - data: 0, + data: vendorApps.data?.pages?.reduce((sum, page) => sum + (page?.Results?.length ?? 0), 0) ?? 0, icon: , + offcanvas: { + title: "Vendor Applications", + propertyItems: vendorApps.data?.pages + ?.reduce((sum, page) => sum.concat(page?.Results ?? []), []) + .map((app) => ({ + label: app?.displayName, + value: app?.appId, + })), + } }, ]} /> @@ -193,7 +219,7 @@ const Page = () => { }, valueField: "appId", }} - disabled={vendorApps?.data?.Results?.length > 0 ? false : true} + disabled={vendorApps?.data?.pages?.[0]?.Results?.length > 0 ? false : true} /> { tenantInTitle={false} queryKey="ListTenantOnboarding" cardButton={ - } + maxHeightOffset="460px" /> ); }; diff --git a/src/pages/tenant/gdap-management/onboarding/start.js b/src/pages/tenant/gdap-management/onboarding/start.js index cb6a58d3d675..d52982666c0c 100644 --- a/src/pages/tenant/gdap-management/onboarding/start.js +++ b/src/pages/tenant/gdap-management/onboarding/start.js @@ -13,7 +13,7 @@ import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import GDAPRoles from "/src/data/GDAPRoles"; import { Box, Stack } from "@mui/system"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { CippPropertyList } from "/src/components/CippComponents/CippPropertyList"; import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "/src/api/ApiCall"; import { useEffect, useState } from "react"; @@ -53,12 +53,9 @@ const Page = () => { data: { TenantFilter: "", Endpoint: "tenantRelationships/delegatedAdminRelationships", - $filter: - "(status eq 'active' or status eq 'approvalPending') and not startsWith(displayName,'MLT_')", }, queryKey: "GDAPRelationshipOnboarding", }); - const onboardingList = ApiGetCallWithPagination({ url: "/api/ListTenantOnboarding", queryKey: "ListTenantOnboarding", @@ -108,7 +105,11 @@ const Page = () => { (relationship) => relationship?.id === queryId ); - if (relationship) { + if ( + relationship && + (relationship?.status === "active" || relationship?.status === "approvalPending") && + !relationship?.customer?.displayName.startsWith("MLT_") + ) { formValue = { label: (relationship?.customer?.displayName ?? "Pending Invite") + @@ -304,11 +305,9 @@ const Page = () => { api={{ url: "/api/ListGraphRequest", data: { - TenantFilter: "", Endpoint: "tenantRelationships/delegatedAdminRelationships", - $filter: - "(status eq 'active' or status eq 'approvalPending') and not startsWith(displayName,'MLT_')", }, + excludeTenantFilter: true, queryKey: "GDAPRelationships", dataKey: "Results", labelField: (option) => @@ -320,12 +319,22 @@ const Page = () => { addedField: { customer: "customer", id: "id", + displayName: "displayName", createdDateTime: "createdDateTime", accessDetails: "accessDetails", status: "status", autoExtendDuration: "autoExtendDuration", lastModifiedDateTime: "lastModifiedDateTime", }, + dataFilter: (data) => { + return data?.filter( + (relationship) => + (relationship?.addedFields?.status === "active" || + relationship?.addedFields?.status === "approvalPending") && + !relationship?.addedFields?.displayName?.startsWith("MLT_") + ); + }, + showRefresh: true, }} multiple={false} creatable={true} @@ -369,6 +378,7 @@ const Page = () => { }, }} multiple={false} + creatable={false} /> )} @@ -487,7 +497,7 @@ const Page = () => { value: getCippFormatting( currentInvite ? currentInvite.RoleMappings - : currentRelationship?.addedFields?.accessDetails.unifiedRoles, + : currentRelationship?.addedFields?.accessDetails?.unifiedRoles, "unifiedRoles", "object" ), diff --git a/src/pages/tenant/gdap-management/relationships/index.js b/src/pages/tenant/gdap-management/relationships/index.js index 8e6b834d3599..5a389831bade 100644 --- a/src/pages/tenant/gdap-management/relationships/index.js +++ b/src/pages/tenant/gdap-management/relationships/index.js @@ -2,64 +2,11 @@ import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; import CippTablePage from "/src/components/CippComponents/CippTablePage"; -import { EyeIcon } from "@heroicons/react/24/outline"; -import { - AdminPanelSettings, - GppBad, - HourglassBottom, - OpenInNew, - PlayArrow, -} from "@mui/icons-material"; +import CippGdapActions from "/src/components/CippComponents/CippGdapActions"; const pageTitle = "GDAP Relationships"; -const actions = [ - { - label: "View Relationship", - link: "/tenant/gdap-management/relationships/relationship?id=[id]", - color: "primary", - icon: , - }, - { - label: "Start Onboarding", - link: "/tenant/gdap-management/onboarding/start?id=[id]", - color: "primary", - icon: , - }, - { - label: "Open Relationship in Partner Center", - link: "https://partner.microsoft.com/en-us/dashboard/commerce2/customers/[customer.tenantId]/adminrelationships/[id]", - color: "info", - icon: , - }, - { - label: "Enable automatic extension", - type: "GET", - url: "/api/ExecAutoExtendGDAP", - data: { ID: "id" }, - confirmText: "Are you sure you want to enable auto-extend for this relationship?", - color: "info", - icon: , - }, - { - label: "Remove Global Administrator from Relationship", - type: "GET", - url: "/api/ExecGDAPRemoveGArole", - data: { GDAPID: "id" }, - confirmText: "Are you sure you want to remove Global Administrator from this relationship?", - color: "danger", - icon: , - }, - { - label: "Terminate Relationship", - type: "GET", - url: "/api/ExecDeleteGDAPRelationship", - data: { GDAPID: "id" }, - confirmText: "Are you sure you want to terminate this relationship?", - color: "error", - icon: , - }, -]; +const actions = CippGdapActions(); const simpleColumns = [ "customer.displayName", @@ -72,6 +19,29 @@ const simpleColumns = [ "accessDetails.unifiedRoles", ]; +const filters = [ + { + filterName: "Active", + value: [{ id: "status", value: "active" }], + type: "column", + }, + { + filterName: "Approval Pending", + value: [{ id: "status", value: "approvalPending" }], + type: "column", + }, + { + filterName: "Terminating", + value: [{ id: "status", value: "terminating" }], + type: "column", + }, + { + filterName: "Terminated", + value: [{ id: "status", value: "terminated" }], + type: "column", + }, +]; + const offCanvas = { actions: actions, extendedInfoFields: simpleColumns, @@ -96,6 +66,9 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + maxHeightOffset="460px" + filters={filters} + defaultSorting={[{ id: "customer.displayName", desc: false }]} /> ); }; diff --git a/src/pages/tenant/gdap-management/relationships/relationship/index.js b/src/pages/tenant/gdap-management/relationships/relationship/index.js index ef305c60b35e..c739ca3a0e21 100644 --- a/src/pages/tenant/gdap-management/relationships/relationship/index.js +++ b/src/pages/tenant/gdap-management/relationships/relationship/index.js @@ -15,11 +15,13 @@ import CIPPDefaultGDAPRoles from "/src/data/CIPPDefaultGDAPRoles.json"; import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; import { Schedule } from "@mui/icons-material"; import { useEffect, useState } from "react"; +import CippGdapActions from "../../../../../components/CippComponents/CippGdapActions"; const Page = () => { const router = useRouter(); const { id } = router.query; const [relationshipProperties, setRelationshipProperties] = useState([]); + const [relationshipData, setRelationshipData] = useState({}); const relationshipRequest = ApiGetCall({ url: `/api/ListGraphRequest?Endpoint=tenantRelationships/delegatedAdminRelationships/${id}`, @@ -67,7 +69,7 @@ const Page = () => { useEffect(() => { if (relationshipRequest.isSuccess) { const data = relationshipRequest?.data?.Results?.[0]; - + setRelationshipData(data); var properties = [ { label: "Customer", @@ -135,6 +137,9 @@ const Page = () => { title={title} subtitle={subtitle} isFetching={relationshipRequest.isLoading} + actions={CippGdapActions()} + actionsData={relationshipData} + backUrl="/tenant/gdap-management/relationships" > {relationshipRequest.isLoading && } {relationshipRequest.isSuccess && ( @@ -182,7 +187,7 @@ const Page = () => { )} - + { propertyItems={relationshipProperties} /> - + { @@ -56,6 +47,7 @@ const Page = () => { title={title} subtitle={subtitle} isFetching={relationshipRequest.isLoading} + backUrl="/tenant/gdap-management/relationships" > {id && ( { queryKey: `AccessAssignments-${id}`, }} simpleColumns={["group.displayName", "status", "createdDateTime", "roles", "members"]} + maxHeightOffset="550px" /> )} diff --git a/src/pages/tenant/gdap-management/role-templates/index.js b/src/pages/tenant/gdap-management/role-templates/index.js index def2717cc986..c884bc8cc484 100644 --- a/src/pages/tenant/gdap-management/role-templates/index.js +++ b/src/pages/tenant/gdap-management/role-templates/index.js @@ -9,7 +9,7 @@ import { useEffect, useState } from "react"; import { Box, Stack } from "@mui/system"; import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; -import { Edit } from "@mui/icons-material"; +import { Edit, AddBox } from "@mui/icons-material"; const Page = () => { const pageTitle = "GDAP Role Templates"; @@ -42,7 +42,6 @@ const Page = () => { urlFromData: true, relatedQueryKeys: "ListGDAPRoleTemplates", }); - useEffect(() => { if (currentTemplates.isSuccess) { @@ -98,11 +97,16 @@ const Page = () => { tenantInTitle={false} sx={{ flexGrow: 1, pb: 4 }} cardButton={ - } queryKey="ListGDAPRoleTemplates" + maxHeightOffset="460px" /> ); diff --git a/src/pages/tenant/gdap-management/roles/add.js b/src/pages/tenant/gdap-management/roles/add.js index 2c5143c156a1..94cd506398c3 100644 --- a/src/pages/tenant/gdap-management/roles/add.js +++ b/src/pages/tenant/gdap-management/roles/add.js @@ -1,24 +1,83 @@ -import { Alert, Button, List, ListItem, SvgIcon, Typography } from "@mui/material"; +import React, { useState } from "react"; +import { Alert, Button, SvgIcon, Typography, Tooltip, Link } from "@mui/material"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { useForm, useWatch } from "react-hook-form"; import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; +import { CippFormCondition } from "/src/components/CippComponents/CippFormCondition"; import GDAPRoles from "/src/data/GDAPRoles"; -import { Box, Stack } from "@mui/system"; -import { ShieldCheckIcon } from "@heroicons/react/24/outline"; +import { Box, Stack, Grid } from "@mui/system"; +import { ShieldCheckIcon, PlusSmallIcon } from "@heroicons/react/24/outline"; import { CippPropertyList } from "/src/components/CippComponents/CippPropertyList"; import cippDefaults from "/src/data/CIPPDefaultGDAPRoles"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { Settings, SyncAlt } from "@mui/icons-material"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { TrashIcon } from "@heroicons/react/24/outline"; const Page = () => { const formControl = useForm({ mode: "onChange", + defaultValues: { + advancedMode: false, + }, }); - const selectedRoles = useWatch({ control: formControl.control, name: "gdapRoles" }); - const customSuffix = useWatch({ control: formControl.control, name: "customSuffix" }); + const selectedGdapRoles = useWatch({ + control: formControl.control, + name: "gdapRoles", + }); + + const customSuffix = useWatch({ + control: formControl.control, + name: "customSuffix", + }); + const [advancedMappings, setAdvancedMappings] = useState([]); const handleDefaults = () => { - formControl.setValue("gdapRoles", cippDefaults); + formControl.setValue("gdapRoles", cippDefaults, { shouldDirty: true }); + formControl.trigger(); + }; + + const groupList = ApiGetCall({ + url: "/api/ExecAddGDAPRole?Action=ListGroups", + queryKey: "ListGroups", + }); + + const handleAddMapping = () => { + const selectedGroup = formControl.getValues("selectedGroup"); + const selectedRole = formControl.getValues("selectedRole"); + + if (!selectedGroup || !selectedRole) { + return; + } + + const newMapping = { + groupName: selectedGroup.label, + groupId: selectedGroup.value, + roleName: selectedRole.label, + roleId: selectedRole.value, + }; + + if ( + advancedMappings.some( + (mapping) => mapping.groupId === newMapping.groupId && mapping.roleId === newMapping.roleId + ) + ) { + return; + } + + setAdvancedMappings([...advancedMappings, newMapping]); + formControl.setValue("selectedGroup", null); // Clear the selected group + formControl.setValue("selectedRole", null); // Clear the selected role + }; + + const handleRemoveMapping = (mappingToRemove) => { + const updatedMappings = advancedMappings.filter( + (mapping) => + mapping.groupId !== mappingToRemove.groupId || mapping.roleId !== mappingToRemove.roleId + ); + setAdvancedMappings(updatedMappings); }; return ( @@ -29,78 +88,231 @@ const Page = () => { title="GDAP Roles" backButtonTitle="GDAP Roles" postUrl="/api/ExecAddGDAPRole" + customDataformatter={(values) => { + if (values.advancedMode) { + return { + Action: "AddRoleAdvanced", + Mappings: advancedMappings, + }; + } else { + return values; + } + }} > + + + GDAP Roles + + + + + + + + + + - - - For each role you select a new group will be created inside of your partner tenant - called "M365 GDAP RoleName". Add your users to these new groups to set their GDAP - permissions. If you need to segment your groups for different teams or to define - custom permissions, use the Custom Suffix to create additional group mappings per - role. - - - - + + + + For each role you select a new group will be created inside of your partner tenant + called "M365 GDAP RoleName". Add your users to these new groups to set their GDAP + permissions. If you need to segment your groups for different teams or to define + custom permissions, use the Custom Suffix to create additional group mappings per + role. + + + + Certain roles may not be compatible with GDAP. See the{" "} + + Microsoft Documentation + {" "} + on GDAP Role Guidance. + + + + + + + + role.ObjectId !== "7495fdc4-34c4-4d15-a289-98788ce399fd" && + role.ObjectId !== "aaf43236-0c0d-4d5f-883a-6955382ac081" + ).map((role) => ({ label: role.Name, value: role.ObjectId }))} + multiple={true} + creatable={false} + required={true} + validators={{ + validate: (value) => { + if (!value || value.length === 0) { + return "Please select at least one GDAP Role"; + } + return true; + }, + }} + sortOptions={true} /> - - - - - ({ label: role.Name, value: role.ObjectId }))} - multiple={true} - creatable={false} - required={true} - validators={{ - validate: (value) => { - if (!value || value.length === 0) { - return "Please select at least one GDAP Role"; - } - return true; - }, - }} - /> - {selectedRoles?.some((role) => role.value === "62e90394-69f5-4237-9190-012177145e10") && ( - - The Company Administrator role is a highly privileged role that should be used with - caution. GDAP Relationships with this role will not be eligible for auto-extend. - - )} - {selectedRoles?.length > 0 && ( - <> The following groups will be created in your partner tenant if they do not already exist: { - return { - label: `M365 GDAP ${role.label}${customSuffix ? ` - ${customSuffix}` : ""}`, - value: GDAPRoles.find((r) => r.ObjectId === role.value).Description, - }; - })} + propertyItems={selectedGdapRoles?.map((role) => ({ + label: `M365 GDAP ${role.label}${customSuffix ? ` - ${customSuffix}` : ""}`, + value: GDAPRoles.find((r) => r.ObjectId === role.value).Description, + }))} /> - - )} + + + + The Company Administrator role is a highly privileged role that should be used with + caution. GDAP Relationships with this role will not be eligible for auto-extend. + + + + + + + + In Advanced Mode, you can manually map existing groups to GDAP roles. This + functionality is designed to help map existing groups to GDAP roles that do not + match the default naming convention. Use extreme caution when mapping roles in this + mode. + + + Limitations + +
      +
    • + Reserved groups and roles are unavailable for mapping, this is to prevent + misconfigurations due to permission overlap. +
    • +
    • + Only one role can be mapped per group. If your current configuration maps + more than one, use the Reset Role Mapping action on the Relationship. +
    • +
    • + Certain roles may not be compatible with GDAP. See the{" "} + + Microsoft Documentation + {" "} + on GDAP Role Guidance. +
    • +
    +
    + + + ({ + label: group.displayName, + value: group.id, + }))} + isFetching={groupList.isFetching} + multiple={false} + required={true} + creatable={false} + sortOptions={true} + /> + + + + + + + + + + + role.ObjectId !== "62e90394-69f5-4237-9190-012177145e10" && // Partner Tier 1 + role.ObjectId !== "17315797-102d-40b4-93e0-432062caca18" // Partner Tier 2 + ).map((role) => ({ label: role.Name, value: role.ObjectId }))} + multiple={false} + required={true} + creatable={false} + sortOptions={true} + /> + + + + + + + + + + + ), + customFunction: handleRemoveMapping, + noConfirm: true, + }, + ]} + /> +
    diff --git a/src/pages/tenant/gdap-management/roles/index.js b/src/pages/tenant/gdap-management/roles/index.js index 2de5ad32b096..9a050a7d5150 100644 --- a/src/pages/tenant/gdap-management/roles/index.js +++ b/src/pages/tenant/gdap-management/roles/index.js @@ -3,15 +3,15 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { AdminPanelSettings, Add, Delete } from "@mui/icons-material"; import Link from "next/link"; -import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; const pageTitle = "GDAP Role Mappings"; const actions = [ { label: "Add to Template", - icon: , + icon: , type: "POST", url: "/api/ExecGDAPRoleTemplate?Action=Add", confirmText: "Select a template to add the selected role mapping(s) to.", @@ -40,7 +40,7 @@ const actions = [ }, { label: "Delete Mapping", - icon: , + icon: , type: "POST", url: "/api/ExecDeleteGDAPRoleMapping", data: { @@ -64,11 +64,16 @@ const Page = () => { simpleColumns={simpleColumns} tenantInTitle={false} cardButton={ - } queryKey="ListGDAPRoles" + maxHeightOffset="460px" /> ); }; diff --git a/src/pages/tenant/administration/application-consent/index.js b/src/pages/tenant/reports/application-consent/index.js similarity index 100% rename from src/pages/tenant/administration/application-consent/index.js rename to src/pages/tenant/reports/application-consent/index.js diff --git a/src/pages/tenant/administration/list-csp-licenses/index.jsx b/src/pages/tenant/reports/list-csp-licenses/index.jsx similarity index 94% rename from src/pages/tenant/administration/list-csp-licenses/index.jsx rename to src/pages/tenant/reports/list-csp-licenses/index.jsx index 611d4f5d9b10..b046b6ea275c 100644 --- a/src/pages/tenant/administration/list-csp-licenses/index.jsx +++ b/src/pages/tenant/reports/list-csp-licenses/index.jsx @@ -1,7 +1,7 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline"; -import { DeleteForever } from "@mui/icons-material"; +import { DeleteForever, ShoppingCart } from "@mui/icons-material"; import { Button } from "@mui/material"; import Link from "next/link"; @@ -93,7 +93,7 @@ const Page = () => { simpleColumns={simpleColumns} cardButton={ <> - diff --git a/src/pages/tenant/administration/list-licenses/index.js b/src/pages/tenant/reports/list-licenses/index.js similarity index 58% rename from src/pages/tenant/administration/list-licenses/index.js rename to src/pages/tenant/reports/list-licenses/index.js index 1a4abfc73707..417e1ef16910 100644 --- a/src/pages/tenant/administration/list-licenses/index.js +++ b/src/pages/tenant/reports/list-licenses/index.js @@ -5,28 +5,16 @@ const Page = () => { const pageTitle = "Licenses Report"; const apiUrl = "/api/ListLicenses"; - const actions = []; // No actions specified, setting to empty array - - const offCanvas = null; // No off-canvas details provided - const simpleColumns = [ "Tenant", "License", "CountUsed", "CountAvailable", "TotalLicenses", - "TermInfo", + "TermInfo", // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby ]; - return ( - - ); + return ; }; Page.getLayout = (page) => {page}; diff --git a/src/pages/tenant/standards/bpa-report/builder.js b/src/pages/tenant/standards/bpa-report/builder.js index 16b1c4e9ecc9..976d79b63c53 100644 --- a/src/pages/tenant/standards/bpa-report/builder.js +++ b/src/pages/tenant/standards/bpa-report/builder.js @@ -1,22 +1,9 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { - Box, - Container, - Typography, - Button, - FormControl, - InputLabel, - Select, - MenuItem, - Grid, - IconButton, - Stack, - SvgIcon, -} from "@mui/material"; +import { Box, Container, Typography, Button, IconButton, Stack, SvgIcon } from "@mui/material"; +import { Grid } from "@mui/system"; import { useEffect, useState } from "react"; -import Head from "next/head"; import DeleteIcon from "@mui/icons-material/Delete"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import CippButtonCard from "../../../../components/CippCards/CippButtonCard"; import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; @@ -24,6 +11,8 @@ import { useRouter } from "next/router"; import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; +import { CippHead } from "../../../../components/CippComponents/CippHead"; +import { Add, Save } from "@mui/icons-material"; const Page = () => { const router = useRouter(); @@ -54,18 +43,28 @@ const Page = () => { template.name = `${template.name} (Clone)`; } setLayoutMode(template.style); - //if the template style is tenant, create enough cards to hold the frontend fields - if (template.style === "Tenant") { - setBlockCards( - template.Fields.map((field, index) => { - return { - id: `block-${index}`, - }; - }) - ); - } + setBlockCards( + template.Fields.map((field, index) => { + return { + id: `block-${index}`, + }; + }) + ); + + const convertedTemplate = { + ...template, + Fields: template.Fields.map((field) => ({ + ...field, + ExtractFields: field.ExtractFields + ? field.ExtractFields.map((ef) => ({ + label: ef, + value: ef, + })) + : [], + })), + }; - formControl.reset(template); + formControl.reset(convertedTemplate); } } }, [bpaTemplateList.isSuccess]); @@ -151,10 +150,9 @@ const Page = () => { addBPATemplate.mutate({ url: "/api/AddBPATemplate", data: cleanedData }); }; - const handleLayoutModeChange = (event) => { - const newMode = event.target.value; + const handleLayoutModeChange = (newMode) => { setLayoutMode(newMode); - formControl.setValue("style", newMode); + //formControl.setValue("style", newMode); // Reset cards based on the layout mode if (newMode === "Table") { @@ -174,12 +172,18 @@ const Page = () => { } }; + const style = useWatch({ control: formControl.control, name: "style" }); + + useEffect(() => { + if (style && style !== layoutMode) { + handleLayoutModeChange(style); + } + }, [style]); + const onSubmit = (data) => {}; return ( <> - - {pageTitle} - + { - } title="Report Settings" > - + {/* First item for Report Name and Layout Mode */} - + - - - Layout Mode - - + + - + - - {/* Third item for Buttons */} - {layoutMode === "Tenant" && ( - - - - - - )} - {/* Canvas Area */} - - Canvas - + + Fields + + {blockCards.map((block, index) => ( handleRemoveBlock(block.id)} // Remove block on click - > - - - ) + handleRemoveBlock(block.id)} // Remove block on click + > + + } > {/* Form inside each card */}
    {/* Report Style - Full Width */} - + - + { type="textField" /> - + { - + { compareValue={"Graph"} formControl={formControl} > - + - + - + @@ -364,14 +354,14 @@ const Page = () => { compareValue={"Exchange"} formControl={formControl} > - + - + { compareValue={"CIPPFunction"} formControl={formControl} > - + - + { /> - + { type="autoComplete" /> - + { /> - + - + {layoutMode === "Table" ? null : ( { compareValue={true} formControl={formControl} > - + `${option}`, - valueField: (option) => `${option}`, - }} + options={ + bpaTemplateList.data + ?.flatMap( + (template) => template.Fields?.map((field) => field.name) ?? [] + ) + .filter( + (value, index, self) => value && self.indexOf(value) === index + ) + .map((field) => ({ label: field, value: field })) + .sort((a, b) => a.label.localeCompare(b.label)) ?? [] + } /> @@ -494,7 +488,7 @@ const Page = () => { compareValue={false} formControl={formControl} > - + {layoutMode === "Table" ? null : ( { const pageTitle = "Best Practice Reports"; + const bpaDialog = useDialog(); + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const actions = [ { label: "View Report", @@ -30,9 +42,53 @@ const Page = () => { color: "success", target: "_self", }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, { label: "Delete Template", - type: "GET", + type: "POST", url: "/api/RemoveBPATemplate", data: { TemplateName: "Name", @@ -44,18 +100,26 @@ const Page = () => { ]; return ( - - Add Template - - } - actions={actions} - simpleColumns={["Name", "Style"]} - queryKey="ListPATemplates" - /> + <> + + + + + } + actions={actions} + simpleColumns={["Name", "Style"]} + queryKey="ListBPATemplates" + /> + + ); }; diff --git a/src/pages/tenant/standards/bpa-report/view.js b/src/pages/tenant/standards/bpa-report/view.js index 2760fdc0a446..0af8c51a7011 100644 --- a/src/pages/tenant/standards/bpa-report/view.js +++ b/src/pages/tenant/standards/bpa-report/view.js @@ -1,16 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { - Box, - Container, - Typography, - Button, - Grid, - Stack, - SvgIcon, - Skeleton, - Chip, - Alert, -} from "@mui/material"; +import { Box, Container, Typography, Button, Stack, SvgIcon, Skeleton, Chip, Alert } from "@mui/material"; +import { Grid } from "@mui/system"; import Head from "next/head"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; import { useRouter } from "next/router"; @@ -28,7 +18,7 @@ const Page = () => { const [layoutMode, setLayoutMode] = useState("Table"); const bpaTemplateList = ApiGetCall({ url: "/api/listBPATemplates", - queryKey: "ListBPATemplates", + queryKey: "ListBPATemplates-All", }); const tenantFilter = useSettings().currentTenant; const bpaData = ApiGetCall({ @@ -113,7 +103,7 @@ const Page = () => { } } } - }, [bpaTemplateList.isSuccess, bpaData.isSuccess, currentTenant, router]); + }, [bpaTemplateList.isSuccess, bpaData.isSuccess, bpaData.data, currentTenant, router]); const pageTitle = `BPA Report Viewer - ${currentTenant}`; return ( @@ -143,7 +133,7 @@ const Page = () => { - + {pageTitle} @@ -154,7 +144,7 @@ const Page = () => { {currentTenant === "AllTenants" && layoutMode !== "Table" ? ( - + { <> {blockCards.map((block, index) => ( { /> ) : block.formatter === "number" ? ( //really big number centered in the card. - + (
    {block.data}
    -
    +
    ) ) : block.formatter === "Percentage" ? ( <>{block.data} ) : block.formatter === "table" ? ( @@ -221,6 +208,8 @@ const Page = () => { noCard={true} incorrectDataMessage={"No data has been found for this report."} simpleColumns={block?.simpleColumns} + isFetching={bpaData.isFetching} + refreshFunction={() => bpaData.refetch()} /> ) : ( diff --git a/src/pages/tenant/standards/domains-analyser/index.js b/src/pages/tenant/standards/domains-analyser/index.js index 218e8a8633b4..5f5d15186ae1 100644 --- a/src/pages/tenant/standards/domains-analyser/index.js +++ b/src/pages/tenant/standards/domains-analyser/index.js @@ -1,4 +1,4 @@ -import { Button, Container } from "@mui/material"; +import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. import Link from "next/link"; @@ -6,11 +6,14 @@ import { ApiGetCall } from "../../../../api/ApiCall"; import { useSettings } from "../../../../hooks/use-settings"; import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; import { CippDomainCards } from "../../../../components/CippCards/CippDomainCards"; -import { DeleteForever } from "@mui/icons-material"; +import { DeleteForever, TravelExplore, Refresh } from "@mui/icons-material"; +import { DomainAnalyserDialog } from "../../../../components/CippComponents/DomainAnalyserDialog"; +import { useDialog } from "../../../../hooks/use-dialog"; const Page = () => { const currentTenant = useSettings().currentTenant; const pageTitle = "Domains Analyser"; + const analyserDialog = useDialog(); const apiGetCall = ApiGetCall({ url: "/api/ExecDomainAnalyser", waiting: false, @@ -31,35 +34,43 @@ const Page = () => { children: (extendedData) => , }; return ( - - - {/* This needs to be replaced with a CippApiDialog. */} - - - } - prependComponents={} - queryKey={`ListDomains-${currentTenant}`} - simpleColumns={[ - "Domain", - "ScorePercentage", - "MailProvider", - "SPFPassAll", - "MXPassTest", - "DMARCPresent", - "DMARCActionPolicy", - "DMARCPercentagePass", - "DNSSECPresent", - "DKIMEnabled", - ]} - offCanvas={offCanvas} - actions={actions} - /> + <> + + + + + } + prependComponents={} + queryKey={`ListDomains-${currentTenant}`} + simpleColumns={[ + "Domain", + "ScorePercentage", + "MailProvider", + "SPFPassAll", + "MXPassTest", + "DMARCPresent", + "DMARCActionPolicy", + "DMARCPercentagePass", + "DNSSECPresent", + "DKIMEnabled", + ]} + offCanvas={offCanvas} + actions={actions} + /> + + ); }; diff --git a/src/pages/tenant/standards/index.js b/src/pages/tenant/standards/index.js deleted file mode 100644 index 5f8bc65212d6..000000000000 --- a/src/pages/tenant/standards/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Standards"; - - return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the standards section.

    -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/tenant/standards/list-applied-standards/index.js b/src/pages/tenant/standards/list-applied-standards/index.js deleted file mode 100644 index 6937c0fd745f..000000000000 --- a/src/pages/tenant/standards/list-applied-standards/index.js +++ /dev/null @@ -1,17 +0,0 @@ - -import { Layout as DashboardLayout } from "/src/layouts/index.js"; - -const Page = () => { - const pageTitle = "Edit Standards"; - - return ( -
    -

    {pageTitle}

    -

    This is a placeholder page for the edit standards section.

    -
    - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/tenant/standards/list-standards/classic-standards/index.js b/src/pages/tenant/standards/list-standards/classic-standards/index.js new file mode 100644 index 000000000000..958502d70541 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/classic-standards/index.js @@ -0,0 +1,198 @@ +import { Alert, Button } from "@mui/material"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import Link from "next/link"; +import { CopyAll, Delete, PlayArrow, AddBox, Edit, GitHub } from "@mui/icons-material"; +import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; +import { Grid } from "@mui/system"; +import { CippApiResults } from "../../../../../components/CippComponents/CippApiResults"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import tabOptions from "../tabOptions.json"; + +const Page = () => { + const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" }); + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + const pageTitle = "Templates"; + const actions = [ + { + label: "View Tenant Report", + link: "/tenant/standards/compare?templateId=[GUID]", + icon: , + color: "info", + target: "_self", + }, + { + label: "Edit Template", + //when using a link it must always be the full path /identity/administration/users/[id] for example. + link: "/tenant/standards/template?id=[GUID]&type=[type]", + icon: , + color: "success", + target: "_self", + }, + { + label: "Clone & Edit Template", + link: "/tenant/standards/template?id=[GUID]&clone=true", + icon: , + color: "success", + target: "_self", + }, + { + label: "Run Template Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: "GUID", + }, + confirmText: "Are you sure you want to force a run of this template?", + multiPost: false, + }, + { + label: "Run Template Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: "GUID", + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this template?", + multiPost: false, + }, + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { + WriteAccess: true, + }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { + required: { value: true, message: "This field is required" }, + }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveStandardTemplate", + icon: , + data: { + ID: "GUID", + }, + confirmText: "Are you sure you want to delete [templateName]?", + multiPost: false, + }, + ]; + const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" }); + const handleConversion = () => { + conversionApi.mutate({ + url: "/api/execStandardConvert", + data: {}, + }); + }; + const tableFilter = ( +
    + {oldStandards.isSuccess && oldStandards.data.length !== 0 && ( + + + + + You have legacy standards available. Press the button to convert these standards to + the new format. This will create a new template for each standard you had, but will + disable the schedule. After conversion, please check the new templates to ensure + they are correct and re-enable the schedule. + + + + + + + + + + + )} +
    + ); + return ( + + + + + } + actions={actions} + tableFilter={tableFilter} + simpleColumns={[ + "templateName", + "type", + "tenantFilter", + "excludedTenants", + "updatedAt", + "updatedBy", + "runManually", + "standards", + ]} + queryKey="listStandardTemplates" + /> + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/tenant/standards/list-standards/drift-alignment/index.js b/src/pages/tenant/standards/list-standards/drift-alignment/index.js new file mode 100644 index 000000000000..561a94daaeb6 --- /dev/null +++ b/src/pages/tenant/standards/list-standards/drift-alignment/index.js @@ -0,0 +1,44 @@ +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import tabOptions from "../tabOptions.json"; + +const Page = () => { + const pageTitle = "Drift Alignment"; + + const actions = [ + { + label: "View Tenant Report", + link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + icon: , + color: "info", + target: "_self", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; \ No newline at end of file diff --git a/src/pages/tenant/standards/list-standards/index.js b/src/pages/tenant/standards/list-standards/index.js index e041ea9cd23b..c4c95550bd02 100644 --- a/src/pages/tenant/standards/list-standards/index.js +++ b/src/pages/tenant/standards/list-standards/index.js @@ -1,130 +1,70 @@ -import { Alert, Button } from "@mui/material"; +import { Button } from "@mui/material"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Layout as DashboardLayout } from "/src/layouts/index.js"; // had to add an extra path here because I added an extra folder structure. We should switch to absolute pathing so we dont have to deal with relative. +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { TabbedLayout } from "/src/layouts/TabbedLayout"; import Link from "next/link"; +import { Delete, Add } from "@mui/icons-material"; import { EyeIcon } from "@heroicons/react/24/outline"; -import { CopyAll, Delete, PlayArrow } from "@mui/icons-material"; -import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; -import { Grid } from "@mui/system"; -import { CippApiResults } from "../../../../components/CippComponents/CippApiResults"; +import tabOptions from "./tabOptions.json"; const Page = () => { - const oldStandards = ApiGetCall({ url: "/api/ListStandards", queryKey: "ListStandards-legacy" }); + const pageTitle = "Standard & Drift Alignment"; - const pageTitle = "Standard Templates"; const actions = [ { - label: "Edit Template", - //when using a link it must always be the full path /identity/administration/users/[id] for example. - link: "/tenant/standards/template?id=[GUID]", + label: "View Tenant Report", + link: "/tenant/standards/manage-drift/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", icon: , - color: "success", + color: "info", target: "_self", }, { - label: "Clone & Edit Template", - link: "/tenant/standards/template?id=[GUID]&clone=true", - icon: , - color: "success", + label: "Manage Drift", + link: "/tenant/standards/manage-drift?templateId=[standardId]&tenantFilter=[tenantFilter]", + icon: , + color: "info", target: "_self", + condition: (row) => row.standardType === "drift", }, { - label: "Run Template Now (Currently Selected Tenant only)", - type: "GET", - url: "/api/ExecStandardsRun", - icon: , - data: { - TemplateId: "GUID", - }, - confirmText: "Are you sure you want to force a run of this template?", - multiPost: false, - }, - { - label: "Run Template Now (All Tenants in Template)", - type: "GET", - url: "/api/ExecStandardsRun", - icon: , - data: { - TemplateId: "GUID", - tenantFilter: "allTenants", - }, - confirmText: "Are you sure you want to force a run of this template?", - multiPost: false, - }, - { - label: "Delete Template", + label: "Remove Drift Customization", type: "POST", - url: "/api/RemoveStandardTemplate", + url: "/api/ExecUpdateDriftDeviation", icon: , data: { - ID: "GUID", + RemoveDriftCustomization: "true", + tenantFilter: "tenantFilter", }, - confirmText: "Are you sure you want to delete this template?", + confirmText: + "Are you sure you want to remove all drift customizations? This resets the Drift Standard to the default template, and will generate alerts for the drifted items.", multiPost: false, + condition: (row) => row.standardType === "drift", }, ]; - const conversionApi = ApiPostCall({ relatedQueryKeys: "listStandardTemplates" }); - const handleConversion = () => { - conversionApi.mutate({ - url: "/api/execStandardConvert", - data: {}, - }); - }; - const tableFilter = ( -
    - {oldStandards.isSuccess && oldStandards.data.length !== 0 && ( - - - - - You have legacy standards available. Press the button to convert these standards to - the new format. This will create a new template for each standard you had, but will - disable the schedule. After conversion, please check the new templates to ensure - they are correct and re-enable the schedule. - - - - - - - - - - - )} -
    - ); + return ( - Add Template - - } actions={actions} - tableFilter={tableFilter} simpleColumns={[ - "templateName", "tenantFilter", - "excludedTenants", - "updatedAt", - "updatedBy", - "runManually", - "standards", + "standardName", + "standardType", + "alignmentScore", + "LicenseMissingPercentage", + "combinedAlignmentScore", ]} - queryKey="listStandardTemplates" + queryKey="listTenantAlignment" /> ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => ( + + {page} + +); export default Page; diff --git a/src/pages/tenant/standards/list-standards/tabOptions.json b/src/pages/tenant/standards/list-standards/tabOptions.json new file mode 100644 index 000000000000..1c522e6ca8ca --- /dev/null +++ b/src/pages/tenant/standards/list-standards/tabOptions.json @@ -0,0 +1,10 @@ +[ + { + "label": "Standard & Drift Alignment", + "path": "/tenant/standards/list-standards" + }, + { + "label": "Templates", + "path": "/tenant/standards/list-standards/classic-standards" + } +] diff --git a/src/pages/tenant/standards/manage-drift/compare.js b/src/pages/tenant/standards/manage-drift/compare.js new file mode 100644 index 000000000000..8ddc70251193 --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/compare.js @@ -0,0 +1,1284 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { + Button, + Card, + Stack, + Typography, + Box, + Divider, + Chip, + Skeleton, + Alert, + IconButton, + Tooltip, + ButtonGroup, + TextField, + InputAdornment, +} from "@mui/material"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { + CheckCircle, + Cancel, + Info, + Microsoft, + Sync, + FilterAlt, + Close, + Search, + FactCheck, + PlayArrow, +} from "@mui/icons-material"; +import { ArrowLeftIcon } from "@mui/x-date-pickers"; +import standards from "/src/data/standards.json"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; +import { SvgIcon } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { useSettings } from "../../../../hooks/use-settings"; +import { ApiGetCall, ApiPostCall } from "../../../../api/ApiCall"; +import { useRouter } from "next/router"; +import { useDialog } from "../../../../hooks/use-dialog"; +import { Grid } from "@mui/system"; +import DOMPurify from "dompurify"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import ReactMarkdown from "react-markdown"; +import tabOptions from "./tabOptions.json"; + +const Page = () => { + const router = useRouter(); + const { templateId } = router.query; + const [comparisonData, setComparisonData] = useState(null); + const settings = useSettings(); + const currentTenant = settings?.currentTenant; + const formControl = useForm({ + mode: "onBlur", + defaultValues: { + comparisonMode: "standard", + }, + }); + const runReportDialog = useDialog(); + const [filter, setFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + + const templateDetails = ApiGetCall({ + url: `/api/listStandardTemplates`, + queryKey: `listStandardTemplates-reports`, + }); + + // Run the report once + const runReport = ApiPostCall({ relatedQueryKeys: ["ListStandardsCompare"] }); + + // Dialog configuration for Run Report Once + const runReportApi = { + type: "GET", + url: "/api/ExecStandardsRun", + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to run this standard report?", + }; + + // Get comparison data + const comparisonApi = ApiGetCall({ + url: "/api/ListStandardsCompare", + data: { + TemplateId: templateId, + tenantFilter: currentTenant, + CompareToStandard: true, // Always compare to standard, even in tenant comparison mode + }, + queryKey: `ListStandardsCompare-${templateId}-${ + formControl.watch("compareTenantId") || "standard" + }-${currentTenant}`, + enabled: !!templateId, // Only run the query if templateId is available + }); + + useEffect(() => { + if (templateId && templateDetails.isSuccess && templateDetails.data) { + const selectedTemplate = templateDetails.data.find( + (template) => template.GUID === templateId + ); + + if (selectedTemplate && comparisonApi.isSuccess && comparisonApi.data) { + const tenantData = comparisonApi.data; + + // Find the current tenant's data by matching tenantFilter with currentTenant + const currentTenantObj = tenantData.find((t) => t.tenantFilter === currentTenant); + const currentTenantData = currentTenantObj ? currentTenantObj.standardsResults || [] : []; + + const allStandards = []; + if (selectedTemplate.standards) { + Object.entries(selectedTemplate.standards).forEach(([standardKey, standardConfig]) => { + // Special handling for IntuneTemplate which is an array of items + if (standardKey === "IntuneTemplate" && Array.isArray(standardConfig)) { + // Process each IntuneTemplate item separately + standardConfig.forEach((templateItem, index) => { + const templateId = templateItem.TemplateList?.value; + if (templateId) { + const standardId = `standards.IntuneTemplate.${templateId}`; + const standardInfo = standards.find((s) => s.name === `standards.IntuneTemplate`); + + // Find the tenant's value for this specific template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + + // Get the standard object and its value from the tenant object + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; + + // Determine compliance status + 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" + ) { + isCompliant = true; + } else if (currentTenantStandard) { + isCompliant = currentTenantStandard.value === true; + } + + // Create a standardValue object that contains the template settings + const templateSettings = { + templateId, + Template: templateItem.TemplateList?.label || "Unknown Template", + "Assign to": templateItem.AssignTo || "On", + "Excluded Group": templateItem.excludeGroup || "", + "Included Group": templateItem.customGroup || "", + }; + + allStandards.push({ + standardId, + standardName: `Intune Template: ${ + templateItem.TemplateList?.label || templateId + }`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + } + : currentTenantStandard?.value, + standardValue: templateSettings, // Use the template settings object instead of true + complianceStatus: isCompliant ? "Compliant" : "Non-Compliant", + complianceDetails: + standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: templateItem.action || [], + }); + } + }); + } else if ( + standardKey === "ConditionalAccessTemplate" && + Array.isArray(standardConfig) + ) { + // Process each ConditionalAccessTemplate item separately + standardConfig.forEach((templateItem, index) => { + const templateId = templateItem.TemplateList?.value; + if (templateId) { + const standardId = `standards.ConditionalAccessTemplate.${templateId}`; + const standardInfo = standards.find( + (s) => s.name === `standards.ConditionalAccessTemplate` + ); + + // Find the tenant's value for this specific template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; + let isCompliant = false; + + // For ConditionalAccessTemplate, the value is true if compliant, or an object with comparison data if not compliant + if (directStandardValue === true) { + isCompliant = true; + } else { + isCompliant = false; + } + + // Create a standardValue object that contains the template settings + const templateSettings = { + templateId, + Template: templateItem.TemplateList?.label || "Unknown Template", + }; + + allStandards.push({ + standardId, + standardName: `Conditional Access Template: ${ + templateItem.TemplateList?.label || templateId + }`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + } + : currentTenantStandard?.value, + standardValue: templateSettings, // Use the template settings object instead of true + complianceStatus: isCompliant ? "Compliant" : "Non-Compliant", + complianceDetails: + standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: templateItem.action || [], + }); + } + }); + } else { + // Regular handling for other standards + const standardId = `standards.${standardKey}`; + const standardInfo = standards.find((s) => s.name === standardId); + const standardSettings = standardConfig.standards?.[standardKey] || {}; + //console.log(standardInfo); + + // Check if reporting is enabled for this standard by checking the action property + // The standard should be reportable if there's an action with value === 'Report' + const actions = standardConfig?.action ?? []; + const reportingEnabled = + //if actions contains Report or Remediate, case insensitive, then we good. + actions.filter( + (action) => + action?.value.toLowerCase() === "report" || + action?.value.toLowerCase() === "remediate" + ).length > 0; + + // Find the tenant's value for this standard + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + + // Determine compliance status + let isCompliant = false; + let reportingDisabled = !reportingEnabled; + + // Check if the standard is directly in the tenant object (like "standards.AuditLog": {...}) + const standardIdWithoutPrefix = standardId.replace("standards.", ""); + const standardObject = currentTenantObj?.[standardId]; + + // Extract the actual value from the standard object (new data structure includes .Value property) + const directStandardValue = standardObject?.Value; + + // Special case for boolean standards that are true in the tenant + if (directStandardValue === true) { + // If the standard is directly in the tenant and is true, it's compliant + isCompliant = true; + } else if (directStandardValue !== undefined) { + // For non-boolean values, use strict equality + isCompliant = + JSON.stringify(directStandardValue) === JSON.stringify(standardSettings); + } else if (currentTenantStandard) { + // Fall back to the previous logic if the standard is not directly in the tenant object + if (typeof standardSettings === "boolean" && standardSettings === true) { + isCompliant = currentTenantStandard.value === true; + } else { + isCompliant = + JSON.stringify(currentTenantStandard.value) === + JSON.stringify(standardSettings); + } + } + + // Determine compliance status text based on reporting flag + const complianceStatus = reportingDisabled + ? "Reporting Disabled" + : isCompliant + ? "Compliant" + : "Non-Compliant"; + + // Use the direct standard value from the tenant object if it exists + allStandards.push({ + standardId, + standardName: standardInfo?.label || standardKey, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + } + : currentTenantStandard?.value, + standardValue: standardSettings, + complianceStatus, + reportingDisabled, + complianceDetails: standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate.templateName || "Standard Template", + templateActions: standardConfig.action || [], + }); + } + }); + } + + setComparisonData(allStandards); + } else { + setComparisonData([]); + } + } else if (comparisonApi.isError) { + setComparisonData([]); + } + }, [ + templateId, + templateDetails.isSuccess, + templateDetails.data, + comparisonApi.isSuccess, + comparisonApi.data, + comparisonApi.isError, + ]); + const comparisonModeOptions = [{ label: "Compare Tenant to Standard", value: "standard" }]; + + // Group standards by category + const groupedStandards = useMemo(() => { + if (!comparisonData) return {}; + + const result = {}; + + comparisonData.forEach((standard) => { + // Find the standard info in the standards.json data + const standardInfo = standards.find((s) => standard.standardId.includes(s.name)); + + // Use the category from standards.json, or default to "Other Standards" + const category = standardInfo?.cat || "Other Standards"; + + if (!result[category]) { + result[category] = []; + } + + result[category].push(standard); + }); + + // Sort standards within each category + Object.keys(result).forEach((category) => { + result[category].sort((a, b) => a.standardName.localeCompare(b.standardName)); + }); + + return result; + }, [comparisonData]); + + const filteredGroupedStandards = useMemo(() => { + if (!groupedStandards) return {}; + + if (!searchQuery && filter === "all") { + return groupedStandards; + } + + const result = {}; + const searchLower = searchQuery.toLowerCase(); + + Object.keys(groupedStandards).forEach((category) => { + const categoryMatchesSearch = !searchQuery || category.toLowerCase().includes(searchLower); + + const filteredStandards = groupedStandards[category].filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + const hasLicenseMissing = + typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); + + const matchesFilter = + filter === "all" || + (filter === "compliant" && standard.complianceStatus === "Compliant") || + (filter === "nonCompliant" && standard.complianceStatus === "Non-Compliant") || + (filter === "nonCompliantWithLicense" && + standard.complianceStatus === "Non-Compliant" && + !hasLicenseMissing) || + (filter === "nonCompliantWithoutLicense" && + standard.complianceStatus === "Non-Compliant" && + hasLicenseMissing); + + const matchesSearch = + !searchQuery || + categoryMatchesSearch || + standard.standardName.toLowerCase().includes(searchLower) || + standard.standardDescription.toLowerCase().includes(searchLower); + + return matchesFilter && matchesSearch; + }); + + if (filteredStandards.length > 0) { + result[category] = filteredStandards; + } + }); + + return result; + }, [groupedStandards, searchQuery, filter]); + + const allCount = comparisonData?.length || 0; + const compliantCount = + comparisonData?.filter((standard) => standard.complianceStatus === "Compliant").length || 0; + const nonCompliantCount = + comparisonData?.filter((standard) => standard.complianceStatus === "Non-Compliant").length || 0; + const reportingDisabledCount = + comparisonData?.filter((standard) => standard.complianceStatus === "Reporting Disabled") + .length || 0; + + // Calculate license-related metrics + const missingLicenseCount = + comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return typeof tenantValue === "string" && tenantValue.startsWith("License Missing:"); + }).length || 0; + + const nonCompliantWithLicenseCount = + comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return ( + standard.complianceStatus === "Non-Compliant" && + !(typeof tenantValue === "string" && tenantValue.startsWith("License Missing:")) + ); + }).length || 0; + + const nonCompliantWithoutLicenseCount = + comparisonData?.filter((standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue; + return ( + standard.complianceStatus === "Non-Compliant" && + typeof tenantValue === "string" && + tenantValue.startsWith("License Missing:") + ); + }).length || 0; + + const compliancePercentage = + allCount > 0 + ? Math.round((compliantCount / (allCount - reportingDisabledCount || 1)) * 100) + : 0; + + const missingLicensePercentage = + allCount > 0 + ? Math.round((missingLicenseCount / (allCount - reportingDisabledCount || 1)) * 100) + : 0; + + // Combined score: compliance percentage + missing license percentage + // This represents the total "addressable" compliance (compliant + could be compliant if licensed) + const combinedScore = compliancePercentage + missingLicensePercentage; + + // Prepare title and subtitle for HeaderedTabbedLayout + const title = + templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0]?.templateName || + "Tenant Report"; + + const subtitle = [ + // Add compliance badges when template data is available (show even if no comparison data yet) + ...(templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0] + ? [ + { + component: ( + + + + + } + label={`${compliancePercentage}% Compliant`} + variant="outlined" + size="small" + color={ + compliancePercentage === 100 + ? "success" + : compliancePercentage >= 50 + ? "warning" + : "error" + } + /> + + = 80 ? "success" : combinedScore >= 60 ? "warning" : "error" + } + /> + + ), + }, + ] + : []), + // Add description if available + ...(templateDetails?.data?.filter((template) => template.GUID === templateId)?.[0]?.description + ? [ + { + component: ( + theme.palette.primary.main, + textDecoration: "underline", + }, + color: "text.secondary", + fontSize: "0.875rem", + "& p": { + my: 0, + }, + mt: 1, + }} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize( + templateDetails?.data?.filter((template) => template.GUID === templateId)[0] + .description + ), + }} + /> + ), + }, + ] + : []), + ]; + + // Actions for the header + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + comparisonApi.refetch(); + templateDetails.refetch(); + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + return ( + + + {comparisonApi.isFetching && ( + <> + {[1, 2, 3].map((item) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + + )} + {!comparisonApi.isFetching && ( + <> + + + setSearchQuery(e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + + + ), + endAdornment: searchQuery && ( + + + setSearchQuery("")} + aria-label="Clear search" + > + + + + + ), + }, + }} + /> + + + + + + + + + + + {comparisonApi.isError && ( + + + Error fetching comparison data + + + There was an error retrieving the comparison data. Please try running the report + again by clicking the "Run Report Once" button above. + + {comparisonApi.error && ( + + + {comparisonApi.error.message || JSON.stringify(comparisonApi.error, null, 2)} + + + )} + + )} + + {comparisonApi.isSuccess && + (!comparisonApi.data || comparisonApi.data.length === 0) && ( + + + No comparison data is available. This might be because: + + + + • The tenant has not been scanned yet + + + • The template has no standards configured + + + • There was an issue with the comparison + + + + Try running the report by clicking the "Run Report Once" button above. + + + )} + + {filteredGroupedStandards && Object.keys(filteredGroupedStandards).length === 0 && ( + + + No standards match the selected filter criteria or search query. + + + Try selecting a different filter or modifying the search query. + + + )} + + {Object.keys(filteredGroupedStandards).map((category) => ( + + + {category} + + + {filteredGroupedStandards[category].map((standard, index) => ( + + + + + + + + {standard.complianceStatus === "Compliant" ? ( + + ) : standard.complianceStatus === "Reporting Disabled" ? ( + + ) : ( + + )} + + + {standard?.standardName} + + + + + + + + + + {!standard.standardValue ? ( + + This data has not yet been collected. Collect the data by pressing the + report button on the top of the page. + + ) : ( + + + + {standard.standardValue && + typeof standard.standardValue === "object" && + Object.keys(standard.standardValue).length > 0 ? ( + Object.entries(standard.standardValue).map(([key, value]) => ( + + + {key}: + + + {typeof value === "object" && value !== null + ? value?.label || JSON.stringify(value) + : value === true + ? "Enabled" + : value === false + ? "Disabled" + : String(value)} + + + )) + ) : ( + + {standard.standardValue === true ? ( + + This setting is configured correctly + + ) : standard.standardValue === false ? ( + + This setting is not configured correctly + + ) : standard.standardValue !== undefined ? ( + typeof standard.standardValue === "object" ? ( + "No settings configured" + ) : ( + String(standard.standardValue) + ) + ) : ( + + This setting is not configured, or data has not been + collected. If you are getting this after data collection, + the tenant might not be licensed for this feature + + )} + + )} + + + + )} + + + + + + + + + + + + + + + + + + {currentTenant} + + + + + + + + + + {standard.complianceStatus} + + + {standard.currentTenantValue?.LastRefresh && ( + + + + } + size="small" + label={`${new Date( + standard.currentTenantValue.LastRefresh + ).toLocaleString()}`} + variant="outlined" + /> + )} + + + + + + {/* Existing tenant comparison content */} + {typeof standard.currentTenantValue?.Value === "object" && + standard.currentTenantValue?.Value !== null ? ( + + {standard.complianceStatus === "Reporting Disabled" ? ( + + Reporting is disabled for this standard in the template + configuration. + + ) : ( + <> + {standard.complianceStatus === "Compliant" ? ( + + This setting is configured correctly + + ) : standard.currentTenantValue?.Value === false ? ( + + This setting is not configured correctly + + ) : null} + + {/* Only show values if they're not simple true/false that's already covered by the alerts above */} + {!( + standard.complianceStatus === "Compliant" && + (standard.currentTenantValue?.Value === true || + standard.currentTenantValue?.Value === false) + ) && + Object.entries(standard.currentTenantValue) + .filter( + ([key]) => + key !== "LastRefresh" && + // Skip showing the Value field separately if it's just true/false + !( + key === "Value" && + (standard.currentTenantValue?.Value === true || + standard.currentTenantValue?.Value === false) + ) + ) + .map(([key, value]) => { + const actualValue = key === "Value" ? value : value; + + const standardValueForKey = + standard.standardValue && + typeof standard.standardValue === "object" + ? standard.standardValue[key] + : undefined; + + const isDifferent = + standardValueForKey !== undefined && + JSON.stringify(actualValue) !== + JSON.stringify(standardValueForKey); + + return ( + + + {key}: + {" "} + + {typeof value === "object" && value !== null + ? value?.label || JSON.stringify(value) + : value === true + ? "Enabled" + : value === false + ? "Disabled" + : String(value)} + + + ); + })} + + )} + + ) : ( + + {standard.complianceStatus === "Reporting Disabled" ? ( + + Reporting is disabled for this standard in the template + configuration. + + ) : standard.complianceStatus === "Compliant" ? ( + + This setting is configured correctly + + ) : standard.currentTenantValue?.Value === false || + standard.currentTenantValue === false ? ( + + This setting is not configured correctly + + ) : standard.currentTenantValue !== undefined ? ( + String( + standard.currentTenantValue?.Value !== undefined + ? standard.currentTenantValue?.Value + : standard.currentTenantValue + ) + ) : ( + + This setting is not configured, or data has not been collected. If + you are getting this after data collection, the tenant might not + be licensed for this feature + + )} + + )} + + + + + {standard.complianceDetails && ( + + + + + + + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + fontSize: "0.875rem", + lineHeight: 1.43, + "& p": { + my: 0, + }, + flex: 1, + }} + > + ( + + {children} + + ), + // Convert paragraphs to spans to avoid unwanted spacing + p: ({ children }) => {children}, + }} + > + {standard.complianceDetails} + + + + + + )} + + ))} + + ))} + + )} + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/standards/manage-drift/history.js b/src/pages/tenant/standards/manage-drift/history.js new file mode 100644 index 000000000000..d7729022430b --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/history.js @@ -0,0 +1,348 @@ +import { useState, useEffect } from "react"; +import { + Box, + Stack, + Typography, + Button, + Chip, + Card, + CardContent, + CircularProgress, + Alert, + Link, +} from "@mui/material"; +import { + Timeline, + TimelineItem, + TimelineSeparator, + TimelineConnector, + TimelineContent, + TimelineDot, + TimelineOppositeContent, +} from "@mui/lab"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useRouter } from "next/router"; +import { + Policy, + Sync, + PlayArrow, + Error as ErrorIcon, + Warning as WarningIcon, + Info as InfoIcon, + CheckCircle as SuccessIcon, + ExpandMore, +} from "@mui/icons-material"; +import tabOptions from "./tabOptions.json"; +import { useSettings } from "../../../../hooks/use-settings"; + +const Page = () => { + const router = useRouter(); + const { templateId } = router.query; + const [daysToLoad, setDaysToLoad] = useState(5); + const tenant = useSettings().currentTenant; + const [expandedMessages, setExpandedMessages] = useState(new Set()); + + // Toggle message expansion + const toggleMessageExpansion = (index) => { + const newExpanded = new Set(expandedMessages); + if (newExpanded.has(index)) { + newExpanded.delete(index); + } else { + newExpanded.add(index); + } + setExpandedMessages(newExpanded); + }; + + // Truncate message if too long + const truncateMessage = (message, maxLength = 256) => { + if (!message || message.length <= maxLength) { + return { text: message, isTruncated: false }; + } + return { + text: message.substring(0, maxLength) + "...", + fullText: message, + isTruncated: true, + }; + }; + + // Calculate date range for API call + const getDateRange = (days) => { + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(endDate.getDate() - days); + + return { + startDate: startDate.toISOString().split("T")[0].replace(/-/g, ""), + endDate: endDate.toISOString().split("T")[0].replace(/-/g, ""), + }; + }; + + const { startDate, endDate } = getDateRange(daysToLoad); + + const logsData = ApiGetCall({ + url: `/api/Listlogs?tenant=${tenant}&StartDate=${startDate}&EndDate=${endDate}&Filter=true`, + queryKey: `Listlogs-${tenant}-${startDate}-${endDate}`, + }); + + // Get severity icon and color + const getSeverityConfig = (severity) => { + const severityLower = severity?.toLowerCase(); + switch (severityLower) { + case "error": + return { icon: , color: "error", chipColor: "error" }; + case "warning": + return { icon: , color: "warning", chipColor: "warning" }; + case "info": + return { icon: , color: "info", chipColor: "info" }; + case "success": + return { icon: , color: "success", chipColor: "success" }; + default: + return { icon: , color: "grey", chipColor: "default" }; + } + }; + + // Format date for display + const formatDate = (dateString) => { + const date = new Date(dateString); + return { + time: date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }), + date: date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }), + }; + }; + + // Load more days + const handleLoadMore = () => { + setDaysToLoad((prev) => prev + 7); + }; + + // Actions for the ActionsMenu + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + logsData.refetch(); + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + // Sort logs by date (newest first) + const sortedLogs = logsData.data + ? [...logsData.data].sort((a, b) => new Date(b.DateTime) - new Date(a.DateTime)) + : []; + + return ( + + + + Activity Timeline + + This timeline shows the history of actions taken on this tenant, by CIPP for the last{" "} + {daysToLoad} days. + + + {logsData.isLoading && ( + + + + )} + + {logsData.isError && ( + Failed to load activity logs. Please try again. + )} + + {logsData.data && sortedLogs.length === 0 && ( + No activity logs found for the selected time period. + )} + + {logsData.data && sortedLogs.length > 0 && ( + + + + {sortedLogs.map((log, index) => { + const { icon, color, chipColor } = getSeverityConfig(log.Severity); + const { time, date } = formatDate(log.DateTime); + const { text, fullText, isTruncated } = truncateMessage(log.Message); + const isExpanded = expandedMessages.has(index); + + return ( + + + + {date} + + + {time} + + + + + + {icon} + + {index < sortedLogs.length - 1 && } + + + + + + + + {log.IP && ( + + )} + + + + + {isExpanded ? fullText : text} + + {isTruncated && ( + toggleMessageExpansion(index)} + sx={{ + mt: 0.5, + display: "block", + textAlign: "left", + fontSize: "0.75rem", + }} + > + {isExpanded ? "Show less" : "Show more"} + + )} + + + {log.User && ( + + User: {log.User} + + )} + + + + ); + })} + + + + + + + + )} + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/standards/manage-drift/index.js b/src/pages/tenant/standards/manage-drift/index.js new file mode 100644 index 000000000000..39a4f03b312a --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/index.js @@ -0,0 +1,968 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useRouter } from "next/router"; +import { + Check, + Warning, + ExpandMore, + CheckCircle, + Sync, + Block, + Science, + CheckBox, + Cancel, + Policy, + Error, + Info, + FactCheck, + PlayArrow, +} from "@mui/icons-material"; +import { Box, Stack, Typography, Button, Menu, MenuItem, Chip, SvgIcon } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useState } from "react"; +import { CippChartCard } from "/src/components/CippCards/CippChartCard"; +import { CippBannerListCard } from "/src/components/CippCards/CippBannerListCard"; +import { CippHead } from "/src/components/CippComponents/CippHead"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import { ApiGetCall } from "/src/api/ApiCall"; +import { useSettings } from "/src/hooks/use-settings"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; +import { useDialog } from "/src/hooks/use-dialog"; +import tabOptions from "./tabOptions.json"; +import standardsData from "/src/data/standards.json"; + +const ManageDriftPage = () => { + const router = useRouter(); + const { templateId } = router.query; + const userSettingsDefaults = useSettings(); + const tenantFilter = userSettingsDefaults.currentTenant || ""; + const [anchorEl, setAnchorEl] = useState({}); + const [bulkActionsAnchorEl, setBulkActionsAnchorEl] = useState(null); + const createDialog = useDialog(); + const [actionData, setActionData] = useState({ data: {}, ready: false }); + + // API calls for drift data + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + TenantFilter: tenantFilter, + }, + queryKey: `TenantDrift-${tenantFilter}`, + }); + + // API call for available drift templates (for What If dropdown) + const standardsApi = ApiGetCall({ + url: "/api/listStandardTemplates", + data: { + type: "drift", + }, + queryKey: "ListDriftTemplates", + }); + + // API call for standards comparison (when templateId is available) + const comparisonApi = ApiGetCall({ + url: "/api/ListStandardsCompare", + data: { + TemplateId: templateId, + TenantFilter: tenantFilter, + CompareToStandard: true, + }, + queryKey: `StandardsCompare-${templateId}-${tenantFilter}`, + enabled: !!templateId && !!tenantFilter, + }); + + // Process drift data for chart - filter by current tenant and aggregate + const rawDriftData = driftApi.data || []; + const tenantDriftData = Array.isArray(rawDriftData) + ? rawDriftData.filter((item) => item.tenantFilter === tenantFilter) + : []; + + // Aggregate data across all standards for this tenant + const processedDriftData = tenantDriftData.reduce( + (acc, item) => { + acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0; + acc.currentDeviationsCount += item.currentDeviationsCount || 0; + acc.alignedCount += item.alignedCount || 0; + acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; + acc.deniedDeviationsCount += item.deniedDeviationsCount || 0; + + // Use the API's direct arrays instead of filtering allDeviations + if (item.currentDeviations && Array.isArray(item.currentDeviations)) { + acc.currentDeviations.push(...item.currentDeviations.filter((dev) => dev !== null)); + } + if (item.acceptedDeviations && Array.isArray(item.acceptedDeviations)) { + acc.acceptedDeviations.push(...item.acceptedDeviations.filter((dev) => dev !== null)); + } + if (item.customerSpecificDeviations && Array.isArray(item.customerSpecificDeviations)) { + acc.customerSpecificDeviationsList.push( + ...item.customerSpecificDeviations.filter((dev) => dev !== null) + ); + } + if (item.deniedDeviations && Array.isArray(item.deniedDeviations)) { + acc.deniedDeviationsList.push(...item.deniedDeviations.filter((dev) => dev !== null)); + } + + // Use the latest data collection timestamp + if ( + item.latestDataCollection && + (!acc.latestDataCollection || + new Date(item.latestDataCollection) > new Date(acc.latestDataCollection)) + ) { + acc.latestDataCollection = item.latestDataCollection; + } + + return acc; + }, + { + acceptedDeviationsCount: 0, + currentDeviationsCount: 0, + alignedCount: 0, + customerSpecificDeviations: 0, + deniedDeviationsCount: 0, + currentDeviations: [], + acceptedDeviations: [], + customerSpecificDeviationsList: [], + deniedDeviationsList: [], + latestDataCollection: null, + } + ); + + const chartLabels = [ + "Aligned Policies", + "Accepted Deviations", + "Current Deviations", + "Customer Specific Deviations", + ]; + const chartSeries = [ + processedDriftData.alignedCount || 0, + processedDriftData.acceptedDeviationsCount || 0, + processedDriftData.currentDeviationsCount || 0, + processedDriftData.customerSpecificDeviations || 0, + ]; + + // Transform currentDeviations into deviation items for display + const getDeviationIcon = (state) => { + switch (state?.toLowerCase()) { + case "current": + return ; + case "denied": + return ; + case "denieddelete": + case "denied - delete": + return ; + case "deniedremediate": + case "denied - remediate": + return ; + case "accepted": + return ; + case "customerspecific": + return ; + default: + return ; + } + }; + + const getDeviationColor = (state) => { + switch (state?.toLowerCase()) { + case "current": + return "warning.main"; + case "denied": + return "error.main"; + case "denieddelete": + case "denied - delete": + return "error.main"; + case "deniedremediate": + case "denied - remediate": + return "error.main"; + case "accepted": + return "success.main"; + case "customerspecific": + return "info.main"; + default: + return "warning.main"; + } + }; + + const getDeviationStatusText = (state) => { + switch (state?.toLowerCase()) { + case "current": + return "Current Deviation"; + case "denied": + return "Denied Deviation"; + case "denieddelete": + case "denied - delete": + return "Denied - Delete"; + case "deniedremediate": + case "denied - remediate": + return "Denied - Remediate"; + case "accepted": + return "Accepted Deviation"; + case "customerspecific": + return "Customer Specific"; + default: + return "Deviation"; + } + }; + + // Helper function to get pretty name from standards.json + const getStandardPrettyName = (standardName) => { + if (!standardName) return "Unknown Standard"; + + // Find the standard in standards.json by name + const standard = standardsData.find((s) => s.name === standardName); + if (standard && standard.label) { + return standard.label; + } + + // If not found in standards.json, try using standardDisplayName from the deviation object + // This will be handled in the createDeviationItems function + return null; + }; + + // Helper function to get description from standards.json + const getStandardDescription = (standardName) => { + if (!standardName) return null; + + // Find the standard in standards.json by name + const standard = standardsData.find((s) => s.name === standardName); + if (standard) { + return standard.helpText || standard.docsDescription || standard.executiveText || null; + } + + return null; + }; + + // Helper function to create deviation items + const createDeviationItems = (deviations, statusOverride = null) => { + return (deviations || []).map((deviation, index) => { + // Prioritize standardDisplayName from drift data (which has user-friendly names for templates) + // then fallback to standards.json lookup, then raw name + const prettyName = + deviation.standardDisplayName || + getStandardPrettyName(deviation.standardName) || + deviation.standardName || + "Unknown Standard"; + + // Get description from standards.json first, then fallback to standardDescription from deviation + const description = + getStandardDescription(deviation.standardName) || + deviation.standardDescription || + "No description available"; + + return { + id: index + 1, + cardLabelBox: { + cardLabelBoxHeader: getDeviationIcon( + statusOverride || deviation.Status || deviation.state + ), + }, + text: prettyName, + subtext: description, + statusColor: getDeviationColor(statusOverride || deviation.Status || deviation.state), + statusText: getDeviationStatusText(statusOverride || deviation.Status || deviation.state), + standardName: deviation.standardName, // Store the original standardName for action handlers + receivedValue: deviation.receivedValue, // Store the original receivedValue for action handlers + expectedValue: deviation.expectedValue, // Store the original expectedValue for action handlers + originalDeviation: deviation, // Store the complete original deviation object for reference + propertyItems: [ + { label: "Standard Name", value: prettyName }, + { label: "Description", value: description }, + { label: "Expected Value", value: deviation.expectedValue || "N/A" }, + { label: "Current Value", value: deviation.receivedValue || "N/A" }, + { + label: "Status", + value: getDeviationStatusText(statusOverride || deviation.Status || deviation.state), + }, + { + label: "Reason", + value: deviation.Reason || "N/A", + }, + { + label: "User", + value: deviation.lastChangedByUser || "N/A", + }, + { + label: "Last Updated", + value: processedDriftData.latestDataCollection + ? new Date(processedDriftData.latestDataCollection).toLocaleString() + : "N/A", + }, + ].filter((item) => item.value !== "N/A" && item.value !== "No description available"), // Filter out N/A values and empty descriptions + }; + }); + }; + + const deviationItems = createDeviationItems(processedDriftData.currentDeviations); + const acceptedDeviationItems = createDeviationItems( + processedDriftData.acceptedDeviations, + "accepted" + ); + const customerSpecificDeviationItems = createDeviationItems( + processedDriftData.customerSpecificDeviationsList, + "customerspecific" + ); + const deniedDeviationItems = createDeviationItems( + processedDriftData.deniedDeviationsList, + "denied" + ); + + const handleMenuClick = (event, itemId) => { + setAnchorEl((prev) => ({ ...prev, [itemId]: event.currentTarget })); + }; + + const handleMenuClose = (itemId) => { + setAnchorEl((prev) => ({ ...prev, [itemId]: null })); + }; + + const handleAction = (action, itemId) => { + const deviation = processedDriftData.currentDeviations[itemId - 1]; + if (!deviation) return; + + let status; + let actionText; + switch (action) { + case "accept-customer-specific": + status = "CustomerSpecific"; + actionText = "accept as customer specific"; + break; + case "accept": + status = "Accepted"; + actionText = "accept"; + break; + case "deny-delete": + status = "DeniedDelete"; + actionText = "deny and delete"; + break; + case "deny-remediate": + status = "DeniedRemediate"; + actionText = "deny and remediate to align with template"; + break; + default: + return; + } + + // Set action data for CippApiDialog + setActionData({ + data: { + deviations: [ + { + standardName: deviation.standardName, + status: status, + receivedValue: deviation.receivedValue, + }, + ], + TenantFilter: tenantFilter, + }, + action: { + text: actionText, + type: "single", + }, + ready: true, + }); + + createDialog.handleOpen(); + handleMenuClose(itemId); + }; + + const handleDeviationAction = (action, deviation) => { + if (!deviation) return; + + let status; + let actionText; + switch (action) { + case "accept-customer-specific": + status = "CustomerSpecific"; + actionText = "accept as customer specific"; + break; + case "accept": + status = "Accepted"; + actionText = "accept"; + break; + case "deny": + status = "Denied"; + actionText = "deny"; + break; + case "deny-delete": + status = "DeniedDelete"; + actionText = "deny and delete"; + break; + case "deny-remediate": + status = "DeniedRemediate"; + actionText = "deny and remediate to align with template"; + break; + default: + return; + } + + // Set action data for CippApiDialog + setActionData({ + data: { + deviations: [ + { + standardName: deviation.standardName, // Use the standardName from the original deviation data + status: status, + receivedValue: deviation.receivedValue, + }, + ], + TenantFilter: tenantFilter, + }, + action: { + text: actionText, + type: "single", + }, + ready: true, + }); + + createDialog.handleOpen(); + }; + + const handleBulkAction = (action) => { + if ( + !processedDriftData.currentDeviations || + processedDriftData.currentDeviations.length === 0 + ) { + setBulkActionsAnchorEl(null); + return; + } + + let status; + let actionText; + switch (action) { + case "accept-all-customer-specific": + status = "CustomerSpecific"; + actionText = "accept all deviations as customer specific"; + break; + case "accept-all": + status = "Accepted"; + actionText = "accept all deviations"; + break; + case "deny-all": + status = "Denied"; + actionText = "deny all deviations"; + break; + case "deny-all-delete": + status = "DeniedDelete"; + actionText = "deny all deviations and delete"; + break; + case "deny-all-remediate": + status = "DeniedRemediate"; + actionText = "deny all deviations and remediate to align with template"; + break; + default: + setBulkActionsAnchorEl(null); + return; + } + + const deviations = processedDriftData.currentDeviations.map((deviation) => ({ + standardName: deviation.standardName, + status: status, + receivedValue: deviation.receivedValue, + })); + + // Set action data for CippApiDialog + setActionData({ + data: { + deviations: deviations, + TenantFilter: tenantFilter, + receivedValues: deviations.map((d) => d.receivedValue), + }, + action: { + text: actionText, + type: "bulk", + count: deviations.length, + }, + ready: true, + }); + + createDialog.handleOpen(); + setBulkActionsAnchorEl(null); + }; + + const handleRemoveDriftCustomization = () => { + // Set action data for CippApiDialog + setActionData({ + data: { + RemoveDriftCustomization: true, + TenantFilter: tenantFilter, + }, + action: { + text: "remove all drift customizations", + type: "reset", + }, + ready: true, + }); + + createDialog.handleOpen(); + setBulkActionsAnchorEl(null); + }; + + // Actions for the ActionsMenu + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + driftApi.refetch(); + standardsApi.refetch(); + if (templateId) { + comparisonApi.refetch(); + } + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + // Add action buttons to each deviation item + const deviationItemsWithActions = deviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate"); + + return { + ...item, + actionButton: ( + <> + + handleMenuClose(item.id)} + > + handleAction("accept-customer-specific", item.id)}> + + Accept Deviation - Customer Specific + + handleAction("accept", item.id)}> + + Accept Deviation + + {supportsDelete && ( + handleAction("deny-delete", item.id)}> + + Deny Deviation - Delete Policy + + )} + handleAction("deny-remediate", item.id)}> + + Deny Deviation - Remediate to align with template + + + + ), + }; + }); + + // Add action buttons to accepted deviation items + const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate"); + + return { + ...item, + actionButton: ( + <> + + handleMenuClose(`accepted-${item.id}`)} + > + {supportsDelete && ( + handleDeviationAction("deny-delete", item)}> + + Deny - Delete Policy + + )} + handleDeviationAction("deny-remediate", item)}> + + Deny - Remediate to align with template + + handleDeviationAction("accept-customer-specific", item)}> + + Accept - Customer Specific + + + + ), + }; + }); + + // Add action buttons to customer specific deviation items + const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => { + // Check if this is a template that supports delete action + const supportsDelete = + item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate"); + + return { + ...item, + actionButton: ( + <> + + handleMenuClose(`customer-${item.id}`)} + > + {supportsDelete && ( + handleDeviationAction("deny-delete", item)}> + + Deny - Delete + + )} + handleDeviationAction("deny-remediate", item)}> + + Deny - Remediate to align with template + + handleDeviationAction("accept", item)}> + + Accept + + + + ), + }; + }); + + // Add action buttons to denied deviation items + const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({ + ...item, + actionButton: ( + <> + + handleMenuClose(`denied-${item.id}`)} + > + handleDeviationAction("accept", item)}> + + Accept + + handleDeviationAction("accept-customer-specific", item)}> + + Accept - Customer Specific + + + + ), + })); + + // Calculate compliance metrics for badges + const totalPolicies = + processedDriftData.alignedCount + + processedDriftData.currentDeviationsCount + + processedDriftData.acceptedDeviationsCount + + processedDriftData.customerSpecificDeviations; + + const compliancePercentage = + totalPolicies > 0 ? Math.round((processedDriftData.alignedCount / totalPolicies) * 100) : 0; + + const missingLicensePercentage = 0; // This would need to be calculated from actual license data + const combinedScore = compliancePercentage + missingLicensePercentage; + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + // Add compliance badges when data is available + ...(totalPolicies > 0 + ? [ + { + component: ( + + + + + } + label={`${compliancePercentage}% Compliant`} + variant="outlined" + size="small" + color={ + compliancePercentage === 100 + ? "success" + : compliancePercentage >= 50 + ? "warning" + : "error" + } + /> + = 80 ? "success" : combinedScore >= 60 ? "warning" : "error" + } + /> + + ), + }, + ] + : []), + ]; + + return ( + + + + {/* Check if there's no drift data */} + {!driftApi.isFetching && + (!rawDriftData || rawDriftData.length === 0 || tenantDriftData.length === 0) ? ( + + + No Drift Data Available + + + This standard does not have any drift entries, or it is not a drift compatible + standard. + + + To enable drift monitoring for this tenant, please ensure: + + + + A drift template has been created and assigned to this tenant + + + The standard is configured for drift monitoring + + + Drift data collection has been completed for this tenant + + + + ) : ( + + {/* Left side - Chart */} + + + + + {/* Right side - Deviation Management */} + + + {/* Current Deviations Section */} + + {/* Header with bulk actions */} + + Current Deviations + + {/* Bulk Actions Dropdown */} + + setBulkActionsAnchorEl(null)} + > + handleBulkAction("accept-all-customer-specific")}> + + Accept All Deviations - Customer Specific + + handleBulkAction("accept-all")}> + + Accept All Deviations + + {/* Only show delete option if there are template deviations that support deletion */} + {processedDriftData.currentDeviations.some( + (deviation) => + deviation.standardName?.includes("ConditionalAccessTemplate") || + deviation.standardName?.includes("IntuneTemplate") + ) && ( + handleBulkAction("deny-all-delete")}> + + Deny All Deviations - Delete + + )} + handleBulkAction("deny-all-remediate")}> + + Deny All Deviations - Remediate to align with template + + + + Remove Drift Customization + + + + + + + + {/* Accepted Deviations Section */} + {acceptedDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations + + + + )} + + {/* Customer Specific Deviations Section */} + {customerSpecificDeviationItemsWithActions.length > 0 && ( + + + Accepted Deviations - Customer Specific + + + + )} + + {/* Denied Deviations Section */} + {deniedDeviationItemsWithActions.length > 0 && ( + + + Denied Deviations + + + + )} + + + + )} + + {actionData.ready && ( + + )} + + ); +}; + +ManageDriftPage.getLayout = (page) => {page}; + +export default ManageDriftPage; diff --git a/src/pages/tenant/standards/manage-drift/policies-deployed.js b/src/pages/tenant/standards/manage-drift/policies-deployed.js new file mode 100644 index 000000000000..909b19a733f4 --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/policies-deployed.js @@ -0,0 +1,376 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import { + Policy, + Security, + AdminPanelSettings, + Devices, + ExpandMore, + Sync, + PlayArrow, +} from "@mui/icons-material"; +import { + Box, + Stack, + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Chip, +} from "@mui/material"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import tabOptions from "./tabOptions.json"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { CippHead } from "/src/components/CippComponents/CippHead"; +import { ApiGetCall } from "/src/api/ApiCall"; +import standardsData from "/src/data/standards.json"; + +const PoliciesDeployedPage = () => { + const userSettingsDefaults = useSettings(); + const router = useRouter(); + const { templateId } = router.query; + const tenantFilter = router.query.tenantFilter || userSettingsDefaults.tenantFilter; + + // API call to get standards template data + const standardsApi = ApiGetCall({ + url: "/api/listStandardTemplates", + queryKey: "ListStandardsTemplates-Drift", + }); + + // API call to get standards comparison data + const comparisonApi = ApiGetCall({ + url: "/api/ListStandardsCompare", + data: { + TemplateId: templateId, + TenantFilter: tenantFilter, + CompareToStandard: true, + }, + queryKey: `StandardsCompare-${templateId}-${tenantFilter}`, + enabled: !!templateId && !!tenantFilter, + }); + + // API call to get drift data for deviation statuses + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + tenantFilter: tenantFilter, + standardsId: templateId, + }, + queryKey: `TenantDrift-${templateId}-${tenantFilter}`, + enabled: !!templateId && !!tenantFilter, + }); + + // Find the current template from standards data + const currentTemplate = (standardsApi.data || []).find( + (template) => template.GUID === templateId + ); + const templateStandards = currentTemplate?.standards || {}; + const comparisonData = comparisonApi.data?.[0] || {}; + + // Helper function to get status from comparison data with deviation status + const getStatus = (standardKey, templateValue = null, templateType = null) => { + const comparisonKey = `standards.${standardKey}`; + const value = comparisonData[comparisonKey]?.Value; + + if (value === true) { + return "Deployed"; + } else { + // Check if there's drift data for this standard to get the deviation status + const driftData = driftApi.data || []; + + // For templates, we need to match against the full template path + let searchKeys = [ + standardKey, + `standards.${standardKey}`, + ]; + + // Add template-specific search keys + if (templateValue && templateType) { + searchKeys.push( + `standards.${templateType}.${templateValue}`, + `${templateType}.${templateValue}`, + templateValue + ); + } + + const deviation = driftData.find(item => + searchKeys.some(key => + item.standardName === key || + item.policyName === key || + item.standardName?.includes(key) || + item.policyName?.includes(key) + ) + ); + + if (deviation && deviation.Status) { + return `Deviation - ${deviation.Status}`; + } + + return "Deviation - New"; + } + }; + + // Helper function to get display name from drift data + const getDisplayNameFromDrift = (standardKey, templateValue = null, templateType = null) => { + const driftData = driftApi.data || []; + + // For templates, we need to match against the full template path + let searchKeys = [ + standardKey, + `standards.${standardKey}`, + ]; + + // Add template-specific search keys + if (templateValue && templateType) { + searchKeys.push( + `standards.${templateType}.${templateValue}`, + `${templateType}.${templateValue}`, + templateValue + ); + } + + const deviation = driftData.find(item => + searchKeys.some(key => + item.standardName === key || + item.policyName === key || + item.standardName?.includes(key) || + item.policyName?.includes(key) + ) + ); + + return deviation?.standardDisplayName || null; + }; + + // Helper function to get last refresh date + const getLastRefresh = (standardKey) => { + const comparisonKey = `standards.${standardKey}`; + const lastRefresh = comparisonData[comparisonKey]?.LastRefresh; + return lastRefresh ? new Date(lastRefresh).toLocaleDateString() : "N/A"; + }; + + // Helper function to get standard name from standards.json + const getStandardName = (standardKey) => { + const standardName = `standards.${standardKey}`; + const standard = standardsData.find(s => s.name === standardName); + return standard?.label || standardKey.replace(/([A-Z])/g, " $1").trim(); + }; + + // Helper function to get template label from standards API data + const getTemplateLabel = (templateValue, templateType) => { + if (!templateValue || !currentTemplate) return "Unknown Template"; + + // Search through all templates in the current template data + const allTemplates = currentTemplate.standards || {}; + + // Look for the template in the specific type array + if (allTemplates[templateType] && Array.isArray(allTemplates[templateType])) { + const template = allTemplates[templateType].find(t => t.TemplateList?.value === templateValue); + if (template?.TemplateList?.label) { + return template.TemplateList.label; + } + } + + // If not found in the specific type, search through all template types + for (const [key, templates] of Object.entries(allTemplates)) { + if (Array.isArray(templates)) { + const template = templates.find(t => t.TemplateList?.value === templateValue); + if (template?.TemplateList?.label) { + return template.TemplateList.label; + } + } + } + + return "Unknown Template"; + }; + + // Process Security Standards (everything NOT IntuneTemplates or ConditionalAccessTemplates) + const deployedStandards = Object.entries(templateStandards) + .filter(([key]) => key !== "IntuneTemplate" && key !== "ConditionalAccessTemplate") + .map(([key, value], index) => ({ + id: index + 1, + name: getStandardName(key), + category: "Security Standard", + status: getStatus(key), + lastModified: getLastRefresh(key), + standardKey: key, + })); + + // Process Intune Templates + const intunePolices = (templateStandards.IntuneTemplate || []).map((template, index) => { + const standardKey = `IntuneTemplate.${template.TemplateList?.value}`; + const driftDisplayName = getDisplayNameFromDrift(standardKey, template.TemplateList?.value, "IntuneTemplate"); + const templateLabel = getTemplateLabel(template.TemplateList?.value, "IntuneTemplate"); + + return { + id: index + 1, + name: driftDisplayName || `Intune - ${templateLabel}`, + category: "Intune Template", + platform: "Multi-Platform", + status: getStatus(standardKey, template.TemplateList?.value, "IntuneTemplate"), + lastModified: getLastRefresh(standardKey), + assignedGroups: template.AssignTo || "N/A", + templateValue: template.TemplateList?.value, + }; + }); + + // Process Conditional Access Templates + const conditionalAccessPolicies = (templateStandards.ConditionalAccessTemplate || []).map( + (template, index) => { + const standardKey = `ConditionalAccessTemplate.${template.TemplateList?.value}`; + const driftDisplayName = getDisplayNameFromDrift(standardKey, template.TemplateList?.value, "ConditionalAccessTemplate"); + const templateLabel = getTemplateLabel(template.TemplateList?.value, "ConditionalAccessTemplate"); + + return { + id: index + 1, + name: driftDisplayName || `Conditional Access - ${templateLabel}`, + state: template.state || "Unknown", + conditions: "Conditional Access Policy", + controls: "Access Control", + lastModified: getLastRefresh(standardKey), + status: getStatus(standardKey, template.TemplateList?.value, "ConditionalAccessTemplate"), + templateValue: template.TemplateList?.value, + }; + } + ); + const actions = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + standardsApi.refetch(); + comparisonApi.refetch(); + driftApi.refetch(); + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + return ( + + + + + {/* Standards Section */} + + }> + + + Security Standards + + + + + + + + + {/* Intune Policies Section */} + + }> + + + Intune Policies + + + + + + + + + {/* Conditional Access Policies Section */} + + }> + + + Conditional Access Policies + + + + + + + + + + + ); +}; + +PoliciesDeployedPage.getLayout = (page) => {page}; + +export default PoliciesDeployedPage; diff --git a/src/pages/tenant/standards/manage-drift/recover-policies.js b/src/pages/tenant/standards/manage-drift/recover-policies.js new file mode 100644 index 000000000000..32b2a059a474 --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/recover-policies.js @@ -0,0 +1,227 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { useSettings } from "/src/hooks/use-settings"; +import { useRouter } from "next/router"; +import { Policy, Restore, ExpandMore, Sync, PlayArrow } from "@mui/icons-material"; +import { + Box, + Stack, + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Chip, + Button, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { HeaderedTabbedLayout } from "/src/layouts/HeaderedTabbedLayout"; +import tabOptions from "./tabOptions.json"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { CippHead } from "/src/components/CippComponents/CippHead"; +import { CippFormComponent } from "/src/components/CippComponents/CippFormComponent"; +import { ApiPostCall } from "/src/api/ApiCall"; +import { CippApiResults } from "/src/components/CippComponents/CippApiResults"; + +const RecoverPoliciesPage = () => { + const router = useRouter(); + const { templateId } = router.query; + const [selectedPolicies, setSelectedPolicies] = useState([]); + + 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 = [ + { + label: "Refresh Data", + icon: , + noConfirm: true, + customFunction: () => { + // Refresh any relevant data here + }, + }, + ...(templateId + ? [ + { + label: "Run Standard Now (Currently Selected Tenant only)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + { + label: "Run Standard Now (All Tenants in Template)", + type: "GET", + url: "/api/ExecStandardsRun", + icon: , + data: { + TemplateId: templateId, + tenantFilter: "allTenants", + }, + confirmText: "Are you sure you want to force a run of this standard?", + multiPost: false, + }, + ] + : []), + ]; + + const title = "Manage Drift"; + const subtitle = [ + { + icon: , + text: `Template ID: ${templateId || "Loading..."}`, + }, + ]; + + return ( + + + + + {/* Backup Date Selection */} + + }> + + + Select Backup Date & Time + + + + + + { + const date = new Date(option.dateTime); + return `${date.toLocaleDateString()} @ ${date.toLocaleTimeString()} (${ + option.policyCount + } policies)`; + }, + valueField: "dateTime", + }} + required={true} + validators={{ + validate: (value) => !!value || "Please select a backup date & time", + }} + /> + + + + + + {/* Recovery Results */} + + + {/* Backup Policies Section */} + {selectedBackup && ( + + }> + + + Policies in Selected Backup + + + + + + + + Select policies to recover from backup:{" "} + {new Date(selectedBackup).toLocaleString()} + + + + setSelectedPolicies(selectedRows)} + /> + + + + )} + + + + ); +}; + +RecoverPoliciesPage.getLayout = (page) => {page}; + +export default RecoverPoliciesPage; diff --git a/src/pages/tenant/standards/manage-drift/tabOptions.json b/src/pages/tenant/standards/manage-drift/tabOptions.json new file mode 100644 index 000000000000..50b3adfd16dc --- /dev/null +++ b/src/pages/tenant/standards/manage-drift/tabOptions.json @@ -0,0 +1,18 @@ +[ + { + "label": "Manage Drift", + "path": "/tenant/standards/manage-drift" + }, + { + "label": "Policies and Settings Deployed", + "path": "/tenant/standards/manage-drift/policies-deployed" + }, + { + "label": "History", + "path": "/tenant/standards/manage-drift/history" + }, + { + "label": "Tenant Report", + "path": "/tenant/standards/manage-drift/compare" + } +] diff --git a/src/pages/tenant/standards/template.jsx b/src/pages/tenant/standards/template.jsx index 2a5934014bc6..0262b6a6e1ad 100644 --- a/src/pages/tenant/standards/template.jsx +++ b/src/pages/tenant/standards/template.jsx @@ -1,41 +1,148 @@ -import { Box, Button, Container, Stack, Typography, SvgIcon, Grid, Skeleton } from "@mui/material"; +import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from "@mui/material"; +import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import { useRouter } from "next/router"; -import { Add } from "@mui/icons-material"; -import { useEffect, useState } from "react"; +import { Add, SaveRounded } from "@mui/icons-material"; +import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from "react"; import standards from "/src/data/standards"; import CippStandardAccordion from "../../../components/CippStandards/CippStandardAccordion"; -import CippStandardDialog from "../../../components/CippStandards/CippStandardDialog"; +// Lazy load the dialog to improve initial page load performance +const CippStandardDialog = lazy(() => + import("../../../components/CippStandards/CippStandardDialog") +); import CippStandardsSideBar from "../../../components/CippStandards/CippStandardsSideBar"; import { ArrowLeftIcon } from "@mui/x-date-pickers"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { useDialog } from "../../../hooks/use-dialog"; import { ApiGetCall } from "../../../api/ApiCall"; +import _ from "lodash"; const Page = () => { const router = useRouter(); const [editMode, setEditMode] = useState(false); const formControl = useForm({ mode: "onBlur" }); + const { formState } = formControl; const [dialogOpen, setDialogOpen] = useState(false); const [expanded, setExpanded] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [selectedStandards, setSelectedStandards] = useState({}); const [updatedAt, setUpdatedAt] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [hasDriftConflict, setHasDriftConflict] = useState(false); + const initialStandardsRef = useRef({}); + + // Check if this is drift mode + const isDriftMode = router.query.type === "drift"; + + // Set drift mode flag in form when in drift mode + useEffect(() => { + if (isDriftMode) { + formControl.setValue("isDriftTemplate", true); + } + }, [isDriftMode, formControl]); + + // Watch form values to check valid configuration + const watchForm = useWatch({ control: formControl.control }); + const existingTemplate = ApiGetCall({ url: `/api/listStandardTemplates`, data: { id: router.query.id }, queryKey: `listStandardTemplates-${router.query.id}`, waiting: editMode, }); + + // Check if the template configuration is valid and update currentStep + useEffect(() => { + const stepsStatus = { + step1: !!_.get(watchForm, "templateName"), + step2: isDriftMode || _.get(watchForm, "tenantFilter", []).length > 0, // Skip tenant requirement for drift mode + step3: Object.keys(selectedStandards).length > 0, + step4: + _.get(watchForm, "standards") && + Object.keys(selectedStandards).length > 0 && + Object.keys(selectedStandards).every((standardName) => { + const standardValues = _.get(watchForm, standardName, {}); + // Always require an action value which should be an array with at least one element + const actionValue = _.get(standardValues, "action"); + return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); + }), + }; + + const completedSteps = Object.values(stepsStatus).filter(Boolean).length; + setCurrentStep(completedSteps); + }, [selectedStandards, watchForm, isDriftMode]); + + // Handle route change events + const handleRouteChange = useCallback( + (url) => { + if (hasUnsavedChanges) { + const confirmLeave = window.confirm( + "You have unsaved changes. Are you sure you want to leave this page?" + ); + if (!confirmLeave) { + router.events.emit("routeChangeError"); + throw "Route change was aborted"; + } + } + }, + [hasUnsavedChanges, router] + ); + + // Handle browser back/forward navigation or tab close + useEffect(() => { + const handleBeforeUnload = (e) => { + if (hasUnsavedChanges) { + e.preventDefault(); + e.returnValue = "You have unsaved changes. Are you sure you want to leave this page?"; + return e.returnValue; + } + }; + + // Add event listeners + window.addEventListener("beforeunload", handleBeforeUnload); + router.events.on("routeChangeStart", handleRouteChange); + + // Remove event listeners on cleanup + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + router.events.off("routeChangeStart", handleRouteChange); + }; + }, [hasUnsavedChanges, handleRouteChange, router.events]); + + // Track form changes + useEffect(() => { + // Compare the current form values with the initial values to check for real changes + const currentValues = formControl.getValues(); + const initialValues = initialStandardsRef.current; + + if ( + formState.isDirty || + JSON.stringify(selectedStandards) !== JSON.stringify(initialStandardsRef.current) + ) { + setHasUnsavedChanges(true); + } else { + setHasUnsavedChanges(false); + } + }, [formState.isDirty, selectedStandards, formControl]); + useEffect(() => { if (router.query.id) { setEditMode(true); } if (existingTemplate.isSuccess) { - formControl.reset(existingTemplate.data?.[0]); + //formControl.reset(existingTemplate.data?.[0]); const apiData = existingTemplate.data?.[0]; + + Object.keys(apiData.standards).forEach((key) => { + if (Array.isArray(apiData.standards[key])) { + apiData.standards[key] = apiData.standards[key].filter( + (value) => value !== null && value !== undefined + ); + } + }); + formControl.reset(apiData); if (router.query.clone) { formControl.setValue("templateName", `${apiData.templateName} (Clone)`); @@ -61,27 +168,40 @@ const Page = () => { }); setSelectedStandards(transformedStandards); + // Store initial state for change detection + initialStandardsRef.current = { ...transformedStandards }; + setHasUnsavedChanges(false); } }, [existingTemplate.isSuccess, router]); - const categories = standards.reduce((acc, standard) => { - const { cat } = standard; - if (!acc[cat]) { - acc[cat] = []; - } - acc[cat].push(standard); - return acc; - }, {}); + // Memoize categories to avoid unnecessary recalculations + const categories = useMemo(() => { + return standards.reduce((acc, standard) => { + const { cat } = standard; + if (!acc[cat]) { + acc[cat] = []; + } + acc[cat].push(standard); + return acc; + }, {}); + }, []); + + const handleOpenDialog = useCallback(() => { + setDialogOpen(true); + }, []); - const handleOpenDialog = () => setDialogOpen(true); - const handleCloseDialog = () => setDialogOpen(false); + const handleCloseDialog = useCallback(() => { + setDialogOpen(false); + setSearchQuery(""); + }, []); const filterStandards = (standardsList) => standardsList.filter( (standard) => - standard.label.toLowerCase().includes(searchQuery) || - standard.helpText.toLowerCase().includes(searchQuery) || - (standard.tag && standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery))) + standard.label.toLowerCase().includes(searchQuery.toLowerCase()) || + standard.helpText.toLowerCase().includes(searchQuery.toLowerCase()) || + (standard.tag && + standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))) ); const handleToggleStandard = (standardName) => { @@ -98,12 +218,10 @@ const Page = () => { if (match) { standardName = match[1]; } - console.log("Adding multiple", standardName); setSelectedStandards((prev) => { const existingInstances = Object.keys(prev).filter((name) => name.startsWith(standardName)); const newIndex = existingInstances.length; - return { ...prev, [`${standardName}[${newIndex}]`]: true, @@ -135,15 +253,23 @@ const Page = () => { setExpanded((prev) => (prev === standardName ? null : standardName)); }; - const actions = [ - { - label: "Save Template", - handler: () => createDialog.handleOpen(), - icon: , - }, - ]; const createDialog = useDialog(); + // Save action that will open the create dialog + const handleSave = () => { + createDialog.handleOpen(); + // Will be set to false after successful save in the dialog component + }; + + // 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 < 3); + + const actions = []; + const steps = [ "Set a name for the Template", "Assigned Template to Tenants", @@ -151,6 +277,19 @@ const Page = () => { "Configured all Standards", ]; + const handleSafeNavigation = (url) => { + if (hasUnsavedChanges) { + const confirmLeave = window.confirm( + "You have unsaved changes. Are you sure you want to leave this page?" + ); + if (confirmLeave) { + router.push(url); + } + } else { + router.push(url); + } + }; + return ( @@ -158,7 +297,13 @@ const Page = () => { + + + + - - {/* Left Column for Accordions */} - - - - - - {existingTemplate.isLoading && } - {/* Show accordions based on selectedStandards (which is populated by API when editing) */} - + + {/* Left Column for Accordions */} + + { + // Reset unsaved changes flag + setHasUnsavedChanges(false); + // Update reference for future change detection + initialStandardsRef.current = { ...selectedStandards }; + }} /> - + + + + {/* Show accordions based on selectedStandards (which is populated by API when editing) */} + {existingTemplate.isLoading ? ( + + ) : ( + + )} + + -
    + - + {/* Only render the dialog when it's needed */} + {dialogOpen && ( + }> + + + )} ); diff --git a/src/pages/tenant/standards/tenant-alignment/index.js b/src/pages/tenant/standards/tenant-alignment/index.js new file mode 100644 index 000000000000..34467ff6ea4b --- /dev/null +++ b/src/pages/tenant/standards/tenant-alignment/index.js @@ -0,0 +1,38 @@ +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { EyeIcon } from "@heroicons/react/24/outline"; + +const Page = () => { + const pageTitle = "Tenant Alignment"; + + const actions = [ + { + label: "View Tenant Report", + link: "/tenant/standards/compare?tenantFilter=[tenantFilter]&templateId=[standardId]", + icon: , + color: "info", + target: "_self", + }, + ]; + + return ( + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/tenant/administration/appapproval/index.js b/src/pages/tenant/tools/appapproval/index.js similarity index 92% rename from src/pages/tenant/administration/appapproval/index.js rename to src/pages/tenant/tools/appapproval/index.js index 10067259d287..a18484cc85ce 100644 --- a/src/pages/tenant/administration/appapproval/index.js +++ b/src/pages/tenant/tools/appapproval/index.js @@ -2,7 +2,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippWizardConfirmation } from "/src/components/CippWizard/CippWizardConfirmation"; import CippWizardPage from "/src/components/CippWizard/CippWizardPage.jsx"; import { CippTenantStep } from "/src/components/CippWizard/CippTenantStep.jsx"; -import { useSettings } from "../../../../hooks/use-settings"; import { CippWizardAppApproval } from "../../../../components/CippWizard/CippWizardAppApproval"; import { Alert } from "@mui/material"; @@ -38,7 +37,7 @@ const Page = () => { return ( <> { customFunction: (row) => setIpAddress(row.RowKey), noConfirm: true, icon: , + hideBulk: true, + }, + { + label: "Add to Whitelist", + url: `/api/ExecAddTrustedIP`, + type: "POST", + data: { + IP: "RowKey", + State: "!Trusted", + }, + icon: , + confirmText: "Are you sure you want to add this IP to the whitelist?", + multiPost: false, + condition: (row) => row.state !== "Trusted", }, { label: "Remove from Whitelist", - customFunction: (row) => - addGeoIP.mutate({ - url: `/api/ExecAddTrustedIP?IP=${row.RowKey}&TenantFilter=${currentTenant}&State=NotTrusted`, - }), + url: `/api/ExecAddTrustedIP`, + type: "POST", + data: { + IP: "RowKey", + State: "!NotTrusted", + }, icon: , confirmText: "Are you sure you want to remove this IP from the whitelist?", + multiPost: false, + condition: (row) => row.state !== "NotTrusted", }, ]; @@ -44,13 +62,23 @@ const Page = () => { const handleAddToWhitelist = () => { addGeoIP.mutate({ - url: `/api/ExecAddTrustedIP?IP=${ip}&TenantFilter=${currentTenant}&State=Trusted`, + url: `/api/ExecAddTrustedIP`, + data: { + IP: ip, + State: "Trusted", + tenantFilter: currentTenant, + } }); }; const handleRemoveFromWhitelist = () => { addGeoIP.mutate({ - url: `/api/ExecAddTrustedIP?IP=${ip}&TenantFilter=${currentTenant}&State=NotTrusted`, + url: `/api/ExecAddTrustedIP`, + data: { + IP: ip, + State: "NotTrusted", + tenantFilter: currentTenant, + } }); }; @@ -63,13 +91,13 @@ const Page = () => { > - + - + { required /> - + + + + } + /> + setOpenCreate(false)}> + Create New Repository + + + { + createForm.setValue("type", e.target.value); + }} + > + } label="User" /> + } label="Org" /> + + + + + + + + + + + + + + + + + + setOpenSearch(false)}> + Add Community Repositories from GitHub + + + searchForm.setValue("searchType", e.target.value)} + > + } label="User" /> + } label="Org" /> + } label="Repository" /> + + + + setRepo(e.target.value)} + required={true} + /> + + + setUser(e.target.value)} + required={true} + /> + + + + + setOrg(e.target.value)} + required={true} + /> + + + + + + {searchMutation.isPending || + (searchMutation.isSuccess && ( + + + Search Results + + ))} + {searchMutation.isPending ? ( + <> + + + Searching... + + + + + + + + + + + + ) : ( + <> + {(searchMutation.isSuccess && results.length === 0) || + (searchMutation.isError && ( + + No search results found. Refine your query and try again. + + ))} + + {results.map((r) => ( + + + + + handleAdd(r.id)}> + + + + + window.open(r.html_url, "_blank")} + > + + + + + + + {r.full_name} + + + + + {r.html_url} + + + + + + ))} + + + )} + + + + + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/tools/community-repos/repo.js b/src/pages/tools/community-repos/repo.js new file mode 100644 index 000000000000..4fdb8ccd2849 --- /dev/null +++ b/src/pages/tools/community-repos/repo.js @@ -0,0 +1,250 @@ +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "/src/layouts"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useState, useEffect } from "react"; +import { ApiPostCall, ApiGetCall } from "/src/api/ApiCall"; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Skeleton, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import CippJSONView from "/src/components/CippFormPages/CippJSONView"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { CippAutoComplete } from "/src/components/CippComponents/CippAutocomplete"; +import React from "react"; +import { CloudDownload } from "@mui/icons-material"; + +const Page = () => { + const router = useRouter(); + const { name, branch } = router.query; + const [openJsonDialog, setOpenJsonDialog] = useState(false); + const [fileResults, setFileResults] = useState([]); + const [jsonContent, setJsonContent] = useState({}); + const [branches, setBranches] = useState([]); + const [selectedBranch, setSelectedBranch] = useState(branch); + const [selectedRepo, setSelectedRepo] = useState(name); + + const fileQuery = ApiPostCall({ + onResult: (resp) => { + let content = resp?.Results?.content?.trim() || "{}"; + // remove non-printable characters from the beginning and end + content = content.replace( + /^[\u0000-\u001F\u007F-\u009F]+|[\u0000-\u001F\u007F-\u009F]+$/g, + "" + ); + try { + setJsonContent(JSON.parse(content)); + } catch (e) { + console.error("Invalid JSON content:", e); + setJsonContent({}); + } + }, + }); + + const branchQuery = ApiGetCall({ + url: "/api/ExecGitHubAction", + data: { + Action: "GetBranches", + FullName: selectedRepo, + }, + onResult: (resp) => { + const branchList = resp?.Results || []; + setBranches(branchList); + if (branchList.length === 1) { + setSelectedBranch(branchList[0].name); + fetchFileTree(branchList[0].name); + } + const mainBranch = branchList.find((branch) => ["main", "master"].includes(branch.name)); + if (mainBranch) { + setSelectedBranch(mainBranch.name); + fetchFileTree(mainBranch.name); + } + }, + queryKey: `${selectedRepo}-branches`, + waiting: selectedRepo !== "", + }); + + const fileTreeQuery = ApiGetCall({ + url: "/api/ExecGitHubAction", + data: { + Action: "GetFileTree", + FullName: selectedRepo, + Branch: selectedBranch, + }, + onResult: (resp) => { + setFileResults(resp?.Results || []); + }, + queryKey: `${selectedRepo}-${selectedBranch}-filetree`, + waiting: selectedRepo !== "" && selectedBranch !== "", + }); + + const fetchFileTree = (branch) => { + if (selectedRepo !== "" && branch !== "") { + if (!fileTreeQuery.waiting) { + fileTreeQuery.waiting = true; + } + fileTreeQuery.refetch(); + } + }; + + const handleJsonView = (path) => { + fileQuery.mutate({ + url: "/api/ExecGitHubAction", + data: { + Action: "GetFileContents", + FullName: selectedRepo, + Path: path, + Branch: branch, + }, + }); + setOpenJsonDialog(true); + }; + + useEffect(() => { + if (selectedRepo) { + branchQuery.refetch(); + } + }, [selectedRepo]); + + useEffect(() => { + if (selectedBranch) { + fetchFileTree(selectedBranch); + } + }, [selectedBranch]); + + const updateQueryParams = (prop, newValue) => { + const query = { ...router.query }; + if (query[prop] !== newValue) { + query[prop] = newValue; + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + } + }; + + const MemoizedCippAutoComplete = React.memo((props) => { + return ; + }); + + return ( + <> + + + { + if (newValue.value === selectedRepo) return; + setSelectedRepo(newValue.value); + updateQueryParams(newValue.value); + }} + api={{ + url: "/api/ListCommunityRepos", + queryKey: "CommunityRepos", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }} + multiple={false} + label="Select Repository" + placeholder="Select Repository" + disableClearable + /> + + + { + if (newValue.value === selectedBranch) return; + setSelectedBranch(newValue.value); + updateQueryParams("branch", newValue.value); + }} + options={branches.map((branch) => ({ label: branch.name, value: branch.name }))} + multiple={false} + label="Select Branch" + placeholder="Select Branch" + disableClearable + isFetching={branchQuery.isPending} + /> + + + } + data={fileResults} + apiDataKey="Results" + queryKey="JsonTemplates" + simpleColumns={["path", "html_url"]} + actions={[ + { + label: "View Template", + customFunction: (row) => handleJsonView(row.path), + noConfirm: true, + icon: , + hideBulk: true, + }, + { + label: "Import Template", + url: "/api/ExecCommunityRepo", + icon: , + type: "POST", + data: { + Action: "ImportTemplate", + FullName: selectedRepo, + Path: "path", + Branch: selectedBranch, + }, + confirmText: "Are you sure you want to import [path]?", + }, + ]} + isFetching={fileTreeQuery.isFetching} + refreshFunction={() => fetchFileTree(selectedBranch)} + /> + setOpenJsonDialog(false)} + > + Template Details + + {fileQuery.isPending ? ( + + + + ) : ( + + )} + + + + + + + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/tools/templatelib/index.jsx b/src/pages/tools/templatelib/index.jsx index 18349fab9b8d..9b2fabb67bba 100644 --- a/src/pages/tools/templatelib/index.jsx +++ b/src/pages/tools/templatelib/index.jsx @@ -1,11 +1,13 @@ -import React from "react"; -import { Grid, Divider, Typography, CircularProgress, Alert } from "@mui/material"; -import { useForm } from "react-hook-form"; +import { useEffect } from "react"; +import { Divider, Typography, Alert, Chip } from "@mui/material"; +import { useForm, useWatch } from "react-hook-form"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import CippFormPage from "/src/components/CippFormPages/CippFormPage"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { useSettings } from "/src/hooks/use-settings"; import { CippFormTenantSelector } from "../../../components/CippComponents/CippFormTenantSelector"; +import { Grid } from "@mui/system"; +import { CippFormCondition } from "../../../components/CippComponents/CippFormCondition"; const TemplateLibrary = () => { const currentTenant = useSettings().currentTenant; @@ -20,81 +22,204 @@ const TemplateLibrary = () => { }, }); + const templateRepo = useWatch({ control: formControl.control, name: "templateRepo" }); + const customDataFormatter = (values) => { const startDate = new Date(); startDate.setHours(0, 0, 0, 0); const unixTime = Math.floor(startDate.getTime() / 1000) - 45; return { - TenantFilter: values.tenantFilter.value, - Name: `CIPP Template ${values.tenantFilter.value}`, + TenantFilter: values?.tenantFilter?.value ? values?.tenantFilter?.value : "No tenant", + Name: `CIPP Template ${ + values.tenantFilter?.value ? values.tenantFilter?.value : values.templateRepo?.value + }`, Command: { value: `New-CIPPTemplateRun` }, Parameters: { TemplateSettings: { ...values } }, ScheduledTime: unixTime, - Recurrence: { value: "4h" }, + Recurrence: { value: values.tenantFilter?.value ? "4h" : "7d" }, }; }; + useEffect(() => { + if (templateRepo?.value) { + formControl.setValue("templateRepoBranch", { + label: templateRepo.addedFields.branch, + value: templateRepo.addedFields.branch, + }); + } + }, [templateRepo?.value]); + return ( - - + + Template libraries are tenants set up to retrieve the latest version of a specific tenants policies. These are then stored in CIPPs templates, allowing you to keep an up to date copy of the policies.This copy occurs every 4 hours. + + There are also template repositories, these are community driven and are used to share + templates with other users. Template repositories are downloaded when new versions are + released. + Enabling this feature will overwrite templates with the same name. - - - - + + + + + + + + + + `${option.Name} (${option.URL})`, + addedField: { + branch: "DefaultBranch", + }, + }} + formControl={formControl} + multiple={false} + /> + + + {templateRepo?.value && ( + + + Repository Branch + + + + )} + + + + Conditional Access + + + - - - - Conditional Access - - - - - Intune - - - - + + + Intune + + + + + + + + + + Template Repository files + + + + + + + ); diff --git a/src/pages/tools/tenantbreachlookup/index.js b/src/pages/tools/tenantbreachlookup/index.js index 37df8d84d648..50b351e0e091 100644 --- a/src/pages/tools/tenantbreachlookup/index.js +++ b/src/pages/tools/tenantbreachlookup/index.js @@ -1,20 +1,16 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { ExclamationTriangleIcon, EyeIcon } from "@heroicons/react/24/outline"; -import { ApiGetCall } from "../../../api/ApiCall"; -import { Button, CircularProgress, SvgIcon } from "@mui/material"; -import { useSettings } from "../../../hooks/use-settings"; +import { EyeIcon } from "@heroicons/react/24/outline"; +import { Button } from "@mui/material"; +import { Search } from "@mui/icons-material"; +import { BreachSearchDialog } from "../../../components/CippComponents/BreachSearchDialog"; +import { useDialog } from "../../../hooks/use-dialog"; const Page = () => { - const tenantFilter = useSettings()?.currentTenant; - const ApiCall = ApiGetCall({ - url: "/api/ExecBreachSearch", - data: { tenantFilter: tenantFilter }, - waiting: false, - }); - const pageTitle = "Potential Breached passwords and information"; const apiUrl = "/api/ListBreachesTenant"; + const breachSearchDialog = useDialog(); + const actions = [ { label: "View User", @@ -26,31 +22,23 @@ const Page = () => { ]; return ( - - - - } - /> + <> + + + + } + /> + + ); }; diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index 47c64308f980..5a1d385a4c4c 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -1,51 +1,80 @@ -import { Box, Container, Grid, Stack } from "@mui/material"; +import { Box, Container, Stack } from "@mui/material"; +import { Grid } from "@mui/system"; import Head from "next/head"; import { CippImageCard } from "../components/CippCards/CippImageCard"; -import { Layout as DashboardLayout } from "../layouts/index.js"; import { ApiGetCall } from "../api/ApiCall"; +import { useMemo } from "react"; const Page = () => { const orgData = ApiGetCall({ + url: "/api/me", + queryKey: "authmecipp", + }); + + const swaStatus = ApiGetCall({ url: "/.auth/me", - queryKey: "me", + queryKey: "authmeswa", + staleTime: 120000, + refetchOnWindowFocus: true, }); + + const blockedRoles = ["anonymous", "authenticated"]; + // Use useMemo to derive userRoles directly + const userRoles = useMemo(() => { + if (orgData.isSuccess && orgData.data?.clientPrincipal?.userRoles) { + return orgData.data.clientPrincipal.userRoles.filter( + (role) => !blockedRoles.includes(role) + ); + } + return []; + }, [orgData.isSuccess, orgData.data?.clientPrincipal?.userRoles]); return ( <> - - - 401 - Access Denied - - - - - - + + 401 - Access Denied + + + + + + + {(orgData.isSuccess || swaStatus.isSuccess) && Array.isArray(userRoles) && ( 0 + ? "Return to Home" + : "Login" + } + link={ + swaStatus?.data?.clientPrincipal !== null && userRoles.length > 0 + ? "/" + : `/.auth/login/aad?post_login_redirect_uri=${encodeURIComponent( + window.location.href + )}` + } /> - + )} - - - - + + + + ); }; diff --git a/src/sections/dashboard/account/account-2fa.js b/src/sections/dashboard/account/account-2fa.js index 8b1d4c782fa2..323619fdc532 100644 --- a/src/sections/dashboard/account/account-2fa.js +++ b/src/sections/dashboard/account/account-2fa.js @@ -1,7 +1,7 @@ import { useCallback } from "react"; import toast from "react-hot-toast"; import { Button, Card, CardContent, Typography } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; export const Account2FA = () => { const handleActivate = useCallback(() => { @@ -12,13 +12,13 @@ export const Account2FA = () => { - + Two-factor authentication (2FA) Enhanced security for your mention account - + diff --git a/src/sections/dashboard/account/account-details.js b/src/sections/dashboard/account/account-details.js index c558ee2237da..a41505d1002e 100644 --- a/src/sections/dashboard/account/account-details.js +++ b/src/sections/dashboard/account/account-details.js @@ -13,7 +13,7 @@ import { TextField, Typography, } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; import { useMockedUser } from "../../../hooks/use-mocked-user"; const companySizeOptions = ["1-10", "11-30", "31-50", "50+"]; @@ -59,10 +59,10 @@ export const AccountDetails = (props) => { - + Settings - + { - + Change password - + { {orientation === "vertical" ? ( - + - + {content} diff --git a/src/sections/dashboard/components/stats/stats-1.js b/src/sections/dashboard/components/stats/stats-1.js index cc167847160e..c0ffad59471b 100644 --- a/src/sections/dashboard/components/stats/stats-1.js +++ b/src/sections/dashboard/components/stats/stats-1.js @@ -13,7 +13,7 @@ import { SvgIcon, Typography, } from "@mui/material"; -import Grid from "@mui/material/Grid2"; +import { Grid } from "@mui/system"; const data = [ { icon: ( @@ -52,7 +52,7 @@ export const Stats1 = () => ( {data.map((item) => { return ( - + ( > {data.map((item) => ( { spacing={2} > { ( {stats.map((item) => ( ({ diff --git a/src/utils/get-cipp-filter-variant.js b/src/utils/get-cipp-filter-variant.js index e8fc3ddd5acc..541bc2db263c 100644 --- a/src/utils/get-cipp-filter-variant.js +++ b/src/utils/get-cipp-filter-variant.js @@ -23,7 +23,7 @@ export const getCippFilterVariant = (providedColumnKeys) => { if (timeAgoArray.includes(providedColumnKeys) || matchDateTime.test(providedColumnKeys)) { return { filterVariant: "datetime-range", - sortingFn: "datetime", + sortingFn: "dateTimeNullsLast", filterFn: "betweenInclusive", }; } @@ -32,6 +32,8 @@ export const getCippFilterVariant = (providedColumnKeys) => { case "assignedLicenses": return { filterVariant: "multi-select", + sortingFn: "alphanumeric", + filterFn: "arrIncludesSome", }; case "accountEnabled": return { diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 73c65b8dbdad..2b3a9bb7f4e7 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -6,6 +6,7 @@ import { MailOutline, Shield, Description, + GroupOutlined, } from "@mui/icons-material"; import { Chip, Link, SvgIcon } from "@mui/material"; import { Box } from "@mui/system"; @@ -17,11 +18,19 @@ import { CippLocationDialog } from "../components/CippComponents/CippLocationDia import { isoDuration, en } from "@musement/iso-duration"; import { CippTimeAgo } from "../components/CippComponents/CippTimeAgo"; import { getCippRoleTranslation } from "./get-cipp-role-translation"; -import { CogIcon, ServerIcon, UserIcon, UsersIcon } from "@heroicons/react/24/outline"; +import { + BuildingOfficeIcon, + CogIcon, + ServerIcon, + UserIcon, + UsersIcon, +} from "@heroicons/react/24/outline"; import { getCippTranslation } from "./get-cipp-translation"; +import DOMPurify from "dompurify"; import { getSignInErrorCodeTranslation } from "./get-cipp-signin-errorcode-translation"; +import { CollapsibleChipList } from "../components/CippComponents/CollapsibleChipList"; -export const getCippFormatting = (data, cellName, type, canReceive) => { +export const getCippFormatting = (data, cellName, type, canReceive, flatten = true) => { const isText = type === "text"; const cellNameLower = cellName.toLowerCase(); // if data is a data object, return a fFormatted date @@ -47,6 +56,52 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { portal_sharepoint: Description, }; + // Create a helper function to render chips with CollapsibleChipList + const renderChipList = (items, maxItems = 4) => { + if (!Array.isArray(items) || items.length === 0) { + return ; + } + + return ( + + {items.map((item, index) => { + // Avoid JSON.stringify which can cause circular reference errors + let key = index; + if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") { + key = item; + } else if (typeof item === "object" && item?.label) { + key = `item-${item.label}-${index}`; + } + + return ( + + ); + })} + + ); + }; + + if (cellName === "baselineOption") { + return "Download Baseline"; + } + + if (cellName === "Severity" || cellName === "logsToInclude") { + if (Array.isArray(data)) { + return isText ? data.join(", ") : renderChipList(data); + } else { + return isText ? ( + data + ) : ( + + ); + } + } + //if the cellName starts with portal_, return text, or a link with an icon if (cellName.startsWith("portal_")) { const IconComponent = portalIcons[cellName]; @@ -112,6 +167,7 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { "LastOccurrence", "NotBefore", "NotAfter", + "latestDataCollection", ]; const matchDateTime = /[dD]ate[tT]ime/; @@ -131,6 +187,40 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { return isText ? "Password hidden" : ; } + // Handle hardware hash fields + const hardwareHashFields = ["hardwareHash", "Hardware Hash"]; + if (hardwareHashFields.includes(cellName) || cellNameLower.includes("hardware")) { + if (typeof data === "string" && data.length > 15) { + return isText ? data : `${data.substring(0, 15)}...`; + } + return isText ? data : data; + } + + // Handle log message field + const messageFields = ["Message"]; + if (messageFields.includes(cellName)) { + if (typeof data === "string" && data.length > 120) { + return isText ? data : `${data.substring(0, 120)}...`; + } + return isText ? data : data; + } + + if (cellName === "alignmentScore" || cellName === "combinedAlignmentScore") { + // Handle alignment score, return a percentage with a label + return isText ? ( + `${data}%` + ) : ( + + ); + } + + if (cellName === "LicenseMissingPercentage") { + return isText ? ( + `${data}%` + ) : ( + + ); + } if (cellName === "RepeatsEvery") { //convert 1d to "Every 1 day", 1w to "Every 1 week" etc. const match = data.match(/(\d+)([a-zA-Z]+)/); @@ -197,36 +287,133 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { return isText ? data : ; } + if (cellName === "delegatedPrivilegeStatus") { + return data === "directTenant" ? "Direct Tenant" : "GDAP Tenant"; + } + //if the cellName is tenantFilter, return a chip with the tenant name. This can sometimes be an array, sometimes be a single item. - if (cellName === "tenantFilter" || cellName === "Tenant") { + if ( + cellName === "tenantFilter" || + cellName === "Tenant" || + cellName === "Tenants" || + cellName === "AllowedTenants" || + cellName === "BlockedTenants" + ) { //check if data is an array. if (Array.isArray(data)) { return isText ? data.join(", ") - : data.map((item) => ( - { + const itemText = item?.label !== undefined ? item.label : item; + let icon = null; + + if (item?.type === "Group") { + icon = ( + + + + ); + } else { + icon = ( + + + + ); + } + + return { + label: itemText, + icon: icon, + key: key, + }; + }) + ); + } else { + const itemText = data?.label !== undefined ? data.label : data; + let icon = null; + + if (data?.type === "Group") { + icon = ( + + + + ); + } else { + icon = ( + + + + ); + } + return isText ? itemText : ; + } + } + + if (cellName === "PostExecution") { + const values = data ? data?.split(",").map((item) => item.trim()) : []; + if (values.length > 0) { + return isText + ? data + : values.map((value, index) => ( + )); - } else { - return isText ? ( - data - ) : ( - - ); } } + if (cellName === "standardType") { + return isText ? ( + data + ) : ( + + ); + } + + if (cellName === "type" && data === "drift") { + return isText ? ( + "Drift Standard" + ) : ( + + ); + } + + if (cellName === "ClientId" || cellName === "role") { + return isText ? data : ; + } if (cellName === "excludedTenants") { + // Handle null or undefined data + if (data === null || data === undefined) { + return isText ? ( + "No data" + ) : ( + + + + ); + } //check if data is an array. if (Array.isArray(data)) { return isText - ? data.join(", ") - : data.map((item) => ( - - )); + ? data + .map((item) => (typeof item === "object" && item?.label ? item.label : item)) + .join(", ") + : renderChipList( + data + .filter((item) => item) + .map((item) => (typeof item === "object" && item?.label ? item.label : item)) + ); } } if (cellName === "bulkUser") { @@ -266,6 +453,29 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { return isText ? data : ; } + if (cellName === "Parameters.ScheduledBackupValues") { + return isText ? ( + JSON.stringify(data) + ) : ( + { + return { key, value: data[key] }; + })} + tableTitle={getCippTranslation(cellName)} + /> + ); + } + + if (cellName === "AccessRights") { + // Handle data as an array or string + const accessRights = Array.isArray(data) + ? data.flatMap((item) => (typeof item === "string" ? item.split(", ") : [])) + : typeof data === "string" + ? data.split(", ") + : []; + return isText ? accessRights.join(", ") : renderChipList(accessRights); + } + // Handle null or undefined data if (data === null || data === undefined) { return isText ? ( @@ -289,27 +499,74 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { if (cellName === "From") { // if data is array if (Array.isArray(data)) { - return isText ? data.join(", ") : data.join(", "); + return isText ? data.join(", ") : renderChipList(data); } else { // split on ; , and create chips per email const emails = data.split(/;|,/); - return isText - ? emails.join(", ") - : emails.map((email) => ); + return isText ? emails.join(", ") : renderChipList(emails); } } // Handle proxyAddresses if (cellName === "proxyAddresses") { + if (!Array.isArray(data)) { + data = [data]; + } + + if (data.length === 0) { + return isText ? ( + "No data" + ) : ( + + ); + } + + const primaryEmail = data.find((email) => email.startsWith("SMTP:")); const emails = data.map((email) => email.replace(/smtp:/i, "")); - return isText - ? emails.join(", ") - : emails.map((email) => ); + return isText ? ( + emails.join(", ") + ) : ( + { + if (primaryEmail && primaryEmail.includes(email)) { + return { + email: email, + primary: true, + }; + } else { + return { + email: email, + primary: false, + }; + } + })} + /> + ); } // Handle assigned licenses if (cellName === "assignedLicenses") { - return isText ? getCippLicenseTranslation(data) : getCippLicenseTranslation(data); + var translatedLicenses = getCippLicenseTranslation(data); + return isText + ? Array.isArray(translatedLicenses) + ? translatedLicenses.join(", ") + : translatedLicenses + : Array.isArray(translatedLicenses) + ? renderChipList(translatedLicenses) + : translatedLicenses; + } + + if (cellName === "unifiedRoles") { + if (Array.isArray(data)) { + const roles = data.map((role) => getCippRoleTranslation(role.roleDefinitionId)); + return isText ? roles.join(", ") : renderChipList(roles, 12); + } + return isText ? ( + "No roles" + ) : ( + + ); } // Handle roleDefinitionId @@ -337,12 +594,53 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { // if data is a json string, parse it and return a table if (typeof data === "string" && (data.startsWith("{") || data.startsWith("["))) { try { + const parsedData = JSON.parse(data); + + // parsedData is an array and only contains one element + if ( + Array.isArray(parsedData) && + parsedData.length === 1 && + typeof parsedData[0] !== "object" + ) { + // Handle boolean values + if (typeof parsedData[0] === "boolean") { + return isText ? ( + parsedData[0] ? ( + "Yes" + ) : ( + "No" + ) + ) : parsedData[0] ? ( + + ) : ( + + ); + } + + return isText ? ( + JSON.stringify(parsedData[0]) + ) : ( + + ); + } + + // Check if parsed data is a simple array of strings + if ( + Array.isArray(parsedData) && + parsedData.every((item) => typeof item === "string" || typeof item === "number") && + flatten + ) { + return isText ? parsedData.join(", ") : renderChipList(parsedData); + } return isText ? ( data ) : ( - + ); - } catch (e) {} + } catch (e) { + // If parsing fails, return the original string + return isText ? data : {data}; + } } if (cellName === "key") { @@ -397,7 +695,31 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { ); } - const durationArray = ["autoExtendDuration"]; + // handle htmlDescription + if (cellName === "htmlDescription") { + return isText ? ( + data + ) : ( + + ); + } + + // ISO 8601 Duration Formatting + // Add property names here to automatically format ISO 8601 duration strings (e.g., "PT1H23M30S") + // into human-readable format (e.g., "1 hour 23 minutes 30 seconds") across all CIPP tables. + // This works for any API response property that contains ISO 8601 duration format. + const durationArray = [ + "autoExtendDuration", // GDAP page (/tenant/gdap-management/relationships) + "deploymentDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + "deploymentTotalDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + "deviceSetupDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + "accountSetupDuration", // AutoPilot deployments (/endpoint/reports/autopilot-deployment) + ]; if (durationArray.includes(cellName)) { isoDuration.setLocales( { @@ -425,20 +747,71 @@ export const getCippFormatting = (data, cellName, type, canReceive) => { ); } + if (cellName === "Visibility") { + const gitHubVisibility = ["public", "private", "internal"]; + if (gitHubVisibility.includes(data)) { + return isText ? ( + data + ) : ( + + ); + } + } + if (cellName === "AutoMapUrl") { return isText ? data : ; } + // handle autocomplete labels + if (data?.label && data?.value) { + return isText ? data.label : ; + } + + // handle array of autocomplete labels + if (Array.isArray(data) && data.length > 0 && data[0]?.label && data[0]?.value) { + return isText + ? data.map((item) => item.label).join(", ") + : renderChipList( + data.map((item) => { + return { + label: item.label, + }; + }) + ); + } + // Handle arrays of strings - if (Array.isArray(data) && data.every((item) => typeof item === "string")) { + if (Array.isArray(data) && data.every((item) => typeof item === "string") && flatten) { + // if string matches json format, parse it + if (data.every((item) => item.startsWith("{") || item.startsWith("["))) { + try { + const parsedData = data.map((item) => JSON.parse(item)); + // Check if parsedData contains simple strings + if (parsedData.every((item) => typeof item === "string")) { + return isText ? parsedData.join(", ") : renderChipList(parsedData); + } + return isText ? ( + JSON.stringify(data) + ) : ( + + ); + } catch (e) { + return isText ? JSON.stringify(data) : data.join(", "); + } + } + //if the array is empty, return "No data" - return isText - ? data.join(", ") - : data.map((item) => ); + return isText ? data.join(", ") : renderChipList(data); } // Handle objects - if (typeof data === "object" && data !== null) { + if (typeof data === "object" && data !== null && flatten) { return isText ? ( JSON.stringify(data) ) : ( diff --git a/src/utils/get-cipp-license-translation.js b/src/utils/get-cipp-license-translation.js index 813e52a5d1e9..9e36c5cc7db1 100644 --- a/src/utils/get-cipp-license-translation.js +++ b/src/utils/get-cipp-license-translation.js @@ -3,6 +3,14 @@ import M365Licenses from "../data/M365Licenses.json"; export const getCippLicenseTranslation = (licenseArray) => { let licenses = []; + if (!Array.isArray(licenseArray) && typeof licenseArray === "object") { + licenseArray = [licenseArray]; + } + + if (!licenseArray || licenseArray.length === 0) { + return ["No Licenses Assigned"]; + } + licenseArray?.forEach((licenseAssignment) => { let found = false; for (let x = 0; x < M365Licenses.length; x++) { @@ -21,5 +29,8 @@ export const getCippLicenseTranslation = (licenseArray) => { } }); - return licenses.join(", "); + if (!licenses || licenses.length === 0) { + return ["No Licenses Assigned"]; + } + return licenses; }; diff --git a/src/utils/get-cipp-translation.js b/src/utils/get-cipp-translation.js index ff3dd0521405..d50e834054ad 100644 --- a/src/utils/get-cipp-translation.js +++ b/src/utils/get-cipp-translation.js @@ -5,6 +5,15 @@ export const getCippTranslation = (field) => { return "No data"; } + // special translations for extensions + if (field.startsWith("extension_")) { + field = field.split("_").pop(); + } + // special translation for schema extensions + if (field.startsWith("ext") && field.includes("_")) { + field = field.split("_").pop(); + } + return ( CippTranslations[field] || field diff --git a/src/utils/get-cipp-validator.js b/src/utils/get-cipp-validator.js index 334bbe7dce49..867113f8110d 100644 --- a/src/utils/get-cipp-validator.js +++ b/src/utils/get-cipp-validator.js @@ -12,18 +12,40 @@ export const getCippValidator = (value, type) => { return "This is invalid JSON"; } case "email": - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || "This is not a valid email"; + return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(value) || "This is not a valid email"; case "url": return /^(http|https):\/\/[^ "]+$/.test(value) || "This is not a valid URL"; case "string": return typeof value === "string" || "This is not a valid string"; case "ip": return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value) || "This is not a valid IP address"; + case "ipv4cidr": + return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}\/([0-9]|[12][0-9]|3[0-2])$/.test(value) || "This is not a valid IPv4 CIDR"; + case "ipv6": + return /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/.test(value) || "This is not a valid IPv6 address"; + case "ipv6cidr": + return /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/.test(value) || "This is not a valid IPv6 CIDR"; + case "hostname": + return /^((\*|\~)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*|\~)?$/.test(value) || "This is not a valid hostname"; + case "sha256": + return /^[A-Fa-f0-9]{64}$/.test(value) || "This is not a valid SHA256 hash"; case "guid": return ( /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) || "This is not a valid GUID" ); + case "domain": + return /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/.test(value) || "This is not a valid domain"; + case "wildcardDomain": + return /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.\*)?$/.test(value) || /^(\*)?[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\*)?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/.test(value) || "This is not a valid domain pattern"; + case "wildcardUrl": + return /^(https?:\/\/)?(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.\*)?([\/\?\*][a-zA-Z0-9\-\.\~\*\/\?=&%]*)?$/.test(value) || "This is not a valid URL pattern"; + case "senderEntry": + return ( + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/.test(value) || + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value) || + "This is not a valid domain or email address" + ); default: return true; } diff --git a/src/utils/permissions.js b/src/utils/permissions.js new file mode 100644 index 000000000000..df66872101d4 --- /dev/null +++ b/src/utils/permissions.js @@ -0,0 +1,227 @@ +import Button from "@mui/material/Button"; +import { usePermissions } from "/src/hooks/use-permissions.js"; +/** + * Permission Helper Utilities + * + * This module provides utilities for checking user permissions with pattern matching support. + * It uses the same logic as the navigation system to ensure consistency across the application. + */ + +/** + * Check if user has permission using pattern matching + * @param {string[]} userPermissions - Array of user permissions + * @param {string[]} requiredPermissions - Array of required permissions (can include wildcards) + * @returns {boolean} - True if user has at least one of the required permissions + */ +export const hasPermission = (userPermissions, requiredPermissions) => { + if (!userPermissions || !requiredPermissions) { + return false; + } + + if (!Array.isArray(userPermissions) || !Array.isArray(requiredPermissions)) { + return false; + } + + if (requiredPermissions.length === 0) { + return true; // No permissions required + } + + return userPermissions.some((userPerm) => { + return requiredPermissions.some((requiredPerm) => { + // Exact match + if (userPerm === requiredPerm) { + return true; + } + + // Pattern matching - check if required permission contains wildcards + if (requiredPerm.includes("*")) { + // Convert wildcard pattern to regex + const regexPattern = requiredPerm + .replace(/\./g, "\\.") // Escape dots + .replace(/\*/g, ".*"); // Convert * to .* + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(userPerm); + } + + return false; + }); + }); +}; + +/** + * Check if user has any of the required roles + * @param {string[]} userRoles - Array of user roles + * @param {string[]} requiredRoles - Array of required roles + * @returns {boolean} - True if user has at least one of the required roles + */ +export const hasRole = (userRoles, requiredRoles) => { + if (!userRoles || !requiredRoles) { + return false; + } + + if (!Array.isArray(userRoles) || !Array.isArray(requiredRoles)) { + return false; + } + + if (requiredRoles.length === 0) { + return true; // No roles required + } + + return requiredRoles.some((requiredRole) => userRoles.includes(requiredRole)); +}; + +/** + * Check if user has access based on both permissions and roles + * @param {Object} config - Configuration object + * @param {string[]} config.userPermissions - Array of user permissions + * @param {string[]} config.userRoles - Array of user roles + * @param {string[]} config.requiredPermissions - Array of required permissions (can include wildcards) + * @param {string[]} config.requiredRoles - Array of required roles + * @returns {boolean} - True if user has access + */ +export const hasAccess = ({ + userPermissions, + userRoles, + requiredPermissions = [], + requiredRoles = [], +}) => { + // Check roles first (if any are required) + if (requiredRoles.length > 0) { + const hasRequiredRole = hasRole(userRoles, requiredRoles); + if (!hasRequiredRole) { + return false; + } + } + + // Check permissions (if any are required) + if (requiredPermissions.length > 0) { + const hasRequiredPermission = hasPermission(userPermissions, requiredPermissions); + if (!hasRequiredPermission) { + return false; + } + } + + return true; +}; + +/** + * Hook for checking permissions in React components + * @param {string[]} requiredPermissions - Array of required permissions (can include wildcards) + * @param {string[]} requiredRoles - Array of required roles + * @returns {boolean} - True if user has access + */ +export const useHasAccess = (requiredPermissions = [], requiredRoles = []) => { + // This would typically use a context or hook to get current user permissions + // For now, we'll return a function that can be called with user data + return (userPermissions, userRoles) => { + return hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + }; +}; + +/** + * Higher-order component to conditionally render based on permissions + * @param {Object} config - Configuration object + * @param {React.Component} config.component - Component to render if user has access + * @param {string[]} config.requiredPermissions - Array of required permissions + * @param {string[]} config.requiredRoles - Array of required roles + * @param {React.Component} config.fallback - Component to render if user doesn't have access + * @returns {React.Component} - Conditional component + */ +export const withPermissions = ({ + component: Component, + requiredPermissions = [], + requiredRoles = [], + fallback = null, +}) => { + return (props) => { + const { userPermissions, userRoles, ...restProps } = props; + + const hasRequiredAccess = hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + + if (hasRequiredAccess) { + return ; + } + + return fallback; + }; +}; + +/** + * Permission-aware Button component + * @param {Object} props - Button props + * @param {string[]} props.requiredPermissions - Array of required permissions + * @param {string[]} props.requiredRoles - Array of required roles + * @param {boolean} props.hideIfNoAccess - Hide button if user doesn't have access (default: false) + * @returns {React.Component} - Permission-aware button + */ +export const PermissionButton = ({ + requiredPermissions = [], + requiredRoles = [], + hideIfNoAccess = false, + children, + ...buttonProps +}) => { + const { userPermissions, userRoles, isAuthenticated } = usePermissions(); + + const hasRequiredAccess = + isAuthenticated && + hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + + if (!hasRequiredAccess && hideIfNoAccess) { + return null; + } + + return ( + + ); +}; + +/** + * Permission-aware conditional rendering component + * @param {Object} props - Component props + * @param {string[]} props.requiredPermissions - Array of required permissions + * @param {string[]} props.requiredRoles - Array of required roles + * @param {React.ReactNode} props.children - Content to render if user has access + * @param {React.ReactNode} props.fallback - Content to render if user doesn't have access + * @returns {React.Component} - Conditional component + */ +export const PermissionCheck = ({ + requiredPermissions = [], + requiredRoles = [], + children, + fallback = null, +}) => { + const { userPermissions, userRoles, isAuthenticated } = usePermissions(); + + const hasRequiredAccess = + isAuthenticated && + hasAccess({ + userPermissions, + userRoles, + requiredPermissions, + requiredRoles, + }); + + if (hasRequiredAccess) { + return children; + } + + return fallback; +}; diff --git a/staticwebapp.config.json b/staticwebapp.config.json index e82c3b401e9e..1f57342751ca 100644 --- a/staticwebapp.config.json +++ b/staticwebapp.config.json @@ -35,17 +35,9 @@ "redirect": "/.auth/logout?post_logout_redirect_uri=/LogoutRedirect" }, { - "route": "/api/ExecSAMSetup", + "route": "/authredirect", "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] }, - { - "route": "/api/AddStandardTemplate", - "allowedRoles": ["admin"] - }, - { - "route": "/api/RemoveStandardTemplate", - "allowedRoles": ["admin"] - }, { "route": "/LogoutRedirect", "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] @@ -54,53 +46,13 @@ "route": "/404", "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] }, - { - "route": "/api/RemoveStandard", - "allowedRoles": ["admin"] - }, - { - "route": "/api/add*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/api/edit*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/api/ExecSendPush", - "allowedRoles": ["admin", "editor", "readonly"] - }, - { - "route": "/api/ExecExcludeTenant", - "allowedRoles": ["admin"] - }, - { - "route": "/api/Exec*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/api/Remove*", - "allowedRoles": ["admin", "editor"] - }, - { - "route": "/cipp/*", - "allowedRoles": ["admin"] - }, - { - "route": "/tenant/standards/*", - "allowedRoles": ["admin"] - }, - { - "route": "/", - "allowedRoles": ["admin", "editor", "readonly", "reader"] - }, { "route": "/api/Public*", - "allowedRoles": ["admin", "editor", "readonly", "reader", "authenticated", "anonymous"] + "allowedRoles": ["admin", "editor", "readonly", "authenticated", "anonymous"] }, { "route": "*", - "allowedRoles": ["admin", "editor", "readonly", "reader"] + "allowedRoles": ["admin", "editor", "readonly", "authenticated"] } ], "navigationFallback": { diff --git a/yarn.lock b/yarn.lock index acea4f80af6a..7d117910a410 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,102 +10,96 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" - integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== dependencies: - "@babel/highlight" "^7.24.7" + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.2", "@babel/compat-data@^7.25.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.4.tgz#7d2a80ce229890edcf4cc259d4d696cb4dae2fcb" - integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.26.8": + version "7.26.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.8.tgz#821c1d35641c355284d4a870b8a4a7b0c141e367" + integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== -"@babel/core@^7.19.6": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" - integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== +"@babel/core@^7.21.3": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9" + integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.25.0" - "@babel/helper-compilation-targets" "^7.25.2" - "@babel/helper-module-transforms" "^7.25.2" - "@babel/helpers" "^7.25.0" - "@babel/parser" "^7.25.0" - "@babel/template" "^7.25.0" - "@babel/traverse" "^7.25.2" - "@babel/types" "^7.25.2" + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.10" + "@babel/helper-compilation-targets" "^7.26.5" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helpers" "^7.26.10" + "@babel/parser" "^7.26.10" + "@babel/template" "^7.26.9" + "@babel/traverse" "^7.26.10" + "@babel/types" "^7.26.10" 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.25.0", "@babel/generator@^7.25.6": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.6.tgz#0df1ad8cb32fe4d2b01d8bf437f153d19342a87c" - integrity sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw== +"@babel/generator@^7.26.10", "@babel/generator@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.0.tgz#764382b5392e5b9aff93cadb190d0745866cbc2c" + integrity sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw== dependencies: - "@babel/types" "^7.25.6" + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" - integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== - dependencies: - "@babel/types" "^7.24.7" + jsesc "^3.0.2" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" - integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" + "@babel/types" "^7.25.9" -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8", "@babel/helper-compilation-targets@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" - integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9", "@babel/helper-compilation-targets@^7.26.5": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz#de0c753b1cd1d9ab55d473c5a5cf7170f0a81880" + integrity sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA== dependencies: - "@babel/compat-data" "^7.25.2" - "@babel/helper-validator-option" "^7.24.8" - browserslist "^4.23.1" + "@babel/compat-data" "^7.26.8" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.24.7", "@babel/helper-create-class-features-plugin@^7.25.0", "@babel/helper-create-class-features-plugin@^7.25.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz#57eaf1af38be4224a9d9dd01ddde05b741f50e14" - integrity sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-member-expression-to-functions" "^7.24.8" - "@babel/helper-optimise-call-expression" "^7.24.7" - "@babel/helper-replace-supers" "^7.25.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/traverse" "^7.25.4" +"@babel/helper-create-class-features-plugin@^7.25.9", "@babel/helper-create-class-features-plugin@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz#518fad6a307c6a96f44af14912b2c20abe9bfc30" + integrity sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.26.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.27.0" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7", "@babel/helper-create-regexp-features-plugin@^7.25.0", "@babel/helper-create-regexp-features-plugin@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz#24c75974ed74183797ffd5f134169316cd1808d9" - integrity sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz#0e41f7d38c2ebe06ebd9cf0e02fb26019c77cd95" + integrity sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - regexpu-core "^5.3.1" + "@babel/helper-annotate-as-pure" "^7.25.9" + regexpu-core "^6.2.0" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" - integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== +"@babel/helper-define-polyfill-provider@^0.6.3", "@babel/helper-define-polyfill-provider@^0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz#15e8746368bfa671785f5926ff74b3064c291fab" + integrity sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -113,843 +107,699 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-member-expression-to-functions@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" - integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== - dependencies: - "@babel/traverse" "^7.24.8" - "@babel/types" "^7.24.8" - -"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" - integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.25.0", "@babel/helper-module-transforms@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" - integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== - dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-simple-access" "^7.24.7" - "@babel/helper-validator-identifier" "^7.24.7" - "@babel/traverse" "^7.25.2" - -"@babel/helper-optimise-call-expression@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" - integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" - integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== - -"@babel/helper-remap-async-to-generator@^7.24.7", "@babel/helper-remap-async-to-generator@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz#d2f0fbba059a42d68e5e378feaf181ef6055365e" - integrity sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-wrap-function" "^7.25.0" - "@babel/traverse" "^7.25.0" - -"@babel/helper-replace-supers@^7.24.7", "@babel/helper-replace-supers@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9" - integrity sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.24.8" - "@babel/helper-optimise-call-expression" "^7.24.7" - "@babel/traverse" "^7.25.0" - -"@babel/helper-simple-access@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" - integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" - integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-string-parser@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" - integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== - -"@babel/helper-validator-identifier@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" - integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== - -"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" - integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== - -"@babel/helper-wrap-function@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz#dab12f0f593d6ca48c0062c28bcfb14ebe812f81" - integrity sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ== - dependencies: - "@babel/template" "^7.25.0" - "@babel/traverse" "^7.25.0" - "@babel/types" "^7.25.0" - -"@babel/helpers@^7.25.0": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.6.tgz#57ee60141829ba2e102f30711ffe3afab357cc60" - integrity sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q== - dependencies: - "@babel/template" "^7.25.0" - "@babel/types" "^7.25.6" - -"@babel/highlight@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" - integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== - dependencies: - "@babel/helper-validator-identifier" "^7.24.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" + integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/parser@^7.25.0", "@babel/parser@^7.25.6": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f" - integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q== +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz#18580d00c9934117ad719392c4f6585c9333cc35" + integrity sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg== + +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== dependencies: - "@babel/types" "^7.25.6" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz#dca427b45a6c0f5c095a1c639dfe2476a3daba7f" - integrity sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA== +"@babel/helper-replace-supers@^7.25.9", "@babel/helper-replace-supers@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz#6cb04e82ae291dae8e72335dfe438b0725f14c8d" + integrity sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.3" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.26.5" -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz#cd0c583e01369ef51676bdb3d7b603e17d2b3f73" - integrity sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA== +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz#749bde80356b295390954643de7635e0dffabe73" - integrity sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== + dependencies: + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + +"@babel/helpers@^7.26.10": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" + integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" - integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== +"@babel/parser@^7.26.10", "@babel/parser@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" + integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.7" + "@babel/types" "^7.27.0" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz#cc2e53ebf0a0340777fff5ed521943e253b4d8fe" + integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz#af9e4fb63ccb8abcb92375b2fcfe36b60c774d30" + integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz#e8dc26fcd616e6c5bf2bd0d5a2c151d4f92a9137" + integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz#3a82a70e7cb7294ad2559465ebcb871dfbf078fb" - integrity sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz#807a667f9158acac6f6164b4beb85ad9ebc9e1d1" + integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.0" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz#de7093f1e7deaf68eadd7cc6b07f2ab82543269e" + integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== +"@babel/plugin-syntax-import-assertions@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz#620412405058efa56e4a564903b79355020f445f" + integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== +"@babel/plugin-syntax-import-attributes@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" + integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== +"@babel/plugin-syntax-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" + integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== +"@babel/plugin-syntax-typescript@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" + integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-syntax-import-assertions@^7.24.7": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.6.tgz#bb918905c58711b86f9710d74a3744b6c56573b5" - integrity sha512-aABl0jHw9bZ2karQ/uUD6XP4u0SG22SJrOHFoL6XB1R7dTovOP4TzTlsxOYC5yQ1pdscVK2JTUnF6QL3ARoAiQ== +"@babel/plugin-transform-arrow-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz#7821d4410bee5daaadbb4cdd9a6649704e176845" + integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-import-attributes@^7.24.7": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz#6d4c78f042db0e82fd6436cd65fec5dc78ad2bde" - integrity sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ== +"@babel/plugin-transform-async-generator-functions@^7.26.8": + version "7.26.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz#5e3991135e3b9c6eaaf5eff56d1ae5a11df45ff8" + integrity sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.26.8" -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== +"@babel/plugin-transform-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz#c80008dacae51482793e5a9c08b39a5be7e12d71" + integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== +"@babel/plugin-transform-block-scoped-functions@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz#3dc4405d31ad1cbe45293aa57205a6e3b009d53e" + integrity sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.26.5" -"@babel/plugin-syntax-jsx@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" - integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== +"@babel/plugin-transform-block-scoping@^7.25.9": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.0.tgz#acc2c0d98a7439bbde4244588ddbd4904701d47f" + integrity sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.26.5" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== +"@babel/plugin-transform-class-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-transform-class-static-block@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-classes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz#7152457f7880b593a63ade8a861e6e26a4469f52" + integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/traverse" "^7.25.9" + globals "^11.1.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== +"@babel/plugin-transform-computed-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz#db36492c78460e534b8852b1d5befe3c923ef10b" + integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/template" "^7.25.9" -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== +"@babel/plugin-transform-destructuring@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz#966ea2595c498224340883602d3cfd7a0c79cea1" + integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== +"@babel/plugin-transform-dotall-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz#bad7945dd07734ca52fe3ad4e872b40ed09bb09a" + integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== +"@babel/plugin-transform-duplicate-keys@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz#8850ddf57dce2aebb4394bb434a7598031059e6d" + integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== dependencies: - "@babel/helper-plugin-utils" "^7.8.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz#6f7259b4de127721a08f1e5165b852fcaa696d31" + integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== +"@babel/plugin-transform-dynamic-import@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz#23e917de63ed23c6600c5dd06d94669dce79f7b8" + integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== dependencies: - "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-typescript@^7.24.7": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz#04db9ce5a9043d9c635e75ae7969a2cd50ca97ff" - integrity sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg== +"@babel/plugin-transform-exponentiation-operator@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz#e29f01b6de302c7c2c794277a48f04a9ca7f03bc" + integrity sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" - integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== +"@babel/plugin-transform-export-namespace-from@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz#90745fe55053394f554e40584cda81f2c8a402a2" + integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" - integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-async-generator-functions@^7.25.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz#2afd4e639e2d055776c9f091b6c0c180ed8cf083" - integrity sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-remap-async-to-generator" "^7.25.0" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/traverse" "^7.25.4" - -"@babel/plugin-transform-async-to-generator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" - integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== - dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-remap-async-to-generator" "^7.24.7" - -"@babel/plugin-transform-block-scoped-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" - integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-block-scoping@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz#23a6ed92e6b006d26b1869b1c91d1b917c2ea2ac" - integrity sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-class-properties@^7.25.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz#bae7dbfcdcc2e8667355cd1fb5eda298f05189fd" - integrity sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.4" - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-class-static-block@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" - integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-transform-classes@^7.25.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz#d29dbb6a72d79f359952ad0b66d88518d65ef89a" - integrity sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-compilation-targets" "^7.25.2" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-replace-supers" "^7.25.0" - "@babel/traverse" "^7.25.4" - globals "^11.1.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-computed-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" - integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== +"@babel/plugin-transform-for-of@^7.26.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz#27231f79d5170ef33b5111f07fe5cafeb2c96a56" + integrity sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/template" "^7.24.7" + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" -"@babel/plugin-transform-destructuring@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" - integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== +"@babel/plugin-transform-function-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz#939d956e68a606661005bfd550c4fc2ef95f7b97" + integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-dotall-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" - integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== +"@babel/plugin-transform-json-strings@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz#c86db407cb827cded902a90c707d2781aaa89660" + integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-duplicate-keys@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" - integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== +"@babel/plugin-transform-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz#1a1c6b4d4aa59bc4cad5b6b3a223a0abd685c9de" + integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz#809af7e3339466b49c034c683964ee8afb3e2604" - integrity sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g== +"@babel/plugin-transform-logical-assignment-operators@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz#b19441a8c39a2fda0902900b306ea05ae1055db7" + integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.0" - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-dynamic-import@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" - integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== +"@babel/plugin-transform-member-expression-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz#63dff19763ea64a31f5e6c20957e6a25e41ed5de" + integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-exponentiation-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" - integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== +"@babel/plugin-transform-modules-amd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz#49ba478f2295101544abd794486cd3088dddb6c5" + integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-export-namespace-from@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" - integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== +"@babel/plugin-transform-modules-commonjs@^7.26.3": + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz#8f011d44b20d02c3de44d8850d971d8497f981fb" + integrity sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/helper-module-transforms" "^7.26.0" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-for-of@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" - integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== +"@babel/plugin-transform-modules-systemjs@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz#8bd1b43836269e3d33307151a114bcf3ba6793f8" + integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" -"@babel/plugin-transform-function-name@^7.25.1": - version "7.25.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz#b85e773097526c1a4fc4ba27322748643f26fc37" - integrity sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA== +"@babel/plugin-transform-modules-umd@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz#6710079cdd7c694db36529a1e8411e49fcbf14c9" + integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== dependencies: - "@babel/helper-compilation-targets" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.1" + "@babel/helper-module-transforms" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-json-strings@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" - integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz#454990ae6cc22fd2a0fa60b3a2c6f63a38064e6a" + integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-literals@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz#deb1ad14fc5490b9a65ed830e025bca849d8b5f3" - integrity sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw== +"@babel/plugin-transform-new-target@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz#42e61711294b105c248336dcb04b77054ea8becd" + integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-logical-assignment-operators@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" - integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== +"@babel/plugin-transform-nullish-coalescing-operator@^7.26.6": + version "7.26.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz#fbf6b3c92cb509e7b319ee46e3da89c5bedd31fe" + integrity sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/helper-plugin-utils" "^7.26.5" -"@babel/plugin-transform-member-expression-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" - integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== +"@babel/plugin-transform-numeric-separator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz#bfed75866261a8b643468b0ccfd275f2033214a1" + integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-modules-amd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" - integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== - dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-modules-commonjs@^7.24.7", "@babel/plugin-transform-modules-commonjs@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" - integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== +"@babel/plugin-transform-object-rest-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== dependencies: - "@babel/helper-module-transforms" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" -"@babel/plugin-transform-modules-systemjs@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz#8f46cdc5f9e5af74f3bd019485a6cbe59685ea33" - integrity sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw== - dependencies: - "@babel/helper-module-transforms" "^7.25.0" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-validator-identifier" "^7.24.7" - "@babel/traverse" "^7.25.0" - -"@babel/plugin-transform-modules-umd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" - integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== - dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" - integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== +"@babel/plugin-transform-object-super@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz#385d5de135162933beb4a3d227a2b7e52bb4cf03" + integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-new-target@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" - integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" -"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" - integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== +"@babel/plugin-transform-optional-catch-binding@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz#10e70d96d52bb1f10c5caaac59ac545ea2ba7ff3" + integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-transform-numeric-separator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" - integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-object-rest-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" - integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== - dependencies: - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.24.7" - -"@babel/plugin-transform-object-super@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" - integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-replace-supers" "^7.24.7" - -"@babel/plugin-transform-optional-catch-binding@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" - integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== +"@babel/plugin-transform-optional-chaining@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz#e142eb899d26ef715435f201ab6e139541eee7dd" + integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" - integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-transform-parameters@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" - integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-private-methods@^7.25.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz#9bbefbe3649f470d681997e0b64a4b254d877242" - integrity sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw== + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-methods@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.4" - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-private-property-in-object@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" - integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-property-in-object@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz#9c8b73e64e6cc3cbb2743633885a7dd2c385fe33" + integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-property-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" - integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== +"@babel/plugin-transform-property-literals@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz#d72d588bd88b0dec8b62e36f6fda91cedfe28e3f" + integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-constant-elements@^7.18.12": - version "7.25.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.1.tgz#71a665ed16ce618067d05f4a98130207349d82ae" - integrity sha512-SLV/giH/V4SmloZ6Dt40HjTGTAIkxn33TVIHxNGNvo8ezMhrxBkzisj4op1KZYPIOHFLqhv60OHvX+YRu4xbmQ== +"@babel/plugin-transform-react-constant-elements@^7.21.3": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.25.9.tgz#08a1de35a301929b60fdf2788a54b46cd8ecd0ef" + integrity sha512-Ncw2JFsJVuvfRsa2lSHiC55kETQVLSnsYGQ1JDDwkUeWGTL/8Tom8aLTnlqgoeuopWrbbGndrc9AlLYrIosrow== dependencies: - "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-display-name@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz#9caff79836803bc666bcfe210aeb6626230c293b" - integrity sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg== +"@babel/plugin-transform-react-display-name@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz#4b79746b59efa1f38c8695065a92a9f5afb24f7d" + integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-react-jsx-development@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" - integrity sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ== - dependencies: - "@babel/plugin-transform-react-jsx" "^7.24.7" +"@babel/plugin-transform-react-jsx-development@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz#8fd220a77dd139c07e25225a903b8be8c829e0d7" + integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.25.9" -"@babel/plugin-transform-react-jsx@^7.24.7": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz#e37e8ebfa77e9f0b16ba07fadcb6adb47412227a" - integrity sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/types" "^7.25.2" +"@babel/plugin-transform-react-jsx@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz#06367940d8325b36edff5e2b9cbe782947ca4166" + integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/types" "^7.25.9" -"@babel/plugin-transform-react-pure-annotations@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" - integrity sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA== +"@babel/plugin-transform-react-pure-annotations@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz#ea1c11b2f9dbb8e2d97025f43a3b5bc47e18ae62" + integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" -"@babel/plugin-transform-regenerator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" - integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== +"@babel/plugin-transform-regenerator@^7.25.9": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.0.tgz#822feebef43d6a59a81f696b2512df5b1682db31" + integrity sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA== dependencies: - "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-plugin-utils" "^7.26.5" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" - integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-shorthand-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" - integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" - integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - -"@babel/plugin-transform-sticky-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" - integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-template-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" - integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-typeof-symbol@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" - integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-typescript@^7.24.7": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz#237c5d10de6d493be31637c6b9fa30b6c5461add" - integrity sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-create-class-features-plugin" "^7.25.0" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-syntax-typescript" "^7.24.7" - -"@babel/plugin-transform-unicode-escapes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" - integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-property-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" - integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" - integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-sets-regex@^7.25.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz#be664c2a0697ffacd3423595d5edef6049e8946c" - integrity sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.2" - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/preset-env@^7.19.4": - version "7.25.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.4.tgz#be23043d43a34a2721cd0f676c7ba6f1481f6af6" - integrity sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw== - dependencies: - "@babel/compat-data" "^7.25.4" - "@babel/helper-compilation-targets" "^7.25.2" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-validator-option" "^7.24.8" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.3" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.0" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.0" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.0" +"@babel/plugin-transform-regexp-modifiers@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz#2f5837a5b5cd3842a919d8147e9903cc7455b850" + integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-reserved-words@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz#0398aed2f1f10ba3f78a93db219b27ef417fb9ce" + integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-shorthand-properties@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz#bb785e6091f99f826a95f9894fc16fde61c163f2" + integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-spread@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz#24a35153931b4ba3d13cec4a7748c21ab5514ef9" + integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + +"@babel/plugin-transform-sticky-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz#c7f02b944e986a417817b20ba2c504dfc1453d32" + integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-template-literals@^7.26.8": + version "7.26.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz#966b15d153a991172a540a69ad5e1845ced990b5" + integrity sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-typeof-symbol@^7.26.7": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz#044a0890f3ca694207c7826d0c7a65e5ac008aae" + integrity sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + +"@babel/plugin-transform-typescript@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.0.tgz#a29fd3481da85601c7e34091296e9746d2cccba8" + integrity sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.27.0" + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/plugin-syntax-typescript" "^7.25.9" + +"@babel/plugin-transform-unicode-escapes@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz#a75ef3947ce15363fccaa38e2dd9bc70b2788b82" + integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-property-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz#a901e96f2c1d071b0d1bb5dc0d3c880ce8f53dd3" + integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz#5eae747fe39eacf13a8bd006a4fb0b5d1fa5e9b1" + integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-unicode-sets-regex@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz#65114c17b4ffc20fa5b163c63c70c0d25621fabe" + integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/preset-env@^7.20.2": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.26.9.tgz#2ec64e903d0efe743699f77a10bdf7955c2123c3" + integrity sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ== + dependencies: + "@babel/compat-data" "^7.26.8" + "@babel/helper-compilation-targets" "^7.26.5" + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.24.7" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-import-assertions" "^7.26.0" + "@babel/plugin-syntax-import-attributes" "^7.26.0" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.24.7" - "@babel/plugin-transform-async-generator-functions" "^7.25.4" - "@babel/plugin-transform-async-to-generator" "^7.24.7" - "@babel/plugin-transform-block-scoped-functions" "^7.24.7" - "@babel/plugin-transform-block-scoping" "^7.25.0" - "@babel/plugin-transform-class-properties" "^7.25.4" - "@babel/plugin-transform-class-static-block" "^7.24.7" - "@babel/plugin-transform-classes" "^7.25.4" - "@babel/plugin-transform-computed-properties" "^7.24.7" - "@babel/plugin-transform-destructuring" "^7.24.8" - "@babel/plugin-transform-dotall-regex" "^7.24.7" - "@babel/plugin-transform-duplicate-keys" "^7.24.7" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.0" - "@babel/plugin-transform-dynamic-import" "^7.24.7" - "@babel/plugin-transform-exponentiation-operator" "^7.24.7" - "@babel/plugin-transform-export-namespace-from" "^7.24.7" - "@babel/plugin-transform-for-of" "^7.24.7" - "@babel/plugin-transform-function-name" "^7.25.1" - "@babel/plugin-transform-json-strings" "^7.24.7" - "@babel/plugin-transform-literals" "^7.25.2" - "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" - "@babel/plugin-transform-member-expression-literals" "^7.24.7" - "@babel/plugin-transform-modules-amd" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.8" - "@babel/plugin-transform-modules-systemjs" "^7.25.0" - "@babel/plugin-transform-modules-umd" "^7.24.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" - "@babel/plugin-transform-new-target" "^7.24.7" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" - "@babel/plugin-transform-numeric-separator" "^7.24.7" - "@babel/plugin-transform-object-rest-spread" "^7.24.7" - "@babel/plugin-transform-object-super" "^7.24.7" - "@babel/plugin-transform-optional-catch-binding" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.8" - "@babel/plugin-transform-parameters" "^7.24.7" - "@babel/plugin-transform-private-methods" "^7.25.4" - "@babel/plugin-transform-private-property-in-object" "^7.24.7" - "@babel/plugin-transform-property-literals" "^7.24.7" - "@babel/plugin-transform-regenerator" "^7.24.7" - "@babel/plugin-transform-reserved-words" "^7.24.7" - "@babel/plugin-transform-shorthand-properties" "^7.24.7" - "@babel/plugin-transform-spread" "^7.24.7" - "@babel/plugin-transform-sticky-regex" "^7.24.7" - "@babel/plugin-transform-template-literals" "^7.24.7" - "@babel/plugin-transform-typeof-symbol" "^7.24.8" - "@babel/plugin-transform-unicode-escapes" "^7.24.7" - "@babel/plugin-transform-unicode-property-regex" "^7.24.7" - "@babel/plugin-transform-unicode-regex" "^7.24.7" - "@babel/plugin-transform-unicode-sets-regex" "^7.25.4" + "@babel/plugin-transform-arrow-functions" "^7.25.9" + "@babel/plugin-transform-async-generator-functions" "^7.26.8" + "@babel/plugin-transform-async-to-generator" "^7.25.9" + "@babel/plugin-transform-block-scoped-functions" "^7.26.5" + "@babel/plugin-transform-block-scoping" "^7.25.9" + "@babel/plugin-transform-class-properties" "^7.25.9" + "@babel/plugin-transform-class-static-block" "^7.26.0" + "@babel/plugin-transform-classes" "^7.25.9" + "@babel/plugin-transform-computed-properties" "^7.25.9" + "@babel/plugin-transform-destructuring" "^7.25.9" + "@babel/plugin-transform-dotall-regex" "^7.25.9" + "@babel/plugin-transform-duplicate-keys" "^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-dynamic-import" "^7.25.9" + "@babel/plugin-transform-exponentiation-operator" "^7.26.3" + "@babel/plugin-transform-export-namespace-from" "^7.25.9" + "@babel/plugin-transform-for-of" "^7.26.9" + "@babel/plugin-transform-function-name" "^7.25.9" + "@babel/plugin-transform-json-strings" "^7.25.9" + "@babel/plugin-transform-literals" "^7.25.9" + "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" + "@babel/plugin-transform-member-expression-literals" "^7.25.9" + "@babel/plugin-transform-modules-amd" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.26.3" + "@babel/plugin-transform-modules-systemjs" "^7.25.9" + "@babel/plugin-transform-modules-umd" "^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" + "@babel/plugin-transform-new-target" "^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.26.6" + "@babel/plugin-transform-numeric-separator" "^7.25.9" + "@babel/plugin-transform-object-rest-spread" "^7.25.9" + "@babel/plugin-transform-object-super" "^7.25.9" + "@babel/plugin-transform-optional-catch-binding" "^7.25.9" + "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-transform-private-property-in-object" "^7.25.9" + "@babel/plugin-transform-property-literals" "^7.25.9" + "@babel/plugin-transform-regenerator" "^7.25.9" + "@babel/plugin-transform-regexp-modifiers" "^7.26.0" + "@babel/plugin-transform-reserved-words" "^7.25.9" + "@babel/plugin-transform-shorthand-properties" "^7.25.9" + "@babel/plugin-transform-spread" "^7.25.9" + "@babel/plugin-transform-sticky-regex" "^7.25.9" + "@babel/plugin-transform-template-literals" "^7.26.8" + "@babel/plugin-transform-typeof-symbol" "^7.26.7" + "@babel/plugin-transform-unicode-escapes" "^7.25.9" + "@babel/plugin-transform-unicode-property-regex" "^7.25.9" + "@babel/plugin-transform-unicode-regex" "^7.25.9" + "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" "@babel/preset-modules" "0.1.6-no-external-plugins" babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-corejs3 "^0.11.0" babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.37.1" + core-js-compat "^3.40.0" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -962,95 +812,97 @@ esutils "^2.0.2" "@babel/preset-react@^7.18.6": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" - integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-transform-react-display-name" "^7.24.7" - "@babel/plugin-transform-react-jsx" "^7.24.7" - "@babel/plugin-transform-react-jsx-development" "^7.24.7" - "@babel/plugin-transform-react-pure-annotations" "^7.24.7" - -"@babel/preset-typescript@^7.18.6": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" - integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.7" - "@babel/plugin-transform-typescript" "^7.24.7" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - -"@babel/runtime@^7.0.0": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" - integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== + version "7.26.3" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.26.3.tgz#7c5e028d623b4683c1f83a0bd4713b9100560caa" + integrity sha512-Nl03d6T9ky516DGK2YMxrTqvnpUW63TnJMOMonj+Zae0JiPC5BC9xPMSL6L8fiSpA5vP88qfygavVQvnLp+6Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-transform-react-display-name" "^7.25.9" + "@babel/plugin-transform-react-jsx" "^7.25.9" + "@babel/plugin-transform-react-jsx-development" "^7.25.9" + "@babel/plugin-transform-react-pure-annotations" "^7.25.9" + +"@babel/preset-typescript@^7.21.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.0.tgz#4dcb8827225975f4290961b0b089f9c694ca50c7" + integrity sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.26.5" + "@babel/helper-validator-option" "^7.25.9" + "@babel/plugin-syntax-jsx" "^7.25.9" + "@babel/plugin-transform-modules-commonjs" "^7.26.3" + "@babel/plugin-transform-typescript" "^7.27.0" + +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.26.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" - integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== +"@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" + integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.27.0" + "@babel/types" "^7.27.0" + +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.0.tgz#11d7e644779e166c0442f9a07274d02cd91d4a70" + integrity sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.27.0" + "@babel/parser" "^7.27.0" + "@babel/template" "^7.27.0" + "@babel/types" "^7.27.0" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.21.3", "@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.27.0", "@babel/types@^7.4.4": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" + integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== dependencies: - regenerator-runtime "^0.14.0" + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" -"@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" - integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== +"@emnapi/core@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.3.1.tgz#9c62d185372d1bddc94682b87f376e03dfac3f16" + integrity sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog== dependencies: - regenerator-runtime "^0.14.0" + "@emnapi/wasi-threads" "1.0.1" + tslib "^2.4.0" -"@babel/template@^7.24.7", "@babel/template@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" - integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== - dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/parser" "^7.25.0" - "@babel/types" "^7.25.0" - -"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.3", "@babel/traverse@^7.25.4": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.6.tgz#04fad980e444f182ecf1520504941940a90fea41" - integrity sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ== - dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.25.6" - "@babel/parser" "^7.25.6" - "@babel/template" "^7.25.0" - "@babel/types" "^7.25.6" - debug "^4.3.1" - globals "^11.1.0" +"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" + integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw== + dependencies: + tslib "^2.4.0" -"@babel/types@^7.20.0", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.25.6", "@babel/types@^7.4.4": - version "7.25.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6" - integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw== +"@emnapi/wasi-threads@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz#d7ae71fd2166b1c916c6cd2d0df2ef565a2e1a5b" + integrity sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw== dependencies: - "@babel/helper-string-parser" "^7.24.8" - "@babel/helper-validator-identifier" "^7.24.7" - to-fast-properties "^2.0.0" + tslib "^2.4.0" -"@emotion/babel-plugin@^11.12.0": - version "11.12.0" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" - integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== +"@emotion/babel-plugin@^11.13.5": + version "11.13.5" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz#eab8d65dbded74e0ecfd28dc218e75607c4e7bc0" + integrity sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ== dependencies: "@babel/helper-module-imports" "^7.16.7" "@babel/runtime" "^7.18.3" "@emotion/hash" "^0.9.2" "@emotion/memoize" "^0.9.0" - "@emotion/serialize" "^1.2.0" + "@emotion/serialize" "^1.3.3" babel-plugin-macros "^3.1.0" convert-source-map "^1.5.0" escape-string-regexp "^4.0.0" @@ -1058,172 +910,186 @@ source-map "^0.5.7" stylis "4.2.0" -"@emotion/cache@*", "@emotion/cache@^11.13.0", "@emotion/cache@^11.13.1": - version "11.13.1" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" - integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== +"@emotion/cache@*", "@emotion/cache@11.14.0", "@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76" + integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA== dependencies: "@emotion/memoize" "^0.9.0" "@emotion/sheet" "^1.4.0" - "@emotion/utils" "^1.4.0" + "@emotion/utils" "^1.4.2" "@emotion/weak-memoize" "^0.4.0" stylis "4.2.0" -"@emotion/cache@11.10.5": - version "11.10.5" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12" - integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== - dependencies: - "@emotion/memoize" "^0.8.0" - "@emotion/sheet" "^1.2.1" - "@emotion/utils" "^1.2.0" - "@emotion/weak-memoize" "^0.3.0" - stylis "4.1.3" - "@emotion/hash@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== "@emotion/is-prop-valid@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz#bd84ba972195e8a2d42462387581560ef780e4e2" - integrity sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ== + version "1.3.1" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz#8d5cf1132f836d7adbe42cf0b49df7816fc88240" + integrity sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw== dependencies: "@emotion/memoize" "^0.9.0" -"@emotion/memoize@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" - integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== - "@emotion/memoize@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== -"@emotion/react@11.13.3": - version "11.13.3" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" - integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== +"@emotion/react@11.14.0": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.14.0.tgz#cfaae35ebc67dd9ef4ea2e9acc6cd29e157dd05d" + integrity sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA== dependencies: "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.12.0" - "@emotion/cache" "^11.13.0" - "@emotion/serialize" "^1.3.1" - "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" - "@emotion/utils" "^1.4.0" + "@emotion/babel-plugin" "^11.13.5" + "@emotion/cache" "^11.14.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" "@emotion/weak-memoize" "^0.4.0" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@*", "@emotion/serialize@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" - integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== - dependencies: - "@emotion/hash" "^0.9.2" - "@emotion/memoize" "^0.9.0" - "@emotion/unitless" "^0.10.0" - "@emotion/utils" "^1.4.1" - csstype "^3.0.2" - -"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.1.tgz#490b660178f43d2de8e92b278b51079d726c05c3" - integrity sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA== +"@emotion/serialize@*", "@emotion/serialize@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.3.tgz#d291531005f17d704d0463a032fe679f376509e8" + integrity sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA== dependencies: "@emotion/hash" "^0.9.2" "@emotion/memoize" "^0.9.0" "@emotion/unitless" "^0.10.0" - "@emotion/utils" "^1.4.0" + "@emotion/utils" "^1.4.2" csstype "^3.0.2" -"@emotion/server@11.10.0": - version "11.10.0" - resolved "https://registry.yarnpkg.com/@emotion/server/-/server-11.10.0.tgz#3edc075b672c75426f682d56aadc6404fb1f6648" - integrity sha512-MTvJ21JPo9aS02GdjFW4nhdwOi2tNNpMmAM/YED0pkxzjDNi5WbiTwXqaCnvLc2Lr8NFtjhT0az1vTJyLIHYcw== +"@emotion/server@11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/server/-/server-11.11.0.tgz#35537176a2a5ed8aed7801f254828e636ec3bd6e" + integrity sha512-6q89fj2z8VBTx9w93kJ5n51hsmtYuFPtZgnc1L8VzRx9ti4EU6EyvF6Nn1H1x3vcCQCF7u2dB2lY4AYJwUW4PA== dependencies: - "@emotion/utils" "^1.2.0" + "@emotion/utils" "^1.2.1" html-tokenize "^2.0.0" multipipe "^1.0.2" through "^2.3.8" -"@emotion/sheet@^1.2.1", "@emotion/sheet@^1.4.0": +"@emotion/sheet@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== -"@emotion/styled@11.13.0": - version "11.13.0" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.13.0.tgz#633fd700db701472c7a5dbef54d6f9834e9fb190" - integrity sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA== +"@emotion/styled@11.14.0": + version "11.14.0" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.14.0.tgz#f47ca7219b1a295186d7661583376fcea95f0ff3" + integrity sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA== dependencies: "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.12.0" + "@emotion/babel-plugin" "^11.13.5" "@emotion/is-prop-valid" "^1.3.0" - "@emotion/serialize" "^1.3.0" - "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" - "@emotion/utils" "^1.4.0" + "@emotion/serialize" "^1.3.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.2.0" + "@emotion/utils" "^1.4.2" "@emotion/unitless@^0.10.0": version "0.10.0" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== -"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" - integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== - -"@emotion/utils@*", "@emotion/utils@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" - integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== - -"@emotion/utils@^1.2.0", "@emotion/utils@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.0.tgz#262f1d02aaedb2ec91c83a0955dd47822ad5fbdd" - integrity sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ== +"@emotion/use-insertion-effect-with-fallbacks@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz#8a8cb77b590e09affb960f4ff1e9a89e532738bf" + integrity sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg== -"@emotion/weak-memoize@^0.3.0": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" - integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== +"@emotion/utils@*", "@emotion/utils@^1.2.1", "@emotion/utils@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.2.tgz#6df6c45881fcb1c412d6688a311a98b7f59c1b52" + integrity sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA== "@emotion/weak-memoize@^0.4.0": version "0.4.0" 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/eslintrc@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" - integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz#b0fc7e06d0c94f801537fd4237edc2706d3b8e4c" + integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa" + integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w== + dependencies: + "@eslint/object-schema" "^2.1.6" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/config-helpers@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.1.0.tgz#62f1b7821e9d9ced1b3f512c7ea731825765d1cc" + integrity sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA== + +"@eslint/core@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.12.0.tgz#5f960c3d57728be9f6c65bd84aa6aa613078798e" + integrity sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964" + integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.4.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@9.22.0": + version "9.22.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.22.0.tgz#4ff53649ded7cbce90b444b494c234137fa1aa3d" + integrity sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ== + +"@eslint/object-schema@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" + integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== + +"@eslint/plugin-kit@^0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz#9901d52c136fb8f375906a73dcc382646c3b6a27" + integrity sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g== + dependencies: + "@eslint/core" "^0.12.0" + levn "^0.4.1" + "@floating-ui/core@^1.6.0": - version "1.6.8" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12" - integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA== + version "1.6.9" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.9.tgz#64d1da251433019dafa091de9b2886ff35ec14e6" + integrity sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw== dependencies: - "@floating-ui/utils" "^0.2.8" + "@floating-ui/utils" "^0.2.9" "@floating-ui/dom@^1.0.0": - version "1.6.12" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.12.tgz#6333dcb5a8ead3b2bf82f33d6bc410e95f54e556" - integrity sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w== + version "1.6.13" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34" + integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w== dependencies: "@floating-ui/core" "^1.6.0" - "@floating-ui/utils" "^0.2.8" + "@floating-ui/utils" "^0.2.9" "@floating-ui/react-dom@^2.1.1": version "2.1.2" @@ -1232,39 +1098,161 @@ dependencies: "@floating-ui/dom" "^1.0.0" -"@floating-ui/utils@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" - integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== +"@floating-ui/utils@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" + integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== -"@heroicons/react@2.0.15": - version "2.0.15" - resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.15.tgz#e1fc966350577a1ab0e38211e7b58e4bf8285b49" - integrity sha512-CZ2dGWgWG3/z5LEoD5D3MEr1syn45JM/OB2aDpw531Ryecgkz2V7TWQ808P0lva7zP003PVW6WlwbofsYyga3A== +"@heroicons/react@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.2.0.tgz#0c05124af50434a800773abec8d3af6a297d904b" + integrity sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ== -"@humanwhocodes/config-array@^0.11.8": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" - debug "^4.3.1" - minimatch "^3.0.5" + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.2.tgz#1860473de7dfa1546767448f333db80cb0ff2161" + integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ== + +"@img/sharp-darwin-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08" + integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.0.4" + +"@img/sharp-darwin-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" + integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.0.4" + +"@img/sharp-libvips-darwin-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f" + integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== + +"@img/sharp-libvips-darwin-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" + integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== + +"@img/sharp-libvips-linux-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" + integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== + +"@img/sharp-libvips-linux-arm@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" + integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== + +"@img/sharp-libvips-linux-s390x@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" + integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== + +"@img/sharp-libvips-linux-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" + integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== + +"@img/sharp-libvips-linuxmusl-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" + integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== + +"@img/sharp-libvips-linuxmusl-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" + integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== + +"@img/sharp-linux-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" + integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.0.4" + +"@img/sharp-linux-arm@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" + integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.0.5" + +"@img/sharp-linux-s390x@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" + integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.0.4" + +"@img/sharp-linux-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" + integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.0.4" + +"@img/sharp-linuxmusl-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" + integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + +"@img/sharp-linuxmusl-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" + integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + +"@img/sharp-wasm32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" + integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== + dependencies: + "@emnapi/runtime" "^1.2.0" + +"@img/sharp-win32-ia32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" + integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== + +"@img/sharp-win32-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" + integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== "@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== dependencies: "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" @@ -1293,145 +1281,166 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@monaco-editor/loader@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558" - integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg== +"@monaco-editor/loader@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.5.0.tgz#dcdbc7fe7e905690fb449bed1c251769f325c55d" + integrity sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw== dependencies: state-local "^1.0.6" "@monaco-editor/react@^4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119" - integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw== + version "4.7.0" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.7.0.tgz#35a1ec01bfe729f38bfc025df7b7bac145602a60" + integrity sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA== dependencies: - "@monaco-editor/loader" "^1.4.0" + "@monaco-editor/loader" "^1.5.0" -"@mui/base@5.0.0-beta.61": - version "5.0.0-beta.61" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.61.tgz#729e816b5104da1eeeacb11d1e61be90f2c21dcc" - integrity sha512-YaMOTXS3ecDNGsPKa6UdlJ8loFLvcL9+VbpCK3hfk71OaNauZRp4Yf7KeXDYr7Ms3M/XBD3SaiR6JMr6vYtfDg== +"@mui/base@5.0.0-beta.69": + version "5.0.0-beta.69" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.69.tgz#fc3635818c6a9fb954b1ee00870109e7e266149d" + integrity sha512-r2YyGUXpZxj8rLAlbjp1x2BnMERTZ/dMqd9cClKj2OJ7ALAuiv/9X5E9eHfRc9o/dGRuLSMq/WTjREktJVjxVA== dependencies: "@babel/runtime" "^7.26.0" "@floating-ui/react-dom" "^2.1.1" - "@mui/types" "^7.2.19" - "@mui/utils" "^6.1.6" + "@mui/types" "^7.2.21" + "@mui/utils" "^6.4.1" "@popperjs/core" "^2.11.8" clsx "^2.1.1" prop-types "^15.8.1" -"@mui/core-downloads-tracker@^6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.6.tgz#73d96e75689b2af922a989123149a3497c8a96fa" - integrity sha512-nz1SlR9TdBYYPz4qKoNasMPRiGb4PaIHFkzLzhju0YVYS5QSuFF2+n7CsiHMIDcHv3piPu/xDWI53ruhOqvZwQ== +"@mui/core-downloads-tracker@^6.4.7": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.9.tgz#6f9f6a220b22ee24a70bbe73e5ec6cb7222a0713" + integrity sha512-3UvsvOjqZJcokHKSzA1lskj2XMM/G5GBgge6ykwmAij2pGGxydGxAXirQlLaeoMwTKDS6BcrLqPZyPVwzri20A== -"@mui/icons-material@6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.1.6.tgz#bfaf32874a9f9ec88c07d1ca132d1a0671e9ed7c" - integrity sha512-5r9urIL2lxXb/sPN3LFfFYEibsXJUb986HhhIeu1gOcte460pwdSiEhBSxkAuyT8Dj7jvu9MjqSBmSumQELo8A== +"@mui/icons-material@6.4.7": + version "6.4.7" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.4.7.tgz#078406b61c7d17230b8633643dbb458f89e02059" + integrity sha512-Rk8cs9ufQoLBw582Rdqq7fnSXXZTqhYRbpe1Y5SAz9lJKZP3CIdrj0PfG8HJLGw1hrsHFN/rkkm70IDzhJsG1g== dependencies: "@babel/runtime" "^7.26.0" -"@mui/lab@6.0.0-beta.14": - version "6.0.0-beta.14" - resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-6.0.0-beta.14.tgz#f548fb4207e7694d6df0f38ca20f6a5600696c70" - integrity sha512-l+g8z6QGcr7HdfCXhVaYcEp9TijH/G4h0lNaDaBL+qDFQ087ipNHC+XozE7mXOmBwtEAWmTJB4E5GwDboi9oxA== +"@mui/lab@6.0.0-beta.30": + version "6.0.0-beta.30" + resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-6.0.0-beta.30.tgz#650973b4d04965f18b3d3390e2dd90e772a4f461" + integrity sha512-ayDYkzTlkm5cnDGa10bvuFygX+2b9Hm1T4QZYMqV8+nSx3frKE0TLAbE7/qQ4vInOO5E4aOkHVBwZjyO+UbMTA== dependencies: "@babel/runtime" "^7.26.0" - "@mui/base" "5.0.0-beta.61" - "@mui/system" "^6.1.6" - "@mui/types" "^7.2.19" - "@mui/utils" "^6.1.6" + "@mui/base" "5.0.0-beta.69" + "@mui/system" "^6.4.7" + "@mui/types" "^7.2.21" + "@mui/utils" "^6.4.6" clsx "^2.1.1" prop-types "^15.8.1" -"@mui/material@6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.1.6.tgz#505d7300401f6af38426006d7fb3b8707dc10fbc" - integrity sha512-1yvejiQ/601l5AK3uIdUlAVElyCxoqKnl7QA+2oFB/2qYPWfRwDgavW/MoywS5Y2gZEslcJKhe0s2F3IthgFgw== +"@mui/material@6.4.7": + version "6.4.7" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.4.7.tgz#887f1efe4a1c244ef7aeebb7d95a6f061f50b89b" + integrity sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ== dependencies: "@babel/runtime" "^7.26.0" - "@mui/core-downloads-tracker" "^6.1.6" - "@mui/system" "^6.1.6" - "@mui/types" "^7.2.19" - "@mui/utils" "^6.1.6" + "@mui/core-downloads-tracker" "^6.4.7" + "@mui/system" "^6.4.7" + "@mui/types" "^7.2.21" + "@mui/utils" "^6.4.6" "@popperjs/core" "^2.11.8" - "@types/react-transition-group" "^4.4.11" + "@types/react-transition-group" "^4.4.12" clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" - react-is "^18.3.1" + react-is "^19.0.0" react-transition-group "^4.4.5" -"@mui/private-theming@^6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.1.6.tgz#9966bf2eca3d626cddd6e173752f46d344c7d7d1" - integrity sha512-ioAiFckaD/fJSnTrUMWgjl9HYBWt7ixCh7zZw7gDZ+Tae7NuprNV6QJK95EidDT7K0GetR2rU3kAeIR61Myttw== +"@mui/private-theming@^6.4.6", "@mui/private-theming@^6.4.8": + version "6.4.8" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.4.8.tgz#210d1a1ab4501f6cf31f1abdf1b0a37fbdfc2991" + integrity sha512-sWwQoNSn6elsPTAtSqCf+w5aaGoh7AASURNmpy+QTTD/zwJ0Jgwt0ZaaP6mXq2IcgHxYnYloM/+vJgHPMkRKTQ== dependencies: "@babel/runtime" "^7.26.0" - "@mui/utils" "^6.1.6" + "@mui/utils" "^6.4.8" prop-types "^15.8.1" -"@mui/styled-engine@^6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.1.6.tgz#61996621a0297aac16061e1739a738a899613fd6" - integrity sha512-I+yS1cSuSvHnZDBO7e7VHxTWpj+R7XlSZvTC4lS/OIbUNJOMMSd3UDP6V2sfwzAdmdDNBi7NGCRv2SZ6O9hGDA== +"@mui/styled-engine@^6.4.6", "@mui/styled-engine@^6.4.9": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.4.9.tgz#d6d6b5f180466001284f897ff7d7010a8e329497" + integrity sha512-qZRWO0cT407NI4ZRjZcH+1SOu8f3JzLHqdMlg52GyEufM9pkSZFnf7xjpwnlvkixcGjco6wLlMD0VB43KRcBuA== dependencies: "@babel/runtime" "^7.26.0" - "@emotion/cache" "^11.13.1" - "@emotion/serialize" "^1.3.2" + "@emotion/cache" "^11.13.5" + "@emotion/serialize" "^1.3.3" "@emotion/sheet" "^1.4.0" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@6.1.6", "@mui/system@^6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.1.6.tgz#d335d6952092f3c758c8b78c2d993aa13ef58175" - integrity sha512-qOf1VUE9wK8syiB0BBCp82oNBAVPYdj4Trh+G1s+L+ImYiKlubWhhqlnvWt3xqMevR+D2h1CXzA1vhX2FvA+VQ== +"@mui/system@6.4.7": + version "6.4.7" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.4.7.tgz#a4a8e541a2f1efef1c85a338723aa2f2d0a31e8e" + integrity sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/private-theming" "^6.4.6" + "@mui/styled-engine" "^6.4.6" + "@mui/types" "^7.2.21" + "@mui/utils" "^6.4.6" + clsx "^2.1.1" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/system@^6.4.7": + version "6.4.9" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.4.9.tgz#19575feb3fc7b465b082c65c9c737e1a40ed06ef" + integrity sha512-JOj7efXGtZn+NIzX8KDyMpO1QKc0DhilPBsxvci1xAvI1e5AtAtfzrEuV5ZvN+lz2BDuzngCWlllnqQ/cg40RQ== dependencies: "@babel/runtime" "^7.26.0" - "@mui/private-theming" "^6.1.6" - "@mui/styled-engine" "^6.1.6" - "@mui/types" "^7.2.19" - "@mui/utils" "^6.1.6" + "@mui/private-theming" "^6.4.8" + "@mui/styled-engine" "^6.4.9" + "@mui/types" "~7.2.24" + "@mui/utils" "^6.4.8" clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" -"@mui/types@^7.2.19": - version "7.2.19" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.19.tgz#c941954dd24393fdce5f07830d44440cf4ab6c80" - integrity sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA== +"@mui/types@^7.2.21": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.4.0.tgz#2304bab870721de1842c9ee0ff13fe8be6b8d0ed" + integrity sha512-TxJ4ezEeedWHBjOmLtxI203a9DII9l4k83RXmz1PYSAmnyEcK2PglTNmJGxswC/wM5cdl9ap2h8lnXvt2swAGQ== + dependencies: + "@babel/runtime" "^7.26.10" + +"@mui/types@~7.2.24": + version "7.2.24" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.24.tgz#5eff63129d9c29d80bbf2d2e561bd0690314dec2" + integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw== -"@mui/utils@^5.16.6 || ^6.0.0", "@mui/utils@^6.1.6": - version "6.1.6" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.1.6.tgz#4b9fd34da3a1dd4700fe506a20ca7da3933ba48e" - integrity sha512-sBS6D9mJECtELASLM+18WUcXF6RH3zNxBRFeyCRg8wad6NbyNrdxLuwK+Ikvc38sTZwBzAz691HmSofLqHd9sQ== +"@mui/utils@^5.16.6 || ^6.0.0", "@mui/utils@^6.4.1", "@mui/utils@^6.4.6", "@mui/utils@^6.4.8": + version "6.4.8" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.8.tgz#f80ee0c0ac47f1cd47c2031a5fb87243322b6bf3" + integrity sha512-C86gfiZ5BfZ51KqzqoHi1WuuM2QdSKoFhbkZeAfQRB+jCc4YNhhj11UXFVMMsqBgZ+Zy8IHNJW3M9Wj/LOwRXQ== dependencies: "@babel/runtime" "^7.26.0" - "@mui/types" "^7.2.19" - "@types/prop-types" "^15.7.13" + "@mui/types" "~7.2.24" + "@types/prop-types" "^15.7.14" clsx "^2.1.1" prop-types "^15.8.1" - react-is "^18.3.1" + react-is "^19.0.0" -"@mui/x-date-pickers@7.22.1": - version "7.22.1" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.22.1.tgz#3abe7ad0b5816051fdcef4ffbe5343965e7dfc8e" - integrity sha512-VBgicE+7PvJrdHSL6HyieHT6a/0dENH8RaMIM2VwUFrGoZzvik50WNwY5U+Hip1BwZLIEvlqtNRQIIj6kgBR6Q== +"@mui/x-date-pickers@7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.27.3.tgz#61171daf74802c17f9d861068eab422231389502" + integrity sha512-igfKTPC4ZVCmS5j/NXcXBtj/hHseQHzRpCpIB1PMnJGhMdRYXnz8qZz5XhlNBKlzJVXkGu6Uil+obZpCLNj1xg== dependencies: "@babel/runtime" "^7.25.7" "@mui/utils" "^5.16.6 || ^6.0.0" - "@mui/x-internals" "7.21.0" + "@mui/x-internals" "7.26.0" "@types/react-transition-group" "^4.4.11" clsx "^2.1.1" prop-types "^15.8.1" react-transition-group "^4.4.5" -"@mui/x-internals@7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.21.0.tgz#daca984059015b27efdb47bb44dc7ff4a6816673" - integrity sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ== +"@mui/x-internals@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.26.0.tgz#e8c3060582c102127ab55b0a93e881930dac107b" + integrity sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg== dependencies: "@babel/runtime" "^7.25.7" "@mui/utils" "^5.16.6 || ^6.0.0" @@ -1441,62 +1450,66 @@ resolved "https://registry.yarnpkg.com/@musement/iso-duration/-/iso-duration-1.0.0.tgz#b45ba8acb0b998488744e41da15a391e5f550c48" integrity sha512-gTJOmIXfsh5AyOdsUwkYcAIdWd9fCa/e0dV7mfV/B+oDOoJne5ciNMazDdQacylbWTQpF5aMdp2xrHVEwiryfg== -"@next/env@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.6.tgz#c1148e2e1aa166614f05161ee8f77ded467062bc" - integrity sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw== - -"@next/eslint-plugin-next@13.1.6": - version "13.1.6" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.1.6.tgz#ad8be22dd3d8aee9a9bd9a2507e2c55a2f7ebdd9" - integrity sha512-o7cauUYsXjzSJkay8wKjpKJf2uLzlggCsGUkPu3lP09Pv97jYlekTC20KJrjQKmSv5DXV0R/uks2ZXhqjNkqAw== - dependencies: - glob "7.1.7" - -"@next/swc-darwin-arm64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz#b15d139d8971360fca29be3bdd703c108c9a45fb" - integrity sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA== - -"@next/swc-darwin-x64@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz#9c72ee31cc356cb65ce6860b658d807ff39f1578" - integrity sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA== - -"@next/swc-linux-arm64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz#59f5f66155e85380ffa26ee3d95b687a770cfeab" - integrity sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg== - -"@next/swc-linux-arm64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz#f012518228017052736a87d69bae73e587c76ce2" - integrity sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q== - -"@next/swc-linux-x64-gnu@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz#339b867a7e9e7ee727a700b496b269033d820df4" - integrity sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw== - -"@next/swc-linux-x64-musl@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz#ae0ae84d058df758675830bcf70ca1846f1028f2" - integrity sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ== - -"@next/swc-win32-arm64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz#a5cc0c16920485a929a17495064671374fdbc661" - integrity sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg== - -"@next/swc-win32-ia32-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz#6a2409b84a2cbf34bf92fe714896455efb4191e4" - integrity sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg== - -"@next/swc-win32-x64-msvc@13.5.6": - version "13.5.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz#4a3e2a206251abc729339ba85f60bc0433c2865d" - integrity sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ== +"@napi-rs/wasm-runtime@^0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz#288f03812a408bc53c2c3686c65f38fe90f295eb" + integrity sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw== + dependencies: + "@emnapi/core" "^1.3.1" + "@emnapi/runtime" "^1.3.1" + "@tybys/wasm-util" "^0.9.0" + +"@next/env@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.4.tgz#060f8d8ddb02be5c825eab4ccd9ab619001efffb" + integrity sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g== + +"@next/eslint-plugin-next@15.2.2": + version "15.2.2" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.2.tgz#55fa96486f9e163ca689d441d31f4431ef423aef" + integrity sha512-1+BzokFuFQIfLaRxUKf2u5In4xhPV7tUgKcK53ywvFl6+LXHWHpFkcV7VNeKlyQKUotwiq4fy/aDNF9EiUp4RQ== + dependencies: + fast-glob "3.3.1" + +"@next/swc-darwin-arm64@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz#3a54f67aa2e0096a9147bd24dff1492e151819ae" + integrity sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw== + +"@next/swc-darwin-x64@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz#9b540f24afde1b7878623fdba9695344d26b7d67" + integrity sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew== + +"@next/swc-linux-arm64-gnu@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz#417a234c9f4dc5495094a8979859ac528c0f1f58" + integrity sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ== + +"@next/swc-linux-arm64-musl@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz#9bca76375508a175956f2d51f8547d0d6f9ffa64" + integrity sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA== + +"@next/swc-linux-x64-gnu@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz#c3d5041d53a5b228bf521ed49649e0f2a7aff947" + integrity sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw== + +"@next/swc-linux-x64-musl@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz#b2a51a108b1c412c69a504556cde0517631768c7" + integrity sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw== + +"@next/swc-win32-arm64-msvc@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz#7d687b42512abd36f44c2c787d58a1590f174b69" + integrity sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg== + +"@next/swc-win32-x64-msvc@15.2.4": + version "15.2.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz#779a0ea272fa4f509387f3b320e2d70803943a95" + integrity sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1511,7 +1524,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@^1.2.3": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -1529,154 +1542,158 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@react-leaflet/core@^2.0.0", "@react-leaflet/core@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-2.1.0.tgz#383acd31259d7c9ae8fb1b02d5e18fe613c2a13d" - integrity sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg== +"@react-leaflet/core@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-3.0.0.tgz#34ccc280ce7d8ac5c09f2b3d5fffded450bdf1a2" + integrity sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ== -"@react-pdf/fns@2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-2.2.1.tgz#04fe664a6f70214569c9c27e249e3395836f37d5" - integrity sha512-s78aDg0vDYaijU5lLOCsUD+qinQbfOvcNeaoX9AiE7+kZzzCo6B/nX+l48cmt9OosJmvZvE9DWR9cLhrhOi2pA== - dependencies: - "@babel/runtime" "^7.20.13" +"@react-pdf/fns@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-3.1.2.tgz#9ce7351d9fdf1cdb6e9c6ffd6801bc65f29f991c" + integrity sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g== -"@react-pdf/font@^2.3.1": - version "2.5.1" - resolved "https://registry.yarnpkg.com/@react-pdf/font/-/font-2.5.1.tgz#656fba2e773c20bb107189e8dca594ec83c49053" - integrity sha512-Hyb2zBb92Glc3lvhmJfy4dO2Mj29KB26Uk12Ua9EhKAdiuCTLBqgP8Oe1cGwrvDI7xA4OOcwvBMdYh0vhOUHzA== +"@react-pdf/font@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-pdf/font/-/font-4.0.2.tgz#58ede51937bb57025dcf221b107e248d0ea9d60b" + integrity sha512-/dAWu7Y2RD1RxarDZ9SkYPHgBYOhmcDnet4W/qN/m8k+A2Hr3ja54GymSR7GGxWBtxjKtNauVKrTa9LS1n8WUw== dependencies: - "@babel/runtime" "^7.20.13" - "@react-pdf/types" "^2.5.0" - cross-fetch "^3.1.5" + "@react-pdf/pdfkit" "^4.0.3" + "@react-pdf/types" "^2.9.0" fontkit "^2.0.2" is-url "^1.2.4" -"@react-pdf/image@^2.3.6": - version "2.3.6" - resolved "https://registry.yarnpkg.com/@react-pdf/image/-/image-2.3.6.tgz#72444842517f8b0c08bbf4c216d8b41110498d16" - integrity sha512-7iZDYZrZlJqNzS6huNl2XdMcLFUo68e6mOdzQeJ63d5eApdthhSHBnkGzHfLhH5t8DCpZNtClmklzuLL63ADfw== +"@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== dependencies: - "@babel/runtime" "^7.20.13" - "@react-pdf/png-js" "^2.3.1" - cross-fetch "^3.1.5" - jay-peg "^1.0.2" + "@react-pdf/png-js" "^3.0.0" + jay-peg "^1.1.1" -"@react-pdf/layout@^3.3.0": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@react-pdf/layout/-/layout-3.12.1.tgz#643cc00117846095b8d7f90e09972a4f148b85dd" - integrity sha512-BxSeykDxvADlpe4OGtQ7NH46QXq3uImAYsTHOPLCwbXMniQ1O3uCBx7H+HthxkCNshgYVPp9qS3KyvQv/oIZwg== - dependencies: - "@babel/runtime" "^7.20.13" - "@react-pdf/fns" "2.2.1" - "@react-pdf/image" "^2.3.6" - "@react-pdf/pdfkit" "^3.1.10" - "@react-pdf/primitives" "^3.1.1" - "@react-pdf/stylesheet" "^4.2.5" - "@react-pdf/textkit" "^4.4.1" - "@react-pdf/types" "^2.5.0" - cross-fetch "^3.1.5" +"@react-pdf/layout@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@react-pdf/layout/-/layout-4.4.0.tgz#26ddf73951ab0ce923689730d3b8eaf0b0db4841" + integrity sha512-Aq+Cc6JYausWLoks2FvHe3PwK9cTuvksB2uJ0AnkKJEUtQbvCq8eCRb1bjbbwIji9OzFRTTzZij7LzkpKHjIeA== + dependencies: + "@react-pdf/fns" "3.1.2" + "@react-pdf/image" "^3.0.3" + "@react-pdf/primitives" "^4.1.1" + "@react-pdf/stylesheet" "^6.1.0" + "@react-pdf/textkit" "^6.0.0" + "@react-pdf/types" "^2.9.0" emoji-regex "^10.3.0" queue "^6.0.1" - yoga-layout "^2.0.1" + yoga-layout "^3.2.1" -"@react-pdf/pdfkit@^3.0.1", "@react-pdf/pdfkit@^3.1.10": - version "3.1.10" - resolved "https://registry.yarnpkg.com/@react-pdf/pdfkit/-/pdfkit-3.1.10.tgz#06c919d6279936a07e75929ed5e1b8c1edc5e1a0" - integrity sha512-P/qPBtCFo2HDJD0i6NfbmoBRrsOVO8CIogYsefwG4fklTo50zNgnMM5U1WLckTuX8Qt1ThiQuokmTG5arheblA== +"@react-pdf/pdfkit@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@react-pdf/pdfkit/-/pdfkit-4.0.3.tgz#8b8a0e7e2aadbbada738a1c164f06ffff2947c8b" + integrity sha512-k+Lsuq8vTwWsCqTp+CCB4+2N+sOTFrzwGA7aw3H9ix/PDWR9QksbmNg0YkzGbLAPI6CeawmiLHcf4trZ5ecLPQ== dependencies: "@babel/runtime" "^7.20.13" - "@react-pdf/png-js" "^2.3.1" + "@react-pdf/png-js" "^3.0.0" browserify-zlib "^0.2.0" crypto-js "^4.2.0" fontkit "^2.0.2" - jay-peg "^1.0.2" + jay-peg "^1.1.1" + linebreak "^1.1.0" vite-compatible-readable-stream "^3.6.1" -"@react-pdf/png-js@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@react-pdf/png-js/-/png-js-2.3.1.tgz#5381d5443ac1134e98fc446fa1debb45950665bc" - integrity sha512-pEZ18I4t1vAUS4lmhvXPmXYP4PHeblpWP/pAlMMRkEyP7tdAeHUN7taQl9sf9OPq7YITMY3lWpYpJU6t4CZgZg== +"@react-pdf/png-js@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@react-pdf/png-js/-/png-js-3.0.0.tgz#c0b7dc7c77e36f0830e9b7bccca7ddd64ada1c5e" + integrity sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA== dependencies: browserify-zlib "^0.2.0" -"@react-pdf/primitives@^3.0.0", "@react-pdf/primitives@^3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@react-pdf/primitives/-/primitives-3.1.1.tgz#45a3253806fa61046f3821b8553bfee3cd848d14" - integrity sha512-miwjxLwTnO3IjoqkTVeTI+9CdyDggwekmSLhVCw+a/7FoQc+gF3J2dSKwsHvAcVFM0gvU8mzCeTofgw0zPDq0w== +"@react-pdf/primitives@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@react-pdf/primitives/-/primitives-4.1.1.tgz#c7bfb7e83173661b6ec50ada4aba8dc9e94d0563" + integrity sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ== -"@react-pdf/render@^3.2.1": - version "3.4.4" - resolved "https://registry.yarnpkg.com/@react-pdf/render/-/render-3.4.4.tgz#d04bc3b3745dad120721ce6d8655128d552de818" - integrity sha512-CfGxWmVgrY3JgmB1iMnz2W6Ck+8pisZeFt8vGlxP+JfT+0onr208pQvGSV5KwA9LGhAdABxqc/+y17V3vtKdFA== +"@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== + dependencies: + object-assign "^4.1.1" + scheduler "0.25.0-rc-603e6108-20241029" + +"@react-pdf/render@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@react-pdf/render/-/render-4.3.0.tgz#454542e87db70a3319323f8fbc5d1003db4e8c1e" + integrity sha512-MdWfWaqO6d7SZD75TZ2z5L35V+cHpyA43YNRlJNG0RJ7/MeVGDQv12y/BXOJgonZKkeEGdzM3EpAt9/g4E22WA== dependencies: "@babel/runtime" "^7.20.13" - "@react-pdf/fns" "2.2.1" - "@react-pdf/primitives" "^3.1.1" - "@react-pdf/textkit" "^4.4.1" - "@react-pdf/types" "^2.5.0" + "@react-pdf/fns" "3.1.2" + "@react-pdf/primitives" "^4.1.1" + "@react-pdf/textkit" "^6.0.0" + "@react-pdf/types" "^2.9.0" 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@3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-pdf/renderer/-/renderer-3.1.2.tgz#314823867f54ea3e6521756de8c047bf34c77040" - integrity sha512-r6L4PISzPDI59PU8e59cotTApiIZeUqkCy1ZluveJBGTsjE9qMhnKAtDFp1Ml3iKew0dyNkbgAOGAZOttiQxvQ== - dependencies: - "@babel/runtime" "^7.16.4" - "@react-pdf/font" "^2.3.1" - "@react-pdf/layout" "^3.3.0" - "@react-pdf/pdfkit" "^3.0.1" - "@react-pdf/primitives" "^3.0.0" - "@react-pdf/render" "^3.2.1" - "@react-pdf/types" "^2.2.0" - loose-envify "^1.1.0" +"@react-pdf/renderer@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@react-pdf/renderer/-/renderer-4.3.0.tgz#21a41e0cf0db703e3cc54f6eb7d6cd78b460de06" + integrity sha512-28gpA69fU9ZQrDzmd5xMJa1bDf8t0PT3ApUKBl2PUpoE/x4JlvCB5X66nMXrfFrgF2EZrA72zWQAkvbg7TE8zw== + dependencies: + "@babel/runtime" "^7.20.13" + "@react-pdf/fns" "3.1.2" + "@react-pdf/font" "^4.0.2" + "@react-pdf/layout" "^4.4.0" + "@react-pdf/pdfkit" "^4.0.3" + "@react-pdf/primitives" "^4.1.1" + "@react-pdf/reconciler" "^1.1.4" + "@react-pdf/render" "^4.3.0" + "@react-pdf/types" "^2.9.0" + events "^3.3.0" object-assign "^4.1.1" prop-types "^15.6.2" queue "^6.0.1" - scheduler "^0.17.0" -"@react-pdf/stylesheet@^4.2.5": - version "4.2.5" - resolved "https://registry.yarnpkg.com/@react-pdf/stylesheet/-/stylesheet-4.2.5.tgz#9ab1c76333d725ecad07109a8531a93f0a6995c6" - integrity sha512-XnmapeCW+hDuNdVwpuvO04WKv71wAs8aH+saIq29Bo2fp1SxznHTcQArTZtK6Wgr/E9BHXeB2iAPpUZuI6G+xA== +"@react-pdf/stylesheet@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@react-pdf/stylesheet/-/stylesheet-6.1.0.tgz#ca6b5b0f7cc749b36379379d943f648f8527d71a" + integrity sha512-BGZ2sYNUp38VJUegjva/jsri3iiRGnVNjWI+G9dTwAvLNOmwFvSJzqaCsEnqQ/DW5mrTBk/577FhDY7pv6AidA== dependencies: - "@babel/runtime" "^7.20.13" - "@react-pdf/fns" "2.2.1" - "@react-pdf/types" "^2.5.0" + "@react-pdf/fns" "3.1.2" + "@react-pdf/types" "^2.9.0" 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@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@react-pdf/textkit/-/textkit-4.4.1.tgz#b4e4181ea7d4269c54a1794d4022b55c5135f0e7" - integrity sha512-Jl9wdTqIvJ5pX+vAGz0EOhP7ut5Two9H6CzTKo/YYPeD79cM2yTXF3JzTERBC28y7LR0Waq9D2LHQjI+b/EYUQ== +"@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== dependencies: - "@babel/runtime" "^7.20.13" - "@react-pdf/fns" "2.2.1" + "@react-pdf/fns" "3.1.2" bidi-js "^1.0.2" hyphen "^1.6.4" unicode-properties "^1.4.1" -"@react-pdf/types@^2.2.0", "@react-pdf/types@^2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.5.0.tgz#3e14a41c5a546faa9811c4105611c7ed7c883e02" - integrity sha512-XsVRkt0hQ60I4e3leAVt+aZR3KJCaJd179BfJHAv4F4x6Vq3yqkry8lcbUWKGKDw1j3/8sW4FsgGR41SFvsG9A== +"@react-pdf/types@^2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.9.0.tgz#a2721a847cb1ace2c31ca29b0303c7b51a2241c2" + integrity sha512-ckj80vZLlvl9oYrQ4tovEaqKWP3O06Eb1D48/jQWbdwz1Yh7Y9v1cEmwlP8ET+a1Whp8xfdM0xduMexkuPANCQ== + dependencies: + "@react-pdf/font" "^4.0.2" + "@react-pdf/primitives" "^4.1.1" + "@react-pdf/stylesheet" "^6.1.0" -"@reduxjs/toolkit@1.9.2": - version "1.9.2" - resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.2.tgz#4cd153491118038e2eebcb63b2264e42a8a2d74c" - integrity sha512-5ZAZ7hwAKWSii5T6NTPmgIBUqyVdlDs+6JjThz6J6dmHLDm6zCzv2OjHIFAi3Vvs1qjmXU0bm6eBojukYXjVMQ== +"@reduxjs/toolkit@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.6.1.tgz#532ef3d3f1656461b421f0ba0a4fa1628163a0c5" + integrity sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw== dependencies: - immer "^9.0.16" - redux "^4.2.0" - redux-thunk "^2.4.2" - reselect "^4.1.7" + immer "^10.0.3" + redux "^5.0.1" + redux-thunk "^3.1.0" + reselect "^5.1.0" "@remirror/core-constants@3.0.0": version "3.0.0" @@ -1688,135 +1705,160 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@rushstack/eslint-patch@^1.1.3": - version "1.10.4" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" - integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== +"@rushstack/eslint-patch@^1.10.3": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz#75dce8e972f90bba488e2b0cc677fb233aa357ab" + integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ== "@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" integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== -"@svgr/babel-plugin-add-jsx-attribute@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" - integrity sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ== +"@svgdotjs/svg.draggable.js@^3.0.4": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz#bca1065ec27b1dbae5a92a0558777ed964a395cb" + integrity sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA== + +"@svgdotjs/svg.filter.js@^3.0.8": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz#758e336b79e73a6797358d655b60842131a9a52b" + integrity sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw== + dependencies: + "@svgdotjs/svg.js" "^3.2.4" + +"@svgdotjs/svg.js@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz#4716be92a64c66b29921b63f7235fcfb953fb13a" + integrity sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg== -"@svgr/babel-plugin-remove-jsx-attribute@*": +"@svgdotjs/svg.resize.js@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz#732e4cae15d09ad3021adeac63bc9fad0dc7255a" + integrity sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA== + +"@svgdotjs/svg.select.js@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.select.js/-/svg.select.js-4.0.2.tgz#80a10409e6c73206218690eac5c9f94f8c8909b5" + integrity sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g== + +"@svgr/babel-plugin-add-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== + +"@svgr/babel-plugin-remove-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== -"@svgr/babel-plugin-remove-jsx-empty-expression@*": +"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== -"@svgr/babel-plugin-replace-jsx-attribute-value@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz#fb9d22ea26d2bc5e0a44b763d4c46d5d3f596c60" - integrity sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg== - -"@svgr/babel-plugin-svg-dynamic-title@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz#01b2024a2b53ffaa5efceaa0bf3e1d5a4c520ce4" - integrity sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw== - -"@svgr/babel-plugin-svg-em-dimensions@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz#dd3fa9f5b24eb4f93bcf121c3d40ff5facecb217" - integrity sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA== - -"@svgr/babel-plugin-transform-react-native-svg@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz#1d8e945a03df65b601551097d8f5e34351d3d305" - integrity sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg== - -"@svgr/babel-plugin-transform-svg-component@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz#48620b9e590e25ff95a80f811544218d27f8a250" - integrity sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ== - -"@svgr/babel-preset@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.5.1.tgz#b90de7979c8843c5c580c7e2ec71f024b49eb828" - integrity sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "^6.5.1" - "@svgr/babel-plugin-remove-jsx-attribute" "*" - "@svgr/babel-plugin-remove-jsx-empty-expression" "*" - "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.5.1" - "@svgr/babel-plugin-svg-dynamic-title" "^6.5.1" - "@svgr/babel-plugin-svg-em-dimensions" "^6.5.1" - "@svgr/babel-plugin-transform-react-native-svg" "^6.5.1" - "@svgr/babel-plugin-transform-svg-component" "^6.5.1" - -"@svgr/core@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.1.tgz#d3e8aa9dbe3fbd747f9ee4282c1c77a27410488a" - integrity sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw== - dependencies: - "@babel/core" "^7.19.6" - "@svgr/babel-preset" "^6.5.1" - "@svgr/plugin-jsx" "^6.5.1" +"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== + +"@svgr/babel-plugin-svg-dynamic-title@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== + +"@svgr/babel-plugin-svg-em-dimensions@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== + +"@svgr/babel-plugin-transform-react-native-svg@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== + +"@svgr/babel-plugin-transform-svg-component@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== + +"@svgr/babel-preset@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" + "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" + "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" + "@svgr/babel-plugin-transform-svg-component" "8.0.0" + +"@svgr/core@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" camelcase "^6.2.0" - cosmiconfig "^7.0.1" + cosmiconfig "^8.1.3" + snake-case "^3.0.4" -"@svgr/hast-util-to-babel-ast@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz#81800bd09b5bcdb968bf6ee7c863d2288fdb80d2" - integrity sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw== +"@svgr/hast-util-to-babel-ast@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== dependencies: - "@babel/types" "^7.20.0" + "@babel/types" "^7.21.3" entities "^4.4.0" -"@svgr/plugin-jsx@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz#0e30d1878e771ca753c94e69581c7971542a7072" - integrity sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw== +"@svgr/plugin-jsx@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== dependencies: - "@babel/core" "^7.19.6" - "@svgr/babel-preset" "^6.5.1" - "@svgr/hast-util-to-babel-ast" "^6.5.1" + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + "@svgr/hast-util-to-babel-ast" "8.0.0" svg-parser "^2.0.4" -"@svgr/plugin-svgo@^6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz#0f91910e988fc0b842f88e0960c2862e022abe84" - integrity sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ== +"@svgr/plugin-svgo@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== dependencies: - cosmiconfig "^7.0.1" - deepmerge "^4.2.2" - svgo "^2.8.0" + cosmiconfig "^8.1.3" + deepmerge "^4.3.1" + svgo "^3.0.2" -"@svgr/webpack@6.5.1": - version "6.5.1" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-6.5.1.tgz#ecf027814fc1cb2decc29dc92f39c3cf691e40e8" - integrity sha512-cQ/AsnBkXPkEK8cLbv4Dm7JGXq2XrumKnL1dRpJD9rIO2fTIlJI9a1uCciYG1F2aUsox/hJQyNGbt3soDxSRkA== +"@svgr/webpack@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" + integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== dependencies: - "@babel/core" "^7.19.6" - "@babel/plugin-transform-react-constant-elements" "^7.18.12" - "@babel/preset-env" "^7.19.4" + "@babel/core" "^7.21.3" + "@babel/plugin-transform-react-constant-elements" "^7.21.3" + "@babel/preset-env" "^7.20.2" "@babel/preset-react" "^7.18.6" - "@babel/preset-typescript" "^7.18.6" - "@svgr/core" "^6.5.1" - "@svgr/plugin-jsx" "^6.5.1" - "@svgr/plugin-svgo" "^6.5.1" + "@babel/preset-typescript" "^7.21.0" + "@svgr/core" "8.1.0" + "@svgr/plugin-jsx" "8.1.0" + "@svgr/plugin-svgo" "8.1.0" -"@swc/helpers@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" - integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== - dependencies: - tslib "^2.4.0" +"@swc/counter@0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/helpers@^0.5.12": - version "0.5.13" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.13.tgz#33e63ff3cd0cade557672bd7888a39ce7d115a8c" - integrity sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w== +"@swc/helpers@0.5.15", "@swc/helpers@^0.5.12": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== dependencies: - tslib "^2.4.0" + tslib "^2.8.0" "@tanstack/match-sorter-utils@8.19.4": version "8.19.4" @@ -1825,245 +1867,291 @@ dependencies: remove-accents "0.5.0" -"@tanstack/query-core@5.54.1": - version "5.54.1" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.54.1.tgz#8d6c5e6691dd023f9181d69f7f9d790f52f1bdda" - integrity sha512-hKS+WRpT5zBFip21pB6Jx1C0hranWQrbv5EJ7qPoiV5MYI3C8rTCqWC9DdBseiPT1JgQWh8Y55YthuYZNiw3Xw== +"@tanstack/query-core@5.69.0": + version "5.69.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.69.0.tgz#c434505987ade936dc53e6e27aa1406b0295516f" + integrity sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ== -"@tanstack/query-devtools@5.54.0": - version "5.54.0" - resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.54.0.tgz#dc0faa2551c6c65e54c273a1d09f1abf38f1329e" - integrity sha512-B8Sa6mh7/4m2fyk2/YnUXeOZ1/us7G/C/i1It8YcCbieXc8vf1AdSYjR+mZIoJeKOKLqA741hZqfj8d4F1NCVg== +"@tanstack/query-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.76.0.tgz#3b4d5d34ce307ba0cf7d1a3e90d7adcdc6c46be0" + integrity sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg== + +"@tanstack/query-devtools@5.67.2": + version "5.67.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.67.2.tgz#890ae9913bd21d3969c7fd85c68b1bd1500cfc57" + integrity sha512-O4QXFFd7xqp6EX7sdvc9tsVO8nm4lpWBqwpgjpVLW5g7IeOY6VnS/xvs/YzbRhBVkKTMaJMOUGU7NhSX+YGoNg== + +"@tanstack/query-persist-client-core@5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.76.0.tgz#a3bcdd687384dc6b5b61b402bef153ad54515321" + integrity sha512-xcTZjILf4q49Nsl6wcnhBYZ4O0gpnuNwV6vPIEWIrwTuSNWz2zd/g9bc8SxnXy7xCV8SM1H0IJn8KjLQIUb2ag== + dependencies: + "@tanstack/query-core" "5.76.0" + +"@tanstack/query-sync-storage-persister@^5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.76.0.tgz#29643062f1a424873afb22032ce70ee72436bb9b" + integrity sha512-N8d8voY61XkM+jfXTySduLrevD6wRM3pwQ1kG0syLiWWx/sX2+CpaTMSPr0GggjQuhmjhUPo83LaV+e449tizA== + dependencies: + "@tanstack/query-core" "5.76.0" + "@tanstack/query-persist-client-core" "5.76.0" "@tanstack/react-query-devtools@^5.51.11": - version "5.54.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.54.1.tgz#6840c665d629bfb3e7b70a14d3544b7445e014f2" - integrity sha512-6kJoLujP1f+8dSoOjK15uJl79XhTAdyPIKIcMJ33s5zIva6d7AUuTWoj7opcfkUvU/Jy0xXivHPsrhFHhi0SxA== + version "5.69.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.69.0.tgz#2cb8083028aab591b9a82caf68cd7a383a0c8b1a" + integrity sha512-sYklnou3IKAemqB5wJeBwjmG5bUGDKAL5/I4pVA+aqSnsNibVLt8/pAU976uuJ5K71w71bHtI/AMxiIs3gtkEA== dependencies: - "@tanstack/query-devtools" "5.54.0" + "@tanstack/query-devtools" "5.67.2" + +"@tanstack/react-query-persist-client@^5.76.0": + version "5.76.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.76.0.tgz#97718fec844708cb98a5902d4b1eeb72adea555b" + integrity sha512-QPKgkHX1yC1Ec21FTQHBTbQcHYI+6157DgsmxABp94H7/ZUJ3szZ7wdpdBPQyZ9VxBXlKRN+aNZkOPC90+r/uA== + dependencies: + "@tanstack/query-persist-client-core" "5.76.0" "@tanstack/react-query@^5.51.11": - version "5.54.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.54.1.tgz#55d9d9b290362160008bf5d5d7dedd485afbfc5e" - integrity sha512-SuMi4JBYv49UtmiRyqjxY7XAnE1qwLht9nlkC8sioxFXz5Uzj30lepiKf2mYXuXfC7fHYjTrAPkNx+427pRHXA== + version "5.69.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.69.0.tgz#8d58e800854cc11d0aa2c39569f53ae32ba442a9" + integrity sha512-Ift3IUNQqTcaFa1AiIQ7WCb/PPy8aexZdq9pZWLXhfLcLxH0+PZqJ2xFImxCpdDZrFRZhLJrh76geevS5xjRhA== dependencies: - "@tanstack/query-core" "5.54.1" + "@tanstack/query-core" "5.69.0" -"@tanstack/react-table@8.20.5", "@tanstack/react-table@^8.19.2": - version "8.20.5" - resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.5.tgz#19987d101e1ea25ef5406dce4352cab3932449d8" - integrity sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA== +"@tanstack/react-table@8.20.6": + version "8.20.6" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.6.tgz#a1f3103327aa59aa621931f4087a7604a21054d0" + integrity sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ== dependencies: "@tanstack/table-core" "8.20.5" -"@tanstack/react-virtual@3.10.6": - version "3.10.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.6.tgz#f90f97d50a8d83dcd3c3a2d425aadbb55d4837db" - integrity sha512-xaSy6uUxB92O8mngHZ6CvbhGuqxQ5lIZWCBy+FjhrbHmOwc6BnOnKkYm2FsB1/BpKw/+FVctlMbEtI+F6I1aJg== +"@tanstack/react-table@^8.19.2": + version "8.21.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.2.tgz#6a7fce828b64547e33f4606ada8114db496007cc" + integrity sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg== dependencies: - "@tanstack/virtual-core" "3.10.6" + "@tanstack/table-core" "8.21.2" + +"@tanstack/react-virtual@3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" + integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ== + dependencies: + "@tanstack/virtual-core" "3.11.2" "@tanstack/table-core@8.20.5": version "8.20.5" resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== -"@tanstack/virtual-core@3.10.6": - version "3.10.6" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.6.tgz#babe3989b2344a5f12fc64129f9bbed5d3402999" - integrity sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw== +"@tanstack/table-core@8.21.2": + version "8.21.2" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.2.tgz#dd57595a1773652bb6fb437e90a5f5386a49fd7e" + integrity sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA== + +"@tanstack/virtual-core@3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" + integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== -"@tiptap/core@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.9.1.tgz#ceed211a9ecfe25a94e0e0863936169990e75aee" - integrity sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw== +"@tiptap/core@^2.11.5", "@tiptap/core@^2.9.1": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.11.5.tgz#2bf1b08c4ca2467778d0a109634c45ab475522f4" + integrity sha512-jb0KTdUJaJY53JaN7ooY3XAxHQNoMYti/H6ANo707PsLXVeEqJ9o8+eBup1JU5CuwzrgnDc2dECt2WIGX9f8Jw== -"@tiptap/extension-blockquote@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.9.1.tgz#e27ae65b6eb753bf0bd4ed717121338a7358e299" - integrity sha512-Y0jZxc/pdkvcsftmEZFyG+73um8xrx6/DMfgUcNg3JAM63CISedNcr+OEI11L0oFk1KFT7/aQ9996GM6Kubdqg== +"@tiptap/extension-blockquote@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.11.5.tgz#d43ae78f5eba7de1b9138820502e950bae83c31c" + integrity sha512-MZfcRIzKRD8/J1hkt/eYv49060GTL6qGR3NY/oTDuw2wYzbQXXLEbjk8hxAtjwNn7G+pWQv3L+PKFzZDxibLuA== -"@tiptap/extension-bold@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.9.1.tgz#8f078766b043ab44208cb0610f1847263b4313cf" - integrity sha512-e2P1zGpnnt4+TyxTC5pX/lPxPasZcuHCYXY0iwQ3bf8qRQQEjDfj3X7EI+cXqILtnhOiviEOcYmeu5op2WhQDg== +"@tiptap/extension-bold@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bold/-/extension-bold-2.11.5.tgz#7fc13d835067fbee4ff2be83a694f5200ba50e41" + integrity sha512-OAq03MHEbl7MtYCUzGuwb0VpOPnM0k5ekMbEaRILFU5ZC7cEAQ36XmPIw1dQayrcuE8GZL35BKub2qtRxyC9iA== -"@tiptap/extension-bubble-menu@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.9.1.tgz#b130d8a0e2879a9378c2e1978c8c3122a1418fa8" - integrity sha512-DWUF6NG08/bZDWw0jCeotSTvpkyqZTi4meJPomG9Wzs/Ol7mEwlNCsCViD999g0+IjyXFatBk4DfUq1YDDu++Q== +"@tiptap/extension-bubble-menu@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.11.5.tgz#75da9bcea2a6579cd3ad41cf82f7bc7369c1816d" + integrity sha512-rx+rMd7EEdht5EHLWldpkzJ56SWYA9799b33ustePqhXd6linnokJCzBqY13AfZ9+xp3RsR6C0ZHI9GGea0tIA== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-bullet-list@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.9.1.tgz#25d28f5f141404142be9f965413ab2ecea61de9e" - integrity sha512-0hizL/0j9PragJObjAWUVSuGhN1jKjCFnhLQVRxtx4HutcvS/lhoWMvFg6ZF8xqWgIa06n6A7MaknQkqhTdhKA== +"@tiptap/extension-bullet-list@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.5.tgz#84c6bf623c5dffcd73dd24d012c9636191031d43" + integrity sha512-VXwHlX6A/T6FAspnyjbKDO0TQ+oetXuat6RY1/JxbXphH42nLuBaGWJ6pgy6xMl6XY8/9oPkTNrfJw/8/eeRwA== -"@tiptap/extension-code-block@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.9.1.tgz#5a8c76729759e4505af40234c6011ad674ae4f7a" - integrity sha512-A/50wPWDqEUUUPhrwRKILP5gXMO5UlQ0F6uBRGYB9CEVOREam9yIgvONOnZVJtszHqOayjIVMXbH/JMBeq11/g== +"@tiptap/extension-code-block@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.11.5.tgz#b90cea403884630f3f86c7629815250e8a266802" + integrity sha512-ksxMMvqLDlC+ftcQLynqZMdlJT1iHYZorXsXw/n+wuRd7YElkRkd6YWUX/Pq/njFY6lDjKiqFLEXBJB8nrzzBA== -"@tiptap/extension-code@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.9.1.tgz#5652c379cbdf06f95c90f93085256b24d421d9d9" - integrity sha512-WQqcVGe7i/E+yO3wz5XQteU1ETNZ00euUEl4ylVVmH2NM4Dh0KDjEhbhHlCM0iCfLUo7jhjC7dmS+hMdPUb+Tg== +"@tiptap/extension-code@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.11.5.tgz#a550c544804e65507ab66dc8ab89a1e2f7d9228d" + integrity sha512-xOvHevNIQIcCCVn9tpvXa1wBp0wHN/2umbAZGTVzS+AQtM7BTo0tz8IyzwxkcZJaImONcUVYLOLzt2AgW1LltA== -"@tiptap/extension-document@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.9.1.tgz#ea65a86a4d2524ec65fc4775122f652840a89386" - integrity sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg== +"@tiptap/extension-document@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.11.5.tgz#1d650d232df46cf07b83e0a5cc64db1c70057f37" + integrity sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g== -"@tiptap/extension-dropcursor@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.9.1.tgz#737a6b40272d5aaaedd068ec93433564ba330909" - integrity sha512-wJZspSmJRkDBtPkzFz1g7gvZOEOayk8s93UHsgbJxcV4VWHYleZ5XhT74sZunSjefNDm3qC6v2BSgLp3vNHVKQ== +"@tiptap/extension-dropcursor@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.5.tgz#a1d6fad3379551449534bdb8135da2577a8ec8fb" + integrity sha512-uIN7L3FU0904ec7FFFbndO7RQE/yiON4VzAMhNn587LFMyWO8US139HXIL4O8dpZeYwYL3d1FnDTflZl6CwLlg== -"@tiptap/extension-floating-menu@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.9.1.tgz#d0c81ec19b0c57e28e92a990f3cf94c8f256fc96" - integrity sha512-MxZ7acNNsoNaKpetxfwi3Z11Bgrh0T2EJlCV77v9N1vWK38+st3H1WJanmLbPNtc2ocvhHJrz+DjDz3CWxQ9rQ== +"@tiptap/extension-floating-menu@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.11.5.tgz#97868901bae46e1826b9d2cfe5a4a33a446adfc1" + integrity sha512-HsMI0hV5Lwzm530Z5tBeyNCBNG38eJ3qjfdV2OHlfSf3+KOEfn6a5AUdoNaZO02LF79/8+7BaYU2drafag9cxQ== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-gapcursor@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.9.1.tgz#04db79acd0d17f4aedfcf23233769ad2bf8a5817" - integrity sha512-jsRBmX01vr+5H02GljiHMo0n5H1vzoMLmFarxe0Yq2d2l9G/WV2VWX2XnGliqZAYWd1bI0phs7uLQIN3mxGQTw== +"@tiptap/extension-gapcursor@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.5.tgz#6771e387d90ef85ee834f4572627d76e303e1297" + integrity sha512-kcWa+Xq9cb6lBdiICvLReuDtz/rLjFKHWpW3jTTF3FiP3wx4H8Rs6bzVtty7uOVTfwupxZRiKICAMEU6iT0xrQ== -"@tiptap/extension-hard-break@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.9.1.tgz#dac8d752801ca217305affb54507f2a1769acf80" - integrity sha512-fCuaOD/b7nDjm47PZ58oanq7y4ccS2wjPh42Qm0B0yipu/1fmC8eS1SmaXmk28F89BLtuL6uOCtR1spe+lZtlQ== +"@tiptap/extension-hard-break@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.11.5.tgz#cf9610846cb7ab0f3a8d8dc37fd1fcee6a39d72f" + integrity sha512-q9doeN+Yg9F5QNTG8pZGYfNye3tmntOwch683v0CCVCI4ldKaLZ0jG3NbBTq+mosHYdgOH2rNbIORlRRsQ+iYQ== -"@tiptap/extension-heading@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.9.1.tgz#83a2cf3174b6e3da66298b5cd424aca8dc4738bb" - integrity sha512-SjZowzLixOFaCrV2cMaWi1mp8REK0zK1b3OcVx7bCZfVSmsOETJyrAIUpCKA8o60NwF7pwhBg0MN8oXlNKMeFw== +"@tiptap/extension-heading@^2.11.5", "@tiptap/extension-heading@^2.9.1": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.11.5.tgz#e9a54e4cbb5c9c7fc95a24cc894a16751ecd185f" + integrity sha512-x/MV53psJ9baRcZ4k4WjnCUBMt8zCX7mPlKVT+9C/o+DEs/j/qxPLs95nHeQv70chZpSwCQCt93xMmuF0kPoAg== -"@tiptap/extension-history@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.9.1.tgz#7e60f4add5cdcbfa18a2edb7e9571c72f4c9c31a" - integrity sha512-wp9qR1NM+LpvyLZFmdNaAkDq0d4jDJ7z7Fz7icFQPu31NVxfQYO3IXNmvJDCNu8hFAbImpA5aG8MBuwzRo0H9w== +"@tiptap/extension-history@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.11.5.tgz#c636c8da784ad25886eb617cff6b4752ac9586d1" + integrity sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ== -"@tiptap/extension-horizontal-rule@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.9.1.tgz#90acbd931aadd52affa3f8ac2aecead70839b342" - integrity sha512-ydUhABeaBI1CoJp+/BBqPhXINfesp1qMNL/jiDcMsB66fsD4nOyphpAJT7FaRFZFtQVF06+nttBtFZVkITQVqg== +"@tiptap/extension-horizontal-rule@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.5.tgz#b876f606386c51bc2ff45d4bd26267f5b104a850" + integrity sha512-3up2r1Du8/5/4ZYzTC0DjTwhgPI3dn8jhOCLu73m5F3OGvK/9whcXoeWoX103hYMnGDxBlfOje71yQuN35FL4A== "@tiptap/extension-image@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.9.1.tgz#774d8c43329920fb0f71df8eebacfeb9d1ddc823" - integrity sha512-aGqJnsuS8oagIhsx7wetm8jw4NEDsOV0OSx4FQ4VPlUqWlnzK0N+erFKKJmXTdAxL8PGzoPSlITFH63MV3eV3Q== - -"@tiptap/extension-italic@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.9.1.tgz#575f5f278d2f6999e0ad1e3b91010a010cb650e2" - integrity sha512-VkNA6Vz96+/+7uBlsgM7bDXXx4b62T1fDam/3UKifA72aD/fZckeWrbT7KrtdUbzuIniJSbA0lpTs5FY29+86Q== - -"@tiptap/extension-list-item@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.9.1.tgz#7e4e3f6805a716e683906901622eb9deb4be24f0" - integrity sha512-6O4NtYNR5N2Txi4AC0/4xMRJq9xd4+7ShxCZCDVL0WDVX37IhaqMO7LGQtA6MVlYyNaX4W1swfdJaqrJJ5HIUw== - -"@tiptap/extension-ordered-list@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.9.1.tgz#fe9d560ac548ce2e16f51fc92dfcc12ac9f92231" - integrity sha512-6J9jtv1XP8dW7/JNSH/K4yiOABc92tBJtgCsgP8Ep4+fjfjdj4HbjS1oSPWpgItucF2Fp/VF8qg55HXhjxHjTw== - -"@tiptap/extension-paragraph@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.9.1.tgz#1cce648545b7b03d9af6fb393b0af602cf567135" - integrity sha512-JOmT0xd4gd3lIhLwrsjw8lV+ZFROKZdIxLi0Ia05XSu4RLrrvWj0zdKMSB+V87xOWfSB3Epo95zAvnPox5Q16A== - -"@tiptap/extension-strike@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.9.1.tgz#8c8553e81696e6c30a6801a1cae6afaa4c37f002" - integrity sha512-V5aEXdML+YojlPhastcu7w4biDPwmzy/fWq0T2qjfu5Te/THcqDmGYVBKESBm5x6nBy5OLkanw2O+KHu2quDdg== + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.11.5.tgz#f16d05c8045dc7d84d85efdbb946a2a8713fb2d3" + integrity sha512-HbUq9AL8gb8eSuQfY/QKkvMc66ZFN/b6jvQAILGArNOgalUfGizoC6baKTJShaExMSPjBZlaAHtJiQKPaGRHaA== + +"@tiptap/extension-italic@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.11.5.tgz#63b09c7fb41ab64681983df7be8cf6bc330c0ede" + integrity sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA== + +"@tiptap/extension-list-item@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.11.5.tgz#6ada38dd4e6db889288242542bc0490b0908d190" + integrity sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg== + +"@tiptap/extension-ordered-list@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.5.tgz#c81e33b5bc885450d412e9ea644cc666407e0c13" + integrity sha512-Cu8KwruBNWAaEfshRQR0yOSaUKAeEwxW7UgbvF9cN/zZuKgK5uZosPCPTehIFCcRe+TBpRtZQh+06f/gNYpYYg== + +"@tiptap/extension-paragraph@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.11.5.tgz#05575f0264a435837483831eebffc5e3af279cb1" + integrity sha512-YFBWeg7xu/sBnsDIF/+nh9Arf7R0h07VZMd0id5Ydd2Qe3c1uIZwXxeINVtH0SZozuPIQFAT8ICe9M0RxmE+TA== + +"@tiptap/extension-strike@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.11.5.tgz#94e214dcede09f6c5f99d0c58290a1d3f5db61eb" + integrity sha512-PVfUiCqrjvsLpbIoVlegSY8RlkR64F1Rr2RYmiybQfGbg+AkSZXDeO0eIrc03//4gua7D9DfIozHmAKv1KN3ow== "@tiptap/extension-table@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.9.1.tgz#e0905e5cfb39ca99d0c9098e0efd7cd1b246e82d" - integrity sha512-OmWZFZOSZwSSEvoVUkDsRFyCXTYei/pV396Xjv9pfFzXQkVbfq/CjTp61zvb/9mmEz3rcfvfG7G39eRlZTvBNg== + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.11.5.tgz#2296c1076ef3381bca4601709956085f093ff9e0" + integrity sha512-NKXLhKWdAdURklm98YkCd2ai4fh8jY8HS/+X2s/2QiQt8Z98CU1keCm35fJEEExM234iB/hCqG5vY4JgTc0Tvw== -"@tiptap/extension-text-style@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.9.1.tgz#b9fc9cd8e90747357fbd4cac541a33aaa8b76875" - integrity sha512-LAxc0SeeiPiAVBwksczeA7BJSZb6WtVpYhy5Esvy9K0mK5kttB4KxtnXWeQzMIJZQbza65yftGKfQlexf/Y7yg== +"@tiptap/extension-text-style@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text-style/-/extension-text-style-2.11.5.tgz#f1b3882de489328203187e6256e6ee130477cfad" + integrity sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ== -"@tiptap/extension-text@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.9.1.tgz#e4cda144b0af916ee0dafb700f833cd40eeae6d9" - integrity sha512-3wo9uCrkLVLQFgbw2eFU37QAa1jq1/7oExa+FF/DVxdtHRS9E2rnUZ8s2hat/IWzvPUHXMwo3Zg2XfhoamQpCA== +"@tiptap/extension-text@^2.11.5": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.11.5.tgz#10cc6ec519aac71a6841ec9bd914ded747f6ec3f" + integrity sha512-Gq1WwyhFpCbEDrLPIHt5A8aLSlf8bfz4jm417c8F/JyU0J5dtYdmx0RAxjnLw1i7ZHE7LRyqqAoS0sl7JHDNSQ== -"@tiptap/pm@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.9.1.tgz#4d2304eb2ec611d2128e9ebcb9f28f1a5c74cd32" - integrity sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ== +"@tiptap/pm@^2.11.5", "@tiptap/pm@^2.9.1": + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.11.5.tgz#6577e277e5a991c605a3dfcebde7c0b794d8def4" + integrity sha512-z9JFtqc5ZOsdQLd9vRnXfTCQ8v5ADAfRt9Nm7SqP6FUHII8E1hs38ACzf5xursmth/VonJYb5+73Pqxk1hGIPw== dependencies: prosemirror-changeset "^2.2.1" prosemirror-collab "^1.3.1" - prosemirror-commands "^1.6.0" + prosemirror-commands "^1.6.2" prosemirror-dropcursor "^1.8.1" prosemirror-gapcursor "^1.3.2" prosemirror-history "^1.4.1" prosemirror-inputrules "^1.4.0" prosemirror-keymap "^1.2.2" - prosemirror-markdown "^1.13.0" + prosemirror-markdown "^1.13.1" prosemirror-menu "^1.2.4" - prosemirror-model "^1.22.3" + prosemirror-model "^1.23.0" prosemirror-schema-basic "^1.2.3" prosemirror-schema-list "^1.4.1" prosemirror-state "^1.4.3" - prosemirror-tables "^1.4.0" + prosemirror-tables "^1.6.3" prosemirror-trailing-node "^3.0.0" - prosemirror-transform "^1.10.0" - prosemirror-view "^1.34.3" + prosemirror-transform "^1.10.2" + prosemirror-view "^1.37.0" "@tiptap/react@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.9.1.tgz#670224686ee6e4fc01ee479b2887be197d92d520" - integrity sha512-LQJ34ZPfXtJF36SZdcn4Fiwsl2WxZ9YRJI87OLnsjJ45O+gV/PfBzz/4ap+LF8LOS0AbbGhTTjBOelPoNm+aYA== + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.11.5.tgz#46ba23a56583e95b0020eb5778c35f3dd98aa673" + integrity sha512-Dp8eHL1G+R/C4+QzAczyb3t1ovexEIZx9ln7SGEM+cT1KHKAw9XGPRgsp92+NQaYI+EdEb/YqoBOSzQcd18/OQ== dependencies: - "@tiptap/extension-bubble-menu" "^2.9.1" - "@tiptap/extension-floating-menu" "^2.9.1" + "@tiptap/extension-bubble-menu" "^2.11.5" + "@tiptap/extension-floating-menu" "^2.11.5" "@types/use-sync-external-store" "^0.0.6" fast-deep-equal "^3" - use-sync-external-store "^1.2.2" + use-sync-external-store "^1" "@tiptap/starter-kit@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.9.1.tgz#d990bfd8b8da5e13bc1c0eada7e00d6c77a09490" - integrity sha512-nsw6UF/7wDpPfHRhtGOwkj1ipIEiWZS1VGw+c14K61vM1CNj0uQ4jogbHwHZqN1dlL5Hh+FCqUHDPxG6ECbijg== - dependencies: - "@tiptap/core" "^2.9.1" - "@tiptap/extension-blockquote" "^2.9.1" - "@tiptap/extension-bold" "^2.9.1" - "@tiptap/extension-bullet-list" "^2.9.1" - "@tiptap/extension-code" "^2.9.1" - "@tiptap/extension-code-block" "^2.9.1" - "@tiptap/extension-document" "^2.9.1" - "@tiptap/extension-dropcursor" "^2.9.1" - "@tiptap/extension-gapcursor" "^2.9.1" - "@tiptap/extension-hard-break" "^2.9.1" - "@tiptap/extension-heading" "^2.9.1" - "@tiptap/extension-history" "^2.9.1" - "@tiptap/extension-horizontal-rule" "^2.9.1" - "@tiptap/extension-italic" "^2.9.1" - "@tiptap/extension-list-item" "^2.9.1" - "@tiptap/extension-ordered-list" "^2.9.1" - "@tiptap/extension-paragraph" "^2.9.1" - "@tiptap/extension-strike" "^2.9.1" - "@tiptap/extension-text" "^2.9.1" - "@tiptap/extension-text-style" "^2.9.1" - "@tiptap/pm" "^2.9.1" + version "2.11.5" + resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.11.5.tgz#7d1b0b866b10c0f9c98214588639cda204c4f3b4" + integrity sha512-SLI7Aj2ruU1t//6Mk8f+fqW+18uTqpdfLUJYgwu0CkqBckrkRZYZh6GVLk/02k3H2ki7QkFxiFbZrdbZdng0JA== + dependencies: + "@tiptap/core" "^2.11.5" + "@tiptap/extension-blockquote" "^2.11.5" + "@tiptap/extension-bold" "^2.11.5" + "@tiptap/extension-bullet-list" "^2.11.5" + "@tiptap/extension-code" "^2.11.5" + "@tiptap/extension-code-block" "^2.11.5" + "@tiptap/extension-document" "^2.11.5" + "@tiptap/extension-dropcursor" "^2.11.5" + "@tiptap/extension-gapcursor" "^2.11.5" + "@tiptap/extension-hard-break" "^2.11.5" + "@tiptap/extension-heading" "^2.11.5" + "@tiptap/extension-history" "^2.11.5" + "@tiptap/extension-horizontal-rule" "^2.11.5" + "@tiptap/extension-italic" "^2.11.5" + "@tiptap/extension-list-item" "^2.11.5" + "@tiptap/extension-ordered-list" "^2.11.5" + "@tiptap/extension-paragraph" "^2.11.5" + "@tiptap/extension-strike" "^2.11.5" + "@tiptap/extension-text" "^2.11.5" + "@tiptap/extension-text-style" "^2.11.5" + "@tiptap/pm" "^2.11.5" "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@tybys/wasm-util@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355" + integrity sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw== + dependencies: + tslib "^2.4.0" + "@types/debug@^4.0.0": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" @@ -2071,6 +2159,18 @@ dependencies: "@types/ms" "*" +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/hast@^2.0.0": version "2.3.10" resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" @@ -2078,14 +2178,26 @@ dependencies: "@types/unist" "^2" +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": - version "3.3.5" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" - integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== + version "3.3.6" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz#6bba74383cdab98e8db4e20ce5b4a6b98caed010" + integrity sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -2096,18 +2208,6 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== -"@types/lodash-es@^4.17.6": - version "4.17.12" - resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b" - integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== - dependencies: - "@types/lodash" "*" - -"@types/lodash@*", "@types/lodash@^4.14.175": - version "4.17.7" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" - integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== - "@types/markdown-it@^14.0.0": version "14.1.2" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" @@ -2116,12 +2216,12 @@ "@types/linkify-it" "^5" "@types/mdurl" "^2" -"@types/mdast@^3.0.0": - version "3.0.15" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" - integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== dependencies: - "@types/unist" "^2" + "@types/unist" "*" "@types/mdurl@^2": version "2.0.0" @@ -2129,21 +2229,21 @@ integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== "@types/ms@*": - version "0.7.34" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" - integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@*": - version "22.5.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.3.tgz#91a374e42c6e7ccb5893a87f1775f36ce1671d65" - integrity sha512-njripolh85IA9SQGTAqbmnNZTdxv7X/4OYGPz8tgy5JDr8MP+uDBa921GpYEoDDnwm0Hmn5ZPeJgiiSTPoOzkQ== + version "22.13.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.14.tgz#70d84ec91013dcd2ba2de35532a5a14c2b4cc912" + integrity sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w== dependencies: - undici-types "~6.19.2" + undici-types "~6.20.0" "@types/papaparse@^5.3.9": - version "5.3.14" - resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.14.tgz#345cc2a675a90106ff1dc33b95500dfb30748031" - integrity sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g== + version "5.3.15" + resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.15.tgz#7cafa16757a1d121422deefbb10b6310b224ecc4" + integrity sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw== dependencies: "@types/node" "*" @@ -2152,15 +2252,17 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -"@types/prop-types@*", "@types/prop-types@^15.0.0": - version "15.7.12" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" - integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/prop-types@^15.7.14": + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== -"@types/prop-types@^15.7.13": - version "15.7.13" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" - integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== +"@types/quill@^1.3.10": + version "1.3.10" + resolved "https://registry.yarnpkg.com/@types/quill/-/quill-1.3.10.tgz#dc1f7b6587f7ee94bdf5291bc92289f6f0497613" + integrity sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw== + dependencies: + parchment "^1.1.2" "@types/raf@^3.4.0": version "3.4.3" @@ -2168,93 +2270,219 @@ integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw== "@types/react-redux@^7.1.20": - version "7.1.33" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.33.tgz#53c5564f03f1ded90904e3c90f77e4bd4dc20b15" - integrity sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg== + version "7.1.34" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" + integrity sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ== dependencies: "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-transition-group@^4.4.11": - version "4.4.11" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" - integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== - dependencies: - "@types/react" "*" +"@types/react-transition-group@^4.4.11", "@types/react-transition-group@^4.4.12": + version "4.4.12" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" + integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== "@types/react@*": - version "18.3.5" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f" - integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA== + version "19.0.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.12.tgz#338b3f7854adbb784be454b3a83053127af96bd3" + integrity sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA== dependencies: - "@types/prop-types" "*" csstype "^3.0.2" +"@types/trusted-types@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + "@types/unist@^2", "@types/unist@^2.0.0": version "2.0.11" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== -"@types/use-sync-external-store@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" - integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== - "@types/use-sync-external-store@^0.0.6": version "0.0.6" 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/parser@^5.42.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" - integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== +"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz#ad1465aa6fe7e937801c291648dec951c4dc38e6" + integrity sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.28.0" + "@typescript-eslint/type-utils" "8.28.0" + "@typescript-eslint/utils" "8.28.0" + "@typescript-eslint/visitor-keys" "8.28.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^2.0.1" + +"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.28.0.tgz#85321707e8711c0e66a949ea228224af35f45c98" + integrity sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ== dependencies: - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/scope-manager" "8.28.0" + "@typescript-eslint/types" "8.28.0" + "@typescript-eslint/typescript-estree" "8.28.0" + "@typescript-eslint/visitor-keys" "8.28.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" - integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== +"@typescript-eslint/scope-manager@8.28.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz#e495b20438a3787e00498774d5625e620d68f9fe" + integrity sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw== dependencies: - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/visitor-keys" "5.62.0" + "@typescript-eslint/types" "8.28.0" + "@typescript-eslint/visitor-keys" "8.28.0" -"@typescript-eslint/types@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" - integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== +"@typescript-eslint/type-utils@8.28.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz#fc565414ebc16de1fc65e0dd8652ce02c78ca61f" + integrity sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg== + dependencies: + "@typescript-eslint/typescript-estree" "8.28.0" + "@typescript-eslint/utils" "8.28.0" + debug "^4.3.4" + ts-api-utils "^2.0.1" -"@typescript-eslint/typescript-estree@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" - integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== +"@typescript-eslint/types@8.28.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.28.0.tgz#7c73878385edfd9674c7aa10975e6c484b4f896e" + integrity sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA== + +"@typescript-eslint/typescript-estree@8.28.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz#56b999f26f7ca67b9d75d6a67af5c8b8e4e80114" + integrity sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA== dependencies: - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/visitor-keys" "5.62.0" + "@typescript-eslint/types" "8.28.0" + "@typescript-eslint/visitor-keys" "8.28.0" debug "^4.3.4" - globby "^11.1.0" + fast-glob "^3.3.2" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.0.1" + +"@typescript-eslint/utils@8.28.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.28.0.tgz#7850856620a896b7ac621ac12d49c282aefbb528" + integrity sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "8.28.0" + "@typescript-eslint/types" "8.28.0" + "@typescript-eslint/typescript-estree" "8.28.0" + +"@typescript-eslint/visitor-keys@8.28.0": + version "8.28.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz#18eb9a25cc9dadb027835c58efe93a5c4ee81969" + integrity sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg== + dependencies: + "@typescript-eslint/types" "8.28.0" + eslint-visitor-keys "^4.2.0" + +"@uiw/react-json-view@^2.0.0-alpha.30": + version "2.0.0-alpha.30" + resolved "https://registry.yarnpkg.com/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.30.tgz#85db25b1a61cccc5c6c51350894515f8b7100e52" + integrity sha512-ufvvirUQcITU9s4R12b7hn/t7ngLCYp1KbBxE+eAD35o3Ey+uxfKvgWmIwGFhV3hFXXxMJ8SHQKwl/ywNCHsDA== + +"@ungap/structured-clone@^1.0.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@unrs/resolver-binding-darwin-arm64@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.3.2.tgz#e2394af11511ed50025f890b3bbb83fc99c26e72" + integrity sha512-ddnlXgRi0Fog5+7U5Q1qY62wl95Q1lB4tXQX1UIA9YHmRCHN2twaQW0/4tDVGCvTVEU3xEayU7VemEr7GcBYUw== + +"@unrs/resolver-binding-darwin-x64@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.3.2.tgz#e971ef77c16ec295f4183dbc3b4d2498f81593de" + integrity sha512-tnl9xoEeg503jis+LW5cuq4hyLGQyqaoBL8VdPSqcewo/FL1C8POHbzl+AL25TidWYJD+R6bGUTE381kA1sT9w== + +"@unrs/resolver-binding-freebsd-x64@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.3.2.tgz#89e0ee4d86c4d5d55d7b3c9b555a1e21946bdd13" + integrity sha512-zyPn9LFCCjhKPeCtECZaiMUgkYN/VpLb4a9Xv7QriJmTaQxsuDtXqOHifrzUXIhorJTyS+5MOKDuNL0X9I4EHA== + +"@unrs/resolver-binding-linux-arm-gnueabihf@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.3.2.tgz#f26b076e3de838161f8163cc81146cf7b959b06c" + integrity sha512-UWx56Wh59Ro69fe+Wfvld4E1n9KG0e3zeouWLn8eSasyi/yVH/7ZW3CLTVFQ81oMKSpXwr5u6RpzttDXZKiO4g== + +"@unrs/resolver-binding-linux-arm-musleabihf@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.3.2.tgz#8b4effb38f066c9058ab3ab099ecc7526f7bb8cf" + integrity sha512-VYGQXsOEJtfaoY2fOm8Z9ii5idFaHFYlrq3yMFZPaFKo8ufOXYm8hnfru7qetbM9MX116iWaPC0ZX5sK+1Dr+g== + +"@unrs/resolver-binding-linux-arm64-gnu@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.3.2.tgz#77fc9bae5f0e481d226fe30c853e9b8c3542639c" + integrity sha512-3zP420zxJfYPD1rGp2/OTIBxF8E3+/6VqCG+DEO6kkDgBiloa7Y8pw1o7N9BfgAC+VC8FPZsFXhV2lpx+lLRMQ== + +"@unrs/resolver-binding-linux-arm64-musl@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.3.2.tgz#1a749cb3f5b54044828161317f67f19d4b50cd73" + integrity sha512-ZWjSleUgr88H4Kei7yT4PlPqySTuWN1OYDDcdbmMCtLWFly3ed+rkrcCb3gvqXdDbYrGOtzv3g2qPEN+WWNv5Q== + +"@unrs/resolver-binding-linux-ppc64-gnu@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.3.2.tgz#d4b0cccbaff413cfd586eefe9f0507d376af1b16" + integrity sha512-p+5OvYJ2UOlpjes3WfBlxyvQok2u26hLyPxLFHkYlfzhZW0juhvBf/tvewz1LDFe30M7zL9cF4OOO5dcvtk+cw== + +"@unrs/resolver-binding-linux-s390x-gnu@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.3.2.tgz#d031764866b1cf0bdbcf153704d2515072f8b62a" + integrity sha512-yweY7I6SqNn3kvj6vE4PQRo7j8Oz6+NiUhmgciBNAUOuI3Jq0bnW29hbHJdxZRSN1kYkQnSkbbA1tT8VnK816w== + +"@unrs/resolver-binding-linux-x64-gnu@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.3.2.tgz#9b859eb8afb094260041b93afc687e2d7426c621" + integrity sha512-fNIvtzJcGN9hzWTIayrTSk2+KHQrqKbbY+I88xMVMOFV9t4AXha4veJdKaIuuks+2JNr6GuuNdsL7+exywZ32w== + +"@unrs/resolver-binding-linux-x64-musl@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.3.2.tgz#e382ce3b9e91a333eb4cbbdea852116ff18ffd7a" + integrity sha512-OaFEw8WAjiwBGxutQgkWhoAGB5BQqZJ8Gjt/mW+m6DWNjimcxU22uWCuEtfw1CIwLlKPOzsgH0429fWmZcTGkg== + +"@unrs/resolver-binding-wasm32-wasi@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.3.2.tgz#ad6afbbc53cec1fcfc22cb57a325b66f07b87f75" + integrity sha512-u+sumtO7M0AGQ9bNQrF4BHNpUyxo23FM/yXZfmVAicTQ+mXtG06O7pm5zQUw3Mr4jRs2I84uh4O0hd8bdouuvQ== + dependencies: + "@napi-rs/wasm-runtime" "^0.2.7" + +"@unrs/resolver-binding-win32-arm64-msvc@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.3.2.tgz#5457aaf7abde1b9ca331f029ee6a4371db3f98a5" + integrity sha512-ZAJKy95vmDIHsRFuPNqPQRON8r2mSMf3p9DoX+OMOhvu2c8OXGg8MvhGRf3PNg45ozRrPdXDnngURKgaFfpGoQ== + +"@unrs/resolver-binding-win32-ia32-msvc@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.3.2.tgz#ea8186a2cb6b4a84893ffdb5974e536ddf030972" + integrity sha512-nQG4YFAS2BLoKVQFK/FrWJvFATI5DQUWQrcPcsWG9Ve5BLLHZuPOrJ2SpAJwLXQrRv6XHSFAYGI8wQpBg/CiFA== -"@typescript-eslint/visitor-keys@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" - integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== - dependencies: - "@typescript-eslint/types" "5.62.0" - eslint-visitor-keys "^3.3.0" +"@unrs/resolver-binding-win32-x64-msvc@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.3.2.tgz#76d02a262d15865bb7ea51060c6c816ca96aecaf" + integrity sha512-XBWpUP0mHya6yGBwNefhyEa6V7HgYKCxEAY4qhTm/PcAQyBPNmjj97VZJOJkVdUsyuuii7xmq0pXWX/c2aToHQ== -"@uiw/react-json-view@^2.0.0-alpha.30": - version "2.0.0-alpha.30" - resolved "https://registry.yarnpkg.com/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.30.tgz#85db25b1a61cccc5c6c51350894515f8b7100e52" - integrity sha512-ufvvirUQcITU9s4R12b7hn/t7ngLCYp1KbBxE+eAD35o3Ey+uxfKvgWmIwGFhV3hFXXxMJ8SHQKwl/ywNCHsDA== +"@yr/monotone-cubic-spline@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9" + integrity sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA== abs-svg-path@^0.1.1: version "0.1.1" @@ -2266,12 +2494,12 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2281,18 +2509,6 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -2300,17 +2516,17 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -apexcharts@3.36.3: - version "3.36.3" - resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.36.3.tgz#debd58ded07163d51e00aeb15827d594e0cc8129" - integrity sha512-8/FXEs0ohXMff07Gv28XjhPwEJphIUdq2/wii/pcvi54Tw6z1mjrV8ydN8rlWi/ve8BAPBefJkLmRWv7UOBsLw== +apexcharts@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-4.5.0.tgz#efddb1a9b48e5add201df1549ac654412182e1f9" + integrity sha512-E7ZkrVqPNBUWy/Rmg8DEIqHNBmElzICE/oxOX5Ekvs2ICQUOK/VkEkMH09JGJu+O/EA0NL31hxlmF+wrwrSLaQ== dependencies: - svg.draggable.js "^2.2.2" - svg.easing.js "^2.0.0" - svg.filter.js "^2.0.2" - svg.pathmorphing.js "^0.1.3" - svg.resize.js "^1.4.3" - svg.select.js "^3.0.1" + "@svgdotjs/svg.draggable.js" "^3.0.4" + "@svgdotjs/svg.filter.js" "^3.0.8" + "@svgdotjs/svg.js" "^3.2.4" + "@svgdotjs/svg.resize.js" "^2.0.2" + "@svgdotjs/svg.select.js" "^4.0.1" + "@yr/monotone-cubic-spline" "^1.0.3" argparse@^1.0.7: version "1.0.10" @@ -2324,20 +2540,18 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" +aria-query@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== -array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" + call-bound "^1.0.3" + is-array-buffer "^3.0.5" array-includes@^3.1.6, array-includes@^3.1.8: version "3.1.8" @@ -2351,11 +2565,6 @@ array-includes@^3.1.6, array-includes@^3.1.8: get-intrinsic "^1.2.4" is-string "^1.0.7" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" @@ -2369,36 +2578,37 @@ array.prototype.findlast@^1.2.5: es-shim-unscopables "^1.0.2" array.prototype.findlastindex@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" - integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + version "1.2.6" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564" + integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" define-properties "^1.2.1" - es-abstract "^1.23.2" + es-abstract "^1.23.9" es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-shim-unscopables "^1.0.2" + es-object-atoms "^1.1.1" + es-shim-unscopables "^1.1.0" array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" -array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== +array.prototype.flatmap@^1.3.2, array.prototype.flatmap@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" array.prototype.tosorted@^1.1.4: version "1.1.4" @@ -2411,25 +2621,29 @@ array.prototype.tosorted@^1.1.4: es-errors "^1.3.0" es-shim-unscopables "^1.0.2" -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== dependencies: array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" + call-bind "^1.0.8" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" ast-types-flow@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2440,10 +2654,10 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -attr-accept@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" - integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== +attr-accept@^2.2.4: + version "2.2.5" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e" + integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ== available-typed-arrays@^1.0.7: version "1.0.7" @@ -2453,14 +2667,14 @@ available-typed-arrays@^1.0.7: possible-typed-array-names "^1.0.0" axe-core@^4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59" - integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== + version "4.10.3" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc" + integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== axios@^1.7.2: - version "1.7.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" - integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + version "1.8.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" + integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -2481,28 +2695,28 @@ babel-plugin-macros@^3.1.0: resolve "^1.19.0" babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.11" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" - integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== + version "0.4.13" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz#7d445f0e0607ebc8fb6b01d7e8fb02069b91dd8b" + integrity sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.2" + "@babel/helper-define-polyfill-provider" "^0.6.4" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.6: - version "0.10.6" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" - integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== +babel-plugin-polyfill-corejs3@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" + integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" - core-js-compat "^3.38.0" + "@babel/helper-define-polyfill-provider" "^0.6.3" + core-js-compat "^3.40.0" babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" - integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== + version "0.6.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz#428c615d3c177292a22b4f93ed99e358d7906a9b" + integrity sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" + "@babel/helper-define-polyfill-provider" "^0.6.4" bail@^2.0.0: version "2.0.2" @@ -2519,6 +2733,11 @@ base64-arraybuffer@^1.0.2: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw== + base64-js@^1.1.2, base64-js@^1.3.0: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -2544,6 +2763,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -2565,15 +2791,15 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.23.1, browserslist@^4.23.3: - version "4.23.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" - integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== +browserslist@^4.24.0, browserslist@^4.24.4: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== dependencies: - caniuse-lite "^1.0.30001646" - electron-to-chromium "^1.5.4" - node-releases "^2.0.18" - update-browserslist-db "^1.1.0" + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" btoa@^1.2.1: version "1.2.1" @@ -2592,16 +2818,31 @@ busboy@1.6.0: dependencies: streamsearch "^1.1.0" -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" + +call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" get-intrinsic "^1.2.4" - set-function-length "^1.2.1" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" callsites@^3.0.0: version "3.1.0" @@ -2613,20 +2854,15 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -can-use-dom@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" - integrity sha512-ceOhN1DL7Y4O6M0j9ICgmTYziV89WMd96SvSl0REd8PMgrY0B/WBOPoed5S1KUmJqXgUXh8gzSe6E3ae27upsQ== - -caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001646: - version "1.0.30001655" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f" - integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== +caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001688: + version "1.0.30001707" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz#c5e104d199e6f4355a898fcd995a066c7eb9bf41" + integrity sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw== -canvg@^3.0.6: - version "3.0.10" - resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3" - integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q== +canvg@^3.0.11: + version "3.0.11" + resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.11.tgz#4b4290a6c7fa36871fac2b14e432eff33b33cf2b" + integrity sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA== dependencies: "@babel/runtime" "^7.12.5" "@types/raf" "^3.4.0" @@ -2637,14 +2873,10 @@ canvg@^3.0.6: stackblur-canvas "^2.0.0" svg-pathdata "^6.0.3" -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== chalk@^4.0.0: version "4.1.2" @@ -2654,11 +2886,21 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + character-entities@^1.0.0: version "1.2.4" resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" @@ -2674,12 +2916,17 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + client-only@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== -clone@^2.1.2: +clone@^2.1.1, clone@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== @@ -2694,13 +2941,6 @@ clsx@^2.0.0, clsx@^2.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -2708,17 +2948,12 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.1: +color-string@^1.9.0, color-string@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -2726,6 +2961,14 @@ color-string@^1.9.1: color-name "^1.0.0" simple-swizzle "^0.2.2" +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2770,24 +3013,24 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" -core-js-compat@^3.37.1, core-js-compat@^3.38.0: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" - integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== +core-js-compat@^3.40.0: + version "3.41.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.41.0.tgz#4cdfce95f39a8f27759b667cf693d96e5dda3d17" + integrity sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A== dependencies: - browserslist "^4.23.3" + browserslist "^4.24.4" core-js@^3.6.0, core-js@^3.8.3: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" - integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== + version "3.41.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.41.0.tgz#57714dafb8c751a6095d028a7428f1fb5834a776" + integrity sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA== core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: +cosmiconfig@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== @@ -2798,22 +3041,25 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.1.3: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + crelt@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== -cross-fetch@^3.1.5: - version "3.1.8" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" - integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== - dependencies: - node-fetch "^2.6.12" - -cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -2838,26 +3084,34 @@ css-line-break@^2.1.0: dependencies: utrie "^1.0.2" -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== dependencies: boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" nth-check "^2.0.1" -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" + mdn-data "2.0.28" + source-map-js "^1.0.1" -css-what@^6.0.1: +css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== @@ -2867,12 +3121,12 @@ cssjanus@^2.0.1: resolved "https://registry.yarnpkg.com/cssjanus/-/cssjanus-2.3.0.tgz#af91e639a34d8b241e5032824f3f1b7f8dd91557" integrity sha512-ZZXXn51SnxRxAZ6fdY7mBDPmA4OZd83q/J9Gdqz3YmE9TUq+9tZl+tdOnCi7PpNygI6PEkehj9rgifv5+W8a5A== -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== dependencies: - css-tree "^1.1.2" + css-tree "~2.2.0" csstype@^3.0.2, csstype@^3.1.3: version "3.1.3" @@ -2884,37 +3138,37 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -data-view-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" - integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== dependencies: - call-bind "^1.0.6" + call-bound "^1.0.3" es-errors "^1.3.0" - is-data-view "^1.0.1" + is-data-view "^1.0.2" -data-view-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" - integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" es-errors "^1.3.0" - is-data-view "^1.0.1" + is-data-view "^1.0.2" -data-view-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" - integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== dependencies: - call-bind "^1.0.6" + call-bound "^1.0.2" es-errors "^1.3.0" is-data-view "^1.0.1" -date-fns@2.29.3: - version "2.29.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" - integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== +date-fns@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== debug@^3.2.7: version "3.2.7" @@ -2923,43 +3177,31 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== +debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: - ms "2.1.2" + ms "^2.1.3" decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz#5d6ce68792808901210dac42a8e9853511e2b8bf" + integrity sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w== dependencies: character-entities "^2.0.0" -deep-equal@^2.0.5: - version "2.2.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" - integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== +deep-equal@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.2.tgz#78a561b7830eef3134c7f6f3a3d6af272a678761" + integrity sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg== dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.5" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.2" is-arguments "^1.1.1" - is-array-buffer "^3.0.2" is-date-object "^1.0.5" is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" object-is "^1.1.5" object-keys "^1.1.1" - object.assign "^4.1.4" regexp.prototype.flags "^1.5.1" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.13" deep-is@^0.1.3: version "0.1.4" @@ -2971,7 +3213,7 @@ deepmerge@^2.1.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== -deepmerge@^4.2.2: +deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -2985,7 +3227,7 @@ define-data-property@^1.0.1, define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" -define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: +define-properties@^1.1.3, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== @@ -3004,23 +3246,23 @@ dequal@^2.0.0: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +detect-libc@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + dfa@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q== -diff@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3028,13 +3270,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -3051,21 +3286,21 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" domelementtype@1, domelementtype@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@^2.0.1, domelementtype@^2.2.0: +domelementtype@^2.0.1, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== @@ -3077,17 +3312,19 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== dependencies: - domelementtype "^2.2.0" + domelementtype "^2.3.0" -dompurify@^2.2.0: - version "2.5.6" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.6.tgz#8402b501611eaa7fb3786072297fcbe2787f8592" - integrity sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ== +dompurify@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e" + integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg== + optionalDependencies: + "@types/trusted-types" "^2.0.7" domutils@^1.5.1: version "1.7.0" @@ -3097,14 +3334,31 @@ domutils@^1.5.1: dom-serializer "0" domelementtype "1" -domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== +domutils@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" duplexer2@^0.1.2: version "0.1.4" @@ -3113,15 +3367,15 @@ duplexer2@^0.1.2: dependencies: readable-stream "^2.0.2" -electron-to-chromium@^1.5.4: - version "1.5.13" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" - integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== +electron-to-chromium@^1.5.73: + version "1.5.127" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.127.tgz#350a16aa09fb7f070ad118fade31260a5c173733" + integrity sha512-Ke5OggqOtEqzCzcUyV+9jgO6L6sv1gQVKGtSExXHjD/FK0p4qzPZbrDsrCdy0DptcQprD0V80RCBYSWLMhTTgQ== -eml-parse-js@^1.1.15: - version "1.1.15" - resolved "https://registry.yarnpkg.com/eml-parse-js/-/eml-parse-js-1.1.15.tgz#fe32b0380b6123e62e870f9644df574507df662c" - integrity sha512-HOhS1y6dVvBHwqdqydJULRhNrwIYoWDOWYZVkvi8U5ByLfyb1eeGi9fTPfYuP9ooQUVwh42fTmCCHbOkd4lGSA== +eml-parse-js@^1.2.0-beta.0: + version "1.2.0-beta.0" + resolved "https://registry.yarnpkg.com/eml-parse-js/-/eml-parse-js-1.2.0-beta.0.tgz#0b024d38093b42cd29168f7f5771726390e4b1f3" + integrity sha512-fDA5OcT9DmU+6Qiv6Ki6/+fIjrZ97SE6KIB0PUK2r0nnRqBbnbaWm844l8SLTd4mc3rF0T3izc8E7E/qXFCthA== dependencies: "@sinonjs/text-encoding" "^0.7.2" js-base64 "^3.7.2" @@ -3141,14 +3395,6 @@ encodeurl@^1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -enhanced-resolve@^5.15.0: - version "5.17.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" - integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -3159,7 +3405,7 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.4.0: +entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -3171,166 +3417,153 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: - version "1.23.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" - integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== +es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: + version "1.23.9" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606" + integrity sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA== dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - data-view-buffer "^1.0.1" - data-view-byte-length "^1.0.1" - data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" + call-bind "^1.0.8" + call-bound "^1.0.3" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" es-errors "^1.3.0" es-object-atoms "^1.0.0" - es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.2.7" + get-proto "^1.0.0" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" + has-proto "^1.2.0" + has-symbols "^1.1.0" hasown "^2.0.2" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" is-callable "^1.2.7" - is-data-view "^1.0.1" - is-negative-zero "^2.0.3" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" + is-data-view "^1.0.2" + is-regex "^1.2.1" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.0" + math-intrinsics "^1.1.0" + object-inspect "^1.13.3" object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.3" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.2" - typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.18" + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== -es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - -es-iterator-helpers@^1.0.19: - version "1.0.19" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" - integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== +es-iterator-helpers@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz#d1dd0f58129054c0ad922e6a9a1e65eef435fe75" + integrity sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - es-abstract "^1.23.3" + es-abstract "^1.23.6" es-errors "^1.3.0" es-set-tostringtag "^2.0.3" function-bind "^1.1.2" - get-intrinsic "^1.2.4" - globalthis "^1.0.3" + get-intrinsic "^1.2.6" + globalthis "^1.0.4" + gopd "^1.2.0" has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - iterator.prototype "^1.1.2" - safe-array-concat "^1.1.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + iterator.prototype "^1.1.4" + safe-array-concat "^1.1.3" -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" -es-set-tostringtag@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" - integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== +es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: - get-intrinsic "^1.2.4" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" has-tostringtag "^1.0.2" - hasown "^2.0.1" + hasown "^2.0.2" -es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" - integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== +es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== dependencies: - hasown "^2.0.0" + hasown "^2.0.2" -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" -escalade@^3.1.2: +escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-next@13.1.6: - version "13.1.6" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.1.6.tgz#ab6894fe5b80080f1e9b9306d1c4b0003230620e" - integrity sha512-0cg7h5wztg/SoLAlxljZ0ZPUQ7i6QKqRiP4M2+MgTZtxWwNKb2JSwNc18nJ6/kXBI6xYvPraTbQSIhAuVw6czw== +eslint-config-next@15.2.2: + version "15.2.2" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.2.2.tgz#114ffec6851ced10fde7aa2f62c3d3a4371df514" + integrity sha512-g34RI7RFS4HybYFwGa/okj+8WZM+/fy+pEM+aqRQoVvM4gQhKrd4wIEddKmlZfWD75j8LTwB5zwkmNv3DceH1A== dependencies: - "@next/eslint-plugin-next" "13.1.6" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" + "@next/eslint-plugin-next" "15.2.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" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" + eslint-plugin-import "^2.31.0" + eslint-plugin-jsx-a11y "^6.10.0" + eslint-plugin-react "^7.37.0" + eslint-plugin-react-hooks "^5.0.0" eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -3342,30 +3575,29 @@ eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: resolve "^1.22.4" eslint-import-resolver-typescript@^3.5.2: - version "3.6.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e" - integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA== + version "3.10.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.0.tgz#5bca4c579e17174e95bf67526b424d07b46c352e" + integrity sha512-aV3/dVsT0/H9BtpNwbaqvl+0xGMRGzncLyhm793NFGvbwGGvzyAykqWZ8oZlZuGwuHkwJjhWJkG1cM3ynvd2pQ== dependencies: "@nolyfill/is-core-module" "1.0.39" - debug "^4.3.5" - enhanced-resolve "^5.15.0" - eslint-module-utils "^2.8.1" - fast-glob "^3.3.2" - get-tsconfig "^4.7.5" - is-bun-module "^1.0.2" - is-glob "^4.0.3" - -eslint-module-utils@^2.8.1, eslint-module-utils@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342" - integrity sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ== + debug "^4.4.0" + get-tsconfig "^4.10.0" + is-bun-module "^2.0.0" + stable-hash "^0.0.5" + tinyglobby "^0.2.12" + unrs-resolver "^1.3.2" + +eslint-module-utils@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz#fe4cfb948d61f49203d7b08871982b65b9af0b0b" + integrity sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== dependencies: debug "^3.2.7" -eslint-plugin-import@^2.26.0: - version "2.30.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449" - integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw== +eslint-plugin-import@^2.31.0: + version "2.31.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" + integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== dependencies: "@rtsao/scc" "^1.1.0" array-includes "^3.1.8" @@ -3375,7 +3607,7 @@ eslint-plugin-import@^2.26.0: debug "^3.2.7" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.9.0" + eslint-module-utils "^2.12.0" hasown "^2.0.2" is-core-module "^2.15.1" is-glob "^4.0.3" @@ -3384,14 +3616,15 @@ eslint-plugin-import@^2.26.0: object.groupby "^1.0.3" object.values "^1.2.0" semver "^6.3.1" + string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" -eslint-plugin-jsx-a11y@^6.5.1: - version "6.10.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz#36fb9dead91cafd085ddbe3829602fb10ef28339" - integrity sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== +eslint-plugin-jsx-a11y@^6.10.0: + version "6.10.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" + integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== dependencies: - aria-query "~5.1.3" + aria-query "^5.3.2" array-includes "^3.1.8" array.prototype.flatmap "^1.3.2" ast-types-flow "^0.0.8" @@ -3399,129 +3632,117 @@ eslint-plugin-jsx-a11y@^6.5.1: axobject-query "^4.1.0" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - es-iterator-helpers "^1.0.19" hasown "^2.0.2" jsx-ast-utils "^3.3.5" language-tags "^1.0.9" minimatch "^3.1.2" object.fromentries "^2.0.8" safe-regex-test "^1.0.3" - string.prototype.includes "^2.0.0" + string.prototype.includes "^2.0.1" -eslint-plugin-react-hooks@^4.5.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" - integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== +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@^7.31.7: - version "7.35.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz#d32500d3ec268656d5071918bfec78cfd8b070ed" - integrity sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ== +eslint-plugin-react@^7.37.0: + version "7.37.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz#1b6c80b6175b6ae4b26055ae4d55d04c414c7181" + integrity sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" - array.prototype.flatmap "^1.3.2" + array.prototype.flatmap "^1.3.3" array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.19" + es-iterator-helpers "^1.2.1" estraverse "^5.3.0" hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" object.entries "^1.1.8" object.fromentries "^2.0.8" - object.values "^1.2.0" + object.values "^1.2.1" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.11" + string.prototype.matchall "^4.0.12" string.prototype.repeat "^1.0.0" -eslint-scope@^7.1.1: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.3.0.tgz#10cd3a918ffdd722f5f3f7b5b83db9b23c87340d" + integrity sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: +eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.32.0: - version "8.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.32.0.tgz#d9690056bb6f1a302bd991e7090f5b68fbaea861" - integrity sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ== - dependencies: - "@eslint/eslintrc" "^1.4.1" - "@humanwhocodes/config-array" "^0.11.8" +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@9.22.0: + version "9.22.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.22.0.tgz#0760043809fbf836f582140345233984d613c552" + integrity sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.19.2" + "@eslint/config-helpers" "^0.1.0" + "@eslint/core" "^0.12.0" + "@eslint/eslintrc" "^3.3.0" + "@eslint/js" "9.22.0" + "@eslint/plugin-kit" "^0.2.7" + "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.2" + cross-spawn "^7.0.6" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" + eslint-scope "^8.3.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" - grapheme-splitter "^1.0.4" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -espree@^9.4.0: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" + eslint-visitor-keys "^4.2.0" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: +esquery@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== @@ -3540,15 +3761,25 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eventemitter2@~0.4.13: - version "0.4.14" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" - integrity sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ== +eventemitter3@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" + integrity sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== export-to-csv@^1.3.0: version "1.4.0" @@ -3562,7 +3793,7 @@ extend-shallow@^2.0.1: dependencies: is-extendable "^0.1.0" -extend@^3.0.0: +extend@^3.0.0, extend@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3572,20 +3803,20 @@ fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-diff@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.0.1.tgz#76532d5b8e49f6770fd464658628f9ed47eb5ac8" - integrity sha512-anEzYJ8VOA5iAMjDOVMTVMrUOXveDTMMk5x0E4p0nJ3VPoIOolF51AqYyE+UD0QIyggUwqppqH7XVA9lF3fdaQ== +fast-diff@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154" + integrity sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig== fast-equals@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7" integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg== -fast-glob@^3.2.9, fast-glob@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== +fast-glob@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -3593,6 +3824,17 @@ fast-glob@^3.2.9, fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -3604,9 +3846,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== dependencies: reusify "^1.0.4" @@ -3617,24 +3859,29 @@ fault@^1.0.0: dependencies: format "^0.2.0" -fflate@^0.4.8: - version "0.4.8" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" - integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== +fdir@^6.4.3: + version "6.4.3" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" + integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +fflate@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" -file-selector@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" - integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== +file-selector@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4" + integrity sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig== dependencies: - tslib "^2.4.0" + tslib "^2.7.0" fill-range@^7.1.1: version "7.1.1" @@ -3656,24 +3903,23 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" + keyv "^4.5.4" flatted@^3.2.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" - integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== follow-redirects@^1.15.6: - version "1.15.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.8.tgz#ae67b97ae32e0a7b36066a5448938374ec18d13d" - integrity sha512-xgrmBhBToVKay1q2Tao5LI26B83UhrB/vM1avwVSDzt8rx3rO6AizBAaF46EgksTVr+rFTQaqZZ9MVBfUe4nig== + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== fontkit@^2.0.2: version "2.0.4" @@ -3690,20 +3936,21 @@ fontkit@^2.0.2: unicode-properties "^1.4.0" unicode-trie "^2.0.0" -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== +for-each@^0.3.3, for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== dependencies: - is-callable "^1.1.3" + is-callable "^1.2.7" form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + version "4.0.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" + integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" mime-types "^2.1.12" format@^0.2.0: @@ -3711,38 +3958,36 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== -formik@2.2.9: - version "2.2.9" - resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0" - integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA== +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== dependencies: + "@types/hoist-non-react-statics" "^3.3.1" deepmerge "^2.1.1" hoist-non-react-statics "^3.3.0" lodash "^4.17.21" lodash-es "^4.17.21" react-fast-compare "^2.0.1" tiny-warning "^1.0.2" - tslib "^1.10.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + tslib "^2.0.0" function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" functions-have-names@^1.2.3: version "1.2.3" @@ -3754,30 +3999,43 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" -get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== dependencies: - call-bind "^1.0.5" + call-bound "^1.0.3" es-errors "^1.3.0" - get-intrinsic "^1.2.4" + get-intrinsic "^1.2.6" -get-tsconfig@^4.7.5: - version "4.8.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.0.tgz#125dc13a316f61650a12b20c97c11b8fd996fedd" - integrity sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw== +get-tsconfig@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz#403a682b373a823612475a4c2928c7326fc0f6bb" + integrity sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A== dependencies: resolve-pkg-maps "^1.0.0" @@ -3795,48 +4053,17 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -globalthis@^1.0.3: +globalthis@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== @@ -3844,39 +4071,20 @@ globalthis@^1.0.3: define-properties "^1.2.1" gopd "^1.0.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -goober@^2.1.10: - version "2.1.14" - resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.14.tgz#4a5c94fc34dc086a8e6035360ae1800005135acd" - integrity sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg== +goober@^2.1.16: + version "2.1.16" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.16.tgz#7d548eb9b83ff0988d102be71f271ca8f9c82a95" + integrity sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g== -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -graceful-fs@^4.1.2, graceful-fs@^4.2.4: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== gray-matter@4.0.3: version "4.0.3" @@ -3888,15 +4096,10 @@ gray-matter@4.0.3: section-matter "^1.0.0" strip-bom-string "^1.0.0" -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== has-flag@^4.0.0: version "4.0.0" @@ -3910,24 +4113,26 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: +has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" -hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: +hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -3939,10 +4144,33 @@ hast-util-parse-selector@^2.0.0: resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== -hast-util-whitespace@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" - integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.6" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98" + integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" hastscript@^6.0.0: version "6.0.0" @@ -3955,10 +4183,10 @@ hastscript@^6.0.0: property-information "^5.0.0" space-separated-tokens "^1.0.0" -highlight-words@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-1.2.2.tgz#9875b75d11814d7356b24f23feeb7d77761fa867" - integrity sha512-Mf4xfPXYm8Ay1wTibCrHpNWeR2nUMynMVFkXCi4mbl+TEgmNOe+I4hV7W3OCZcSvzGL6kupaqpfHOemliMTGxQ== +highlight-words@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-2.0.0.tgz#06853d68f1f7c8e59d6ef2dd072fe2f64fc93936" + integrity sha512-If5n+IhSBRXTScE7wl16VPmd+44Vy7kof24EdqhjsZsDuHikpv1OCagVcJFpB4fS4UPUniedlWqrjIO8vWOsIQ== highlight.js@^10.4.1, highlight.js@~10.7.0: version "10.7.3" @@ -4007,6 +4235,11 @@ html-tokenize@^2.0.0: readable-stream "~1.0.27-1" through2 "~0.4.1" +html-url-attributes@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87" + integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ== + html2canvas@^1.0.0-rc.5: version "1.4.1" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" @@ -4028,31 +4261,31 @@ htmlparser2@^3.9.0: readable-stream "^3.1.1" hyphen@^1.6.4: - version "1.10.4" - resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.10.4.tgz#ae16551b8a56ae7c34ffd4b98777221795e6c912" - integrity sha512-SejXzIpv9gOVdDWXd4suM1fdF1k2dxZGvuTdkOVLoazYfK7O4DykIQbdrvuyG+EaTNlXAGhMndtKrhykgbt0gg== + version "1.10.6" + resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.10.6.tgz#0e779d280e696102b97d7e42f5ca5de2cc97e274" + integrity sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw== -i18next@22.4.9: - version "22.4.9" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.4.9.tgz#98c8384c6bd41ff937da98b1e809ba03d3b41053" - integrity sha512-8gWMmUz460KJDQp/ob3MNUX84cVuDRY9PLFPnV8d+Qezz/6dkjxwOaH70xjrCNDO+JrUL25iXfAIN9wUkInNZw== +i18next@24.2.3: + version "24.2.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.2.3.tgz#3a05f72615cbd7c00d7e348667e2aabef1df753b" + integrity sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A== dependencies: - "@babel/runtime" "^7.20.6" + "@babel/runtime" "^7.26.10" -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -immer@^9.0.16: - version "9.0.21" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" - integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== +immer@^10.0.3: + version "10.1.1" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" + integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== +import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -4062,38 +4295,35 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== +inline-style-parser@0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz#f4af5fe72e612839fcd453d989a586566d695f22" + integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q== -internal-slot@^1.0.4, internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== dependencies: es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" + hasown "^2.0.2" + side-channel "^1.1.0" is-alphabetical@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + is-alphanumerical@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" @@ -4102,21 +4332,30 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + has-tostringtag "^1.0.2" -is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" is-arrayish@^0.2.1: version "0.2.1" @@ -4129,70 +4368,77 @@ is-arrayish@^0.3.1: integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== is-async-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" - integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== dependencies: - has-tostringtag "^1.0.0" + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== dependencies: - has-bigints "^1.0.1" + has-bigints "^1.0.2" -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + call-bound "^1.0.3" + has-tostringtag "^1.0.2" -is-bun-module@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.1.0.tgz#a66b9830869437f6cdad440ba49ab6e4dc837269" - integrity sha512-4mTAVPlrXpaN3jtF0lsnPCMGnq4+qZjVIKq0HCpfcqf8OC1SM5oATCIAPM5V5FN05qp2NNnFndphmdZS9CV3hA== +is-bun-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" + integrity sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ== dependencies: - semver "^7.6.3" + semver "^7.7.1" -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: hasown "^2.0.2" -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" is-typed-array "^1.1.13" -is-date-object@^1.0.1, is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + has-tostringtag "^1.0.2" is-decimal@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + is-extendable@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -4203,19 +4449,22 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-finalizationregistry@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" - integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" is-generator-function@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" @@ -4229,78 +4478,79 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== -is-map@^2.0.2, is-map@^2.0.3: +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + has-tostringtag "^1.0.2" is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - is-plain-obj@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== +is-regex@^1.1.4, is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" -is-set@^2.0.2, is-set@^2.0.3: +is-set@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== +is-string@^1.0.7, is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== dependencies: - has-tostringtag "^1.0.0" + call-bound "^1.0.3" + has-tostringtag "^1.0.2" -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== dependencies: - has-symbols "^1.0.2" + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" -is-typed-array@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== dependencies: - which-typed-array "^1.1.14" + which-typed-array "^1.1.16" is-url@^1.2.4: version "1.2.4" @@ -4312,20 +4562,20 @@ is-weakmap@^2.0.2: resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== +is-weakref@^1.0.2, is-weakref@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" is-weakset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" - integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" isarray@0.0.1: version "0.0.1" @@ -4347,16 +4597,17 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -iterator.prototype@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" - integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== +iterator.prototype@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== dependencies: - define-properties "^1.2.1" - get-intrinsic "^1.2.1" - has-symbols "^1.0.3" - reflect.getprototypeof "^1.0.4" - set-function-name "^2.0.1" + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + get-proto "^1.0.0" + has-symbols "^1.1.0" + set-function-name "^2.0.2" javascript-time-ago@^2.5.11: version "2.5.11" @@ -4365,10 +4616,10 @@ javascript-time-ago@^2.5.11: dependencies: relative-time-format "^1.1.6" -jay-peg@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jay-peg/-/jay-peg-1.0.2.tgz#17a54d386e472f5f313f3d6e88770b170ea569f4" - integrity sha512-fyV3NVvv6pTys/3BTapBUGAWAuU9rM2gRcgijZHzptd5KKL+s+S7hESFN+wOsbDH1MzFwdlRAXi0aGxS6uiMKg== +jay-peg@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jay-peg/-/jay-peg-1.1.1.tgz#fdf410b89fa7a295bf74424ffe4c9083dbe7c363" + integrity sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww== dependencies: restructure "^3.0.0" @@ -4377,11 +4628,6 @@ js-base64@^3.7.2: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== -js-sdsl@^4.1.4: - version "4.4.2" - resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" - integrity sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4402,15 +4648,15 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== json-buffer@3.0.1: version "3.0.1" @@ -4444,24 +4690,24 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jspdf-autotable@^3.8.2: - version "3.8.3" - resolved "https://registry.yarnpkg.com/jspdf-autotable/-/jspdf-autotable-3.8.3.tgz#b469730c28376a81298d04d18136f1fb464cd4b8" - integrity sha512-PQFdljBt+ijm6ZWXYxhZ54A/awV63UKcipYoA2+YGsz0BXXiXTIL/FIg+V30j7wPdSdzClfbB3qKX9UeuFylPQ== +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@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.5.1.tgz#00c85250abf5447a05f3b32ab9935ab4a56592cc" - integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA== +jspdf@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-3.0.1.tgz#d81e1964f354f60412516eb2449ea2cccd4d2a3b" + integrity sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg== dependencies: - "@babel/runtime" "^7.14.0" + "@babel/runtime" "^7.26.7" atob "^2.1.2" btoa "^1.2.1" - fflate "^0.4.8" + fflate "^0.8.1" optionalDependencies: - canvg "^3.0.6" + canvg "^3.0.11" core-js "^3.6.0" - dompurify "^2.2.0" + dompurify "^3.2.4" html2canvas "^1.0.0-rc.5" "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: @@ -4474,7 +4720,7 @@ jspdf@^2.5.1: object.assign "^4.1.4" object.values "^1.1.6" -keyv@^4.5.3: +keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -4486,11 +4732,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" @@ -4513,7 +4754,7 @@ leaflet.markercluster@^1.5.3: resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056" integrity sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA== -leaflet@^1.8.0, leaflet@^1.9.4: +leaflet@^1.9.4: version "1.9.4" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d" integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA== @@ -4526,6 +4767,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +linebreak@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/linebreak/-/linebreak-1.1.0.tgz#831cf378d98bced381d8ab118f852bd50d81e46b" + integrity sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ== + dependencies: + base64-js "0.0.8" + unicode-trie "^2.0.0" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -4565,23 +4814,30 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.21: +lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -lodash@~2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e" - integrity sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw== +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lowlight@^1.17.0: version "1.20.0" resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" @@ -4610,67 +4866,134 @@ markdown-it@^14.0.0: uc.micro "^2.1.0" material-react-table@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-3.0.1.tgz#a6d592a1e370acfd453c37f1deaa870c47e7bf5b" - integrity sha512-RP+bnpsOAH5j6zwP04u9HB37fyqbd6mVv9mkT4IUJC3e3gEqixZmkNdJMVM1ZVHoq7yIaM381xf22mpBVe0IaA== + version "3.2.1" + resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-3.2.1.tgz#56f595755cab3b669b399999fed9eb305fbb6dd7" + integrity sha512-sQtTf7bETpkPN2Hm5BVtz89wrfXCVQguz6XlwMChSnfKFO5QCKAJJC5aSIKnUc3S0AvTz/k/ILi00FnnY1Gixw== dependencies: "@tanstack/match-sorter-utils" "8.19.4" - "@tanstack/react-table" "8.20.5" - "@tanstack/react-virtual" "3.10.6" - highlight-words "1.2.2" + "@tanstack/react-table" "8.20.6" + "@tanstack/react-virtual" "3.11.2" + highlight-words "2.0.0" -mdast-util-definitions@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" - integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -mdast-util-from-markdown@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" - integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== +mdast-util-from-markdown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" - -mdast-util-to-hast@^12.1.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" - integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096" + integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-definitions "^5.0.0" - micromark-util-sanitize-uri "^1.1.0" - trim-lines "^3.0.0" - unist-util-generated "^2.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-to-string@^3.1.0: +mdast-util-mdx-jsx@^3.0.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" - integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz#fd04c67a2a7499efb905a8a5c578dddc9fdada0d" + integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" + integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== mdurl@^2.0.0: version "2.0.0" @@ -4692,206 +5015,206 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== -merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromark-core-commonmark@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" - integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== +micromark-core-commonmark@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== dependencies: decode-named-character-reference "^1.0.0" - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - -micromark-factory-destination@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" - integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-factory-label@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" - integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" -micromark-factory-title@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" - integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-factory-whitespace@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" - integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-util-character@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-util-chunked@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" - integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== dependencies: - micromark-util-symbol "^1.0.0" + micromark-util-symbol "^2.0.0" -micromark-util-classify-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" - integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-util-combine-extensions@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" - integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" - integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== dependencies: - micromark-util-symbol "^1.0.0" + micromark-util-symbol "^2.0.0" -micromark-util-decode-string@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" - integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== dependencies: decode-named-character-reference "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" -micromark-util-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" - integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== -micromark-util-html-tag-name@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" - integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== -micromark-util-normalize-identifier@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" - integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== dependencies: - micromark-util-symbol "^1.0.0" + micromark-util-symbol "^2.0.0" -micromark-util-resolve-all@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" - integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== dependencies: - micromark-util-types "^1.0.0" + micromark-util-types "^2.0.0" -micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" - integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" -micromark-util-subtokenize@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" - integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== +micromark-util-subtokenize@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-util-symbol@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== -micromark@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" - integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== +micromark@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== dependencies: "@types/debug" "^4.0.0" debug "^4.0.0" decode-named-character-reference "^1.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - -micromatch@^4.0.4: + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -4911,42 +5234,39 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6, minimist@~1.2.5: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== monaco-editor@^0.52.0: - version "0.52.0" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.52.0.tgz#d47c02b191eae208d68878d679b3ee7456031be7" - integrity sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw== - -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + version "0.52.2" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.52.2.tgz#53c75a6fcc6802684e99fd1b2700299857002205" + integrity sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ== -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "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.14.0" - resolved "https://registry.yarnpkg.com/mui-tiptap/-/mui-tiptap-1.14.0.tgz#43725fc82d0033b038753e81d4bddd22c92ad2d5" - integrity sha512-xp3h6Toy7rE3vCFC7kWJZzf3JR7Gm6DaD5O40bMbqwVj4cb9zMMar6rXrMYxQybFAGUOBsOkzzIu3rpDQf86/A== + version "1.18.0" + resolved "https://registry.yarnpkg.com/mui-tiptap/-/mui-tiptap-1.18.0.tgz#99f42928638d4cce0a396c713c49454cadbc8441" + integrity sha512-SW4PS4jJuOXQHdS96eGq1dkNiLOOTP8yiBnOH6c49SF+Sg6Bowd1hnrDmqRR+l8t6Uer5O7DWhYpYuixvrrlYw== dependencies: encodeurl "^1.0.2" lodash "^4.17.21" @@ -4962,55 +5282,51 @@ multipipe@^1.0.2: duplexer2 "^0.1.2" object-assign "^4.1.0" -nanoclone@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== - nanoid@^3.3.6: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@^13.5.6: - version "13.5.6" - resolved "https://registry.yarnpkg.com/next/-/next-13.5.6.tgz#e964b5853272236c37ce0dd2c68302973cf010b1" - integrity sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw== +next@^15.2.2: + version "15.2.4" + resolved "https://registry.yarnpkg.com/next/-/next-15.2.4.tgz#e05225e9511df98e3b2edc713e17f4c970bff961" + integrity sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ== dependencies: - "@next/env" "13.5.6" - "@swc/helpers" "0.5.2" + "@next/env" "15.2.4" + "@swc/counter" "0.1.3" + "@swc/helpers" "0.5.15" busboy "1.6.0" - caniuse-lite "^1.0.30001406" + caniuse-lite "^1.0.30001579" postcss "8.4.31" - styled-jsx "5.1.1" - watchpack "2.4.0" + styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "13.5.6" - "@next/swc-darwin-x64" "13.5.6" - "@next/swc-linux-arm64-gnu" "13.5.6" - "@next/swc-linux-arm64-musl" "13.5.6" - "@next/swc-linux-x64-gnu" "13.5.6" - "@next/swc-linux-x64-musl" "13.5.6" - "@next/swc-win32-arm64-msvc" "13.5.6" - "@next/swc-win32-ia32-msvc" "13.5.6" - "@next/swc-win32-x64-msvc" "13.5.6" - -node-fetch@^2.6.12: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + "@next/swc-darwin-arm64" "15.2.4" + "@next/swc-darwin-x64" "15.2.4" + "@next/swc-linux-arm64-gnu" "15.2.4" + "@next/swc-linux-arm64-musl" "15.2.4" + "@next/swc-linux-x64-gnu" "15.2.4" + "@next/swc-linux-x64-musl" "15.2.4" + "@next/swc-win32-arm64-msvc" "15.2.4" + "@next/swc-win32-x64-msvc" "15.2.4" + sharp "^0.33.5" + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== dependencies: - whatwg-url "^5.0.0" + lower-case "^2.0.2" + tslib "^2.0.3" -node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-svg-path@^1.1.0: version "1.1.0" @@ -5041,10 +5357,10 @@ object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-is@^1.1.5: version "1.1.6" @@ -5064,24 +5380,27 @@ object-keys@~0.4.0: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" integrity sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw== -object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== +object.assign@^4.1.4, object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - has-symbols "^1.0.3" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" object-keys "^1.1.1" object.entries@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" - integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + version "1.1.9" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" define-properties "^1.2.1" - es-object-atoms "^1.0.0" + es-object-atoms "^1.1.1" object.fromentries@^2.0.8: version "2.0.8" @@ -5102,23 +5421,17 @@ object.groupby@^1.0.3: define-properties "^1.2.1" es-abstract "^1.23.2" -object.values@^1.1.6, object.values@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" - integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== +object.values@^1.1.6, object.values@^1.2.0, object.values@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" es-object-atoms "^1.0.0" -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -optionator@^0.9.1: +optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== @@ -5135,6 +5448,15 @@ orderedmap@^2.0.0: resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -5160,9 +5482,14 @@ pako@~1.0.5: integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== papaparse@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.4.1.tgz#f45c0f871853578bd3a30f92d96fdcfb6ebea127" - integrity sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw== + version "5.5.2" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.5.2.tgz#fb67cc5a03ba8930cb435dc4641a25d6804bd4d7" + integrity sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA== + +parchment@^1.1.2, parchment@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5" + integrity sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg== parent-module@^1.0.0: version "1.0.1" @@ -5183,7 +5510,20 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" -parse-json@^5.0.0: +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -5203,11 +5543,6 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -5228,20 +5563,25 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^1.0.0, picocolors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" - integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== +picocolors@^1.0.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== postcss-value-parser@^4.1.0: version "4.2.0" @@ -5263,9 +5603,9 @@ prelude-ls@^1.2.1: integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prismjs@^1.27.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" - integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + version "1.30.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9" + integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw== prismjs@~1.27.0: version "1.27.0" @@ -5277,7 +5617,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -prop-types@15.8.1, prop-types@15.x, prop-types@^15.0.0, prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@15.x, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5286,7 +5626,7 @@ prop-types@15.8.1, prop-types@15.x, prop-types@^15.0.0, prop-types@^15.5.7, prop object-assign "^4.1.1" react-is "^16.13.1" -property-expr@^2.0.4: +property-expr@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== @@ -5298,10 +5638,10 @@ property-information@^5.0.0: dependencies: xtend "^4.0.0" -property-information@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" - integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== +property-information@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.0.0.tgz#3508a6d6b0b8eb3ca6eb2c6623b164d2ed2ab112" + integrity sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg== prosemirror-changeset@^2.2.1: version "2.2.1" @@ -5317,10 +5657,10 @@ prosemirror-collab@^1.3.1: dependencies: prosemirror-state "^1.0.0" -prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.6.2.tgz#d9cf6654912442cff47daa1677eb43ebd0b1f117" - integrity sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA== +prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.7.0.tgz#c0a60c808f51157caa146922494fc59fe257f27c" + integrity sha512-6toodS4R/Aah5pdsrIwnTYPEjW70SlO5a66oo5Kk+CIrgJz3ukOoS+FYDGqvQlAX5PxoGWDX1oD++tn5X3pyRA== dependencies: prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" @@ -5356,14 +5696,14 @@ prosemirror-history@^1.0.0, prosemirror-history@^1.4.1: rope-sequence "^1.3.0" prosemirror-inputrules@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz#ef1519bb2cb0d1e0cec74bad1a97f1c1555068bb" - integrity sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg== + version "1.5.0" + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz#e22bfaf1d6ea4fe240ad447c184af3d520d43c37" + integrity sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA== dependencies: prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.2: +prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e" integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ== @@ -5371,14 +5711,14 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.2: prosemirror-state "^1.0.0" w3c-keyname "^2.2.0" -prosemirror-markdown@^1.13.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz#23feb6652dacb3dd78ffd8f131da37c20e4e4cf8" - integrity sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw== +prosemirror-markdown@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz#863eb3fd5f57a444e4378174622b562735b1c503" + integrity sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g== dependencies: "@types/markdown-it" "^14.0.0" markdown-it "^14.0.0" - prosemirror-model "^1.20.0" + prosemirror-model "^1.25.0" prosemirror-menu@^1.2.4: version "1.2.4" @@ -5390,30 +5730,30 @@ prosemirror-menu@^1.2.4: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.19.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.22.3, prosemirror-model@^1.8.1: - version "1.23.0" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.23.0.tgz#652058182ed90dc15c8f0f2cf2df488306fa1dcd" - integrity sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ== +prosemirror-model@^1.0.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.23.0, prosemirror-model@^1.24.1, prosemirror-model@^1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.25.0.tgz#c147113edc0718a14f03881e4c20367d0221f7af" + integrity sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw== dependencies: orderedmap "^2.0.0" prosemirror-schema-basic@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz#649c349bb21c61a56febf9deb71ac68fca4cedf2" - integrity sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA== + version "1.2.4" + resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz#389ce1ec09b8a30ea9bbb92c58569cb690c2d695" + integrity sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ== dependencies: - prosemirror-model "^1.19.0" + prosemirror-model "^1.25.0" prosemirror-schema-list@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz#78b8d25531db48ca9688836dbde50e13ac19a4a1" - integrity sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg== + version "1.5.1" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz#5869c8f749e8745c394548bb11820b0feb1e32f5" + integrity sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q== dependencies: prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" prosemirror-transform "^1.7.3" -prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.3: +prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080" integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q== @@ -5422,16 +5762,16 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, pr prosemirror-transform "^1.0.0" prosemirror-view "^1.27.0" -prosemirror-tables@^1.4.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.6.1.tgz#8df27facbf7632a574afb32a665aaadf7f2ed69a" - integrity sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w== +prosemirror-tables@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.6.4.tgz#e36ebca70d9e398c4a3b99b122ba86bfc985293d" + integrity sha512-TkDY3Gw52gRFRfRn2f4wJv5WOgAOXLJA2CQJYIJ5+kdFbfj3acR4JUW6LX2e1hiEBiUwvEhzH5a3cZ5YSztpIA== dependencies: - prosemirror-keymap "^1.1.2" - prosemirror-model "^1.8.1" - prosemirror-state "^1.3.1" - prosemirror-transform "^1.2.1" - prosemirror-view "^1.13.3" + prosemirror-keymap "^1.2.2" + prosemirror-model "^1.24.1" + prosemirror-state "^1.4.3" + prosemirror-transform "^1.10.2" + prosemirror-view "^1.37.2" prosemirror-trailing-node@^3.0.0: version "3.0.0" @@ -5441,17 +5781,17 @@ prosemirror-trailing-node@^3.0.0: "@remirror/core-constants" "3.0.0" escape-string-regexp "^4.0.0" -prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3: - version "1.10.2" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz#8ebac4e305b586cd96595aa028118c9191bbf052" - integrity sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ== +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.7.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.3.tgz#fae660bd7ffef3159aff44bc21e9e044aa31b67d" + integrity sha512-Nhh/+1kZGRINbEHmVu39oynhcap4hWTs/BlU7NnxWj3+l0qi8I1mu67v6mMdEe/ltD8hHvU4FV6PHiCw2VSpMw== dependencies: prosemirror-model "^1.21.0" -prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.34.3: - version "1.36.0" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.36.0.tgz#ab6e444db08b7e3a79c6841c6667df72c7c4f2ec" - integrity sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA== +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.37.2: + version "1.38.1" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.38.1.tgz#566d30cc8b00a68d6b4c60f5d8a6ab97c82990b3" + integrity sha512-4FH/uM1A4PNyrxXbD+RAbAsf0d/mM0D/wAKSVVWK7o0A9Q/oOXJBrw786mBf2Vnrs/Edly6dH6Z2gsb7zWwaUw== dependencies: prosemirror-model "^1.20.0" prosemirror-state "^1.0.0" @@ -5467,7 +5807,7 @@ punycode.js@^2.3.1: resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -5484,14 +5824,26 @@ queue@^6.0.1: dependencies: inherits "~2.0.3" -quilljs@^0.18.1: - version "0.18.1" - resolved "https://registry.yarnpkg.com/quilljs/-/quilljs-0.18.1.tgz#7d7696ed95e0e1db79c439e5924b6eea5f67706c" - integrity sha512-VKaO7GNehgnH4LlFPx5ZAl+KFDoRVtboY0I6UUbYXUsPHP8kR80Tg/CFEYqrqrpCOGQr4OQ5Tjm813gV1DUyQw== +quill-delta@^3.6.2: + version "3.6.3" + resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032" + integrity sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg== + dependencies: + deep-equal "^1.0.1" + extend "^3.0.2" + fast-diff "1.1.2" + +quill@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8" + integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g== dependencies: - eventemitter2 "~0.4.13" - lodash "~2.4.1" - rich-text "~1.0.2" + clone "^2.1.1" + deep-equal "^1.0.1" + eventemitter3 "^2.0.3" + extend "^3.0.2" + parchment "^1.1.4" + quill-delta "^3.6.2" raf-schd@^4.0.2: version "4.0.3" @@ -5505,12 +5857,12 @@ raf@^3.4.1: dependencies: performance-now "^2.1.0" -react-apexcharts@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.4.0.tgz#e3619104b34750da67a2ca80289dc87085c2aa27" - integrity sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q== +react-apexcharts@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.7.0.tgz#bbd08425674224adb27c9f2c62477d43bd5de539" + integrity sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA== dependencies: - prop-types "^15.5.7" + prop-types "^15.8.1" react-beautiful-dnd@13.1.1: version "13.1.1" @@ -5538,13 +5890,12 @@ react-copy-to-clipboard@^5.1.0: copy-to-clipboard "^3.3.1" prop-types "^15.8.1" -react-dom@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" - integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== +react-dom@19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" + integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.0" + scheduler "^0.25.0" react-draggable@^4.0.3, react-draggable@^4.4.5: version "4.4.6" @@ -5554,19 +5905,19 @@ react-draggable@^4.0.3, react-draggable@^4.4.5: clsx "^1.1.1" prop-types "^15.8.1" -react-dropzone@14.2.3: - version "14.2.3" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" - integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== +react-dropzone@14.3.8: + version "14.3.8" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.8.tgz#a7eab118f8a452fe3f8b162d64454e81ba830582" + integrity sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug== dependencies: - attr-accept "^2.2.2" - file-selector "^0.6.0" + attr-accept "^2.2.4" + file-selector "^2.1.0" prop-types "^15.8.1" -react-error-boundary@^4.0.13: - version "4.0.13" - resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.13.tgz#80386b7b27b1131c5fbb7368b8c0d983354c7947" - integrity sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ== +react-error-boundary@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-5.0.0.tgz#6b6c7e075c922afb0283147e5b084efa44e68570" + integrity sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ== dependencies: "@babel/runtime" "^7.12.5" @@ -5576,9 +5927,9 @@ react-fast-compare@^2.0.1: integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== react-grid-layout@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.5.0.tgz#b6cc9412b58cf8226aebc0df7673d6fa782bdee2" - integrity sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w== + version "1.5.1" + resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.5.1.tgz#800899fb17aa568e5f32574d07c12579f3d76fb2" + integrity sha512-4Fr+kKMk0+m1HL/BWfHxi/lRuaOmDNNKQDcu7m12+NEYcen20wIuZFo789u3qWCyvUsNUxCiyf0eKq4WiJSNYw== dependencies: clsx "^2.0.0" fast-equals "^4.0.3" @@ -5588,16 +5939,17 @@ react-grid-layout@^1.5.0: resize-observer-polyfill "^1.5.1" react-hook-form@^7.53.0: - version "7.53.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.0.tgz#3cf70951bf41fa95207b34486203ebefbd3a05ab" - integrity sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ== + version "7.54.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.54.2.tgz#8c26ed54c71628dff57ccd3c074b1dd377cfb211" + integrity sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg== -react-hot-toast@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.0.tgz#b91e7a4c1b6e3068fc599d3d83b4fb48668ae51d" - integrity sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA== +react-hot-toast@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.5.2.tgz#b55328966a26add56513e2dc1682e2cb4753c244" + integrity sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw== dependencies: - goober "^2.1.10" + csstype "^3.1.3" + goober "^2.1.16" react-html-parser@^2.0.2: version "2.0.2" @@ -5606,12 +5958,12 @@ react-html-parser@^2.0.2: dependencies: htmlparser2 "^3.9.0" -react-i18next@12.1.4: - version "12.1.4" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.1.4.tgz#be0a60d3a45acc4321909f8a4b8cde16518a2926" - integrity sha512-XQND7jYtgM7ht5PH3yIZljCRpAMTlH/zmngM9ZjToqa+0BR6xuu8c7QF0WIIOEjcMTB2S3iOfpN/xG/ZrAnO6g== +react-i18next@15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.1.tgz#33f3e89c2f6c68e2bfcbf9aa59986ad42fe78758" + integrity sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw== dependencies: - "@babel/runtime" "^7.20.6" + "@babel/runtime" "^7.25.0" html-parse-stringify "^3.0.1" react-is@^16.13.1, react-is@^16.7.0: @@ -5624,48 +5976,44 @@ react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0, react-is@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +react-is@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.0.0.tgz#d6669fd389ff022a9684f708cf6fa4962d1fea7a" + integrity sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g== -react-leaflet-markercluster@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/react-leaflet-markercluster/-/react-leaflet-markercluster-4.2.1.tgz#74a9501925f5920585aa700ec3790c9cca48012a" - integrity sha512-lRJwCMGJVKXOKdP/dZIxszfHXjJaf8BpP2E+cNIYx5XxvqFj7NADG1HeK1nouNUgMcXVhqpZejiV6wPxwCibJw== +react-leaflet-markercluster@^5.0.0-rc.0: + version "5.0.0-rc.0" + resolved "https://registry.yarnpkg.com/react-leaflet-markercluster/-/react-leaflet-markercluster-5.0.0-rc.0.tgz#42b1b9786de565fe69ec95abc6ff3232713f5300" + integrity sha512-jWa4bPD5LfLV3Lid1RWgl+yKUuQtnqeYtJzzLb/fiRjvX+rtwzY8pMoUFuygqyxNrWxMTQlWKBHxkpI7Sxvu4Q== dependencies: - "@react-leaflet/core" "^2.0.0" - leaflet "^1.8.0" + "@react-leaflet/core" "^3.0.0" + leaflet "^1.9.4" leaflet.markercluster "^1.5.3" - react-leaflet "^4.0.0" + react-leaflet "^5.0.0" -react-leaflet@4.2.1, react-leaflet@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-4.2.1.tgz#c300e9eccaf15cb40757552e181200aa10b94780" - integrity sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q== +react-leaflet@5.0.0, react-leaflet@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-5.0.0.tgz#945d40bad13b69e8606278b19446b00bab57376a" + integrity sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw== dependencies: - "@react-leaflet/core" "^2.1.0" + "@react-leaflet/core" "^3.0.0" -react-markdown@8.0.5: - version "8.0.5" - resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.5.tgz#c9a70a33ca9aeeafb769c6582e7e38843b9d70ad" - integrity sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A== - dependencies: - "@types/hast" "^2.0.0" - "@types/prop-types" "^15.0.0" - "@types/unist" "^2.0.0" - comma-separated-tokens "^2.0.0" - hast-util-whitespace "^2.0.0" - prop-types "^15.0.0" - property-information "^6.0.0" - react-is "^18.0.0" - remark-parse "^10.0.0" - remark-rehype "^10.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.4.0" - unified "^10.0.0" - unist-util-visit "^4.0.0" - vfile "^5.0.0" +react-markdown@10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-10.1.0.tgz#e22bc20faddbc07605c15284255653c0f3bad5ca" + integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" react-media-hook@^0.5.0: version "0.5.0" @@ -5680,24 +6028,22 @@ react-papaparse@^4.4.0: "@types/papaparse" "^5.3.9" papaparse "^5.4.1" -react-quill@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/react-quill/-/react-quill-0.0.2.tgz#e16866aa2780e839eb1e728b6dce689564ec7d54" - integrity sha512-PeiHXZ63Sumh41OdovBQExXJH7B4UsJpyCW8CtRvXrNBa2RJXdciaJvTeb0x6pYQfqkoCYPT5EbUvEr0Z1tohg== +react-quill@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-quill/-/react-quill-2.0.0.tgz#67a0100f58f96a246af240c9fa6841b363b3e017" + integrity sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg== dependencies: - quilljs "^0.18.1" + "@types/quill" "^1.3.10" + lodash "^4.17.4" + quill "^1.3.7" -react-redux@8.0.5: - version "8.0.5" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd" - integrity sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw== +react-redux@9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.2.0.tgz#96c3ab23fb9a3af2cb4654be4b51c989e32366f5" + integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== dependencies: - "@babel/runtime" "^7.12.1" - "@types/hoist-non-react-statics" "^3.3.1" - "@types/use-sync-external-store" "^0.0.3" - hoist-non-react-statics "^3.3.2" - react-is "^18.0.0" - use-sync-external-store "^1.0.0" + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" react-redux@^7.2.0: version "7.2.9" @@ -5750,20 +6096,23 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" +react-virtuoso@^4.12.8: + version "4.12.8" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.12.8.tgz#db1dbba617f91c1dcd760aa90e09ef991e65a356" + integrity sha512-NMMKfDBr/+xZZqCQF3tN1SZsh6FwOJkYgThlfnsPLkaEhdyQo0EuWUzu3ix6qjnI7rYwJhMwRGoJBi+aiDfGsA== + react-window@^1.8.10: - version "1.8.10" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" - integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== + version "1.8.11" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525" + integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" - integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== - dependencies: - loose-envify "^1.1.0" +react@19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" + integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== readable-stream@^2.0.2: version "2.3.8" @@ -5807,30 +6156,36 @@ redux-persist@^6.0.0: resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== -redux-thunk@2.4.2, redux-thunk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" - integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== +redux-thunk@3.1.0, redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + +redux@5.0.1, redux@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== -redux@4.2.1, redux@^4.0.0, redux@^4.0.4, redux@^4.2.0: +redux@^4.0.0, redux@^4.0.4: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: "@babel/runtime" "^7.9.2" -reflect.getprototypeof@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" - integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" define-properties "^1.2.1" - es-abstract "^1.23.1" + es-abstract "^1.23.9" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - globalthis "^1.0.3" - which-builtin-type "^1.1.3" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" refractor@^3.6.0: version "3.6.0" @@ -5841,10 +6196,10 @@ refractor@^3.6.0: parse-entities "^2.0.0" prismjs "~1.27.0" -regenerate-unicode-properties@^10.1.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" - integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== dependencies: regenerate "^1.4.2" @@ -5870,63 +6225,67 @@ regenerator-transform@^0.15.2: dependencies: "@babel/runtime" "^7.8.4" -regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== dependencies: - call-bind "^1.0.6" + call-bind "^1.0.8" define-properties "^1.2.1" es-errors "^1.3.0" - set-function-name "^2.0.1" - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== dependencies: - "@babel/regjsgen" "^0.8.0" regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== dependencies: - jsesc "~0.5.0" + jsesc "~3.0.2" relative-time-format@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/relative-time-format/-/relative-time-format-1.1.6.tgz#724a5fbc3794b8e0471b6b61419af2ce699eb9f1" integrity sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ== -remark-parse@^10.0.0: - version "10.0.2" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" - integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - unified "^10.0.0" + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" -remark-rehype@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" - integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== +remark-rehype@^11.0.0: + version "11.1.1" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.1.tgz#f864dd2947889a11997c0a2667cd6b38f685bca7" + integrity sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ== dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-to-hast "^12.1.0" - unified "^10.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" remove-accents@0.5.0: version "0.5.0" @@ -5938,10 +6297,10 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -reselect@^4.1.7: - version "4.1.8" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" - integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== +reselect@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" + integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== resize-observer-polyfill@^1.5.1: version "1.5.1" @@ -5959,11 +6318,11 @@ resolve-pkg-maps@^1.0.0: integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.4: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.16.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -5982,29 +6341,15 @@ restructure@^3.0.0: integrity sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw== reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== rgbcolor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== -rich-text@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/rich-text/-/rich-text-1.0.3.tgz#ac4d825f2fca123d8282842ed55fc57fc7b9c710" - integrity sha512-L+Mi0fBH4/TBGH68XZqUXdUr5Ze+ViYkrKuwEvCpeyB1Blbp4CLO4LyYleutTNybujCMQfcmivaNrE3YLrEUgg== - dependencies: - fast-diff "~1.0.0" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rope-sequence@^1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425" @@ -6017,21 +6362,15 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" isarray "^2.0.5" safe-buffer@~5.1.0, safe-buffer@~5.1.1: @@ -6044,29 +6383,32 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== dependencies: - call-bind "^1.0.6" es-errors "^1.3.0" - is-regex "^1.1.4" + isarray "^2.0.5" -scheduler@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe" - integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA== +safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" -scheduler@^0.23.0: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" +scheduler@0.25.0-rc-603e6108-20241029: + version "0.25.0-rc-603e6108-20241029" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-603e6108-20241029.tgz#684dd96647e104d23e0d29a37f18937daf82df19" + integrity sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA== + +scheduler@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" + integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== section-matter@^1.0.0: version "1.0.0" @@ -6081,12 +6423,12 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.7, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +semver@^7.6.0, semver@^7.6.3, semver@^7.7.1: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== -set-function-length@^1.2.1: +set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== @@ -6098,7 +6440,7 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" -set-function-name@^2.0.1, set-function-name@^2.0.2: +set-function-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== @@ -6108,6 +6450,44 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + +sharp@^0.33.5: + version "0.33.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" + integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== + dependencies: + color "^4.2.3" + detect-libc "^2.0.3" + semver "^7.6.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.33.5" + "@img/sharp-darwin-x64" "0.33.5" + "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-linux-arm" "0.33.5" + "@img/sharp-linux-arm64" "0.33.5" + "@img/sharp-linux-s390x" "0.33.5" + "@img/sharp-linux-x64" "0.33.5" + "@img/sharp-linuxmusl-arm64" "0.33.5" + "@img/sharp-linuxmusl-x64" "0.33.5" + "@img/sharp-wasm32" "0.33.5" + "@img/sharp-win32-ia32" "0.33.5" + "@img/sharp-win32-x64" "0.33.5" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -6120,15 +6500,45 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" simple-swizzle@^0.2.2: version "0.2.2" @@ -6137,50 +6547,45 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -simplebar-core@^1.2.0: - version "1.2.6" - resolved "https://registry.yarnpkg.com/simplebar-core/-/simplebar-core-1.2.6.tgz#8317bb6c1bcb38739eae43f1edc902cdd1d858d9" - integrity sha512-H5NYU+O+uvqOH5VXw3+lgoc1vTI6jL8LOZJsw4xgRpV7uIPjRpmLPdz0TrouxwKHBhpVLzVIlyKhaRLelIThMw== +simplebar-core@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/simplebar-core/-/simplebar-core-1.3.0.tgz#166cfbb4c1a2dc0a60833fe8e1fd590cdb32158b" + integrity sha512-LpWl3w0caz0bl322E68qsrRPpIn+rWBGAaEJ0lUJA7Xpr2sw92AkIhg6VWj988IefLXYh50ILatfAnbNoCFrlA== dependencies: - "@types/lodash-es" "^4.17.6" lodash "^4.17.21" - lodash-es "^4.17.21" -simplebar-react@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/simplebar-react/-/simplebar-react-3.2.0.tgz#0316c0220f85f6f29d7e7c4830fdd98aca294ec0" - integrity sha512-rsKUAAARhZ/w5f/uoUf3PWjNVQN7CsFrOPMqJSsXtM2lTyXmiN5femuPLHeY0+95EF2QcQigKQABq9A2a6EvJg== +simplebar-react@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/simplebar-react/-/simplebar-react-3.3.0.tgz#7170f29f0ea785c6881db81a8447c408fbc9056e" + integrity sha512-sxzy+xRuU41He4tT4QLGYutchtOuye/xxVeq7xhyOiwMiHNK1ZpvbOTyy+7P0i7gfpXLGTJ8Bep8+4Mhdgtz/g== dependencies: - simplebar-core "^1.2.0" + simplebar-core "^1.3.0" -simplebar@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/simplebar/-/simplebar-6.2.0.tgz#a7c1494b31eadad0622b93d455edd6a40e08f6db" - integrity sha512-KiXO9hGg0wK7Oh5zbcdHKEPGlyem3IWA27HLKqolrhpToP0SA8iLf5tbXFBS4qRH4QTXiUn87wh9zCd4GnCZeQ== +simplebar@6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/simplebar/-/simplebar-6.3.0.tgz#5581558e532d9ecf6e42faef932d81537f94d3ca" + integrity sha512-SQJfKSvUPJxlOhYCpswEn5ke5WQGsgDZNmpScWL+MKXgYpCDTq1bGiv6uWXwSHMYTkMco32fDUL35sVwCMmzCw== dependencies: - can-use-dom "^0.1.0" - simplebar-core "^1.2.0" + simplebar-core "^1.3.0" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" -source-map-js@^1.0.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" - integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +source-map-js@^1.0.1, source-map-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" @@ -6196,10 +6601,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stable-hash@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.5.tgz#94e8837aaeac5b4d0f631d2972adef2924b40269" + integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA== stackblur-canvas@^2.0.0: version "2.7.0" @@ -6211,43 +6616,38 @@ state-local@^1.0.6: resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5" integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w== -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -string.prototype.includes@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" - integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== +string.prototype.includes@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" + integrity sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" -string.prototype.matchall@^4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" - integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== +string.prototype.matchall@^4.0.12: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - es-abstract "^1.23.2" + es-abstract "^1.23.6" es-errors "^1.3.0" es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - regexp.prototype.flags "^1.5.2" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" set-function-name "^2.0.2" - side-channel "^1.0.6" + side-channel "^1.1.0" string.prototype.repeat@^1.0.0: version "1.0.0" @@ -6257,22 +6657,26 @@ string.prototype.repeat@^1.0.0: define-properties "^1.1.3" es-abstract "^1.17.5" -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.5" es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== +string.prototype.trimend@^1.0.8, string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.2" define-properties "^1.2.1" es-object-atoms "^1.0.0" @@ -6304,12 +6708,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== dependencies: - ansi-regex "^5.0.1" + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" strip-bom-string@^1.0.0: version "1.0.0" @@ -6321,22 +6726,29 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -style-to-object@^0.4.0: - version "0.4.4" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" - integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== +style-to-js@^1.0.0: + version "1.1.16" + resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.16.tgz#e6bd6cd29e250bcf8fa5e6591d07ced7575dbe7a" + integrity sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw== dependencies: - inline-style-parser "0.1.1" + style-to-object "1.0.8" -styled-jsx@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" - integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== +style-to-object@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.8.tgz#67a29bca47eaa587db18118d68f9d95955e81292" + integrity sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g== + dependencies: + inline-style-parser "0.2.4" + +styled-jsx@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499" + integrity sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== dependencies: client-only "0.0.1" @@ -6347,23 +6759,11 @@ stylis-plugin-rtl@2.1.1: dependencies: cssjanus "^2.0.1" -stylis@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" - integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== - stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -6391,78 +6791,18 @@ svg-pathdata@^6.0.3: resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac" integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw== -svg.draggable.js@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba" - integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw== - dependencies: - svg.js "^2.0.1" - -svg.easing.js@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12" - integrity sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA== - dependencies: - svg.js ">=2.3.x" - -svg.filter.js@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203" - integrity sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw== - dependencies: - svg.js "^2.2.5" - -svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5: - version "2.7.1" - resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d" - integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA== - -svg.pathmorphing.js@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65" - integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww== - dependencies: - svg.js "^2.4.0" - -svg.resize.js@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332" - integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw== - dependencies: - svg.js "^2.6.5" - svg.select.js "^2.1.2" - -svg.select.js@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73" - integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ== - dependencies: - svg.js "^2.2.5" - -svg.select.js@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917" - integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw== - dependencies: - svg.js "^2.6.5" - -svgo@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== +svgo@^3.0.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8" + integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== dependencies: "@trysound/sax" "0.2.0" commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" + css-select "^5.1.0" + css-tree "^2.3.1" + css-what "^6.1.0" + csso "^5.0.5" picocolors "^1.0.0" - stable "^0.1.8" - -tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== text-segmentation@^1.0.3: version "1.0.3" @@ -6471,11 +6811,6 @@ text-segmentation@^1.0.3: dependencies: utrie "^1.0.2" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - through2@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" @@ -6489,6 +6824,11 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + tiny-inflate@^1.0.0, tiny-inflate@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" @@ -6504,6 +6844,14 @@ tiny-warning@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinyglobby@^0.2.12: + version "0.2.12" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" + integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== + dependencies: + fdir "^6.4.3" + picomatch "^4.0.2" + tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" @@ -6511,11 +6859,6 @@ tippy.js@^6.3.7: dependencies: "@popperjs/core" "^2.9.0" -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -6533,11 +6876,6 @@ toposort@^2.0.2: resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -6548,6 +6886,11 @@ 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.0.1: + 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== + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -6558,32 +6901,20 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.4.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tss-react@^4.8.3: - version "4.9.13" - resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.9.13.tgz#e6ac0bfea3977e58b5ef5a261cd0b035e6bd8254" - integrity sha512-Gu19qqPH8/SAyKVIgDE5qHygirEDnNIQcXhiEc+l4Q9T7C1sfvUnbVWs+yBpmN26/wyk4FTOupjYS2wq4vH0yA== + version "4.9.15" + resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.9.15.tgz#a26fc24889a462ab4858094bc5b33cdda36e45ab" + integrity sha512-rLiEmDwUtln9RKTUR/ZPYBrufF0Tq/PFggO1M7P8M3/FAcodPQ746Ug9MCEFkURKDlntN17+Oja0DMMz5yBnsQ== dependencies: "@emotion/cache" "*" "@emotion/serialize" "*" "@emotion/utils" "*" -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -6591,89 +6922,90 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== type-fest@^3.12.0: version "3.13.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706" integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== -typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== dependencies: - call-bind "^1.0.7" + call-bound "^1.0.3" es-errors "^1.3.0" - is-typed-array "^1.1.13" + is-typed-array "^1.1.14" -typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" -typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" + call-bind "^1.0.8" for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" -typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== dependencies: call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-proto "^1.0.3" is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" -typescript@4.9.4: - version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@5.8.2: + version "5.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" + integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== dependencies: - call-bind "^1.0.2" + call-bound "^1.0.3" has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-match-property-ecmascript@^2.0.0: version "2.0.0" @@ -6684,9 +7016,9 @@ unicode-match-property-ecmascript@^2.0.0: unicode-property-aliases-ecmascript "^2.0.0" unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== unicode-properties@^1.4.0, unicode-properties@^1.4.1: version "1.4.1" @@ -6709,69 +7041,85 @@ unicode-trie@^2.0.0: pako "^0.2.5" tiny-inflate "^1.0.0" -unified@^10.0.0: - version "10.1.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" - integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== +unified@^11.0.0: + version "11.0.5" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== dependencies: - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" bail "^2.0.0" + devlop "^1.0.0" extend "^3.0.0" - is-buffer "^2.0.0" is-plain-obj "^4.0.0" trough "^2.0.0" - vfile "^5.0.0" + vfile "^6.0.0" -unist-util-generated@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" - integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== - -unist-util-is@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" - integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== dependencies: - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" -unist-util-position@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" - integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== dependencies: - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== dependencies: - "@types/unist" "^2.0.0" + "@types/unist" "^3.0.0" -unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" -unist-util-visit@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" -update-browserslist-db@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" - integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== +unrs-resolver@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.3.2.tgz#7c1dc0adabb1c3971c8c5cbdd8c1c2f742286e6d" + integrity sha512-ZKQBC351Ubw0PY8xWhneIfb6dygTQeUHtCcNGd0QB618zabD/WbFMYdRyJ7xeVT+6G82K5v/oyZO0QSHFtbIuw== + optionalDependencies: + "@unrs/resolver-binding-darwin-arm64" "1.3.2" + "@unrs/resolver-binding-darwin-x64" "1.3.2" + "@unrs/resolver-binding-freebsd-x64" "1.3.2" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.3.2" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.3.2" + "@unrs/resolver-binding-linux-arm64-gnu" "1.3.2" + "@unrs/resolver-binding-linux-arm64-musl" "1.3.2" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.3.2" + "@unrs/resolver-binding-linux-s390x-gnu" "1.3.2" + "@unrs/resolver-binding-linux-x64-gnu" "1.3.2" + "@unrs/resolver-binding-linux-x64-musl" "1.3.2" + "@unrs/resolver-binding-wasm32-wasi" "1.3.2" + "@unrs/resolver-binding-win32-arm64-msvc" "1.3.2" + "@unrs/resolver-binding-win32-ia32-msvc" "1.3.2" + "@unrs/resolver-binding-win32-x64-msvc" "1.3.2" + +update-browserslist-db@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== dependencies: - escalade "^3.1.2" - picocolors "^1.0.1" + escalade "^3.2.0" + picocolors "^1.1.1" uri-js@^4.2.2: version "4.4.1" @@ -6785,10 +7133,10 @@ use-memo-one@^1.1.1: resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== -use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" - integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== +use-sync-external-store@^1, use-sync-external-store@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" + integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" @@ -6802,33 +7150,21 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - -vfile-message@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" - integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" -vfile@^5.0.0: - version "5.3.7" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" - integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" vite-compatible-readable-stream@^3.6.1: version "3.6.1" @@ -6849,57 +7185,37 @@ w3c-keyname@^2.2.0: resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== -watchpack@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" -which-builtin-type@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" - integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== dependencies: + call-bound "^1.0.2" function.prototype.name "^1.1.6" has-tostringtag "^1.0.2" is-async-function "^2.0.0" - is-date-object "^1.0.5" - is-finalizationregistry "^1.0.2" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" is-generator-function "^1.0.10" - is-regex "^1.1.4" + is-regex "^1.2.1" is-weakref "^1.0.2" isarray "^2.0.5" - which-boxed-primitive "^1.0.2" + which-boxed-primitive "^1.1.0" which-collection "^1.0.2" - which-typed-array "^1.1.15" + which-typed-array "^1.1.16" -which-collection@^1.0.1, which-collection@^1.0.2: +which-collection@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== @@ -6909,15 +7225,17 @@ which-collection@^1.0.1, which-collection@^1.0.2: is-weakmap "^2.0.2" is-weakset "^2.0.3" -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== +which-typed-array@^1.1.16, which-typed-array@^1.1.18: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== dependencies: available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" has-tostringtag "^1.0.2" which@^2.0.1: @@ -6932,11 +7250,6 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -6964,20 +7277,22 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -yoga-layout@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-2.0.1.tgz#4bc686abe2464f977866650ddccc1dbcf9f0d03c" - integrity sha512-tT/oChyDXelLo2A+UVnlW9GU7CsvFMaEnd9kVFsaiCQonFAXd3xrHhkLYu+suwwosrAEQ746xBU+HvYtm1Zs2Q== +yoga-layout@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-3.2.1.tgz#d2d1ba06f0e81c2eb650c3e5ad8b0b4adde1e843" + integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ== -yup@0.32.11: - version "0.32.11" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== +yup@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.6.1.tgz#8defcff9daaf9feac178029c0e13b616563ada4b" + integrity sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA== dependencies: - "@babel/runtime" "^7.15.4" - "@types/lodash" "^4.14.175" - lodash "^4.17.21" - lodash-es "^4.17.21" - nanoclone "^0.2.1" - property-expr "^2.0.4" + property-expr "^2.0.5" + tiny-case "^1.0.3" toposort "^2.0.2" + type-fest "^2.19.0" + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==